From 877b4dc3361ebe0f3169d4fef38d1048245f4ea1 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 12 Apr 2012 14:23:01 -0600 Subject: [PATCH] RootView constructor can be called with serialized view state data Move the saving of serialized root view data to window.coffee. The window.startup method looks for window state on the atom object and instantiates the root view with that if it is present. --- spec/app/root-view-spec.coffee | 169 ++++++++++++++++----------------- spec/app/window-spec.coffee | 11 +++ spec/spec-helper.coffee | 2 - src/app/atom.coffee | 3 +- src/app/root-view.coffee | 107 +++++++++++---------- src/app/window.coffee | 10 +- 6 files changed, 156 insertions(+), 146 deletions(-) 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)