diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index c54e4a62f..d245b4a16 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -11,55 +11,99 @@ describe "RootView", -> beforeEach -> path = require.resolve 'fixtures/dir/a' - rootView = new RootView({path}) + rootView = new RootView(pathToOpen: path) rootView.enableKeymap() project = rootView.project - describe "initialize", -> - describe "when called with a path that references a file", -> - it "creates a project for the file's parent directory and opens it in the editor", -> - expect(rootView.project.path).toBe fs.directory(path) - expect(rootView.activeEditor().buffer.getPath()).toBe path + describe "initialize(viewState)", -> + describe "when called with a pathToOpen", -> + describe "when pathToOpen references a file", -> + it "creates a project for the file's parent directory and opens it in the editor", -> + expect(rootView.project.path).toBe fs.directory(path) + expect(rootView.editors().length).toBe 1 + expect(rootView.editors()[0]).toHaveClass 'active' + expect(rootView.activeEditor().buffer.getPath()).toBe path - describe "when called with a path that references a directory", -> - it "creates a project for the directory and opens an empty buffer", -> - path = require.resolve 'fixtures/dir/' - rootView = new RootView({path}) + describe "when pathToOpen references a directory", -> + it "creates a project for the directory does not open an editor", -> + path = require.resolve 'fixtures/dir/' + rootView = new RootView(pathToOpen: path) - expect(rootView.project.path).toBe path - expect(rootView.activeEditor().buffer.path).toBeUndefined() + expect(rootView.project.path).toBe path + expect(rootView.editors().length).toBe 0 - describe "when not called with a path", -> + describe "when called with view state data returned from a previous call to RootView.prototype.serialize", -> + viewState = null + + describe "when the serialized RootView does not have a project, only an unsaved buffer", -> + buffer = null + + beforeEach -> + rootView = new RootView + editor1 = rootView.activeEditor() + buffer = editor1.buffer + editor1.splitRight() + viewState = rootView.serialize() + + it "constructs the view with the same panes", -> + rootView = new RootView(viewState) + expect(rootView.project).toBeUndefined() + expect(rootView.editors().length).toBe 2 + + describe "when the serialized RootView has a project", -> + beforeEach -> + editor1 = rootView.activeEditor() + editor2 = editor1.splitRight() + editor3 = editor2.splitRight() + editor4 = editor2.splitDown() + editor2.setBuffer(new Buffer(require.resolve 'fixtures/dir/b')) + editor3.setBuffer(new Buffer(require.resolve 'fixtures/sample.js')) + editor3.setCursorScreenPosition([2, 3]) + editor4.setBuffer(new Buffer(require.resolve 'fixtures/sample.txt')) + editor4.setCursorScreenPosition([0, 2]) + rootView.attachToDom() + editor2.focus() + viewState = rootView.serialize() + rootView.remove() + + it "constructs the view with the same project and panes", -> + rootView = new RootView(viewState) + rootView.attachToDom() + + expect(rootView.editors().length).toBe 4 + editor1 = rootView.panes.find('.row > .pane .editor:eq(0)').view() + editor3 = rootView.panes.find('.row > .pane .editor:eq(1)').view() + editor2 = rootView.panes.find('.row > .column > .pane .editor:eq(0)').view() + editor4 = rootView.panes.find('.row > .column > .pane .editor:eq(1)').view() + + expect(editor1.buffer.path).toBe require.resolve('fixtures/dir/a') + expect(editor2.buffer.path).toBe require.resolve('fixtures/dir/b') + expect(editor3.buffer.path).toBe require.resolve('fixtures/sample.js') + expect(editor3.getCursorScreenPosition()).toEqual [2, 3] + expect(editor4.buffer.path).toBe require.resolve('fixtures/sample.txt') + expect(editor4.getCursorScreenPosition()).toEqual [0, 2] + + # ensure adjustSplitPanes is called + expect(editor1.width()).toBeGreaterThan 0 + expect(editor2.width()).toBeGreaterThan 0 + expect(editor3.width()).toBeGreaterThan 0 + expect(editor4.width()).toBeGreaterThan 0 + + # ensure correct editor is focused again + expect(editor2.isFocused).toBeTruthy() + expect(editor1.isFocused).toBeFalsy() + expect(editor3.isFocused).toBeFalsy() + expect(editor4.isFocused).toBeFalsy() + + describe "when called with no state data", -> it "opens an empty buffer", -> rootView = new RootView expect(rootView.editors().length).toBe 1 expect(rootView.activeEditor().buffer.path).toBeUndefined() - describe "when there is a window state for the current window stored on the atom object", -> - it "sets the window state on the root view", -> - spyOn(RootView.prototype, 'setWindowState') - - describe "when the window is reloaded", -> - afterEach -> - delete atom.windowStatesByWindowNumber[$windowNumber] - - it "stores its window state on the atom object by window number, then reassigns it next time the root view is constructed", -> - rootView.activeEditor().splitLeft() - expectedWindowState = rootView.getWindowState() - - # simulate unload - $(window).trigger 'beforeunload' - expect(atom.windowStatesByWindowNumber[$windowNumber]).toEqual expectedWindowState - - # simulate reload - newRootView = new RootView - - expect(newRootView.getWindowState()).toEqual expectedWindowState - expect(newRootView.editors().length).toBe 2 - describe "focus", -> it "can receive focus if there is no active editor, but otherwise hands off focus to the active editor", -> - rootView = new RootView({path: require.resolve 'fixtures'}) + rootView = new RootView(pathToOpen: require.resolve 'fixtures') rootView.attachToDom() expect(rootView).toMatchSelector(':focus') @@ -71,59 +115,6 @@ describe "RootView", -> expect(rootView).not.toMatchSelector(':focus') expect(rootView.activeEditor().isFocused).toBeTruthy() - describe "windowState getter and setter", -> - [editor1, editor2, editor3, editor4, panesHtml] = [] - - beforeEach -> - editor1 = rootView.activeEditor() - editor2 = editor1.splitRight() - editor3 = editor2.splitRight() - editor4 = editor2.splitDown() - editor2.setBuffer(new Buffer(require.resolve 'fixtures/dir/b')) - editor3.setBuffer(new Buffer(require.resolve 'fixtures/sample.js')) - editor3.setCursorScreenPosition([2, 3]) - editor4.setBuffer(new Buffer(require.resolve 'fixtures/sample.txt')) - editor4.setCursorScreenPosition([0, 2]) - rootView.attachToDom() - editor2.focus() - panesHtml = rootView.panes.html() - - it "can reconstruct the split pane arrangement from the window state hash returned by getWindowState", -> - windowState = rootView.getWindowState() - - editor2.remove() - editor3.remove() - editor4.remove() - expect(rootView.panes.find('.editor').length).toBe 1 - - rootView.setWindowState(windowState) - expect(rootView.editors().length).toBe 4 - - editor1 = rootView.panes.find('.row > .pane .editor:eq(0)').view() - editor3 = rootView.panes.find('.row > .pane .editor:eq(1)').view() - editor2 = rootView.panes.find('.row > .column > .pane .editor:eq(0)').view() - editor4 = rootView.panes.find('.row > .column > .pane .editor:eq(1)').view() - - expect(editor1.buffer.path).toBe require.resolve('fixtures/dir/a') - expect(editor2.buffer.path).toBe require.resolve('fixtures/dir/b') - expect(editor3.buffer.path).toBe require.resolve('fixtures/sample.js') - expect(editor3.getCursorScreenPosition()).toEqual [2, 3] - expect(editor4.buffer.path).toBe require.resolve('fixtures/sample.txt') - expect(editor4.getCursorScreenPosition()).toEqual [0, 2] - - # ensure adjustSplitPanes is called - expect(editor1.width()).toBeGreaterThan 0 - expect(editor2.width()).toBeGreaterThan 0 - expect(editor3.width()).toBeGreaterThan 0 - expect(editor4.width()).toBeGreaterThan 0 - - # ensure correct editor is focused again - expect(editor2.isFocused).toBeTruthy() - expect(editor1.isFocused).toBeFalsy() - expect(editor3.isFocused).toBeFalsy() - expect(editor4.isFocused).toBeFalsy() - - describe "split editor panes", -> editor1 = null diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 242f714d5..5e49405e2 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -30,3 +30,14 @@ describe "Window", -> requireStylesheet('atom.css') expect($('head style').length).toBe 1 + + describe "before the window is unloaded", -> + afterEach -> + delete atom.rootViewStates[$windowNumber] + + it "saves the serialized state of the root view to the atom object so it can be rehydrated after reload", -> + expect(atom.rootViewStates[$windowNumber]).toBeUndefined() + $(window).trigger 'beforeunload' + expect(atom.rootViewStates[$windowNumber]).toEqual window.rootView.serialize() + + diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 5b326c2e3..1ec1e852f 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -9,11 +9,9 @@ window.showConsole() beforeEach -> window.resetTimeouts() - delete atom.windowStatesByWindowNumber[$windowNumber] afterEach -> $('#jasmine-content').empty() - $(window).off 'beforeunload' window.keymap.bindKeys '*', 'meta-w': 'close' $(document).on 'close', -> window.close() diff --git a/src/app/atom.coffee b/src/app/atom.coffee index ce48c3307..94b7ff726 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -10,12 +10,13 @@ class Atom keymap: null windows: null userConfigurationPath: null + rootViewStates: null constructor: (@loadPath, nativeMethods)-> @windows = [] @setUpKeymap() @userConfigurationPath = fs.absolute "~/.atom/atom.coffee" - @windowStatesByWindowNumber = {} + @rootViewStates = {} setUpKeymap: -> @keymap = new Keymap() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 6af95a3e9..73e47a285 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -18,10 +18,7 @@ class RootView extends View @div id: 'root-view', tabindex: -1, => @div id: 'panes', outlet: 'panes' - initialize: (params) -> - {path} = params - @createProject(path) - + initialize: ({ pathToOpen, projectPath, panesViewState }) -> @on 'toggle-file-finder', => @toggleFileFinder() @on 'show-console', => window.showConsole() @on 'find-in-file', => @commandPanel.show("/") @@ -34,18 +31,53 @@ class RootView extends View @commandPanel = new CommandPanel({rootView: this}) - $(window).on 'beforeunload', => - atom.windowStatesByWindowNumber[$windowNumber] = @getWindowState() + if projectPath? + @project = new Project(projectPath) + else if pathToOpen? + @project = new Project(fs.directory(pathToOpen)) + @open(pathToOpen) if fs.isFile(pathToOpen) + else if not panesViewState + @activeEditor().setBuffer(new Buffer) - if windowState = atom.windowStatesByWindowNumber[$windowNumber] - @setWindowState(windowState) + @deserializePanes(panesViewState) if panesViewState - createProject: (path) -> - if path - @project = new Project(fs.directory(path)) - @open(path) if fs.isFile(path) - else - @activeEditor().setBuffer(new Buffer()) + serialize: -> + projectPath: @project?.path + panesViewState: @serializePanes() + + serializePanes: (element = @panes.children(':eq(0)')) -> + if element.hasClass('pane') + ['editor', element.view().content.getEditorState()] + else if element.hasClass('row') + ['row'].concat element.children().toArray().map (elt) => + @serializePanes($(elt)) + else if element.hasClass('column') + ['column'].concat element.children().toArray().map (elt) => + @serializePanes($(elt)) + + deserializePanes: (panesViewState, parent) -> + adjustSplitPanes = false + unless parent + @panes.empty() + adjustSplitPanes = true + parent = @panes + + switch panesViewState.shift() + when 'editor' + editor = new Editor(panesViewState...) + parent.append(new Pane(editor)) + when 'row' + row = $$ -> @div class: 'row' + parent.append row + for child in panesViewState + @deserializePanes(child, row) + when 'column' + column = $$ -> @div class: 'column' + parent.append column + for child in panesViewState + @deserializePanes(child, column) + + @adjustSplitPanes() if adjustSplitPanes open: (path) -> @activeEditor().setBuffer(@project.open(path)) @@ -80,45 +112,16 @@ class RootView extends View if editor.length editor.view() else - editor = new Editor - pane = new Pane(editor) - @panes.append(pane) - editor.focus() - editor + editor = @panes.find('.editor:first') + if editor.length + editor.view() + else + editor = new Editor + pane = new Pane(editor) + @panes.append(pane) + editor.focus() + editor - getWindowState: (element = @panes.children(':eq(0)')) -> - if element.hasClass('pane') - ['editor', element.view().content.getEditorState()] - else if element.hasClass('row') - ['row'].concat element.children().toArray().map (elt) => - @getWindowState($(elt)) - else if element.hasClass('column') - ['column'].concat element.children().toArray().map (elt) => - @getWindowState($(elt)) - - setWindowState: (windowState, parent) -> - adjustSplitPanes = false - unless parent - @panes.empty() - adjustSplitPanes = true - parent = @panes - - switch windowState.shift() - when 'editor' - editor = new Editor(windowState...) - parent.append(new Pane(editor)) - when 'row' - row = $$ -> @div class: 'row' - parent.append row - for child in windowState - @setWindowState(child, row) - when 'column' - column = $$ -> @div class: 'column' - parent.append column - for child in windowState - @setWindowState(child, column) - - @adjustSplitPanes() if adjustSplitPanes addPane: (view, sibling, axis, side) -> unless sibling.parent().hasClass(axis) diff --git a/src/app/window.coffee b/src/app/window.coffee index e7f4bf14a..2de2205ba 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -24,6 +24,7 @@ windowAdditions = @attachRootView(path) @loadUserConfiguration() $(window).on 'close', => @close() + $(window).on 'beforeunload', => @saveRootViewState() $(window).focus() atom.windowOpened this @@ -31,12 +32,17 @@ windowAdditions = @rootView.remove() $(window).unbind('focus') $(window).unbind('blur') + $(window).off('before') atom.windowClosed this - attachRootView: (path) -> - @rootView = new RootView {path} + attachRootView: (pathToOpen) -> + rootViewState = atom.rootViewStates[$windowNumber] or { pathToOpen } + @rootView = new RootView(rootViewState) $(@rootViewParentSelector).append @rootView + saveRootViewState: -> + atom.rootViewStates[$windowNumber] = @rootView.serialize() + loadUserConfiguration: -> try require atom.userConfigurationPath if fs.exists(atom.userConfigurationPath)