From a4d2b4d21a058efdebb7a9c4d06afef16c356faf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 Dec 2013 02:25:47 -0800 Subject: [PATCH] Make TokenizedBuffer a telepath.Model subclass There's a bunch of improvised code to make this work right now because of the circularity of this refactoring. It will stabilize over time. --- spec/editor-spec.coffee | 11 +++-- spec/pane-spec.coffee | 20 ++++----- spec/tokenized-buffer-spec.coffee | 24 ++++------- src/atom.coffee | 7 +++- src/display-buffer.coffee | 11 +++-- src/editor-view.coffee | 2 +- src/project.coffee | 6 +-- src/text-buffer.coffee | 6 +-- src/tokenized-buffer.coffee | 70 +++++++++++++++---------------- 9 files changed, 75 insertions(+), 82 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 5f5e9a371..2193eaca9 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -2389,17 +2389,16 @@ describe "Editor", -> describe "when a better-matched grammar is added to syntax", -> it "switches to the better-matched grammar and re-tokenizes the buffer", -> - editor.destroy() jsGrammar = atom.syntax.selectGrammar('a.js') atom.syntax.removeGrammar(jsGrammar) - editor = atom.project.openSync('sample.js', autoIndent: false) - expect(editor.getGrammar()).toBe atom.syntax.nullGrammar - expect(editor.lineForScreenRow(0).tokens.length).toBe 1 + editor2 = atom.project.openSync('sample.js', autoIndent: false) + expect(editor2.getGrammar()).toBe atom.syntax.nullGrammar + expect(editor2.lineForScreenRow(0).tokens.length).toBe 1 atom.syntax.addGrammar(jsGrammar) - expect(editor.getGrammar()).toBe jsGrammar - expect(editor.lineForScreenRow(0).tokens.length).toBeGreaterThan 1 + expect(editor2.getGrammar()).toBe jsGrammar + expect(editor2.lineForScreenRow(0).tokens.length).toBeGreaterThan 1 describe "auto-indent", -> copyText = (text, {startColumn}={}) -> diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 029765227..75d3661dc 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -698,18 +698,16 @@ describe "Pane", -> expect(newPane.items.length).toBe pane.items.length - 1 it "focuses the pane after attach only if had focus when serialized", -> - reloadContainer = -> - containerState = container.serialize() - container.remove() - container = atom.deserializers.deserialize(containerState) - pane = container.getRoot() - container.attachToDom() - container.attachToDom() pane.focus() - reloadContainer() - expect(pane).toMatchSelector(':has(:focus)') + + container2 = atom.deserializers.deserialize(container.serialize()) + pane2 = container2.getRoot() + container2.attachToDom() + expect(pane2).toMatchSelector(':has(:focus)') $(document.activeElement).blur() - reloadContainer() - expect(pane).not.toMatchSelector(':has(:focus)') + container3 = atom.deserializers.deserialize(container.serialize()) + pane3 = container3.getRoot() + container3.attachToDom() + expect(pane3).not.toMatchSelector(':has(:focus)') diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 1fd9b98de..90adf05d4 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -18,22 +18,14 @@ describe "TokenizedBuffer", -> advanceClock() while tokenizedBuffer.firstInvalidRow()? changeHandler?.reset() - describe "@deserialize(state)", -> - it "constructs a tokenized buffer with the same buffer and tabLength setting", -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer1 = new TokenizedBuffer(buffer: buffer, tabLength: 4) - tokenizedBuffer2 = atom.deserializers.deserialize(tokenizedBuffer1.serialize()) - expect(tokenizedBuffer2.buffer).toBe tokenizedBuffer1.buffer - expect(tokenizedBuffer2.getTabLength()).toBe tokenizedBuffer1.getTabLength() - describe "when the buffer is destroyed", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = atom.create(new TokenizedBuffer({buffer})) startTokenizing(tokenizedBuffer) it "stops tokenization", -> - tokenizedBuffer.destroy() + tokenizedBuffer.state.destroy() spyOn(tokenizedBuffer, 'tokenizeNextChunk') advanceClock() expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() @@ -41,7 +33,7 @@ describe "TokenizedBuffer", -> describe "when the buffer contains soft-tabs", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = atom.create(new TokenizedBuffer({buffer})) startTokenizing(tokenizedBuffer) tokenizedBuffer.on "changed", changeHandler = jasmine.createSpy('changeHandler') @@ -321,7 +313,7 @@ describe "TokenizedBuffer", -> beforeEach -> atom.packages.activatePackage('language-coffee-script', sync: true) buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = atom.create(new TokenizedBuffer({buffer})) startTokenizing(tokenizedBuffer) afterEach -> @@ -355,7 +347,7 @@ describe "TokenizedBuffer", -> 'abc\uD835\uDF97def' //\uD835\uDF97xyz """ - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = atom.create(new TokenizedBuffer({buffer})) fullyTokenize(tokenizedBuffer) afterEach -> @@ -392,7 +384,7 @@ describe "TokenizedBuffer", -> buffer = atom.project.bufferForPathSync() buffer.setText "
<%= User.find(2).full_name %>
" - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = atom.create(new TokenizedBuffer({buffer})) tokenizedBuffer.setGrammar(atom.syntax.selectGrammar('test.erb')) fullyTokenize(tokenizedBuffer) @@ -411,7 +403,7 @@ describe "TokenizedBuffer", -> it "returns the correct token (regression)", -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = atom.create(new TokenizedBuffer({buffer})) fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenForPosition([1,0]).scopes).toEqual ["source.js"] expect(tokenizedBuffer.tokenForPosition([1,1]).scopes).toEqual ["source.js"] @@ -420,7 +412,7 @@ describe "TokenizedBuffer", -> describe ".bufferRangeForScopeAtPosition(selector, position)", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = atom.create(new TokenizedBuffer({buffer})) fullyTokenize(tokenizedBuffer) describe "when the selector does not match the token at the position", -> diff --git a/src/atom.coffee b/src/atom.coffee index 4cabe7372..9fdcd106b 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -80,6 +80,11 @@ class Atom setBodyPlatformClass: -> document.body.classList.add("platform-#{process.platform}") + # Public: Create a new telepath model. This won't be needed when Atom is itself + # a telepath model. + create: (model) -> + @site.createDocument(model) + # Public: Get the current window getCurrentWindow: -> remote.getCurrentWindow() @@ -420,7 +425,7 @@ class Atom serializedWindowState = @loadSerializedWindowState() doc = Document.deserialize(serializedWindowState) if serializedWindowState? doc ?= Document.create() - doc.registerModelClasses(require('./text-buffer'), require('./project')) + doc.registerModelClasses(require('./text-buffer'), require('./project'), require('./tokenized-buffer')) # TODO: Remove this when everything is using telepath models if @site? @site.setRootDocument(doc) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 938551e85..d2e60028b 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -27,17 +27,18 @@ class DisplayBuffer if optionsOrState instanceof telepath.Document @state = optionsOrState @id = @state.get('id') - @tokenizedBuffer = atom.deserializers.deserialize(@state.get('tokenizedBuffer')) + @tokenizedBuffer = @state.get('tokenizedBuffer') + @tokenizedBuffer.created() @buffer = @tokenizedBuffer.buffer else - {@buffer, softWrap, editorWidthInChars} = optionsOrState + {@buffer, softWrap, editorWidthInChars, tabLength} = optionsOrState @id = guid.create().toString() - @tokenizedBuffer = new TokenizedBuffer(optionsOrState) + @tokenizedBuffer = new TokenizedBuffer({tabLength, @buffer, project: atom.project}) @state = atom.site.createDocument deserializer: @constructor.name version: @constructor.version id: @id - tokenizedBuffer: @tokenizedBuffer.getState() + tokenizedBuffer: @tokenizedBuffer softWrap: softWrap ? atom.config.get('editor.softWrap') ? false editorWidthInChars: editorWidthInChars @@ -62,6 +63,7 @@ class DisplayBuffer @updateWrappedScreenLines() if @getSoftWrap() serialize: -> @state.clone() + getState: -> @state copy: -> @@ -654,6 +656,7 @@ class DisplayBuffer softWraps = 0 while wrapScreenColumn = @findWrapColumn(tokenizedLine.text) [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn) + newScreenLines.push(wrappedLine) softWraps++ newScreenLines.push(tokenizedLine) diff --git a/src/editor-view.coffee b/src/editor-view.coffee index 8f45492bf..a11c0fa93 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -106,7 +106,7 @@ class EditorView extends View @edit(editor) else if @mini @edit(new Editor - buffer: TextBuffer.createAsRoot() + buffer: atom.create(new TextBuffer) softWrap: false tabLength: 2 softTabs: true diff --git a/src/project.coffee b/src/project.coffee index 8b1758e28..4caa1a392 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -34,7 +34,7 @@ class Project extends telepath.Model # Private: Called by telepath. created: -> for buffer in @buffers.getValues() - buffer.once 'destroyed', (buffer) => @removeBuffer(buffer) + buffer.once 'destroyed', (buffer) => @removeBuffer(buffer) if @isAlive() @openers = [] @editors = [] @@ -64,7 +64,7 @@ class Project extends telepath.Model # Private: destroyed: -> editor.destroy() for editor in @getEditors() - buffer.release() for buffer in @getBuffers() + buffer.destroy() for buffer in @getBuffers() @destroyRepo() # Private: @@ -239,7 +239,7 @@ class Project extends telepath.Model # Private: addBufferAtIndex: (buffer, index, options={}) -> buffer = @buffers.insert(index, buffer) - buffer.once 'destroyed', => @removeBuffer(buffer) + buffer.once 'destroyed', => @removeBuffer(buffer) if @isAlive() @emit 'buffer-created', buffer buffer diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index a32913679..d84ee24da 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -81,12 +81,12 @@ class TextBuffer extends telepath.Model @emit 'changed', bufferChangeEvent @scheduleModifiedEvents() - destroy: -> - unless @destroyed + destroyed: -> + unless @alreadyDestroyed @cancelStoppedChangingTimeout() @file?.off() @unsubscribe() - @destroyed = true + @alreadyDestroyed = true @emit 'destroyed', this isRetained: -> @refcount > 0 diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 0b6ba31dd..a3146262c 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -1,16 +1,16 @@ _ = require 'underscore-plus' TokenizedLine = require './tokenized-line' -{Emitter, Subscriber} = require 'emissary' Token = require './token' -telepath = require 'telepath' -{Point, Range} = telepath +{Model, Point, Range} = require 'telepath' ### Internal ### module.exports = -class TokenizedBuffer - Emitter.includeInto(this) - Subscriber.includeInto(this) +class TokenizedBuffer extends Model + @properties + bufferPath: null + tabLength: -> atom.config.get('editor.tabLength') ? 2 + project: null grammar: null currentGrammarScore: null @@ -20,24 +20,19 @@ class TokenizedBuffer invalidRows: null visible: false - @acceptsDocuments: true - atom.deserializers.add(this) + constructor: -> + super + @deserializing = @state? - @deserialize: (state) -> - new this(state) + created: -> + if @deserializing + @deserializing = false + return this - constructor: (optionsOrState) -> - if optionsOrState instanceof telepath.Document - @state = optionsOrState - - # TODO: This needs to be made async, but should wait until the new Telepath changes land - @buffer = atom.project.bufferForPathSync(optionsOrState.get('bufferPath')) + if @buffer? and @buffer.isAlive() + @bufferPath = @buffer.getPath() else - { @buffer, tabLength } = optionsOrState - @state = atom.site.createDocument - deserializer: @constructor.name - bufferPath: @buffer.getPath() - tabLength: tabLength ? atom.config.get('editor.tabLength') ? 2 + @buffer = @project.bufferForPathSync(@bufferPath) @subscribe atom.syntax, 'grammar-added grammar-updated', (grammar) => if grammar.injectionSelector? @@ -48,12 +43,22 @@ class TokenizedBuffer @on 'grammar-changed grammar-updated', => @resetTokenizedLines() @subscribe @buffer, "changed", (e) => @handleBufferChange(e) - @subscribe @buffer, "path-changed", => @state.set('bufferPath', @buffer.getPath()) + @subscribe @buffer, "path-changed", => @bufferPath = @buffer.getPath() + + @subscribe @$tabLength.changes.onValue (tabLength) => + lastRow = @buffer.getLastRow() + @tokenizedLines = @buildPlaceholderTokenizedLinesForRows(0, lastRow) + @invalidateRow(0) + @emit "changed", { start: 0, end: lastRow, delta: 0 } @reloadGrammar() - serialize: -> @state.clone() - getState: -> @state + # TODO: Remove when everything is a telepath model + destroy: -> + @destroyed() + + destroyed: -> + @unsubscribe() setGrammar: (grammar, score) -> return if grammar is @grammar @@ -87,24 +92,19 @@ class TokenizedBuffer # # Returns a {Number}. getTabLength: -> - @state.get('tabLength') + @tabLength # Specifies the tab length. # # tabLength - A {Number} that defines the new tab length. - setTabLength: (tabLength) -> - @state.set('tabLength', tabLength) - lastRow = @buffer.getLastRow() - @tokenizedLines = @buildPlaceholderTokenizedLinesForRows(0, lastRow) - @invalidateRow(0) - @emit "changed", { start: 0, end: lastRow, delta: 0 } + setTabLength: (@tabLength) -> tokenizeInBackground: -> - return if not @visible or @pendingChunk or @destroyed + return if not @visible or @pendingChunk or not @isAlive() @pendingChunk = true _.defer => @pendingChunk = false - @tokenizeNextChunk() unless @destroyed + @tokenizeNextChunk() if @isAlive() and @buffer.isAlive() tokenizeNextChunk: -> rowsRemaining = @chunkSize @@ -248,10 +248,6 @@ class TokenizedBuffer endColumn = tokenizedLine.bufferColumnForToken(lastToken) + lastToken.bufferDelta new Range([position.row, startColumn], [position.row, endColumn]) - destroy: -> - @unsubscribe() - @destroyed = true - iterateTokensInBufferRange: (bufferRange, iterator) -> bufferRange = Range.fromObject(bufferRange) { start, end } = bufferRange