From 425c07622153cf7eaac8b3038b02825731e7fe68 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 Dec 2013 17:32:34 -0800 Subject: [PATCH 1/4] Make Editor a telepath.Model subclass --- spec/editor-spec.coffee | 15 ----- spec/project-spec.coffee | 16 ----- spec/selection-spec.coffee | 2 +- src/atom.coffee | 8 ++- src/deserializer-manager.coffee | 4 +- src/editor-view.coffee | 4 +- src/editor.coffee | 100 ++++++++++++++------------------ src/pane.coffee | 4 +- src/project.coffee | 2 +- 9 files changed, 59 insertions(+), 96 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 2193eaca9..32590e628 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -21,21 +21,6 @@ describe "Editor", -> buffer = editor.buffer lineLengths = buffer.getLines().map (line) -> line.length - describe "@deserialize(state)", -> - it "restores selections and folds based on markers in the buffer", -> - editor.setSelectedBufferRange([[1, 2], [3, 4]]) - editor.addSelectionForBufferRange([[5, 6], [7, 5]], isReversed: true) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - - editor2 = atom.deserializers.deserialize(editor.serialize()) - - expect(editor2.id).toBe editor.id - expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() - expect(editor2.getSelectedBufferRanges()).toEqual [[[1, 2], [3, 4]], [[5, 6], [7, 5]]] - expect(editor2.getSelection(1).isReversed()).toBeTruthy() - expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - describe ".copy()", -> it "returns a different edit session with the same initial state", -> editor.setSelectedBufferRange([[1, 2], [3, 4]]) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index adc6e15d0..24cb8be07 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -59,22 +59,6 @@ describe "Project", -> editor.saveAs(tempFile) expect(atom.project.getPath()).toBe path.dirname(tempFile) - describe "when an edit session is deserialized", -> - it "emits an 'editor-created' event and stores the edit session", -> - handler = jasmine.createSpy('editorCreatedHandler') - atom.project.on 'editor-created', handler - - editor1 = atom.project.openSync("a") - expect(handler.callCount).toBe 1 - expect(atom.project.getEditors().length).toBe 1 - expect(atom.project.getEditors()[0]).toBe editor1 - - editor2 = atom.deserializers.deserialize(editor1.serialize()) - expect(handler.callCount).toBe 2 - expect(atom.project.getEditors().length).toBe 2 - expect(atom.project.getEditors()[0]).toBe editor1 - expect(atom.project.getEditors()[1]).toBe editor2 - describe "when an edit session is copied", -> it "emits an 'editor-created' event and stores the edit session", -> handler = jasmine.createSpy('editorCreatedHandler') diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index a9290c5e1..cc6f26597 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -5,7 +5,7 @@ describe "Selection", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - editor = new Editor(buffer: buffer, tabLength: 2) + editor = atom.create(new Editor(buffer: buffer, tabLength: 2)) selection = editor.getSelection() afterEach -> diff --git a/src/atom.coffee b/src/atom.coffee index 214efafde..16b073194 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -427,7 +427,13 @@ class Atom serializedWindowState = @loadSerializedWindowState() doc = Document.deserialize(serializedWindowState) if serializedWindowState? doc ?= Document.create() - doc.registerModelClasses(require('./text-buffer'), require('./project'), require('./tokenized-buffer'), require('./display-buffer')) + doc.registerModelClasses( + require('./text-buffer'), + require('./project'), + require('./tokenized-buffer'), + require('./display-buffer'), + require('./editor') + ) # TODO: Remove this when everything is using telepath models if @site? @site.setRootDocument(doc) diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index ccf842540..4f6b51588 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -1,4 +1,4 @@ -{Document} = require 'telepath' +{Document, Model} = require 'telepath' # Public: Manages the deserializers used for serialized state # @@ -25,6 +25,8 @@ class DeserializerManager deserialize: (state, params) -> return unless state? + return state if state instanceof Model + if deserializer = @get(state) stateVersion = state.get?('version') ? state.version return if deserializer.version? and deserializer.version isnt stateVersion diff --git a/src/editor-view.coffee b/src/editor-view.coffee index a11c0fa93..fb1540d61 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -105,12 +105,12 @@ class EditorView extends View if editor? @edit(editor) else if @mini - @edit(new Editor + @edit(atom.create(new Editor buffer: atom.create(new TextBuffer) softWrap: false tabLength: 2 softTabs: true - ) + )) else throw new Error("Must supply an Editor or mini: true") diff --git a/src/editor.coffee b/src/editor.coffee index 51cf56c98..3d7dc7f2d 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1,13 +1,11 @@ _ = require 'underscore-plus' path = require 'path' -telepath = require 'telepath' guid = require 'guid' -{Point, Range} = telepath +{Model, Point, Range} = require 'telepath' LanguageMode = require './language-mode' DisplayBuffer = require './display-buffer' Cursor = require './cursor' Selection = require './selection' -{Emitter, Subscriber} = require 'emissary' TextMateScopeSelector = require('first-mate').ScopeSelector # Public: The core model of Atom. @@ -36,65 +34,56 @@ TextMateScopeSelector = require('first-mate').ScopeSelector # FIXME: Describe how there are both local and remote cursors and selections and # why that is. module.exports = -class Editor - Emitter.includeInto(this) - Subscriber.includeInto(this) +class Editor extends Model - @acceptsDocuments: true + @properties + displayBuffer: null + softTabs: null + scrollTop: 0 + scrollLeft: 0 - atom.deserializers.add(this) - - @version: 6 - - @deserialize: (state) -> - new Editor(state) - - id: null + deserializing: false + callDisplayBufferCreatedHook: false + buffer: null languageMode: null - displayBuffer: null cursors: null remoteCursors: null selections: null remoteSelections: null suppressSelectionMerging: false - # Private: - constructor: (optionsOrState) -> + constructor: -> + super + @deserializing = @state? + + created: -> + if @deserializing + @deserializing = false + @callDisplayBufferCreatedHook = true + return + @cursors = [] @remoteCursors = [] @selections = [] @remoteSelections = [] - if optionsOrState instanceof telepath.Document - @state = optionsOrState - @id = @state.get('id') - displayBuffer = @state.get('displayBuffer') - displayBuffer.created() - @setBuffer(displayBuffer.buffer) - @setDisplayBuffer(displayBuffer) - for marker in @findMarkers(@getSelectionMarkerAttributes()) - marker.setAttributes(preserveFolds: true) - @addSelection(marker) - @setScrollTop(@state.get('scrollTop')) - @setScrollLeft(@state.get('scrollLeft')) - registerEditor = true - else - {buffer, displayBuffer, tabLength, softTabs, softWrap, suppressCursorCreation, initialLine} = optionsOrState - @id = guid.create().toString() - displayBuffer ?= atom.create(new DisplayBuffer({buffer, tabLength, softWrap})) - @state = atom.site.createDocument - deserializer: @constructor.name - version: @constructor.version - id: @id - displayBuffer: displayBuffer - softTabs: buffer.usesSoftTabs() ? softTabs ? atom.config.get('editor.softTabs') ? true - scrollTop: 0 - scrollLeft: 0 - @setBuffer(buffer) - @setDisplayBuffer(displayBuffer) - if @getCursors().length is 0 and not suppressCursorCreation - if initialLine - position = [initialLine, 0] + unless @displayBuffer? + @displayBuffer = new DisplayBuffer({@buffer, @tabLength, @softWrap}) + @softTabs = @buffer.usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true + + @displayBuffer.created() if @callDisplayBufferCreatedHook + @buffer = @displayBuffer.buffer + + for marker in @findMarkers(@getSelectionMarkerAttributes()) + marker.setAttributes(preserveFolds: true) + @addSelection(marker) + + @subscribeToBuffer() + @subscribeToDisplayBuffer() + + if @getCursors().length is 0 and not @suppressCursorCreation + if @initialLine + position = [@initialLine, 0] else position = _.last(@getRemoteCursors())?.getBufferPosition() ? [0, 0] @addCursorAtBufferPosition(position) @@ -108,10 +97,11 @@ class Editor when 'scrollLeft' @emit 'scroll-left-changed', newValue - atom.project.addEditor(this) if registerEditor + # Deprecated: The goal is a world where we don't call serialize explicitly + serialize: -> this # Private: - setBuffer: (@buffer) -> + subscribeToBuffer: -> @buffer.retain() @subscribe @buffer, "path-changed", => unless atom.project.getPath()? @@ -124,7 +114,7 @@ class Editor @preserveCursorPositionOnBufferReload() # Private: - setDisplayBuffer: (@displayBuffer) -> + subscribeToDisplayBuffer: -> @subscribe @displayBuffer, 'marker-created', @handleMarkerCreated @subscribe @displayBuffer, "changed", (e) => @emit 'screen-lines-changed', e @subscribe @displayBuffer, "markers-updated", => @mergeIntersectingSelections() @@ -148,18 +138,12 @@ class Editor @emit 'destroyed' @off() - # Private: - serialize: -> @state.clone() - - # Private: - getState: -> @state - # Private: Creates an {Editor} with the same initial state copy: -> tabLength = @getTabLength() displayBuffer = @displayBuffer.copy() softTabs = @getSoftTabs() - newEditor = new Editor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true}) + newEditor = @create(new Editor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true})) newEditor.setScrollTop(@getScrollTop()) newEditor.setScrollLeft(@getScrollLeft()) for marker in @findMarkers(editorId: @id) diff --git a/src/pane.coffee b/src/pane.coffee index bd17a0aa4..e7f85a8f3 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -34,7 +34,9 @@ class Pane extends View if args[0] instanceof telepath.Document @state = args[0] @items = _.compact @state.get('items').map (item) -> - atom.deserializers.deserialize(item) + item = atom.deserializers.deserialize(item) + item?.created?() + item else @items = args @state = atom.site.createDocument diff --git a/src/project.coffee b/src/project.coffee index 4caa1a392..27f3c0880 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -334,7 +334,7 @@ class Project extends telepath.Model # Private: buildEditorForBuffer: (buffer, editorOptions) -> - editor = new Editor(_.extend({buffer}, editorOptions)) + editor = @create(new Editor(_.extend({buffer}, editorOptions))) @addEditor(editor) editor From c4fc75215b5baf5a3d88db33f00f9023f2077106 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 Dec 2013 19:07:50 -0800 Subject: [PATCH 2/4] Reintroduce editor serialization specs I added some features to telepath that make it easier to test model objects. - First, you can now call replicate on any telepath document/model object. The entire underlying database will be replicated and you will be handed the equivalent of that object in the replicated world. This is easier than forcing you to attach the model to the window state and then call replicate there. However, remember that the entire window state is actually being replicated so any references the model has will also be replicated. - Second, you can also replicate orphaned objects. Most objects in these specs we're converting are orphans because there's no reason to attach them to the root document just to test them in isolation. --- package.json | 2 +- spec/editor-spec.coffee | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ca3f54c1b..519c01385 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "season": "0.14.0", "semver": "1.1.4", "space-pen": "2.0.1", - "telepath": "0.65.0", + "telepath": "0.66.0", "temp": "0.5.0", "underscore-plus": "0.3.0" }, diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 32590e628..85693921d 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -21,6 +21,25 @@ describe "Editor", -> buffer = editor.buffer lineLengths = buffer.getLines().map (line) -> line.length + describe "when the editor is deserialized", -> + it "restores selections and folds based on markers in the buffer", -> + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 5]], isReversed: true) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + # Simulate serialization with replicate + editor2 = editor.replicate() + # FIXME: The created hook is called manually on deserialization because globals aren't ready otherwise + editor2.created() + + expect(editor2.id).toBe editor.id + expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() + expect(editor2.getSelectedBufferRanges()).toEqual [[[1, 2], [3, 4]], [[5, 6], [7, 5]]] + expect(editor2.getSelection(1).isReversed()).toBeTruthy() + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + editor2.destroy() + describe ".copy()", -> it "returns a different edit session with the same initial state", -> editor.setSelectedBufferRange([[1, 2], [3, 4]]) From 8471ffed29c0a59d989fb35439359efc8abdd674 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 11 Dec 2013 11:10:06 -0800 Subject: [PATCH 3/4] Observe scroll* behaviors instead of the entire state document --- src/editor.coffee | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 3d7dc7f2d..3051b3014 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -89,13 +89,9 @@ class Editor extends Model @addCursorAtBufferPosition(position) @languageMode = new LanguageMode(this, @buffer.getExtension()) - @subscribe @state, 'changed', ({newValues}) => - for key, newValue of newValues - switch key - when 'scrollTop' - @emit 'scroll-top-changed', newValue - when 'scrollLeft' - @emit 'scroll-left-changed', newValue + + @subscribe @$scrollTop, 'value', (scrollTop) => @emit 'scroll-top-changed', scrollTop + @subscribe @$scrollLeft, 'value', (scrollLeft) => @emit 'scroll-left-changed', scrollLeft # Deprecated: The goal is a world where we don't call serialize explicitly serialize: -> this From 7710845cdc3dd070020870ecd4dffed41a0bb4c0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 11 Dec 2013 11:10:43 -0800 Subject: [PATCH 4/4] Use property accessors instead of accessing state directly --- src/editor.coffee | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 3051b3014..2201c3132 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -191,17 +191,17 @@ class Editor extends Model # Public: Controls visiblity based on the given Boolean. setVisible: (visible) -> @displayBuffer.setVisible(visible) - # Public: FIXME: I don't understand this. - setScrollTop: (scrollTop) -> @state.set('scrollTop', scrollTop) + # Deprecated: Use the ::scrollTop property directly + setScrollTop: (@scrollTop) -> @scrollTop - # Public: Returns the current `scrollTop` value - getScrollTop: -> @state.get('scrollTop') ? 0 + # Deprecated: Use the ::scrollTop property directly + getScrollTop: -> @scrollTop - # Public: FIXME: I don't understand this. - setScrollLeft: (scrollLeft) -> @state.set('scrollLeft', scrollLeft) + # Deprecated: Use the ::scrollLeft property directly + setScrollLeft: (@scrollLeft) -> @scrollLeft - # Public: Returns the current `scrollLeft` value - getScrollLeft: -> @state.get('scrollLeft') + # Deprecated: Use the ::scrollLeft property directly + getScrollLeft: -> @scrollLeft # Set the number of characters that can be displayed horizontally in the # editor that contains this edit session. @@ -213,12 +213,11 @@ class Editor extends Model # Public: Sets the column at which columsn will soft wrap getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() - # Public: Returns whether soft tabs are enabled or not. - getSoftTabs: -> @state.get('softTabs') + # Deprecated: Use the ::softTabs property directly. Indicates whether soft tabs are enabled. + getSoftTabs: -> @softTabs - # Public: Controls whether soft tabs are enabled or not. - setSoftTabs: (softTabs) -> - @state.set('softTabs', softTabs) + # Deprecated: Use the ::softTabs property directly. Indicates whether soft tabs are enabled. + setSoftTabs: (@softTabs) -> @softTabs # Public: Returns whether soft wrap is enabled or not. getSoftWrap: -> @displayBuffer.getSoftWrap()