diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index aea5313e8..d90001205 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -639,7 +639,6 @@ describe('AtomEnvironment', () => { describe('::openLocations(locations) (called via IPC from browser process)', () => { beforeEach(() => { - spyOn(atom.workspace, 'open') atom.project.setPaths([]) }) @@ -649,48 +648,50 @@ describe('AtomEnvironment', () => { }) describe('when the opened path exists', () => { - it("adds it to the project's paths", async () => { + it('opens a file', async () => { const pathToOpen = __filename await atom.openLocations([{pathToOpen}]) - expect(atom.project.getPaths()[0]).toBe(__dirname) + expect(atom.project.getPaths()).toEqual([]) }) - describe('then a second path is opened with forceAddToWindow', () => { - it("adds the second path to the project's paths", async () => { - const firstPathToOpen = __dirname - const secondPathToOpen = path.resolve(__dirname, './fixtures') - await atom.openLocations([{pathToOpen: firstPathToOpen}]) - await atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}]) - expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen]) - }) - }) - }) - - describe('when the opened path does not exist but its parent directory does', () => { - it('adds the parent directory to the project paths', async () => { - const pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') - await atom.openLocations([{pathToOpen}]) - expect(atom.project.getPaths()[0]).toBe(__dirname) - }) - }) - - describe('when the opened path is a file', () => { - it('opens it in the workspace', async () => { - const pathToOpen = __filename - await atom.openLocations([{pathToOpen}]) - expect(atom.workspace.open.mostRecentCall.args[0]).toBe(__filename) - }) - }) - - describe('when the opened path is a directory', () => { - it('does not open it in the workspace', async () => { + it('opens a directory as a project folder', async () => { const pathToOpen = __dirname await atom.openLocations([{pathToOpen}]) - expect(atom.workspace.open.callCount).toBe(0) + expect(atom.workspace.getTextEditors().map(e => e.getPath())).toEqual([]) + expect(atom.project.getPaths()).toEqual([pathToOpen]) }) }) - describe('when the opened path is a uri', () => { + describe('when the opened path does not exist', () => { + it('opens it as a new file', async () => { + const pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') + await atom.openLocations([{pathToOpen}]) + expect(atom.workspace.getTextEditors().map(e => e.getPath())).toEqual([pathToOpen]) + expect(atom.project.getPaths()).toEqual([]) + }) + }) + + describe('when the opened path is handled by a registered directory provider', () => { + let serviceDisposable + + beforeEach(() => { + serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { + directoryForURISync (uri) { + if (uri.startsWith('remote://')) { + return { getPath() { return uri } } + } else { + return null + } + } + }) + + waitsFor(() => atom.project.directoryProviders.length > 0) + }) + + afterEach(() => { + serviceDisposable.dispose() + }) + it("adds it to the project's paths as is", async () => { const pathToOpen = 'remote://server:7644/some/dir/path' spyOn(atom.project, 'addPath') @@ -741,7 +742,7 @@ describe('AtomEnvironment', () => { const fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt') await atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}]) expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen]) - expect(atom.project.getPaths()).toEqual([__dirname]) + expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]) }) }) }) diff --git a/spec/integration/smoke-spec.coffee b/spec/integration/smoke-spec.coffee index e147cf5c0..dd6c9776f 100644 --- a/spec/integration/smoke-spec.coffee +++ b/spec/integration/smoke-spec.coffee @@ -23,10 +23,14 @@ describe "Smoke Test", -> it "can open a file in Atom and perform basic operations on it", -> tempDirPath = temp.mkdirSync("empty-dir") - runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) -> + filePath = path.join(tempDirPath, "new-file") + + fs.writeFileSync filePath, "", {encoding: "utf8"} + + runAtom [filePath], {ATOM_HOME: atomHome}, (client) -> client .treeViewRootDirectories() - .then ({value}) -> expect(value).toEqual([tempDirPath]) + .then ({value}) -> expect(value).toEqual([]) .waitForExist("atom-text-editor", 5000) .then (exists) -> expect(exists).toBe true .waitForPaneItemCount(1, 1000) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index c49e36b5d..c271608c4 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -1,3 +1,5 @@ +/* globals assert */ + const temp = require('temp').track() const season = require('season') const dedent = require('dedent') @@ -44,55 +46,226 @@ describe('AtomApplication', function () { }) describe('launch', () => { - it('can open to a specific line number of a file', async () => { - const filePath = path.join(makeTempDir(), 'new-file') - fs.writeFileSync(filePath, '1\n2\n3\n4\n') - const atomApplication = buildAtomApplication() + describe('with no paths', () => { + it('reopens any previously opened windows', async () => { + if (process.platform === 'win32') return // Test is too flakey on Windows - const [window] = await atomApplication.launch(parseCommandLine([filePath + ':3'])) - await focusWindow(window) + const tempDirPath1 = makeTempDir() + const tempDirPath2 = makeTempDir() - const cursorRow = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { - atom.workspace.observeTextEditors(textEditor => { - sendBackToMainProcess(textEditor.getCursorBufferPosition().row) - }) + const atomApplication1 = buildAtomApplication() + const [app1Window1] = await atomApplication1.launch(parseCommandLine([tempDirPath1])) + await emitterEventPromise(app1Window1, 'window:locations-opened') + + const [app1Window2] = await atomApplication1.launch(parseCommandLine([tempDirPath2])) + await emitterEventPromise(app1Window2, 'window:locations-opened') + + await Promise.all([ + app1Window1.prepareToUnload(), + app1Window2.prepareToUnload() + ]) + + const atomApplication2 = buildAtomApplication() + const [app2Window1, app2Window2] = await atomApplication2.launch(parseCommandLine([])) + await Promise.all([ + emitterEventPromise(app2Window1, 'window:locations-opened'), + emitterEventPromise(app2Window2, 'window:locations-opened') + ]) + + assert.deepEqual(await getTreeViewRootDirectories(app2Window1), [tempDirPath1]) + assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2]) }) - assert.equal(cursorRow, 2) + it('when windows already exist, opens a new window with a single untitled buffer', async () => { + const atomApplication = buildAtomApplication() + const [window1] = await atomApplication.launch(parseCommandLine([])) + await focusWindow(window1) + const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) + }) + assert.equal(window1EditorTitle, 'untitled') + + const window2 = atomApplication.openWithOptions(parseCommandLine([])) + await window2.loadedPromise + const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) + }) + assert.equal(window2EditorTitle, 'untitled') + + assert.deepEqual(atomApplication.getAllWindows(), [window2, window1]) + }) + + it('when no windows are open but --new-window is passed, opens a new window with a single untitled buffer', async () => { + // Populate some saved state + const tempDirPath1 = makeTempDir() + const tempDirPath2 = makeTempDir() + + const atomApplication1 = buildAtomApplication() + const [app1Window1] = await atomApplication1.launch(parseCommandLine([tempDirPath1])) + await emitterEventPromise(app1Window1, 'window:locations-opened') + + const [app1Window2] = await atomApplication1.launch(parseCommandLine([tempDirPath2])) + await emitterEventPromise(app1Window2, 'window:locations-opened') + + await Promise.all([ + app1Window1.prepareToUnload(), + app1Window2.prepareToUnload() + ]) + + // Launch with --new-window + const atomApplication2 = buildAtomApplication() + const appWindows2 = await atomApplication2.launch(parseCommandLine(['--new-window'])) + assert.lengthOf(appWindows2, 1) + const [appWindow2] = appWindows2 + await appWindow2.loadedPromise + const window2EditorTitle = await evalInWebContents(appWindow2.browserWindow.webContents, sendBackToMainProcess => { + sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) + }) + assert.equal(window2EditorTitle, 'untitled') + }) + + it('does not open an empty editor if core.openEmptyEditorOnStart is false', async () => { + const configPath = path.join(process.env.ATOM_HOME, 'config.cson') + const config = season.readFileSync(configPath) + if (!config['*'].core) config['*'].core = {} + config['*'].core.openEmptyEditorOnStart = false + season.writeFileSync(configPath, config) + + const atomApplication = buildAtomApplication() + const [window1] = await atomApplication.launch(parseCommandLine([])) + await focusWindow(window1) + + // wait a bit just to make sure we don't pass due to querying the render process before it loads + await timeoutPromise(1000) + + const itemCount = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + sendBackToMainProcess(atom.workspace.getActivePane().getItems().length) + }) + assert.equal(itemCount, 0) + }) }) - it('can open to a specific line and column of a file', async () => { - const filePath = path.join(makeTempDir(), 'new-file') - fs.writeFileSync(filePath, '1\n2\n3\n4\n') - const atomApplication = buildAtomApplication() + describe('with file or folder paths', () => { + it('shows all directories in the tree view when multiple directory paths are passed to Atom', async () => { + const dirAPath = makeTempDir('a') + const dirBPath = makeTempDir('b') + const dirBSubdirPath = path.join(dirBPath, 'c') + fs.mkdirSync(dirBSubdirPath) - const [window] = await atomApplication.launch(parseCommandLine([filePath + ':2:2'])) - await focusWindow(window) + const atomApplication = buildAtomApplication() + const [window1] = await atomApplication.launch(parseCommandLine([dirAPath, dirBPath])) + await focusWindow(window1) - const cursorPosition = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { - atom.workspace.observeTextEditors(textEditor => { - sendBackToMainProcess(textEditor.getCursorBufferPosition()) - }) + await conditionPromise(async () => (await getTreeViewRootDirectories(window1)).length === 2) + assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirBPath]) }) - assert.deepEqual(cursorPosition, {row: 1, column: 1}) + it('can open to a specific line number of a file', async () => { + const filePath = path.join(makeTempDir(), 'new-file') + fs.writeFileSync(filePath, '1\n2\n3\n4\n') + const atomApplication = buildAtomApplication() + + const [window] = await atomApplication.launch(parseCommandLine([filePath + ':3'])) + await focusWindow(window) + + const cursorRow = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { + sendBackToMainProcess(textEditor.getCursorBufferPosition().row) + }) + }) + + assert.equal(cursorRow, 2) + }) + + it('can open to a specific line and column of a file', async () => { + const filePath = path.join(makeTempDir(), 'new-file') + fs.writeFileSync(filePath, '1\n2\n3\n4\n') + const atomApplication = buildAtomApplication() + + const [window] = await atomApplication.launch(parseCommandLine([filePath + ':2:2'])) + await focusWindow(window) + + const cursorPosition = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { + sendBackToMainProcess(textEditor.getCursorBufferPosition()) + }) + }) + + assert.deepEqual(cursorPosition, {row: 1, column: 1}) + }) + + it('removes all trailing whitespace and colons from the specified path', async () => { + let filePath = path.join(makeTempDir(), 'new-file') + fs.writeFileSync(filePath, '1\n2\n3\n4\n') + const atomApplication = buildAtomApplication() + + const [window] = await atomApplication.launch(parseCommandLine([filePath + ':: '])) + await focusWindow(window) + + const openedPath = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(textEditor => { + sendBackToMainProcess(textEditor.getPath()) + }) + }) + + assert.equal(openedPath, filePath) + }) + + it('opens an empty text editor when launched with a new file path', async () => { + // Choosing "Don't save" + mockElectronShowMessageBox({response: 2}) + + const atomApplication = buildAtomApplication() + const newFilePath = path.join(makeTempDir(), 'new-file') + const [window] = await atomApplication.launch(parseCommandLine([newFilePath])) + await focusWindow(window) + const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { + atom.workspace.observeTextEditors(editor => { + sendBackToMainProcess({editorTitle: editor.getTitle(), editorText: editor.getText()}) + }) + }) + assert.equal(editorTitle, path.basename(newFilePath)) + assert.equal(editorText, '') + assert.deepEqual(await getTreeViewRootDirectories(window), []) + }) }) - it('removes all trailing whitespace and colons from the specified path', async () => { - let filePath = path.join(makeTempDir(), 'new-file') - fs.writeFileSync(filePath, '1\n2\n3\n4\n') - const atomApplication = buildAtomApplication() + describe('when the --add option is specified', () => { + it('adds folders to existing windows when the --add option is used', async () => { + const dirAPath = makeTempDir('a') + const dirBPath = makeTempDir('b') + const dirCPath = makeTempDir('c') + const existingDirCFilePath = path.join(dirCPath, 'existing-file') + fs.writeFileSync(existingDirCFilePath, 'this is an existing file') - const [window] = await atomApplication.launch(parseCommandLine([filePath + ':: '])) - await focusWindow(window) + const atomApplication = buildAtomApplication() + const [window1] = await atomApplication.launch(parseCommandLine([dirAPath])) + await focusWindow(window1) - const openedPath = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { - atom.workspace.observeTextEditors(textEditor => { - sendBackToMainProcess(textEditor.getPath()) + await conditionPromise(async () => (await getTreeViewRootDirectories(window1)).length === 1) + assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath]) + + // When opening *files* with --add, reuses an existing window + let [reusedWindow] = await atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add'])) + assert.equal(reusedWindow, window1) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) + let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { + const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => { + sendBackToMainProcess(textEditor.getPath()) + subscription.dispose() + }) }) - }) + assert.equal(activeEditorPath, existingDirCFilePath) + assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath]) - assert.equal(openedPath, filePath) + // When opening *directories* with --add, reuses an existing window and adds the directory to the project + reusedWindow = (await atomApplication.launch(parseCommandLine([dirBPath, '-a'])))[0] + assert.equal(reusedWindow, window1) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) + + await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length === 2) + assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirBPath]) + }) }) if (process.platform === 'darwin' || process.platform === 'win32') { @@ -114,95 +287,15 @@ describe('AtomApplication', function () { }) } - it('reuses existing windows when opening paths, but not directories', async () => { - const dirAPath = makeTempDir("a") - const dirBPath = makeTempDir("b") - const dirCPath = makeTempDir("c") - const existingDirCFilePath = path.join(dirCPath, 'existing-file') - fs.writeFileSync(existingDirCFilePath, 'this is an existing file') - - const atomApplication = buildAtomApplication() - const [window1] = await atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')])) - await emitterEventPromise(window1, 'window:locations-opened') - await focusWindow(window1) - - let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { - atom.workspace.observeTextEditors(textEditor => { - sendBackToMainProcess(textEditor.getPath()) - }) - }) - assert.equal(activeEditorPath, path.join(dirAPath, 'new-file')) - - // Reuses the window when opening *files*, even if they're in a different directory - // Does not change the project paths when doing so. - const [reusedWindow] = await atomApplication.launch(parseCommandLine([existingDirCFilePath])) - assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.getAllWindows(), [window1]) - activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { - const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => { - sendBackToMainProcess(textEditor.getPath()) - subscription.dispose() - }) - }) - assert.equal(activeEditorPath, existingDirCFilePath) - assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath]) - - // Opens new windows when opening directories - const [window2] = await atomApplication.launch(parseCommandLine([dirCPath])) - await emitterEventPromise(window2, 'window:locations-opened') - assert.notEqual(window2, window1) - await focusWindow(window2) - assert.deepEqual(await getTreeViewRootDirectories(window2), [dirCPath]) - }) - - it('adds folders to existing windows when the --add option is used', async () => { - const dirAPath = makeTempDir("a") - const dirBPath = makeTempDir("b") - const dirCPath = makeTempDir("c") - const existingDirCFilePath = path.join(dirCPath, 'existing-file') - fs.writeFileSync(existingDirCFilePath, 'this is an existing file') - - const atomApplication = buildAtomApplication() - const [window1] = await atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')])) - await focusWindow(window1) - - let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { - atom.workspace.observeTextEditors(textEditor => { - sendBackToMainProcess(textEditor.getPath()) - }) - }) - assert.equal(activeEditorPath, path.join(dirAPath, 'new-file')) - - // When opening *files* with --add, reuses an existing window and adds - // parent directory to the project - let [reusedWindow] = await atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add'])) - assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.getAllWindows(), [window1]) - activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { - const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => { - sendBackToMainProcess(textEditor.getPath()) - subscription.dispose() - }) - }) - assert.equal(activeEditorPath, existingDirCFilePath) - assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath]) - - // When opening *directories* with add reuses an existing window and adds - // the directory to the project - reusedWindow = (await atomApplication.launch(parseCommandLine([dirBPath, '-a'])))[0] - assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.getAllWindows(), [window1]) - - await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length === 3) - assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath]) - }) - it('persists window state based on the project directories', async () => { + // Choosing "Don't save" + mockElectronShowMessageBox({response: 2}) + const tempDirPath = makeTempDir() const atomApplication = buildAtomApplication() const nonExistentFilePath = path.join(tempDirPath, 'new-file') - const [window1] = await atomApplication.launch(parseCommandLine([nonExistentFilePath])) + const [window1] = await atomApplication.launch(parseCommandLine([tempDirPath, nonExistentFilePath])) await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { atom.workspace.observeTextEditors(textEditor => { textEditor.insertText('Hello World!') @@ -213,7 +306,7 @@ describe('AtomApplication', function () { window1.close() await window1.closedPromise - // Restore unsaved state when opening the directory itself + // Restore unsaved state when opening the same project directory const [window2] = await atomApplication.launch(parseCommandLine([tempDirPath])) await window2.loadedPromise const window2Text = await evalInWebContents(window2.browserWindow.webContents, sendBackToMainProcess => { @@ -223,95 +316,6 @@ describe('AtomApplication', function () { sendBackToMainProcess(textEditor.getText()) }) assert.equal(window2Text, 'Hello World! How are you?') - await window2.prepareToUnload() - window2.close() - await window2.closedPromise - - // Restore unsaved state when opening a path to a non-existent file in the directory - const [window3] = await atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')])) - await window3.loadedPromise - const window3Texts = await evalInWebContents(window3.browserWindow.webContents, (sendBackToMainProcess, nonExistentFilePath) => { - sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText())) - }) - assert.include(window3Texts, 'Hello World! How are you?') - }) - - it('shows all directories in the tree view when multiple directory paths are passed to Atom', async () => { - const dirAPath = makeTempDir("a") - const dirBPath = makeTempDir("b") - const dirBSubdirPath = path.join(dirBPath, 'c') - fs.mkdirSync(dirBSubdirPath) - - const atomApplication = buildAtomApplication() - const [window1] = await atomApplication.launch(parseCommandLine([dirAPath, dirBPath])) - await focusWindow(window1) - - assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirBPath]) - }) - - it('reuses windows with no project paths to open directories', async () => { - const tempDirPath = makeTempDir() - const atomApplication = buildAtomApplication() - const [window1] = await atomApplication.launch(parseCommandLine([])) - await focusWindow(window1) - - const [reusedWindow] = await atomApplication.launch(parseCommandLine([tempDirPath])) - assert.equal(reusedWindow, window1) - await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length > 0) - }) - - it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async () => { - const atomApplication = buildAtomApplication() - const [window1] = await atomApplication.launch(parseCommandLine([])) - await focusWindow(window1) - const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { - sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) - }) - assert.equal(window1EditorTitle, 'untitled') - - const window2 = atomApplication.openWithOptions(parseCommandLine([])) - await focusWindow(window2) - const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { - sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) - }) - assert.equal(window2EditorTitle, 'untitled') - - assert.deepEqual(atomApplication.getAllWindows(), [window2, window1]) - }) - - it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async () => { - const configPath = path.join(process.env.ATOM_HOME, 'config.cson') - const config = season.readFileSync(configPath) - if (!config['*'].core) config['*'].core = {} - config['*'].core.openEmptyEditorOnStart = false - season.writeFileSync(configPath, config) - - const atomApplication = buildAtomApplication() - const [window1] = await atomApplication.launch(parseCommandLine([])) - await focusWindow(window1) - - // wait a bit just to make sure we don't pass due to querying the render process before it loads - await timeoutPromise(1000) - - const itemCount = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => { - sendBackToMainProcess(atom.workspace.getActivePane().getItems().length) - }) - assert.equal(itemCount, 0) - }) - - it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async () => { - const atomApplication = buildAtomApplication() - const newFilePath = path.join(makeTempDir(), 'new-file') - const [window] = await atomApplication.launch(parseCommandLine([newFilePath])) - await focusWindow(window) - const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => { - atom.workspace.observeTextEditors(editor => { - sendBackToMainProcess({editorTitle: editor.getTitle(), editorText: editor.getText()}) - }) - }) - assert.equal(editorTitle, path.basename(newFilePath)) - assert.equal(editorText, '') - assert.deepEqual(await getTreeViewRootDirectories(window), [path.dirname(newFilePath)]) }) it('adds a remote directory to the project when launched with a remote directory', async () => { @@ -343,38 +347,11 @@ describe('AtomApplication', function () { } }) - it('reopens any previously opened windows when launched with no path', async () => { - if (process.platform === 'win32') return; // Test is too flakey on Windows - - const tempDirPath1 = makeTempDir() - const tempDirPath2 = makeTempDir() - - const atomApplication1 = buildAtomApplication() - const [app1Window1] = await atomApplication1.launch(parseCommandLine([tempDirPath1])) - await emitterEventPromise(app1Window1, 'window:locations-opened') - const [app1Window2] = await atomApplication1.launch(parseCommandLine([tempDirPath2])) - await emitterEventPromise(app1Window2, 'window:locations-opened') - - await Promise.all([ - app1Window1.prepareToUnload(), - app1Window2.prepareToUnload() - ]) - - const atomApplication2 = buildAtomApplication() - const [app2Window1, app2Window2] = await atomApplication2.launch(parseCommandLine([])) - await Promise.all([ - emitterEventPromise(app2Window1, 'window:locations-opened'), - emitterEventPromise(app2Window2, 'window:locations-opened') - ]) - - assert.deepEqual(await getTreeViewRootDirectories(app2Window1), [tempDirPath1]) - assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2]) - }) - it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async () => { const atomApplication1 = buildAtomApplication() const [app1Window1] = await atomApplication1.launch(parseCommandLine([makeTempDir()])) await focusWindow(app1Window1) + const [app1Window2] = await atomApplication1.launch(parseCommandLine([makeTempDir()])) await focusWindow(app1Window2) @@ -424,15 +401,16 @@ describe('AtomApplication', function () { }) it('kills the specified pid after a newly-opened file in an existing window is closed', async () => { - const [window] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '101'])) - await focusWindow(window) - - const filePath1 = temp.openSync('test').path - const filePath2 = temp.openSync('test').path + const projectDir = makeTempDir('existing') + const filePath1 = path.join(projectDir, 'file-1') + const filePath2 = path.join(projectDir, 'file-2') fs.writeFileSync(filePath1, 'File 1') fs.writeFileSync(filePath2, 'File 2') - const [reusedWindow] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '102', filePath1, filePath2])) + const [window] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '101', projectDir])) + await focusWindow(window) + + const [reusedWindow] = await atomApplication.launch(parseCommandLine(['--add', '--wait', '--pid', '102', filePath1, filePath2])) assert.equal(reusedWindow, window) const activeEditorPath = await evalInWebContents(window.browserWindow.webContents, send => { @@ -471,8 +449,9 @@ describe('AtomApplication', function () { await focusWindow(window) const dirPath1 = makeTempDir() - const [reusedWindow] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '101', dirPath1])) + const [reusedWindow] = await atomApplication.launch(parseCommandLine(['--add', '--wait', '--pid', '101', dirPath1])) assert.equal(reusedWindow, window) + await conditionPromise(async () => (await getTreeViewRootDirectories(window)).length === 1) assert.deepEqual(await getTreeViewRootDirectories(window), [dirPath1]) assert.deepEqual(killedPids, []) @@ -498,7 +477,7 @@ describe('AtomApplication', function () { if (process.platform === 'linux' || process.platform === 'win32') { it('quits the application', async () => { const atomApplication = buildAtomApplication() - const [window] = await atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')])) + const [window] = await atomApplication.launch(parseCommandLine([path.join(makeTempDir('a'), 'file-a')])) await focusWindow(window) window.close() await window.closedPromise @@ -508,7 +487,7 @@ describe('AtomApplication', function () { } else if (process.platform === 'darwin') { it('leaves the application open', async () => { const atomApplication = buildAtomApplication() - const [window] = await atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')])) + const [window] = await atomApplication.launch(parseCommandLine([path.join(makeTempDir('a'), 'file-a')])) await focusWindow(window) window.close() await window.closedPromise @@ -524,24 +503,24 @@ describe('AtomApplication', function () { const dirB = makeTempDir() const atomApplication = buildAtomApplication() - const [window] = await atomApplication.launch(parseCommandLine([dirA, dirB])) - await emitterEventPromise(window, 'window:locations-opened') - await focusWindow(window) - assert.deepEqual(await getTreeViewRootDirectories(window), [dirA, dirB]) + const [window0] = await atomApplication.launch(parseCommandLine([dirA, dirB])) + await focusWindow(window0) + await conditionPromise(async () => (await getTreeViewRootDirectories(window0)).length === 2) + assert.deepEqual(await getTreeViewRootDirectories(window0), [dirA, dirB]) const saveStatePromise = emitterEventPromise(atomApplication, 'application:did-save-state') - await evalInWebContents(window.browserWindow.webContents, (sendBackToMainProcess) => { + await evalInWebContents(window0.browserWindow.webContents, (sendBackToMainProcess) => { atom.project.removePath(atom.project.getPaths()[0]) sendBackToMainProcess(null) }) - assert.deepEqual(await getTreeViewRootDirectories(window), [dirB]) + assert.deepEqual(await getTreeViewRootDirectories(window0), [dirB]) await saveStatePromise // Window state should be saved when the project folder is removed const atomApplication2 = buildAtomApplication() const [window2] = await atomApplication2.launch(parseCommandLine([])) - await emitterEventPromise(window2, 'window:locations-opened') await focusWindow(window2) + await conditionPromise(async () => (await getTreeViewRootDirectories(window2)).length === 1) assert.deepEqual(await getTreeViewRootDirectories(window2), [dirB]) }) }) @@ -562,11 +541,11 @@ describe('AtomApplication', function () { let reached = await evalInWebContents(windows[0].browserWindow.webContents, sendBackToMainProcess => { sendBackToMainProcess(global.reachedUrlMain) }) - assert.equal(reached, true); - windows[0].close(); + assert.isTrue(reached) + windows[0].close() }) - it('triggers /core/open/file in the correct window', async function() { + it('triggers /core/open/file in the correct window', async function () { const dirAPath = makeTempDir('a') const dirBPath = makeTempDir('b') @@ -594,12 +573,12 @@ describe('AtomApplication', function () { }) it('waits until all the windows have saved their state before quitting', async () => { - const dirAPath = makeTempDir("a") - const dirBPath = makeTempDir("b") + const dirAPath = makeTempDir('a') + const dirBPath = makeTempDir('b') const atomApplication = buildAtomApplication() - const [window1] = await atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')])) + const [window1] = await atomApplication.launch(parseCommandLine([dirAPath])) await focusWindow(window1) - const [window2] = await atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')])) + const [window2] = await atomApplication.launch(parseCommandLine([dirBPath])) await focusWindow(window2) electron.app.quit() await new Promise(process.nextTick) @@ -662,7 +641,7 @@ describe('AtomApplication', function () { function buildAtomApplication (params = {}) { const atomApplication = new AtomApplication(Object.assign({ resourcePath: ATOM_RESOURCE_PATH, - atomHomeDirPath: process.env.ATOM_HOME, + atomHomeDirPath: process.env.ATOM_HOME }, params)) atomApplicationsToDestroy.push(atomApplication) return atomApplication @@ -680,7 +659,7 @@ describe('AtomApplication', function () { electron.app.quit = function () { this.quit.callCount++ let defaultPrevented = false - this.emit('before-quit', {preventDefault() { defaultPrevented = true }}) + this.emit('before-quit', {preventDefault () { defaultPrevented = true }}) if (!defaultPrevented) didQuit = true } @@ -710,12 +689,15 @@ describe('AtomApplication', function () { resolve(result) } - webContents.executeJavaScript(dedent` + const js = dedent` function sendBackToMainProcess (result) { require('electron').ipcRenderer.send('${channelId}', result) } (${source})(sendBackToMainProcess, ${args.map(JSON.stringify).join(', ')}) - `) + ` + // console.log(`about to execute:\n${js}`) + + webContents.executeJavaScript(js) }) } @@ -728,6 +710,8 @@ describe('AtomApplication', function () { .from(treeView.element.querySelectorAll('.project-root > .header .name')) .map(element => element.dataset.path) ) + } else { + sendBackToMainProcess([]) } }) }) diff --git a/src/atom-environment.js b/src/atom-environment.js index 40b417269..30f19d87d 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -1,5 +1,6 @@ const crypto = require('crypto') const path = require('path') +const util = require('util') const {ipcRenderer} = require('electron') const _ = require('underscore-plus') @@ -44,6 +45,8 @@ const TextBuffer = require('text-buffer') const TextEditorRegistry = require('./text-editor-registry') const AutoUpdateManager = require('./auto-update-manager') +const stat = util.promisify(fs.stat) + let nextId = 0 // Essential: Atom global for dealing with packages, themes, menus, and the window. @@ -1359,42 +1362,56 @@ or use Pane::saveItemAs for programmatic saving.`) async openLocations (locations) { const needsProjectPaths = this.project && this.project.getPaths().length === 0 - const foldersToAddToProject = [] + const foldersToAddToProject = new Set() const fileLocationsToOpen = [] - function pushFolderToOpen (folder) { - if (!foldersToAddToProject.includes(folder)) { - foldersToAddToProject.push(folder) - } - } + // Asynchronously fetch stat information about each requested path to open. + const locationStats = await Promise.all( + locations.map(async location => { + const stats = location.pathToOpen ? await stat(location.pathToOpen).catch(() => null) : null + return {location, stats} + }), + ) - for (const location of locations) { + for (const {location, stats} of locationStats) { const {pathToOpen} = location - if (pathToOpen && (needsProjectPaths || location.forceAddToWindow)) { - if (fs.existsSync(pathToOpen)) { - pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) - } else if (fs.existsSync(path.dirname(pathToOpen))) { - pushFolderToOpen(this.project.getDirectoryForProjectPath(path.dirname(pathToOpen)).getPath()) - } else { - pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) - } + if (!pathToOpen) { + continue } - if (!fs.isDirectorySync(pathToOpen)) { - fileLocationsToOpen.push(location) + if (stats !== null) { + // Path exists + if (stats.isDirectory()) { + // Directory: add as a project folder + foldersToAddToProject.add(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) + } else if (stats.isFile()) { + // File: add as a file location + fileLocationsToOpen.push(location) + } + } else { + // Path does not exist + // Attempt to interpret as a URI from a non-default directory provider + const directory = this.project.getProvidedDirectoryForProjectPath(pathToOpen) + if (directory) { + // Found: add as a project folder + foldersToAddToProject.add(directory.getPath()) + } else { + // Not found: open as a new file + fileLocationsToOpen.push(location) + } } if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen) } let restoredState = false - if (foldersToAddToProject.length > 0) { - const state = await this.loadState(this.getStateKey(foldersToAddToProject)) + if (foldersToAddToProject.size > 0) { + const state = await this.loadState(this.getStateKey(Array.from(foldersToAddToProject))) // only restore state if this is the first path added to the project if (state && needsProjectPaths) { const files = fileLocationsToOpen.map((location) => location.pathToOpen) - await this.attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) + await this.attemptRestoreProjectStateForPaths(state, Array.from(foldersToAddToProject), files) restoredState = true } else { for (let folder of foldersToAddToProject) { diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index bd769eb2b..04f7fba5c 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -187,6 +187,8 @@ class AtomApplication extends EventEmitter { (options.urlsToOpen && options.urlsToOpen.length > 0)) { optionsForWindowsToOpen.push(options) shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') === 'always' + } else if (options.newWindow) { + shouldReopenPreviousWindows = false } else { shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') !== 'no' } @@ -206,7 +208,6 @@ class AtomApplication extends EventEmitter { openWithOptions (options) { const { - initialPaths, pathsToOpen, executedFrom, urlsToOpen, @@ -216,7 +217,6 @@ class AtomApplication extends EventEmitter { pidToKillWhenClosed, devMode, safeMode, - newWindow, logFile, profileStartup, timeout, @@ -250,11 +250,9 @@ class AtomApplication extends EventEmitter { }) } else if (pathsToOpen.length > 0) { return this.openPaths({ - initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, - newWindow, devMode, safeMode, profileStartup, @@ -267,9 +265,7 @@ class AtomApplication extends EventEmitter { } else { // Always open a editor window if this is the first instance of Atom. return this.openPath({ - initialPaths, pidToKillWhenClosed, - newWindow, devMode, safeMode, profileStartup, @@ -777,17 +773,14 @@ class AtomApplication extends EventEmitter { // 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 ({ - initialPaths, pathToOpen, pidToKillWhenClosed, - newWindow, devMode, safeMode, profileStartup, @@ -797,10 +790,8 @@ class AtomApplication extends EventEmitter { env } = {}) { return this.openPaths({ - initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, - newWindow, devMode, safeMode, profileStartup, @@ -816,18 +807,15 @@ class AtomApplication extends EventEmitter { // options - // :pathsToOpen - The array of file paths 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. // :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. openPaths ({ - initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, - newWindow, devMode, safeMode, windowDimensions, @@ -843,26 +831,21 @@ class AtomApplication extends EventEmitter { safeMode = Boolean(safeMode) clearWindowState = Boolean(clearWindowState) - const locationsToOpen = [] - for (let i = 0; i < pathsToOpen.length; i++) { - const location = this.parsePathToOpen(pathsToOpen[i], executedFrom, addToLastWindow) - location.forceAddToWindow = addToLastWindow - location.hasWaitSession = pidToKillWhenClosed != null - locationsToOpen.push(location) - pathsToOpen[i] = location.pathToOpen - } + const locationsToOpen = pathsToOpen.map(pathToOpen => { + return this.parsePathToOpen(pathToOpen, executedFrom, { + forceAddToWindow: addToLastWindow, + hasWaitSession: pidToKillWhenClosed != null + }) + }) + const normalizedPathsToOpen = locationsToOpen.map(location => location.pathToOpen).filter(Boolean) let existingWindow - if (!newWindow) { - existingWindow = this.windowForPaths(pathsToOpen, devMode) + if (addToLastWindow && normalizedPathsToOpen.length > 0) { + existingWindow = this.windowForPaths(normalizedPathsToOpen, devMode) if (!existingWindow) { let lastWindow = window || this.getLastFocusedWindow() if (lastWindow && lastWindow.devMode === devMode) { - if (addToLastWindow || ( - locationsToOpen.every(({stat}) => stat && stat.isFile()) || - (locationsToOpen.some(({stat}) => stat && stat.isDirectory()) && !lastWindow.hasProjectPath()))) { - existingWindow = lastWindow - } + existingWindow = lastWindow } } } @@ -895,7 +878,6 @@ class AtomApplication extends EventEmitter { if (!windowDimensions) windowDimensions = this.getDimensionsForNewWindow() openedWindow = new AtomWindow(this, this.fileRecoveryService, { - initialPaths, locationsToOpen, windowInitializationScript, resourcePath, @@ -916,7 +898,7 @@ class AtomApplication extends EventEmitter { } this.waitSessionsByWindow.get(openedWindow).push({ pid: pidToKillWhenClosed, - remainingPaths: new Set(pathsToOpen) + remainingPaths: new Set(normalizedPathsToOpen) }) } @@ -984,8 +966,7 @@ class AtomApplication extends EventEmitter { const states = await this.storageFolder.load('application.json') if (states) { return states.map(state => ({ - initialPaths: state.initialPaths, - pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)), + pathsToOpen: state.initialPaths, urlsToOpen: [], devMode: this.devMode, safeMode: this.safeMode @@ -1264,7 +1245,7 @@ class AtomApplication extends EventEmitter { } } - parsePathToOpen (pathToOpen, executedFrom = '') { + parsePathToOpen (pathToOpen, executedFrom, extra) { let initialColumn, initialLine if (!pathToOpen) { return {pathToOpen} @@ -1286,10 +1267,9 @@ class AtomApplication extends EventEmitter { } const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(pathToOpen))) - const stat = fs.statSyncNoException(normalizedPath) - if (stat || !url.parse(pathToOpen).protocol) pathToOpen = normalizedPath + if (!url.parse(pathToOpen).protocol) pathToOpen = normalizedPath - return {pathToOpen, stat, initialLine, initialColumn} + return Object.assign({pathToOpen, initialLine, initialColumn}, extra) } // Opens a native dialog to prompt the user for a path. diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js index a56679143..d7f480f16 100644 --- a/src/main-process/atom-window.js +++ b/src/main-process/atom-window.js @@ -23,9 +23,7 @@ class AtomWindow extends EventEmitter { this.devMode = settings.devMode this.resourcePath = settings.resourcePath - let {pathToOpen, locationsToOpen} = settings - if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}] - if (!locationsToOpen) locationsToOpen = [] + const locationsToOpen = settings.locationsToOpen || [] this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve }) this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve }) @@ -73,23 +71,7 @@ class AtomWindow extends EventEmitter { if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false - if (!this.loadSettings.initialPaths) { - this.loadSettings.initialPaths = [] - for (const {pathToOpen, stat} of locationsToOpen) { - if (!pathToOpen) continue - if (stat && stat.isDirectory()) { - this.loadSettings.initialPaths.push(pathToOpen) - } else { - const parentDirectory = path.dirname(pathToOpen) - if (stat && stat.isFile() || fs.existsSync(parentDirectory)) { - this.loadSettings.initialPaths.push(parentDirectory) - } else { - this.loadSettings.initialPaths.push(pathToOpen) - } - } - } - } - + this.loadSettings.initialPaths = locationsToOpen.map(location => location.pathToOpen).filter(Boolean) this.loadSettings.initialPaths.sort() // Only send to the first non-spec window created diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 5d7849eac..3aa019565 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -11,16 +11,20 @@ module.exports = function parseCommandLine (processArgs) { dedent`Atom Editor v${version} Usage: + atom atom [options] [path ...] atom file[:line[:column]] - One or more paths to files or folders may be specified. If there is an - existing Atom window that contains all of the given folders, the paths - will be opened in that window. Otherwise, they will be opened in a new - window. + If no arguments are given and no Atom windows are already open, restore all windows + from the previous editing session. Use "atom --new-window" to open a single empty + Atom window instead. - A file may be opened at the desired line (and optionally column) by - appending the numbers right after the file name, e.g. \`atom file:5:8\`. + If no arguments are given and at least one Atom window is open, open a new, empty + Atom window. + + One or more paths to files or folders may be specified. All paths will be opened + in a new Atom window. Each file may be opened at the desired line (and optionally + column) by appending the numbers after the file name, e.g. \`atom file:5:8\`. Paths that start with \`atom://\` will be interpreted as URLs. @@ -39,7 +43,7 @@ module.exports = function parseCommandLine (processArgs) { options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the main process in the foreground.') options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.') options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.') - options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.') + options.alias('n', 'new-window').boolean('n').describe('n', 'Launch an empty Atom window instead of restoring previous session.') options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.') options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.') options.boolean('safe').describe( diff --git a/src/project.js b/src/project.js index 8ccf60c0b..05a9e34c9 100644 --- a/src/project.js +++ b/src/project.js @@ -441,14 +441,20 @@ class Project extends Model { } } - getDirectoryForProjectPath (projectPath) { - let directory = null + getProvidedDirectoryForProjectPath (projectPath) { for (let provider of this.directoryProviders) { if (typeof provider.directoryForURISync === 'function') { - directory = provider.directoryForURISync(projectPath) - if (directory) break + const directory = provider.directoryForURISync(projectPath) + if (directory) { + return directory + } } } + return null + } + + getDirectoryForProjectPath (projectPath) { + let directory = this.getProvidedDirectoryForProjectPath(projectPath) if (directory == null) { directory = this.defaultDirectoryProvider.directoryForURISync(projectPath) }