Atom/src/main-process/atom-application.js

2117 lines
61 KiB
JavaScript

const AtomWindow = require('./atom-window');
const ApplicationMenu = require('./application-menu');
const AtomProtocolHandler = require('./atom-protocol-handler');
const AutoUpdateManager = require('./auto-update-manager');
const StorageFolder = require('../storage-folder');
const Config = require('../config');
const ConfigFile = require('../config-file');
const FileRecoveryService = require('./file-recovery-service');
const StartupTime = require('../startup-time');
const ipcHelpers = require('../ipc-helpers');
const {
BrowserWindow,
Menu,
app,
clipboard,
dialog,
ipcMain,
shell,
screen
} = require('electron');
const { CompositeDisposable, Disposable } = require('event-kit');
const crypto = require('crypto');
const fs = require('fs-plus');
const path = require('path');
const os = require('os');
const net = require('net');
const url = require('url');
const { promisify } = require('util');
const { EventEmitter } = require('events');
const _ = require('underscore-plus');
let FindParentDir = null;
let Resolve = null;
const ConfigSchema = require('../config-schema');
const LocationSuffixRegExp = /(:\d+)(:\d+)?$/;
// Increment this when changing the serialization format of `${ATOM_HOME}/storage/application.json` used by
// AtomApplication::saveCurrentWindowOptions() and AtomApplication::loadPreviousWindowOptions() in a backward-
// incompatible way.
const APPLICATION_STATE_VERSION = '1';
const getDefaultPath = () => {
const editor = atom.workspace.getActiveTextEditor();
if (!editor || !editor.getPath()) {
return;
}
const paths = atom.project.getPaths();
if (paths) {
return paths[0];
}
};
const getSocketSecretPath = atomVersion => {
const { username } = os.userInfo();
const atomHome = path.resolve(process.env.ATOM_HOME);
return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`);
};
const getSocketPath = socketSecret => {
if (!socketSecret) {
return null;
}
// Hash the secret to create the socket name to not expose it.
const socketName = crypto
.createHmac('sha256', socketSecret)
.update('socketName')
.digest('hex')
.substr(0, 12);
if (process.platform === 'win32') {
return `\\\\.\\pipe\\atom-${socketName}-sock`;
} else {
return path.join(os.tmpdir(), `atom-${socketName}.sock`);
}
};
const getExistingSocketSecret = atomVersion => {
const socketSecretPath = getSocketSecretPath(atomVersion);
if (!fs.existsSync(socketSecretPath)) {
return null;
}
return fs.readFileSync(socketSecretPath, 'utf8');
};
const getRandomBytes = promisify(crypto.randomBytes);
const writeFile = promisify(fs.writeFile);
const createSocketSecret = async atomVersion => {
const socketSecret = (await getRandomBytes(16)).toString('hex');
await writeFile(getSocketSecretPath(atomVersion), socketSecret, {
encoding: 'utf8',
mode: 0o600
});
return socketSecret;
};
const encryptOptions = (options, secret) => {
const message = JSON.stringify(options);
// Even if the following IV is not cryptographically secure, there's a really good chance
// it's going to be unique between executions which is the requirement for GCM.
// We're not using `crypto.randomBytes()` because in electron v2, that API is really slow
// on Windows machines, which affects the startup time of Atom.
// TodoElectronIssue: Once we upgrade to electron v3 we can use `crypto.randomBytes()`
const initVectorHash = crypto.createHash('sha1');
initVectorHash.update(Date.now() + '');
initVectorHash.update(Math.random() + '');
const initVector = initVectorHash.digest();
const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector);
let content = cipher.update(message, 'utf8', 'hex');
content += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return JSON.stringify({
authTag,
content,
initVector: initVector.toString('hex')
});
};
const decryptOptions = (optionsMessage, secret) => {
const { authTag, content, initVector } = JSON.parse(optionsMessage);
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
secret,
Buffer.from(initVector, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let message = decipher.update(content, 'hex', 'utf8');
message += decipher.final('utf8');
return JSON.parse(message);
};
// The application's singleton class.
//
// It's the entry point into the Atom application and maintains the global state
// of the application.
//
module.exports = class AtomApplication extends EventEmitter {
// Public: The entry point into the Atom application.
static open(options) {
StartupTime.addMarker('main-process:atom-application:open');
const socketSecret = getExistingSocketSecret(options.version);
const socketPath = getSocketPath(socketSecret);
const createApplication =
options.createApplication ||
(async () => {
const app = new AtomApplication(options);
await app.initialize(options);
return app;
});
// FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
// take a few seconds to trigger 'error' event, it could be a bug of node
// or electron, before it's fixed we check the existence of socketPath to
// speedup startup.
if (
!socketPath ||
options.test ||
options.benchmark ||
options.benchmarkTest ||
(process.platform !== 'win32' && !fs.existsSync(socketPath))
) {
return createApplication(options);
}
return new Promise(resolve => {
const client = net.connect({ path: socketPath }, () => {
client.write(encryptOptions(options, socketSecret), () => {
client.end();
app.quit();
resolve(null);
});
});
client.on('error', () => resolve(createApplication(options)));
});
}
exit(status) {
app.exit(status);
}
constructor(options) {
StartupTime.addMarker('main-process:atom-application:constructor:start');
super();
this.quitting = false;
this.quittingForUpdate = false;
this.getAllWindows = this.getAllWindows.bind(this);
this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this);
this.resourcePath = options.resourcePath;
this.devResourcePath = options.devResourcePath;
this.version = options.version;
this.devMode = options.devMode;
this.safeMode = options.safeMode;
this.logFile = options.logFile;
this.userDataDir = options.userDataDir;
this._killProcess = options.killProcess || process.kill.bind(process);
this.waitSessionsByWindow = new Map();
this.windowStack = new WindowStack();
this.initializeAtomHome(process.env.ATOM_HOME);
const configFilePath = fs.existsSync(
path.join(process.env.ATOM_HOME, 'config.json')
)
? path.join(process.env.ATOM_HOME, 'config.json')
: path.join(process.env.ATOM_HOME, 'config.cson');
this.configFile = ConfigFile.at(configFilePath);
this.config = new Config({
saveCallback: settings => {
if (!this.quitting) {
return this.configFile.update(settings);
}
}
});
this.config.setSchema(null, {
type: 'object',
properties: _.clone(ConfigSchema)
});
this.fileRecoveryService = new FileRecoveryService(
path.join(process.env.ATOM_HOME, 'recovery')
);
this.storageFolder = new StorageFolder(process.env.ATOM_HOME);
this.autoUpdateManager = new AutoUpdateManager(
this.version,
options.test || options.benchmark || options.benchmarkTest,
this.config
);
this.disposable = new CompositeDisposable();
this.handleEvents();
StartupTime.addMarker('main-process:atom-application:constructor:end');
}
// This stuff was previously done in the constructor, but we want to be able to construct this object
// for testing purposes without booting up the world. As you add tests, feel free to move instantiation
// of these various sub-objects into the constructor, but you'll need to remove the side-effects they
// perform during their construction, adding an initialize method that you call here.
async initialize(options) {
StartupTime.addMarker('main-process:atom-application:initialize:start');
global.atomApplication = this;
// DEPRECATED: This can be removed at some point (added in 1.13)
// It converts `useCustomTitleBar: true` to `titleBar: "custom"`
if (
process.platform === 'darwin' &&
this.config.get('core.useCustomTitleBar')
) {
this.config.unset('core.useCustomTitleBar');
this.config.set('core.titleBar', 'custom');
}
this.applicationMenu = new ApplicationMenu(
this.version,
this.autoUpdateManager
);
this.atomProtocolHandler = new AtomProtocolHandler(
this.resourcePath,
this.safeMode
);
// Don't await for the following method to avoid delaying the opening of a new window.
// (we await it just after opening it).
// We need to do this because `listenForArgumentsFromNewProcess()` calls `crypto.randomBytes`,
// which is really slow on Windows machines.
// (TodoElectronIssue: This got fixed in electron v3: https://github.com/electron/electron/issues/2073).
let socketServerPromise;
if (options.test || options.benchmark || options.benchmarkTest) {
socketServerPromise = Promise.resolve();
} else {
socketServerPromise = this.listenForArgumentsFromNewProcess();
}
this.setupDockMenu();
const result = await this.launch(options);
this.autoUpdateManager.initialize();
await socketServerPromise;
StartupTime.addMarker('main-process:atom-application:initialize:end');
return result;
}
async destroy() {
const windowsClosePromises = this.getAllWindows().map(window => {
window.close();
return window.closedPromise;
});
await Promise.all(windowsClosePromises);
this.disposable.dispose();
}
async launch(options) {
if (!this.configFilePromise) {
this.configFilePromise = this.configFile.watch().then(disposable => {
this.disposable.add(disposable);
this.config.onDidChange('core.titleBar', () => this.promptForRestart());
this.config.onDidChange('core.colorProfile', () =>
this.promptForRestart()
);
});
// TodoElectronIssue: In electron v2 awaiting the watcher causes some delay
// in Windows machines, which affects directly the startup time.
if (process.platform !== 'win32') {
await this.configFilePromise;
}
}
let optionsForWindowsToOpen = [];
let shouldReopenPreviousWindows = false;
if (options.test || options.benchmark || options.benchmarkTest) {
optionsForWindowsToOpen.push(options);
} else if (options.newWindow) {
shouldReopenPreviousWindows = false;
} else if (
(options.pathsToOpen && options.pathsToOpen.length > 0) ||
(options.urlsToOpen && options.urlsToOpen.length > 0)
) {
optionsForWindowsToOpen.push(options);
shouldReopenPreviousWindows =
this.config.get('core.restorePreviousWindowsOnStart') === 'always';
} else {
shouldReopenPreviousWindows =
this.config.get('core.restorePreviousWindowsOnStart') !== 'no';
}
if (shouldReopenPreviousWindows) {
optionsForWindowsToOpen = [
...(await this.loadPreviousWindowOptions()),
...optionsForWindowsToOpen
];
}
if (optionsForWindowsToOpen.length === 0) {
optionsForWindowsToOpen.push(options);
}
// Preserve window opening order
const windows = [];
for (const options of optionsForWindowsToOpen) {
windows.push(await this.openWithOptions(options));
}
return windows;
}
openWithOptions(options) {
const {
pathsToOpen,
executedFrom,
foldersToOpen,
urlsToOpen,
benchmark,
benchmarkTest,
test,
pidToKillWhenClosed,
devMode,
safeMode,
newWindow,
logFile,
profileStartup,
timeout,
clearWindowState,
addToLastWindow,
preserveFocus,
env
} = options;
if (!preserveFocus) {
app.focus();
}
if (test) {
return this.runTests({
headless: true,
devMode,
resourcePath: this.resourcePath,
executedFrom,
pathsToOpen,
logFile,
timeout,
env
});
} else if (benchmark || benchmarkTest) {
return this.runBenchmarks({
headless: true,
test: benchmarkTest,
resourcePath: this.resourcePath,
executedFrom,
pathsToOpen,
timeout,
env
});
} else if (
(pathsToOpen && pathsToOpen.length > 0) ||
(foldersToOpen && foldersToOpen.length > 0)
) {
return this.openPaths({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
clearWindowState,
addToLastWindow,
env
});
} else if (urlsToOpen && urlsToOpen.length > 0) {
return Promise.all(
urlsToOpen.map(urlToOpen =>
this.openUrl({ urlToOpen, devMode, safeMode, env })
)
);
} else {
// Always open an editor window if this is the first instance of Atom.
return this.openPath({
pathToOpen: null,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
clearWindowState,
addToLastWindow,
env
});
}
}
// Public: Create a new {AtomWindow} bound to this application.
createWindow(settings) {
return new AtomWindow(this, this.fileRecoveryService, settings);
}
// Public: Removes the {AtomWindow} from the global window list.
removeWindow(window) {
this.windowStack.removeWindow(window);
if (this.getAllWindows().length === 0) {
if (this.applicationMenu != null) {
this.applicationMenu.enableWindowSpecificItems(false);
}
if (['win32', 'linux'].includes(process.platform)) {
app.quit();
return;
}
}
if (!window.isSpec) this.saveCurrentWindowOptions(true);
}
// Public: Adds the {AtomWindow} to the global window list.
addWindow(window) {
this.windowStack.addWindow(window);
if (this.applicationMenu)
this.applicationMenu.addWindow(window.browserWindow);
window.once('window:loaded', () => {
this.autoUpdateManager &&
this.autoUpdateManager.emitUpdateAvailableEvent(window);
});
if (!window.isSpec) {
const focusHandler = () => this.windowStack.touch(window);
const blurHandler = () => this.saveCurrentWindowOptions(false);
window.browserWindow.on('focus', focusHandler);
window.browserWindow.on('blur', blurHandler);
window.browserWindow.once('closed', () => {
this.windowStack.removeWindow(window);
window.browserWindow.removeListener('focus', focusHandler);
window.browserWindow.removeListener('blur', blurHandler);
});
window.browserWindow.webContents.once('did-finish-load', blurHandler);
this.saveCurrentWindowOptions(false);
}
}
getAllWindows() {
return this.windowStack.all().slice();
}
getLastFocusedWindow(predicate) {
return this.windowStack.getLastFocusedWindow(predicate);
}
// Creates server to listen for additional atom application launches.
//
// You can run the atom command multiple times, but after the first launch
// the other launches will just pass their information to this server and then
// close immediately.
async listenForArgumentsFromNewProcess() {
this.socketSecretPromise = createSocketSecret(this.version);
this.socketSecret = await this.socketSecretPromise;
this.socketPath = getSocketPath(this.socketSecret);
await this.deleteSocketFile();
const server = net.createServer(connection => {
let data = '';
connection.on('data', chunk => {
data += chunk;
});
connection.on('end', () => {
try {
const options = decryptOptions(data, this.socketSecret);
this.openWithOptions(options);
} catch (e) {
// Error while parsing/decrypting the options passed by the client.
// We cannot trust the client, aborting.
}
});
});
return new Promise(resolve => {
server.listen(this.socketPath, resolve);
server.on('error', error =>
console.error('Application server failed', error)
);
});
}
async deleteSocketFile() {
if (process.platform === 'win32') return;
if (!this.socketSecretPromise) {
return;
}
await this.socketSecretPromise;
if (fs.existsSync(this.socketPath)) {
try {
fs.unlinkSync(this.socketPath);
} catch (error) {
// Ignore ENOENT errors in case the file was deleted between the exists
// check and the call to unlink sync. This occurred occasionally on CI
// which is why this check is here.
if (error.code !== 'ENOENT') throw error;
}
}
}
async deleteSocketSecretFile() {
if (!this.socketSecretPromise) {
return;
}
await this.socketSecretPromise;
const socketSecretPath = getSocketSecretPath(this.version);
if (fs.existsSync(socketSecretPath)) {
try {
fs.unlinkSync(socketSecretPath);
} catch (error) {
// Ignore ENOENT errors in case the file was deleted between the exists
// check and the call to unlink sync.
if (error.code !== 'ENOENT') throw error;
}
}
}
// Registers basic application commands, non-idempotent.
handleEvents() {
const createOpenSettings = ({ event, sameWindow }) => {
const targetWindow = event
? this.atomWindowForEvent(event)
: this.focusedWindow();
return {
devMode: targetWindow ? targetWindow.devMode : false,
safeMode: targetWindow ? targetWindow.safeMode : false,
window: sameWindow && targetWindow ? targetWindow : null
};
};
this.on('application:quit', () => app.quit());
this.on('application:new-window', () =>
this.openPath(createOpenSettings({}))
);
this.on('application:new-file', () =>
(this.focusedWindow() || this).openPath()
);
this.on('application:open-dev', () =>
this.promptForPathToOpen('all', { devMode: true })
);
this.on('application:open-safe', () =>
this.promptForPathToOpen('all', { safeMode: true })
);
this.on('application:inspect', ({ x, y, atomWindow }) => {
if (!atomWindow) atomWindow = this.focusedWindow();
if (atomWindow) atomWindow.browserWindow.inspectElement(x, y);
});
this.on('application:open-documentation', () =>
shell.openExternal('http://flight-manual.atom.io')
);
this.on('application:open-discussions', () =>
shell.openExternal('https://discuss.atom.io')
);
this.on('application:open-faq', () =>
shell.openExternal('https://atom.io/faq')
);
this.on('application:open-terms-of-use', () =>
shell.openExternal('https://atom.io/terms')
);
this.on('application:report-issue', () =>
shell.openExternal(
'https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs'
)
);
this.on('application:search-issues', () =>
shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom')
);
this.on('application:install-update', () => {
this.quitting = true;
this.quittingForUpdate = true;
this.autoUpdateManager.install();
});
this.on('application:check-for-update', () =>
this.autoUpdateManager.check()
);
if (process.platform === 'darwin') {
this.on('application:reopen-project', ({ paths }) => {
this.openPaths({ pathsToOpen: paths });
});
this.on('application:open', () => {
this.promptForPathToOpen(
'all',
createOpenSettings({ sameWindow: true }),
getDefaultPath()
);
});
this.on('application:open-file', () => {
this.promptForPathToOpen(
'file',
createOpenSettings({ sameWindow: true }),
getDefaultPath()
);
});
this.on('application:open-folder', () => {
this.promptForPathToOpen(
'folder',
createOpenSettings({ sameWindow: true }),
getDefaultPath()
);
});
this.on('application:bring-all-windows-to-front', () =>
Menu.sendActionToFirstResponder('arrangeInFront:')
);
this.on('application:hide', () =>
Menu.sendActionToFirstResponder('hide:')
);
this.on('application:hide-other-applications', () =>
Menu.sendActionToFirstResponder('hideOtherApplications:')
);
this.on('application:minimize', () =>
Menu.sendActionToFirstResponder('performMiniaturize:')
);
this.on('application:unhide-all-applications', () =>
Menu.sendActionToFirstResponder('unhideAllApplications:')
);
this.on('application:zoom', () =>
Menu.sendActionToFirstResponder('zoom:')
);
} else {
this.on('application:minimize', () => {
const window = this.focusedWindow();
if (window) window.minimize();
});
this.on('application:zoom', function() {
const window = this.focusedWindow();
if (window) window.maximize();
});
}
this.openPathOnEvent('application:about', 'atom://about');
this.openPathOnEvent('application:show-settings', 'atom://config');
this.openPathOnEvent('application:open-your-config', 'atom://.atom/config');
this.openPathOnEvent(
'application:open-your-init-script',
'atom://.atom/init-script'
);
this.openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap');
this.openPathOnEvent(
'application:open-your-snippets',
'atom://.atom/snippets'
);
this.openPathOnEvent(
'application:open-your-stylesheet',
'atom://.atom/stylesheet'
);
this.openPathOnEvent(
'application:open-license',
path.join(process.resourcesPath, 'LICENSE.md')
);
this.configFile.onDidChange(settings => {
for (let window of this.getAllWindows()) {
window.didChangeUserSettings(settings);
}
this.config.resetUserSettings(settings);
});
this.configFile.onDidError(message => {
const window = this.focusedWindow() || this.getLastFocusedWindow();
if (window) {
window.didFailToReadUserSettings(message);
} else {
console.error(message);
}
});
this.disposable.add(
ipcHelpers.on(app, 'before-quit', async event => {
let resolveBeforeQuitPromise;
this.lastBeforeQuitPromise = new Promise(resolve => {
resolveBeforeQuitPromise = resolve;
});
if (!this.quitting) {
this.quitting = true;
event.preventDefault();
const windowUnloadPromises = this.getAllWindows().map(
async window => {
const unloaded = await window.prepareToUnload();
if (unloaded) {
window.close();
await window.closedPromise;
}
return unloaded;
}
);
const windowUnloadedResults = await Promise.all(windowUnloadPromises);
if (windowUnloadedResults.every(Boolean)) {
app.quit();
} else {
this.quitting = false;
}
}
resolveBeforeQuitPromise();
})
);
this.disposable.add(
ipcHelpers.on(app, 'will-quit', () => {
this.killAllProcesses();
return Promise.all([
this.deleteSocketFile(),
this.deleteSocketSecretFile()
]);
})
);
// Triggered by the 'open-file' event from Electron:
// https://electronjs.org/docs/api/app#event-open-file-macos
// For example, this is fired when a file is dragged and dropped onto the Atom application icon in the dock.
this.disposable.add(
ipcHelpers.on(app, 'open-file', (event, pathToOpen) => {
event.preventDefault();
this.openPath({ pathToOpen });
})
);
this.disposable.add(
ipcHelpers.on(app, 'open-url', (event, urlToOpen) => {
event.preventDefault();
this.openUrl({
urlToOpen,
devMode: this.devMode,
safeMode: this.safeMode
});
})
);
this.disposable.add(
ipcHelpers.on(app, 'activate', (event, hasVisibleWindows) => {
if (hasVisibleWindows) return;
if (event) event.preventDefault();
this.emit('application:new-window');
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'restart-application', () => {
this.restart();
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'resolve-proxy', (event, requestId, url) => {
event.sender.session.resolveProxy(url, proxy => {
if (!event.sender.isDestroyed())
event.sender.send('did-resolve-proxy', requestId, proxy);
});
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'did-change-history-manager', event => {
for (let atomWindow of this.getAllWindows()) {
const { webContents } = atomWindow.browserWindow;
if (webContents !== event.sender)
webContents.send('did-change-history-manager');
}
})
);
// A request from the associated render process to open a set of paths using the standard window location logic.
// Used for application:reopen-project.
this.disposable.add(
ipcHelpers.on(ipcMain, 'open', (event, options) => {
if (options) {
if (typeof options.pathsToOpen === 'string') {
options.pathsToOpen = [options.pathsToOpen];
}
if (options.here) {
options.window = this.atomWindowForEvent(event);
}
if (options.pathsToOpen && options.pathsToOpen.length > 0) {
this.openPaths(options);
} else {
this.addWindow(this.createWindow(options));
}
} else {
this.promptForPathToOpen('all', {});
}
})
);
// Prompt for a file, folder, or either, then open the chosen paths. Files will be opened in the originating
// window; folders will be opened in a new window unless an existing window exactly contains all of them.
this.disposable.add(
ipcHelpers.on(ipcMain, 'open-chosen-any', (event, defaultPath) => {
this.promptForPathToOpen(
'all',
createOpenSettings({ event, sameWindow: true }),
defaultPath
);
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'open-chosen-file', (event, defaultPath) => {
this.promptForPathToOpen(
'file',
createOpenSettings({ event, sameWindow: true }),
defaultPath
);
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'open-chosen-folder', (event, defaultPath) => {
this.promptForPathToOpen(
'folder',
createOpenSettings({ event }),
defaultPath
);
})
);
this.disposable.add(
ipcHelpers.on(
ipcMain,
'update-application-menu',
(event, template, menu) => {
const window = BrowserWindow.fromWebContents(event.sender);
if (this.applicationMenu)
this.applicationMenu.update(window, template, menu);
}
)
);
this.disposable.add(
ipcHelpers.on(
ipcMain,
'run-package-specs',
(event, packageSpecPath, options = {}) => {
this.runTests(
Object.assign(
{
resourcePath: this.devResourcePath,
pathsToOpen: [packageSpecPath],
headless: false
},
options
)
);
}
)
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => {
this.runBenchmarks({
resourcePath: this.devResourcePath,
pathsToOpen: [benchmarksPath],
headless: false,
test: false
});
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'command', (event, command) => {
this.emit(command);
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => {
const window = BrowserWindow.fromWebContents(event.sender);
return window && window.emit(command, ...args);
})
);
this.disposable.add(
ipcHelpers.respondTo(
'window-method',
(browserWindow, method, ...args) => {
const window = this.atomWindowForBrowserWindow(browserWindow);
if (window) window[method](...args);
}
)
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'pick-folder', (event, responseChannel) => {
this.promptForPath('folder', paths =>
event.sender.send(responseChannel, paths)
);
})
);
this.disposable.add(
ipcHelpers.respondTo('set-window-size', (window, width, height) => {
window.setSize(width, height);
})
);
this.disposable.add(
ipcHelpers.respondTo('set-window-position', (window, x, y) => {
window.setPosition(x, y);
})
);
this.disposable.add(
ipcHelpers.respondTo(
'set-user-settings',
(window, settings, filePath) => {
if (!this.quitting) {
return ConfigFile.at(filePath || this.configFilePath).update(
JSON.parse(settings)
);
}
}
)
);
this.disposable.add(
ipcHelpers.respondTo('center-window', window => window.center())
);
this.disposable.add(
ipcHelpers.respondTo('focus-window', window => window.focus())
);
this.disposable.add(
ipcHelpers.respondTo('show-window', window => window.show())
);
this.disposable.add(
ipcHelpers.respondTo('hide-window', window => window.hide())
);
this.disposable.add(
ipcHelpers.respondTo(
'get-temporary-window-state',
window => window.temporaryState
)
);
this.disposable.add(
ipcHelpers.respondTo('set-temporary-window-state', (win, state) => {
win.temporaryState = state;
})
);
this.disposable.add(
ipcHelpers.on(
ipcMain,
'write-text-to-selection-clipboard',
(event, text) => clipboard.writeText(text, 'selection')
)
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'write-to-stdout', (event, output) =>
process.stdout.write(output)
)
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'write-to-stderr', (event, output) =>
process.stderr.write(output)
)
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'add-recent-document', (event, filename) =>
app.addRecentDocument(filename)
)
);
this.disposable.add(
ipcHelpers.on(
ipcMain,
'execute-javascript-in-dev-tools',
(event, code) =>
event.sender.devToolsWebContents &&
event.sender.devToolsWebContents.executeJavaScript(code)
)
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'get-auto-update-manager-state', event => {
event.returnValue = this.autoUpdateManager.getState();
})
);
this.disposable.add(
ipcHelpers.on(ipcMain, 'get-auto-update-manager-error', event => {
event.returnValue = this.autoUpdateManager.getErrorMessage();
})
);
this.disposable.add(
ipcHelpers.respondTo('will-save-path', (window, path) =>
this.fileRecoveryService.willSavePath(window, path)
)
);
this.disposable.add(
ipcHelpers.respondTo('did-save-path', (window, path) =>
this.fileRecoveryService.didSavePath(window, path)
)
);
this.disposable.add(this.disableZoomOnDisplayChange());
}
setupDockMenu() {
if (process.platform === 'darwin') {
return app.dock.setMenu(
Menu.buildFromTemplate([
{
label: 'New Window',
click: () => this.emit('application:new-window')
}
])
);
}
}
initializeAtomHome(configDirPath) {
if (!fs.existsSync(configDirPath)) {
const templateConfigDirPath = fs.resolve(this.resourcePath, 'dot-atom');
fs.copySync(templateConfigDirPath, configDirPath);
}
}
// Public: Executes the given command.
//
// If it isn't handled globally, delegate to the currently focused window.
//
// command - The string representing the command.
// args - The optional arguments to pass along.
sendCommand(command, ...args) {
if (!this.emit(command, ...args)) {
const focusedWindow = this.focusedWindow();
if (focusedWindow) {
return focusedWindow.sendCommand(command, ...args);
} else {
return this.sendCommandToFirstResponder(command);
}
}
}
// Public: Executes the given command on the given window.
//
// command - The string representing the command.
// atomWindow - The {AtomWindow} to send the command to.
// args - The optional arguments to pass along.
sendCommandToWindow(command, atomWindow, ...args) {
if (!this.emit(command, ...args)) {
if (atomWindow) {
return atomWindow.sendCommand(command, ...args);
} else {
return this.sendCommandToFirstResponder(command);
}
}
}
// Translates the command into macOS action and sends it to application's first
// responder.
sendCommandToFirstResponder(command) {
if (process.platform !== 'darwin') return false;
switch (command) {
case 'core:undo':
Menu.sendActionToFirstResponder('undo:');
break;
case 'core:redo':
Menu.sendActionToFirstResponder('redo:');
break;
case 'core:copy':
Menu.sendActionToFirstResponder('copy:');
break;
case 'core:cut':
Menu.sendActionToFirstResponder('cut:');
break;
case 'core:paste':
Menu.sendActionToFirstResponder('paste:');
break;
case 'core:select-all':
Menu.sendActionToFirstResponder('selectAll:');
break;
default:
return false;
}
return true;
}
// Public: Open the given path in the focused window when the event is
// triggered.
//
// A new window will be created if there is no currently focused window.
//
// eventName - The event to listen for.
// pathToOpen - The path to open when the event is triggered.
openPathOnEvent(eventName, pathToOpen) {
this.on(eventName, () => {
const window = this.focusedWindow();
if (window) {
return window.openPath(pathToOpen);
} else {
return this.openPath({ pathToOpen });
}
});
}
// Returns the {AtomWindow} for the given locations.
windowForLocations(locationsToOpen, devMode, safeMode) {
return this.getLastFocusedWindow(
window =>
!window.isSpec &&
window.devMode === devMode &&
window.safeMode === safeMode &&
window.containsLocations(locationsToOpen)
);
}
// Returns the {AtomWindow} for the given ipcMain event.
atomWindowForEvent({ sender }) {
return this.atomWindowForBrowserWindow(
BrowserWindow.fromWebContents(sender)
);
}
atomWindowForBrowserWindow(browserWindow) {
return this.getAllWindows().find(
atomWindow => atomWindow.browserWindow === browserWindow
);
}
// Public: Returns the currently focused {AtomWindow} or undefined if none.
focusedWindow() {
return this.getAllWindows().find(window => window.isFocused());
}
// Get the platform-specific window offset for new windows.
getWindowOffsetForCurrentPlatform() {
const offsetByPlatform = {
darwin: 22,
win32: 26
};
return offsetByPlatform[process.platform] || 0;
}
// Get the dimensions for opening a new window by cascading as appropriate to
// the platform.
getDimensionsForNewWindow() {
const window = this.focusedWindow() || this.getLastFocusedWindow();
if (!window || window.isMaximized()) return;
const dimensions = window.getDimensions();
if (dimensions) {
const offset = this.getWindowOffsetForCurrentPlatform();
dimensions.x += offset;
dimensions.y += offset;
return dimensions;
}
}
// Public: Opens a single path, in an existing window if possible.
//
// options -
// :pathToOpen - The file path to open
// :pidToKillWhenClosed - The integer of the pid to kill
// :newWindow - Boolean of whether this should be opened in a new window.
// :devMode - Boolean to control the opened window's dev mode.
// :safeMode - Boolean to control the opened window's safe mode.
// :profileStartup - Boolean to control creating a profile of the startup time.
// :window - {AtomWindow} to open file paths in.
// :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath({
pathToOpen,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
} = {}) {
return this.openPaths({
pathsToOpen: [pathToOpen],
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
});
}
// Public: Opens multiple paths, in existing windows if possible.
//
// options -
// :pathsToOpen - The array of file paths to open
// :foldersToOpen - An array of additional paths to open that must be existing directories
// :pidToKillWhenClosed - The integer of the pid to kill
// :newWindow - Boolean of whether this should be opened in a new window.
// :devMode - Boolean to control the opened window's dev mode.
// :safeMode - Boolean to control the opened window's safe mode.
// :windowDimensions - Object with height and width keys.
// :window - {AtomWindow} to open file paths in.
// :addToLastWindow - Boolean of whether this should be opened in last focused window.
async openPaths({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
windowDimensions,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
} = {}) {
if (!env) env = process.env;
if (!pathsToOpen) pathsToOpen = [];
if (!foldersToOpen) foldersToOpen = [];
devMode = Boolean(devMode);
safeMode = Boolean(safeMode);
clearWindowState = Boolean(clearWindowState);
const locationsToOpen = await Promise.all(
pathsToOpen.map(pathToOpen =>
this.parsePathToOpen(pathToOpen, executedFrom, {
hasWaitSession: pidToKillWhenClosed != null
})
)
);
for (const folderToOpen of foldersToOpen) {
locationsToOpen.push({
pathToOpen: folderToOpen,
initialLine: null,
initialColumn: null,
exists: true,
isDirectory: true,
hasWaitSession: pidToKillWhenClosed != null
});
}
if (locationsToOpen.length === 0) {
return;
}
const hasNonEmptyPath = locationsToOpen.some(
location => location.pathToOpen
);
const createNewWindow = newWindow || !hasNonEmptyPath;
let existingWindow;
if (!createNewWindow) {
// An explicitly provided AtomWindow has precedence.
existingWindow = window;
// If no window is specified and at least one path is provided, locate an existing window that contains all
// provided paths.
if (!existingWindow && hasNonEmptyPath) {
existingWindow = this.windowForLocations(
locationsToOpen,
devMode,
safeMode
);
}
// No window specified, no existing window found, and addition to the last window requested. Find the last
// focused window that matches the requested dev and safe modes.
if (!existingWindow && addToLastWindow) {
existingWindow = this.getLastFocusedWindow(win => {
return (
!win.isSpec && win.devMode === devMode && win.safeMode === safeMode
);
});
}
// Fall back to the last focused window that has no project roots.
if (!existingWindow) {
existingWindow = this.getLastFocusedWindow(
win => !win.isSpec && !win.hasProjectPaths()
);
}
// One last case: if *no* paths are directories, add to the last focused window.
if (!existingWindow) {
const noDirectories = locationsToOpen.every(
location => !location.isDirectory
);
if (noDirectories) {
existingWindow = this.getLastFocusedWindow(win => {
return (
!win.isSpec &&
win.devMode === devMode &&
win.safeMode === safeMode
);
});
}
}
}
let openedWindow;
if (existingWindow) {
openedWindow = existingWindow;
StartupTime.addMarker('main-process:atom-application:open-in-existing');
openedWindow.openLocations(locationsToOpen);
if (openedWindow.isMinimized()) {
openedWindow.restore();
} else {
openedWindow.focus();
}
openedWindow.replaceEnvironment(env);
} else {
let resourcePath, windowInitializationScript;
if (devMode) {
try {
windowInitializationScript = require.resolve(
path.join(
this.devResourcePath,
'src',
'initialize-application-window'
)
);
resourcePath = this.devResourcePath;
} catch (error) {}
}
if (!windowInitializationScript) {
windowInitializationScript = require.resolve(
'../initialize-application-window'
);
}
if (!resourcePath) resourcePath = this.resourcePath;
if (!windowDimensions)
windowDimensions = this.getDimensionsForNewWindow();
StartupTime.addMarker('main-process:atom-application:create-window');
openedWindow = this.createWindow({
locationsToOpen,
windowInitializationScript,
resourcePath,
devMode,
safeMode,
windowDimensions,
profileStartup,
clearWindowState,
env
});
this.addWindow(openedWindow);
openedWindow.focus();
}
if (pidToKillWhenClosed != null) {
if (!this.waitSessionsByWindow.has(openedWindow)) {
this.waitSessionsByWindow.set(openedWindow, []);
}
this.waitSessionsByWindow.get(openedWindow).push({
pid: pidToKillWhenClosed,
remainingPaths: new Set(
locationsToOpen.map(location => location.pathToOpen).filter(Boolean)
)
});
}
openedWindow.browserWindow.once('closed', () =>
this.killProcessesForWindow(openedWindow)
);
return openedWindow;
}
// Kill all processes associated with opened windows.
killAllProcesses() {
for (let window of this.waitSessionsByWindow.keys()) {
this.killProcessesForWindow(window);
}
}
killProcessesForWindow(window) {
const sessions = this.waitSessionsByWindow.get(window);
if (!sessions) return;
for (const session of sessions) {
this.killProcess(session.pid);
}
this.waitSessionsByWindow.delete(window);
}
windowDidClosePathWithWaitSession(window, initialPath) {
const waitSessions = this.waitSessionsByWindow.get(window);
if (!waitSessions) return;
for (let i = waitSessions.length - 1; i >= 0; i--) {
const session = waitSessions[i];
session.remainingPaths.delete(initialPath);
if (session.remainingPaths.size === 0) {
this.killProcess(session.pid);
waitSessions.splice(i, 1);
}
}
}
// Kill the process with the given pid.
killProcess(pid) {
try {
const parsedPid = parseInt(pid);
if (isFinite(parsedPid)) this._killProcess(parsedPid);
} catch (error) {
if (error.code !== 'ESRCH') {
console.log(
`Killing process ${pid} failed: ${
error.code != null ? error.code : error.message
}`
);
}
}
}
async saveCurrentWindowOptions(allowEmpty = false) {
if (this.quitting) return;
const state = {
version: APPLICATION_STATE_VERSION,
windows: this.getAllWindows()
.filter(window => !window.isSpec)
.map(window => ({ projectRoots: window.projectRoots }))
};
state.windows.reverse();
if (state.windows.length > 0 || allowEmpty) {
await this.storageFolder.store('application.json', state);
this.emit('application:did-save-state');
}
}
async loadPreviousWindowOptions() {
const state = await this.storageFolder.load('application.json');
if (!state) {
return [];
}
if (state.version === APPLICATION_STATE_VERSION) {
// Atom >=1.36.1
// Schema: {version: '1', windows: [{projectRoots: ['<root-dir>', ...]}, ...]}
return state.windows.map(each => ({
foldersToOpen: each.projectRoots,
devMode: this.devMode,
safeMode: this.safeMode,
newWindow: true
}));
} else if (state.version === undefined) {
// Atom <= 1.36.0
// Schema: [{initialPaths: ['<root-dir>', ...]}, ...]
return Promise.all(
state.map(async windowState => {
// Classify each window's initialPaths as directories or non-directories
const classifiedPaths = await Promise.all(
windowState.initialPaths.map(
initialPath =>
new Promise(resolve => {
fs.isDirectory(initialPath, isDir =>
resolve({ initialPath, isDir })
);
})
)
);
// Only accept initialPaths that are existing directories
return {
foldersToOpen: classifiedPaths
.filter(({ isDir }) => isDir)
.map(({ initialPath }) => initialPath),
devMode: this.devMode,
safeMode: this.safeMode,
newWindow: true
};
})
);
} else {
// Unrecognized version (from a newer Atom?)
return [];
}
}
// Open an atom:// url.
//
// The host of the URL being opened is assumed to be the package name
// responsible for opening the URL. A new window will be created with
// that package's `urlMain` as the bootstrap script.
//
// options -
// :urlToOpen - The atom:// url to open.
// :devMode - Boolean to control the opened window's dev mode.
// :safeMode - Boolean to control the opened window's safe mode.
openUrl({ urlToOpen, devMode, safeMode, env }) {
const parsedUrl = url.parse(urlToOpen, true);
if (parsedUrl.protocol !== 'atom:') return;
const pack = this.findPackageWithName(parsedUrl.host, devMode);
if (pack && pack.urlMain) {
return this.openPackageUrlMain(
parsedUrl.host,
pack.urlMain,
urlToOpen,
devMode,
safeMode,
env
);
} else {
return this.openPackageUriHandler(
urlToOpen,
parsedUrl,
devMode,
safeMode,
env
);
}
}
openPackageUriHandler(url, parsedUrl, devMode, safeMode, env) {
let bestWindow;
if (parsedUrl.host === 'core') {
const predicate = require('../core-uri-handlers').windowPredicate(
parsedUrl
);
bestWindow = this.getLastFocusedWindow(
win => !win.isSpecWindow() && predicate(win)
);
}
if (!bestWindow)
bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow());
if (bestWindow) {
bestWindow.sendURIMessage(url);
bestWindow.focus();
return bestWindow;
} else {
let windowInitializationScript;
let { resourcePath } = this;
if (devMode) {
try {
windowInitializationScript = require.resolve(
path.join(
this.devResourcePath,
'src',
'initialize-application-window'
)
);
resourcePath = this.devResourcePath;
} catch (error) {}
}
if (!windowInitializationScript) {
windowInitializationScript = require.resolve(
'../initialize-application-window'
);
}
const windowDimensions = this.getDimensionsForNewWindow();
const window = this.createWindow({
resourcePath,
windowInitializationScript,
devMode,
safeMode,
windowDimensions,
env
});
this.addWindow(window);
window.on('window:loaded', () => window.sendURIMessage(url));
return window;
}
}
findPackageWithName(packageName, devMode) {
return this.getPackageManager(devMode)
.getAvailablePackageMetadata()
.find(({ name }) => name === packageName);
}
openPackageUrlMain(
packageName,
packageUrlMain,
urlToOpen,
devMode,
safeMode,
env
) {
const packagePath = this.getPackageManager(devMode).resolvePackagePath(
packageName
);
const windowInitializationScript = path.resolve(
packagePath,
packageUrlMain
);
const windowDimensions = this.getDimensionsForNewWindow();
const window = this.createWindow({
windowInitializationScript,
resourcePath: this.resourcePath,
devMode,
safeMode,
urlToOpen,
windowDimensions,
env
});
this.addWindow(window);
return window;
}
getPackageManager(devMode) {
if (this.packages == null) {
const PackageManager = require('../package-manager');
this.packages = new PackageManager({});
this.packages.initialize({
configDirPath: process.env.ATOM_HOME,
devMode,
resourcePath: this.resourcePath
});
}
return this.packages;
}
// Opens up a new {AtomWindow} to run specs within.
//
// options -
// :headless - A Boolean that, if true, will close the window upon
// completion.
// :resourcePath - The path to include specs from.
// :specPath - The directory to load specs from.
// :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
// and ~/.atom/dev/packages, defaults to false.
runTests({
headless,
resourcePath,
executedFrom,
pathsToOpen,
logFile,
safeMode,
timeout,
env
}) {
let windowInitializationScript;
if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) {
({ resourcePath } = this);
}
const timeoutInSeconds = Number.parseFloat(timeout);
if (!Number.isNaN(timeoutInSeconds)) {
const timeoutHandler = function() {
console.log(
`The test suite has timed out because it has been running for more than ${timeoutInSeconds} seconds.`
);
return process.exit(124); // Use the same exit code as the UNIX timeout util.
};
setTimeout(timeoutHandler, timeoutInSeconds * 1000);
}
try {
windowInitializationScript = require.resolve(
path.resolve(this.devResourcePath, 'src', 'initialize-test-window')
);
} catch (error) {
windowInitializationScript = require.resolve(
path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window')
);
}
const testPaths = [];
if (pathsToOpen != null) {
for (let pathToOpen of pathsToOpen) {
testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)));
}
}
if (testPaths.length === 0) {
process.stderr.write('Error: Specify at least one test path\n\n');
process.exit(1);
}
const legacyTestRunnerPath = this.resolveLegacyTestRunnerPath();
const testRunnerPath = this.resolveTestRunnerPath(testPaths[0]);
const devMode = true;
const isSpec = true;
if (safeMode == null) {
safeMode = false;
}
const window = this.createWindow({
windowInitializationScript,
resourcePath,
headless,
isSpec,
devMode,
testRunnerPath,
legacyTestRunnerPath,
testPaths,
logFile,
safeMode,
env
});
this.addWindow(window);
if (env) window.replaceEnvironment(env);
return window;
}
runBenchmarks({
headless,
test,
resourcePath,
executedFrom,
pathsToOpen,
env
}) {
let windowInitializationScript;
if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) {
({ resourcePath } = this);
}
try {
windowInitializationScript = require.resolve(
path.resolve(this.devResourcePath, 'src', 'initialize-benchmark-window')
);
} catch (error) {
windowInitializationScript = require.resolve(
path.resolve(
__dirname,
'..',
'..',
'src',
'initialize-benchmark-window'
)
);
}
const benchmarkPaths = [];
if (pathsToOpen != null) {
for (let pathToOpen of pathsToOpen) {
benchmarkPaths.push(
path.resolve(executedFrom, fs.normalize(pathToOpen))
);
}
}
if (benchmarkPaths.length === 0) {
process.stderr.write('Error: Specify at least one benchmark path.\n\n');
process.exit(1);
}
const devMode = true;
const isSpec = true;
const safeMode = false;
const window = this.createWindow({
windowInitializationScript,
resourcePath,
headless,
test,
isSpec,
devMode,
benchmarkPaths,
safeMode,
env
});
this.addWindow(window);
return window;
}
resolveTestRunnerPath(testPath) {
let packageRoot;
if (FindParentDir == null) {
FindParentDir = require('find-parent-dir');
}
if ((packageRoot = FindParentDir.sync(testPath, 'package.json'))) {
const packageMetadata = require(path.join(packageRoot, 'package.json'));
if (packageMetadata.atomTestRunner) {
let testRunnerPath;
if (Resolve == null) {
Resolve = require('resolve');
}
if (
(testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, {
basedir: packageRoot,
extensions: Object.keys(require.extensions)
}))
) {
return testRunnerPath;
} else {
process.stderr.write(
`Error: Could not resolve test runner path '${
packageMetadata.atomTestRunner
}'`
);
process.exit(1);
}
}
}
return this.resolveLegacyTestRunnerPath();
}
resolveLegacyTestRunnerPath() {
try {
return require.resolve(
path.resolve(this.devResourcePath, 'spec', 'jasmine-test-runner')
);
} catch (error) {
return require.resolve(
path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner')
);
}
}
async parsePathToOpen(pathToOpen, executedFrom, extra) {
const result = Object.assign(
{
pathToOpen,
initialColumn: null,
initialLine: null,
exists: false,
isDirectory: false,
isFile: false
},
extra
);
if (!pathToOpen) {
return result;
}
result.pathToOpen = result.pathToOpen.replace(/[:\s]+$/, '');
const match = result.pathToOpen.match(LocationSuffixRegExp);
if (match != null) {
result.pathToOpen = result.pathToOpen.slice(0, -match[0].length);
if (match[1]) {
result.initialLine = Math.max(0, parseInt(match[1].slice(1), 10) - 1);
}
if (match[2]) {
result.initialColumn = Math.max(0, parseInt(match[2].slice(1), 10) - 1);
}
}
const normalizedPath = path.normalize(
path.resolve(executedFrom, fs.normalize(result.pathToOpen))
);
if (!url.parse(pathToOpen).protocol) {
result.pathToOpen = normalizedPath;
}
await new Promise((resolve, reject) => {
fs.stat(result.pathToOpen, (err, st) => {
if (err) {
if (err.code === 'ENOENT' || err.code === 'EACCES') {
result.exists = false;
resolve();
} else {
reject(err);
}
return;
}
result.exists = true;
result.isFile = st.isFile();
result.isDirectory = st.isDirectory();
resolve();
});
});
return result;
}
// Opens a native dialog to prompt the user for a path.
//
// Once paths are selected, they're opened in a new or existing {AtomWindow}s.
//
// options -
// :type - A String which specifies the type of the dialog, could be 'file',
// 'folder' or 'all'. The 'all' is only available on macOS.
// :devMode - A Boolean which controls whether any newly opened windows
// should be in dev mode or not.
// :safeMode - A Boolean which controls whether any newly opened windows
// should be in safe mode or not.
// :window - An {AtomWindow} to use for opening selected file paths as long as
// all are files.
// :path - An optional String which controls the default path to which the
// file dialog opens.
promptForPathToOpen(type, { devMode, safeMode, window }, path = null) {
return this.promptForPath(
type,
async pathsToOpen => {
let targetWindow;
// Open in :window as long as no chosen paths are folders. If any chosen path is a folder, open in a
// new window instead.
if (type === 'folder') {
targetWindow = null;
} else if (type === 'file') {
targetWindow = window;
} else if (type === 'all') {
const areDirectories = await Promise.all(
pathsToOpen.map(
pathToOpen =>
new Promise(resolve => fs.isDirectory(pathToOpen, resolve))
)
);
if (!areDirectories.some(Boolean)) {
targetWindow = window;
}
}
return this.openPaths({
pathsToOpen,
devMode,
safeMode,
window: targetWindow
});
},
path
);
}
promptForPath(type, callback, path) {
const properties = (() => {
switch (type) {
case 'file':
return ['openFile'];
case 'folder':
return ['openDirectory'];
case 'all':
return ['openFile', 'openDirectory'];
default:
throw new Error(`${type} is an invalid type for promptForPath`);
}
})();
// Show the open dialog as child window on Windows and Linux, and as an independent dialog on macOS. This matches
// most native apps.
const parentWindow =
process.platform === 'darwin' ? null : BrowserWindow.getFocusedWindow();
const openOptions = {
properties: properties.concat(['multiSelections', 'createDirectory']),
title: (() => {
switch (type) {
case 'file':
return 'Open File';
case 'folder':
return 'Open Folder';
default:
return 'Open';
}
})()
};
// File dialog defaults to project directory of currently active editor
if (path) openOptions.defaultPath = path;
dialog.showOpenDialog(parentWindow, openOptions, callback);
}
promptForRestart() {
dialog.showMessageBox(
BrowserWindow.getFocusedWindow(),
{
type: 'warning',
title: 'Restart required',
message:
'You will need to restart Atom for this change to take effect.',
buttons: ['Restart Atom', 'Cancel']
},
response => {
if (response === 0) this.restart();
}
);
}
restart() {
const args = [];
if (this.safeMode) args.push('--safe');
if (this.logFile != null) args.push(`--log-file=${this.logFile}`);
if (this.userDataDir != null)
args.push(`--user-data-dir=${this.userDataDir}`);
if (this.devMode) {
args.push('--dev');
args.push(`--resource-path=${this.resourcePath}`);
}
app.relaunch({ args });
app.quit();
}
disableZoomOnDisplayChange() {
const callback = () => {
this.getAllWindows().map(window => window.disableZoom());
};
// Set the limits every time a display is added or removed, otherwise the
// configuration gets reset to the default, which allows zooming the
// webframe.
screen.on('display-added', callback);
screen.on('display-removed', callback);
return new Disposable(() => {
screen.removeListener('display-added', callback);
screen.removeListener('display-removed', callback);
});
}
};
class WindowStack {
constructor(windows = []) {
this.addWindow = this.addWindow.bind(this);
this.touch = this.touch.bind(this);
this.removeWindow = this.removeWindow.bind(this);
this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this);
this.all = this.all.bind(this);
this.windows = windows;
}
addWindow(window) {
this.removeWindow(window);
return this.windows.unshift(window);
}
touch(window) {
return this.addWindow(window);
}
removeWindow(window) {
const currentIndex = this.windows.indexOf(window);
if (currentIndex > -1) {
return this.windows.splice(currentIndex, 1);
}
}
getLastFocusedWindow(predicate) {
if (predicate == null) {
predicate = win => true;
}
return this.windows.find(predicate);
}
all() {
return this.windows;
}
}