Merge pull request #9426 from atom/ns-editor-marker-layers

Optimize markers via new layers API and treap-based index
This commit is contained in:
Nathan Sobo
2015-11-09 23:02:26 -07:00
22 changed files with 5930 additions and 4455 deletions

View File

@@ -52,7 +52,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "7.1.3",
"text-buffer": "^8.0.3",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -87,7 +87,7 @@
"dev-live-reload": "0.47.0",
"encoding-selector": "0.21.0",
"exception-reporting": "0.37.0",
"find-and-replace": "0.190.0",
"find-and-replace": "0.191.0",
"fuzzy-finder": "0.93.0",
"git-diff": "0.57.0",
"go-to-line": "0.30.0",
@@ -104,7 +104,7 @@
"package-generator": "0.41.0",
"release-notes": "0.53.0",
"settings-view": "0.231.0",
"snippets": "0.101.0",
"snippets": "0.101.1",
"spell-check": "0.62.0",
"status-bar": "0.80.0",
"styleguide": "0.45.0",

View File

@@ -0,0 +1,28 @@
exports.beforeEach = (fn) ->
global.beforeEach ->
result = fn()
if result instanceof Promise
waitsForPromise(-> result)
exports.afterEach = (fn) ->
global.afterEach ->
result = fn()
if result instanceof Promise
waitsForPromise(-> result)
['it', 'fit', 'ffit', 'fffit'].forEach (name) ->
exports[name] = (description, fn) ->
global[name] description, ->
result = fn()
if result instanceof Promise
waitsForPromise(-> result)
waitsForPromise = (fn) ->
promise = fn()
waitsFor 'spec promise to resolve', 30000, (done) ->
promise.then(
done,
(error) ->
jasmine.getEnv().currentSpec.fail(error)
done()
)

View File

@@ -243,23 +243,6 @@ describe "AtomEnvironment", ->
atomEnvironment.destroy()
describe "::destroy()", ->
it "unsubscribes from all buffers", ->
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document})
waitsForPromise ->
atomEnvironment.workspace.open("sample.js")
runs ->
buffer = atomEnvironment.workspace.getActivePaneItem().buffer
pane = atomEnvironment.workspace.getActivePane()
pane.splitRight(copyActiveItem: true)
expect(atomEnvironment.workspace.getTextEditors().length).toBe 2
atomEnvironment.destroy()
expect(buffer.getSubscriptionCount()).toBe 0
describe "::openLocations(locations) (called via IPC from browser process)", ->
beforeEach ->
spyOn(atom.workspace, 'open')

View File

@@ -418,11 +418,11 @@ describe "DisplayBuffer", ->
describe "when creating a fold where one already exists", ->
it "returns existing fold and does't create new fold", ->
fold = displayBuffer.createFold(0, 10)
expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1
expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
newFold = displayBuffer.createFold(0, 10)
expect(newFold).toBe fold
expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1
expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
describe "when a fold is created inside an existing folded region", ->
it "creates/destroys the fold, but does not trigger change event", ->
@@ -829,7 +829,6 @@ describe "DisplayBuffer", ->
it "unsubscribes all display buffer markers from their underlying buffer marker (regression)", ->
marker = displayBuffer.markBufferPosition([12, 2])
displayBuffer.destroy()
expect(marker.bufferMarker.getSubscriptionCount()).toBe 0
expect( -> buffer.insert([12, 2], '\n')).not.toThrow()
describe "markers", ->
@@ -879,7 +878,7 @@ describe "DisplayBuffer", ->
[markerChangedHandler, marker] = []
beforeEach ->
marker = displayBuffer.markScreenRange([[5, 4], [5, 10]], maintainHistory: true)
marker = displayBuffer.addMarkerLayer(maintainHistory: true).markScreenRange([[5, 4], [5, 10]])
marker.onDidChange markerChangedHandler = jasmine.createSpy("markerChangedHandler")
it "triggers the 'changed' event whenever the markers head's screen position changes in the buffer or on screen", ->
@@ -1016,7 +1015,7 @@ describe "DisplayBuffer", ->
expect(markerChangedHandler).not.toHaveBeenCalled()
it "updates markers before emitting buffer change events, but does not notify their observers until the change event", ->
marker2 = displayBuffer.markBufferRange([[8, 1], [8, 1]], maintainHistory: true)
marker2 = displayBuffer.addMarkerLayer(maintainHistory: true).markBufferRange([[8, 1], [8, 1]])
marker2.onDidChange marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler")
displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange()
@@ -1237,11 +1236,6 @@ describe "DisplayBuffer", ->
decoration.destroy()
expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined()
it "does not leak disposables", ->
disposablesSize = displayBuffer.disposables.disposables.size
decoration.destroy()
expect(displayBuffer.disposables.disposables.size).toBe(disposablesSize - 1)
describe "when a decoration is updated via Decoration::update()", ->
it "emits an 'updated' event containing the new and old params", ->
decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
@@ -1249,7 +1243,7 @@ describe "DisplayBuffer", ->
{oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
expect(oldProperties).toEqual decorationProperties
expect(newProperties).toEqual type: 'line-number', gutterName: 'line-number', class: 'two', id: decoration.id
expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
describe "::getDecorations(properties)", ->
it "returns decorations matching the given optional properties", ->

View File

@@ -1 +0,0 @@
undefined

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,13 @@ describe "TextEditorPresenter", ->
expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn)
waitsForStateToUpdate = (presenter, fn) ->
waitsFor "presenter state to update", 1000, (done) ->
fn?()
disposable = presenter.onDidUpdateState ->
disposable.dispose()
process.nextTick(done)
tiledContentContract = (stateFn) ->
it "contains states for tiles that are visible on screen", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
@@ -1147,55 +1154,62 @@ describe "TextEditorPresenter", ->
describe ".decorationClasses", ->
it "adds decoration classes to the relevant line state objects, both initially and when decorations change", ->
marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a')
presenter = buildPresenter()
marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter
runs ->
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
expect(marker1.isValid()).toBe false
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
runs ->
expect(marker1.isValid()).toBe false
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.undo()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> editor.undo()
runs ->
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
runs ->
expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> decoration1.destroy()
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> decoration1.destroy()
runs ->
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker2.destroy()
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> marker2.destroy()
runs ->
expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
it "honors the 'onlyEmpty' option on line decorations", ->
presenter = buildPresenter()
@@ -1206,11 +1220,12 @@ describe "TextEditorPresenter", ->
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker.clearTail()
waitsForStateToUpdate presenter, -> marker.clearTail()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
runs ->
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "honors the 'onlyNonEmpty' option on line decorations", ->
presenter = buildPresenter()
@@ -1221,40 +1236,49 @@ describe "TextEditorPresenter", ->
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
expectStateUpdate presenter, -> marker.clearTail()
waitsForStateToUpdate presenter, -> marker.clearTail()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
runs ->
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "honors the 'onlyHead' option on line decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 2]])
decoration = editor.decorateMarker(marker, type: 'line', class: 'a', onlyHead: true)
waitsForStateToUpdate presenter, ->
marker = editor.markBufferRange([[4, 0], [6, 2]])
editor.decorateMarker(marker, type: 'line', class: 'a', onlyHead: true)
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
runs ->
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "does not decorate the last line of a non-empty line decoration range if it ends at column 0", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 0]])
decoration = editor.decorateMarker(marker, type: 'line', class: 'a')
waitsForStateToUpdate presenter, ->
marker = editor.markBufferRange([[4, 0], [6, 0]])
editor.decorateMarker(marker, type: 'line', class: 'a')
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
runs ->
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "does not apply line decorations to mini editors", ->
editor.setMini(true)
presenter = buildPresenter(explicitHeight: 10)
marker = editor.markBufferRange([[0, 0], [0, 0]])
decoration = editor.decorateMarker(marker, type: 'line', class: 'a')
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.setMini(false)
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'a']
waitsForStateToUpdate presenter, ->
marker = editor.markBufferRange([[0, 0], [0, 0]])
decoration = editor.decorateMarker(marker, type: 'line', class: 'a')
expectStateUpdate presenter, -> editor.setMini(true)
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull()
runs ->
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.setMini(false)
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'a']
expectStateUpdate presenter, -> editor.setMini(true)
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull()
it "only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped", ->
editor.setText("a line that wraps, ok")
@@ -1268,9 +1292,12 @@ describe "TextEditorPresenter", ->
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
marker.setBufferRange([[0, 0], [0, Infinity]])
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
waitsForStateToUpdate presenter, ->
marker.setBufferRange([[0, 0], [0, Infinity]])
runs ->
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
describe ".cursors", ->
stateForCursor = (presenter, cursorIndex) ->
@@ -1740,41 +1767,51 @@ describe "TextEditorPresenter", ->
expectUndefinedStateForSelection(presenter, 1)
# moving into view
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
expectValues stateForSelectionInTile(presenter, 1, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
runs ->
expectValues stateForSelectionInTile(presenter, 1, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
# becoming empty
expectStateUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false)
expectUndefinedStateForSelection(presenter, 1)
waitsForStateToUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false)
runs ->
expectUndefinedStateForSelection(presenter, 1)
# becoming non-empty
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
expectValues stateForSelectionInTile(presenter, 1, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
runs ->
expectValues stateForSelectionInTile(presenter, 1, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving out of view
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
expectUndefinedStateForSelection(presenter, 1)
waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
runs ->
expectUndefinedStateForSelection(presenter, 1)
# adding
expectStateUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
expectValues stateForSelectionInTile(presenter, 2, 0), {
regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
waitsForStateToUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
runs ->
expectValues stateForSelectionInTile(presenter, 2, 0), {
regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving added selection
expectStateUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
expectValues stateForSelectionInTile(presenter, 2, 0), {
regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}]
}
waitsForStateToUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
# destroying
destroyedSelection = editor.getSelections()[2]
expectStateUpdate presenter, -> destroyedSelection.destroy()
expectUndefinedStateForHighlight(presenter, destroyedSelection.decoration)
destroyedSelection = null
runs ->
expectValues stateForSelectionInTile(presenter, 2, 0), {
regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}]
}
# destroying
destroyedSelection = editor.getSelections()[2]
waitsForStateToUpdate presenter, -> destroyedSelection.destroy()
runs ->
expectUndefinedStateForHighlight(presenter, destroyedSelection.decoration)
it "updates when highlight decorations' properties are updated", ->
marker = editor.markBufferPosition([2, 2])
@@ -1784,44 +1821,45 @@ describe "TextEditorPresenter", ->
expectUndefinedStateForHighlight(presenter, highlight)
expectStateUpdate presenter, ->
waitsForStateToUpdate presenter, ->
marker.setBufferRange([[2, 2], [2, 4]])
highlight.setProperties(class: 'b', type: 'highlight')
expectValues stateForHighlightInTile(presenter, highlight, 2), {class: 'b'}
runs ->
expectValues stateForHighlightInTile(presenter, highlight, 2), {class: 'b'}
it "increments the .flashCount and sets the .flashClass and .flashDuration when the highlight model flashes", ->
presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2)
marker = editor.markBufferPosition([2, 2])
highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a')
expectStateUpdate presenter, ->
waitsForStateToUpdate presenter, ->
marker.setBufferRange([[2, 2], [5, 2]])
highlight.flash('b', 500)
runs ->
expectValues stateForHighlightInTile(presenter, highlight, 2), {
flashClass: 'b'
flashDuration: 500
flashCount: 1
}
expectValues stateForHighlightInTile(presenter, highlight, 4), {
flashClass: 'b'
flashDuration: 500
flashCount: 1
}
expectValues stateForHighlightInTile(presenter, highlight, 2), {
flashClass: 'b'
flashDuration: 500
flashCount: 1
}
expectValues stateForHighlightInTile(presenter, highlight, 4), {
flashClass: 'b'
flashDuration: 500
flashCount: 1
}
expectStateUpdate presenter, -> highlight.flash('c', 600)
expectValues stateForHighlightInTile(presenter, highlight, 2), {
flashClass: 'c'
flashDuration: 600
flashCount: 2
}
expectValues stateForHighlightInTile(presenter, highlight, 4), {
flashClass: 'c'
flashDuration: 600
flashCount: 2
}
waitsForStateToUpdate presenter, -> highlight.flash('c', 600)
runs ->
expectValues stateForHighlightInTile(presenter, highlight, 2), {
flashClass: 'c'
flashDuration: 600
flashCount: 2
}
expectValues stateForHighlightInTile(presenter, highlight, 4), {
flashClass: 'c'
flashDuration: 600
flashCount: 2
}
describe ".overlays", ->
[item] = []
@@ -1829,7 +1867,7 @@ describe "TextEditorPresenter", ->
presenter.getState().content.overlays[decoration.id]
it "contains state for overlay decorations both initially and when their markers move", ->
marker = editor.markBufferPosition([2, 13], invalidate: 'touch', maintainHistory: true)
marker = editor.addMarkerLayer(maintainHistory: true).markBufferPosition([2, 13], invalidate: 'touch')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
presenter = buildPresenter(explicitHeight: 30, scrollTop: 20)
@@ -1840,40 +1878,47 @@ describe "TextEditorPresenter", ->
}
# Change range
expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]])
expectValues stateForOverlay(presenter, decoration), {
item: item
pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10}
}
waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]])
runs ->
expectValues stateForOverlay(presenter, decoration), {
item: item
pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10}
}
# Valid -> invalid
expectStateUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x')
expect(stateForOverlay(presenter, decoration)).toBeUndefined()
# Valid -> invalid
waitsForStateToUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x')
runs ->
expect(stateForOverlay(presenter, decoration)).toBeUndefined()
# Invalid -> valid
expectStateUpdate presenter, -> editor.undo()
expectValues stateForOverlay(presenter, decoration), {
item: item
pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10}
}
# Invalid -> valid
waitsForStateToUpdate presenter, -> editor.undo()
runs ->
expectValues stateForOverlay(presenter, decoration), {
item: item
pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10}
}
# Reverse direction
expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true)
expectValues stateForOverlay(presenter, decoration), {
item: item
pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10}
}
waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true)
runs ->
expectValues stateForOverlay(presenter, decoration), {
item: item
pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10}
}
# Destroy
decoration.destroy()
expect(stateForOverlay(presenter, decoration)).toBeUndefined()
waitsForStateToUpdate presenter, -> decoration.destroy()
runs ->
expect(stateForOverlay(presenter, decoration)).toBeUndefined()
# Add
decoration2 = editor.decorateMarker(marker, {type: 'overlay', item})
expectValues stateForOverlay(presenter, decoration2), {
item: item
pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10}
}
decoration2 = null
waitsForStateToUpdate presenter, -> decoration2 = editor.decorateMarker(marker, {type: 'overlay', item})
runs ->
expectValues stateForOverlay(presenter, decoration2), {
item: item
pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10}
}
it "updates when character widths changes", ->
scrollTop = 20
@@ -2308,11 +2353,11 @@ describe "TextEditorPresenter", ->
describe ".decorationClasses", ->
it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", ->
marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a')
presenter = buildPresenter()
marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b')
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
@@ -2320,85 +2365,92 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
expect(marker1.isValid()).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
runs ->
expect(marker1.isValid()).toBe false
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> editor.undo()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> editor.undo()
runs ->
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
runs ->
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> decoration1.destroy()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> decoration1.destroy()
runs ->
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker2.destroy()
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
waitsForStateToUpdate presenter, -> marker2.destroy()
runs ->
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
it "honors the 'onlyEmpty' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 1]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyEmpty: true)
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
expectStateUpdate presenter, -> marker.clearTail()
waitsForStateToUpdate presenter, -> marker.clearTail()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
runs ->
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "honors the 'onlyNonEmpty' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 2]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyNonEmpty: true)
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
expectStateUpdate presenter, -> marker.clearTail()
waitsForStateToUpdate presenter, -> marker.clearTail()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
runs ->
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "honors the 'onlyHead' option on line-number decorations", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 2]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyHead: true)
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
it "does not decorate the last line of a non-empty line-number decoration range if it ends at column 0", ->
presenter = buildPresenter()
marker = editor.markBufferRange([[4, 0], [6, 0]])
decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a')
presenter = buildPresenter()
expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
@@ -2430,9 +2482,10 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
marker.setBufferRange([[0, 0], [0, Infinity]])
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
waitsForStateToUpdate presenter, -> marker.setBufferRange([[0, 0], [0, Infinity]])
runs ->
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
describe ".foldable", ->
it "marks line numbers at the start of a foldable region as foldable", ->
@@ -2565,14 +2618,15 @@ describe "TextEditorPresenter", ->
it "updates when a decoration's marker is modified", ->
# This update will move decoration1 out of view.
expectStateUpdate presenter, ->
waitsForStateToUpdate presenter, ->
newRange = new Range([13, 0], [14, 0])
marker1.setBufferRange(newRange)
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id].top).toBeDefined()
expect(decorationState[decoration3.id]).toBeUndefined()
runs ->
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id].top).toBeDefined()
expect(decorationState[decoration3.id]).toBeUndefined()
describe "when a decoration's properties are modified", ->
it "updates the item applied to the decoration, if the decoration item is changed", ->
@@ -2584,12 +2638,14 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter'
class: 'test-class'
item: newItem
expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id].item).toBe newItem
expect(decorationState[decoration2.id].item).toBe decorationItem
expect(decorationState[decoration3.id]).toBeUndefined()
waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams)
runs ->
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id].item).toBe newItem
expect(decorationState[decoration2.id].item).toBe decorationItem
expect(decorationState[decoration3.id]).toBeUndefined()
it "updates the class applied to the decoration, if the decoration class is changed", ->
# This changes the decoration item. The visibility of the decoration should not be affected.
@@ -2598,12 +2654,13 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter'
class: 'new-test-class'
item: decorationItem
expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams)
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id].class).toBe 'new-test-class'
expect(decorationState[decoration2.id].class).toBe 'test-class'
expect(decorationState[decoration3.id]).toBeUndefined()
runs ->
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id].class).toBe 'new-test-class'
expect(decorationState[decoration2.id].class).toBe 'test-class'
expect(decorationState[decoration3.id]).toBeUndefined()
it "updates the type of the decoration, if the decoration type is changed", ->
# This changes the type of the decoration. This should remove the decoration from the gutter.
@@ -2612,12 +2669,13 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter' # This is an invalid/meaningless option here, but it shouldn't matter.
class: 'test-class'
item: decorationItem
expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams)
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id].top).toBeDefined()
expect(decorationState[decoration3.id]).toBeUndefined()
runs ->
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id].top).toBeDefined()
expect(decorationState[decoration3.id]).toBeUndefined()
it "updates the gutter the decoration targets, if the decoration gutterName is changed", ->
# This changes which gutter this decoration applies to. Since this gutter does not exist,
@@ -2627,24 +2685,25 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter-2'
class: 'new-test-class'
item: decorationItem
expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams)
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id].top).toBeDefined()
expect(decorationState[decoration3.id]).toBeUndefined()
runs ->
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id].top).toBeDefined()
expect(decorationState[decoration3.id]).toBeUndefined()
# After adding the targeted gutter, the decoration will appear in the state for that gutter,
# since it should be visible.
expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'})
newGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter-2')
expect(newGutterDecorationState[decoration1.id].top).toBeDefined()
expect(newGutterDecorationState[decoration2.id]).toBeUndefined()
expect(newGutterDecorationState[decoration3.id]).toBeUndefined()
oldGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(oldGutterDecorationState[decoration1.id]).toBeUndefined()
expect(oldGutterDecorationState[decoration2.id].top).toBeDefined()
expect(oldGutterDecorationState[decoration3.id]).toBeUndefined()
# After adding the targeted gutter, the decoration will appear in the state for that gutter,
# since it should be visible.
expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'})
newGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter-2')
expect(newGutterDecorationState[decoration1.id].top).toBeDefined()
expect(newGutterDecorationState[decoration2.id]).toBeUndefined()
expect(newGutterDecorationState[decoration3.id]).toBeUndefined()
oldGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(oldGutterDecorationState[decoration1.id]).toBeUndefined()
expect(oldGutterDecorationState[decoration2.id].top).toBeDefined()
expect(oldGutterDecorationState[decoration3.id]).toBeUndefined()
it "updates when the editor's mini state changes, and is cleared when the editor is mini", ->
expectStateUpdate presenter, -> editor.setMini(true)
@@ -2679,13 +2738,17 @@ describe "TextEditorPresenter", ->
class: 'test-class'
marker4 = editor.markBufferRange([[0, 0], [1, 0]])
decoration4 = editor.decorateMarker(marker4, decorationParams)
expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'})
decorationState = getContentForGutterWithName(presenter, 'test-gutter-2')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id]).toBeUndefined()
expect(decorationState[decoration3.id]).toBeUndefined()
expect(decorationState[decoration4.id].top).toBeDefined()
waitsForStateToUpdate presenter
runs ->
expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'})
decorationState = getContentForGutterWithName(presenter, 'test-gutter-2')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration2.id]).toBeUndefined()
expect(decorationState[decoration3.id]).toBeUndefined()
expect(decorationState[decoration4.id].top).toBeDefined()
it "updates when editor lines are folded", ->
oldDimensionsForDecoration1 =

View File

@@ -4589,7 +4589,10 @@ describe "TextEditor", ->
expect(buffer.getLineCount()).toBe(count - 1)
describe "when the line being deleted preceeds a fold, and the command is undone", ->
it "restores the line and preserves the fold", ->
# TODO: This seemed to have only been passing due to an accident in the text
# buffer implementation. Once we moved selections to a different layer it
# broke. We need to revisit our representation of folds and then reenable it.
xit "restores the line and preserves the fold", ->
editor.setCursorBufferPosition([4])
editor.foldCurrentRow()
expect(editor.isFoldedAtScreenRow(4)).toBeTruthy()
@@ -5057,11 +5060,12 @@ describe "TextEditor", ->
expect(coffeeEditor.lineTextForBufferRow(2)).toBe ""
describe ".destroy()", ->
it "destroys all markers associated with the edit session", ->
editor.foldAll()
expect(buffer.getMarkerCount()).toBeGreaterThan 0
it "destroys marker layers associated with the text editor", ->
selectionsMarkerLayerId = editor.selectionsMarkerLayer.id
foldsMarkerLayerId = editor.displayBuffer.foldsMarkerLayer.id
editor.destroy()
expect(buffer.getMarkerCount()).toBe 0
expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined()
expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined()
it "notifies ::onDidDestroy observers when the editor is destroyed", ->
destroyObserverCalled = false
@@ -5500,101 +5504,189 @@ describe "TextEditor", ->
it "does not allow a custom gutter with the 'line-number' name.", ->
expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow()
describe '::decorateMarker', ->
[marker] = []
describe '::decorateMarker', ->
[marker] = []
beforeEach ->
marker = editor.markBufferRange([[1, 0], [1, 0]])
beforeEach ->
marker = editor.markBufferRange([[1, 0], [1, 0]])
it 'reflects an added decoration when one of its custom gutters is decorated.', ->
gutter = editor.addGutter {'name': 'custom-gutter'}
decoration = gutter.decorateMarker marker, {class: 'custom-class'}
gutterDecorations = editor.getDecorations
type: 'gutter'
gutterName: 'custom-gutter'
class: 'custom-class'
expect(gutterDecorations.length).toBe 1
expect(gutterDecorations[0]).toBe decoration
it 'reflects an added decoration when one of its custom gutters is decorated.', ->
gutter = editor.addGutter {'name': 'custom-gutter'}
decoration = gutter.decorateMarker marker, {class: 'custom-class'}
gutterDecorations = editor.getDecorations
type: 'gutter'
gutterName: 'custom-gutter'
class: 'custom-class'
expect(gutterDecorations.length).toBe 1
expect(gutterDecorations[0]).toBe decoration
it 'reflects an added decoration when its line-number gutter is decorated.', ->
decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'}
gutterDecorations = editor.getDecorations
type: 'line-number'
gutterName: 'line-number'
class: 'test-class'
expect(gutterDecorations.length).toBe 1
expect(gutterDecorations[0]).toBe decoration
it 'reflects an added decoration when its line-number gutter is decorated.', ->
decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'}
gutterDecorations = editor.getDecorations
type: 'line-number'
gutterName: 'line-number'
class: 'test-class'
expect(gutterDecorations.length).toBe 1
expect(gutterDecorations[0]).toBe decoration
describe '::observeGutters', ->
[payloads, callback] = []
describe '::observeGutters', ->
[payloads, callback] = []
beforeEach ->
payloads = []
callback = (payload) ->
payloads.push(payload)
beforeEach ->
payloads = []
callback = (payload) ->
payloads.push(payload)
it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', ->
lineNumberGutter = editor.gutterWithName('line-number')
editor.observeGutters(callback)
expect(payloads).toEqual [lineNumberGutter]
gutter1 = editor.addGutter({name: 'test-gutter-1'})
expect(payloads).toEqual [lineNumberGutter, gutter1]
gutter2 = editor.addGutter({name: 'test-gutter-2'})
expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2]
it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', ->
lineNumberGutter = editor.gutterWithName('line-number')
editor.observeGutters(callback)
expect(payloads).toEqual [lineNumberGutter]
gutter1 = editor.addGutter({name: 'test-gutter-1'})
expect(payloads).toEqual [lineNumberGutter, gutter1]
gutter2 = editor.addGutter({name: 'test-gutter-2'})
expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2]
it 'does not call the callback when a gutter is removed.', ->
gutter = editor.addGutter({name: 'test-gutter'})
editor.observeGutters(callback)
payloads = []
gutter.destroy()
expect(payloads).toEqual []
it 'does not call the callback when a gutter is removed.', ->
gutter = editor.addGutter({name: 'test-gutter'})
editor.observeGutters(callback)
payloads = []
gutter.destroy()
expect(payloads).toEqual []
it 'does not call the callback after the subscription has been disposed.', ->
subscription = editor.observeGutters(callback)
payloads = []
subscription.dispose()
editor.addGutter({name: 'test-gutter'})
expect(payloads).toEqual []
it 'does not call the callback after the subscription has been disposed.', ->
subscription = editor.observeGutters(callback)
payloads = []
subscription.dispose()
editor.addGutter({name: 'test-gutter'})
expect(payloads).toEqual []
describe '::onDidAddGutter', ->
[payloads, callback] = []
describe '::onDidAddGutter', ->
[payloads, callback] = []
beforeEach ->
payloads = []
callback = (payload) ->
payloads.push(payload)
beforeEach ->
payloads = []
callback = (payload) ->
payloads.push(payload)
it 'calls the callback with each newly-added gutter, but not with existing gutters.', ->
editor.onDidAddGutter(callback)
expect(payloads).toEqual []
gutter = editor.addGutter({name: 'test-gutter'})
expect(payloads).toEqual [gutter]
it 'calls the callback with each newly-added gutter, but not with existing gutters.', ->
editor.onDidAddGutter(callback)
expect(payloads).toEqual []
gutter = editor.addGutter({name: 'test-gutter'})
expect(payloads).toEqual [gutter]
it 'does not call the callback after the subscription has been disposed.', ->
subscription = editor.onDidAddGutter(callback)
payloads = []
subscription.dispose()
editor.addGutter({name: 'test-gutter'})
expect(payloads).toEqual []
it 'does not call the callback after the subscription has been disposed.', ->
subscription = editor.onDidAddGutter(callback)
payloads = []
subscription.dispose()
editor.addGutter({name: 'test-gutter'})
expect(payloads).toEqual []
describe '::onDidRemoveGutter', ->
[payloads, callback] = []
describe '::onDidRemoveGutter', ->
[payloads, callback] = []
beforeEach ->
payloads = []
callback = (payload) ->
payloads.push(payload)
beforeEach ->
payloads = []
callback = (payload) ->
payloads.push(payload)
it 'calls the callback when a gutter is removed.', ->
gutter = editor.addGutter({name: 'test-gutter'})
editor.onDidRemoveGutter(callback)
expect(payloads).toEqual []
gutter.destroy()
expect(payloads).toEqual ['test-gutter']
it 'calls the callback when a gutter is removed.', ->
gutter = editor.addGutter({name: 'test-gutter'})
editor.onDidRemoveGutter(callback)
expect(payloads).toEqual []
gutter.destroy()
expect(payloads).toEqual ['test-gutter']
it 'does not call the callback after the subscription has been disposed.', ->
gutter = editor.addGutter({name: 'test-gutter'})
subscription = editor.onDidRemoveGutter(callback)
subscription.dispose()
gutter.destroy()
expect(payloads).toEqual []
it 'does not call the callback after the subscription has been disposed.', ->
gutter = editor.addGutter({name: 'test-gutter'})
subscription = editor.onDidRemoveGutter(callback)
subscription.dispose()
gutter.destroy()
expect(payloads).toEqual []
describe "decorations", ->
describe "::decorateMarker", ->
it "includes the decoration in the object returned from ::decorationsStateForScreenRowRange", ->
marker = editor.markBufferRange([[2, 4], [6, 8]])
decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo')
expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual {
properties: {type: 'highlight', class: 'foo'}
screenRange: marker.getScreenRange(),
rangeIsReversed: false
}
describe "::decorateMarkerLayer", ->
it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange", ->
layer1 = editor.getBuffer().addMarkerLayer()
marker1 = layer1.markRange([[2, 4], [6, 8]])
marker2 = layer1.markRange([[11, 0], [11, 12]])
layer2 = editor.getBuffer().addMarkerLayer()
marker3 = layer2.markRange([[8, 0], [9, 0]])
layer1Decoration1 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'foo')
layer1Decoration2 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'bar')
layer2Decoration = editor.decorateMarkerLayer(layer2, type: 'highlight', class: 'baz')
decorationState = editor.decorationsStateForScreenRowRange(0, 13)
expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
rangeIsReversed: false
}
layer1Decoration1.destroy()
decorationState = editor.decorationsStateForScreenRowRange(0, 12)
expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toBeUndefined()
expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toBeUndefined()
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
rangeIsReversed: false
}
layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'})
decorationState = editor.decorationsStateForScreenRowRange(0, 12)
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'quux'},
screenRange: marker1.getRange(),
rangeIsReversed: false
}
layer1Decoration2.setPropertiesForMarker(marker1, null)
decorationState = editor.decorationsStateForScreenRowRange(0, 12)
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
rangeIsReversed: false
}

View File

@@ -209,3 +209,21 @@ describe "ViewRegistry", ->
window.dispatchEvent(new UIEvent('resize'))
expect(events).toEqual ['poll 1', 'poll 2']
describe "::getNextUpdatePromise()", ->
it "returns a promise that resolves at the end of the next update cycle", ->
updateCalled = false
readCalled = false
pollCalled = false
waitsFor 'getNextUpdatePromise to resolve', (done) ->
registry.getNextUpdatePromise().then ->
expect(updateCalled).toBe true
expect(readCalled).toBe true
expect(pollCalled).toBe true
done()
registry.updateDocument -> updateCalled = true
registry.readDocument -> readCalled = true
registry.pollDocument -> pollCalled = true
registry.pollAfterNextUpdate()

View File

@@ -7,7 +7,7 @@ Model = require './model'
# where text can be inserted.
#
# Cursors belong to {TextEditor}s and have some metadata attached in the form
# of a {Marker}.
# of a {TextEditorMarker}.
module.exports =
class Cursor extends Model
screenPosition: null
@@ -127,7 +127,7 @@ class Cursor extends Model
Section: Cursor Position Details
###
# Public: Returns the underlying {Marker} for the cursor.
# Public: Returns the underlying {TextEditorMarker} for the cursor.
# Useful with overlay {Decoration}s.
getMarker: -> @marker

View File

@@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
decorationParams.gutterName = 'line-number'
decorationParams
# Essential: Represents a decoration that follows a {Marker}. A decoration is
# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is
# basically a visual representation of a marker. It allows you to add CSS
# classes to line numbers in the gutter, lines, and add selection-line regions
# around marked ranges of text.
@@ -25,7 +25,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
# ```
#
# Best practice for destroying the decoration is by destroying the {Marker}.
# Best practice for destroying the decoration is by destroying the {TextEditorMarker}.
#
# ```coffee
# marker.destroy()
@@ -67,20 +67,19 @@ class Decoration
@emitter = new Emitter
@id = nextId()
@setProperties properties
@properties.id = @id
@flashQueue = null
@destroyed = false
@markerDestroyDisposable = @marker.onDidDestroy => @destroy()
# Essential: Destroy this marker.
#
# If you own the marker, you should use {Marker::destroy} which will destroy
# If you own the marker, you should use {TextEditorMarker::destroy} which will destroy
# this decoration.
destroy: ->
return if @destroyed
@markerDestroyDisposable.dispose()
@markerDestroyDisposable = null
@destroyed = true
@displayBuffer.didDestroyDecoration(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
@@ -150,9 +149,9 @@ class Decoration
return if @destroyed
oldProperties = @properties
@properties = translateDecorationParamsOldToNew(newProperties)
@properties.id = @id
if newProperties.type?
@displayBuffer.decorationDidChangeType(this)
@displayBuffer.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
###
@@ -165,15 +164,10 @@ class Decoration
return false if @properties[key] isnt value
true
onDidFlash: (callback) ->
@emitter.on 'did-flash', callback
flash: (klass, duration=500) ->
flashObject = {class: klass, duration}
@flashQueue ?= []
@flashQueue.push(flashObject)
@properties.flashCount ?= 0
@properties.flashCount++
@properties.flashClass = klass
@properties.flashDuration = duration
@displayBuffer.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-flash'
consumeNextFlash: ->
return @flashQueue.shift() if @flashQueue?.length > 0
null

View File

@@ -7,7 +7,8 @@ Fold = require './fold'
Model = require './model'
Token = require './token'
Decoration = require './decoration'
Marker = require './marker'
LayerDecoration = require './layer-decoration'
TextEditorMarkerLayer = require './text-editor-marker-layer'
class BufferToScreenConversionError extends Error
constructor: (@message, @metadata) ->
@@ -25,9 +26,12 @@ class DisplayBuffer extends Model
defaultCharWidth: null
height: null
width: null
didUpdateDecorationsEventScheduled: false
updatedSynchronously: false
@deserialize: (state, atomEnvironment) ->
state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
state.foldsMarkerLayer = state.tokenizedBuffer.buffer.getMarkerLayer(state.foldsMarkerLayerId)
state.config = atomEnvironment.config
state.assert = atomEnvironment.assert
state.grammarRegistry = atomEnvironment.grammars
@@ -38,8 +42,8 @@ class DisplayBuffer extends Model
super
{
tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles,
@largeFileMode, @config, @assert, @grammarRegistry, @packageManager
tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer,
ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager
} = params
@emitter = new Emitter
@@ -51,17 +55,22 @@ class DisplayBuffer extends Model
})
@buffer = @tokenizedBuffer.buffer
@charWidthsByScope = {}
@markers = {}
@defaultMarkerLayer = new TextEditorMarkerLayer(this, @buffer.getDefaultMarkerLayer(), true)
@customMarkerLayersById = {}
@foldsByMarkerId = {}
@decorationsById = {}
@decorationsByMarkerId = {}
@overlayDecorationsById = {}
@layerDecorationsByMarkerLayerId = {}
@decorationCountsByLayerId = {}
@layerUpdateDisposablesByLayerId = {}
@disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
@disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange
@disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated
@disposables.add @buffer.onDidUpdateMarkers => @emitter.emit 'did-update-markers'
@foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id})
folds = (new Fold(this, marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()))
@disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker
@foldsMarkerLayer ?= @buffer.addMarkerLayer()
folds = (new Fold(this, marker) for marker in @foldsMarkerLayer.getMarkers())
@updateAllScreenLines()
@decorateFold(fold) for fold in folds
@@ -107,17 +116,15 @@ class DisplayBuffer extends Model
editorWidthInChars: @editorWidthInChars
tokenizedBuffer: @tokenizedBuffer.serialize()
largeFileMode: @largeFileMode
foldsMarkerLayerId: @foldsMarkerLayer.id
copy: ->
newDisplayBuffer = new DisplayBuffer({
foldsMarkerLayer = @foldsMarkerLayer.copy()
new DisplayBuffer({
@buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert,
@grammarRegistry, @packageManager
@grammarRegistry, @packageManager, foldsMarkerLayer
})
for marker in @findMarkers(displayBufferId: @id)
marker.copy(displayBufferId: newDisplayBuffer.id)
newDisplayBuffer
updateAllScreenLines: ->
@maxLineLength = 0
@screenLines = []
@@ -158,6 +165,9 @@ class DisplayBuffer extends Model
onDidUpdateMarkers: (callback) ->
@emitter.on 'did-update-markers', callback
onDidUpdateDecorations: (callback) ->
@emitter.on 'did-update-decorations', callback
emitDidChange: (eventProperties, refreshMarkers=true) ->
@emitter.emit 'did-change', eventProperties
if refreshMarkers
@@ -177,6 +187,8 @@ class DisplayBuffer extends Model
# visible - A {Boolean} indicating of the tokenized buffer is shown
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
setUpdatedSynchronously: (@updatedSynchronously) ->
getVerticalScrollMargin: ->
maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2)
Math.min(@verticalScrollMargin, maxScrollMargin)
@@ -386,10 +398,14 @@ class DisplayBuffer extends Model
# Returns the new {Fold}.
createFold: (startRow, endRow) ->
unless @largeFileMode
foldMarker =
@findFoldMarker({startRow, endRow}) ?
@buffer.markRange([[startRow, 0], [endRow, Infinity]], @getFoldMarkerAttributes())
@foldForMarker(foldMarker)
if foldMarker = @findFoldMarker({startRow, endRow})
@foldForMarker(foldMarker)
else
foldMarker = @foldsMarkerLayer.markRange([[startRow, 0], [endRow, Infinity]])
fold = new Fold(this, foldMarker)
fold.updateDisplayBuffer()
@decorateFold(fold)
fold
isFoldedAtBufferRow: (bufferRow) ->
@largestFoldContainingBufferRow(bufferRow)?
@@ -769,52 +785,68 @@ class DisplayBuffer extends Model
decorationsByMarkerId[marker.id] = decorations
decorationsByMarkerId
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsState = {}
for layerId of @decorationCountsByLayerId
layer = @getMarkerLayer(layerId)
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
screenRange = marker.getScreenRange()
rangeIsReversed = marker.isReversed()
if decorations = @decorationsByMarkerId[marker.id]
for decoration in decorations
decorationsState[decoration.id] = {
properties: decoration.properties
screenRange, rangeIsReversed
}
if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
for layerDecoration in layerDecorations
decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
screenRange, rangeIsReversed
}
decorationsState
decorateMarker: (marker, decorationParams) ->
marker = @getMarker(marker.id)
marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id)
decoration = new Decoration(marker, this, decorationParams)
decorationDestroyedDisposable = decoration.onDidDestroy =>
@removeDecoration(decoration)
@disposables.remove(decorationDestroyedDisposable)
@disposables.add(decorationDestroyedDisposable)
@decorationsByMarkerId[marker.id] ?= []
@decorationsByMarkerId[marker.id].push(decoration)
@overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
@decorationsById[decoration.id] = decoration
@observeDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
@emitter.emit 'did-add-decoration', decoration
decoration
removeDecoration: (decoration) ->
{marker} = decoration
return unless decorations = @decorationsByMarkerId[marker.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @decorationsById[decoration.id]
@emitter.emit 'did-remove-decoration', decoration
delete @decorationsByMarkerId[marker.id] if decorations.length is 0
delete @overlayDecorationsById[decoration.id]
decorateMarkerLayer: (markerLayer, decorationParams) ->
decoration = new LayerDecoration(markerLayer, this, decorationParams)
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
@observeDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
decoration
decorationsForMarkerId: (markerId) ->
@decorationsByMarkerId[markerId]
# Retrieves a {Marker} based on its id.
# Retrieves a {TextEditorMarker} based on its id.
#
# id - A {Number} representing a marker id
#
# Returns the {Marker} (if it exists).
# Returns the {TextEditorMarker} (if it exists).
getMarker: (id) ->
unless marker = @markers[id]
if bufferMarker = @buffer.getMarker(id)
marker = new Marker({bufferMarker, displayBuffer: this})
@markers[id] = marker
marker
@defaultMarkerLayer.getMarker(id)
# Retrieves the active markers in the buffer.
#
# Returns an {Array} of existing {Marker}s.
# Returns an {Array} of existing {TextEditorMarker}s.
getMarkers: ->
@buffer.getMarkers().map ({id}) => @getMarker(id)
@defaultMarkerLayer.getMarkers()
getMarkerCount: ->
@buffer.getMarkerCount()
@@ -822,54 +854,46 @@ class DisplayBuffer extends Model
# Public: Constructs a new marker at the given screen range.
#
# range - The marker {Range} (representing the distance between the head and tail)
# options - Options to pass to the {Marker} constructor
# options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markScreenRange: (args...) ->
bufferRange = @bufferRangeForScreenRange(args.shift())
@markBufferRange(bufferRange, args...)
markScreenRange: (screenRange, options) ->
@defaultMarkerLayer.markScreenRange(screenRange, options)
# Public: Constructs a new marker at the given buffer range.
#
# range - The marker {Range} (representing the distance between the head and tail)
# options - Options to pass to the {Marker} constructor
# options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markBufferRange: (range, options) ->
@getMarker(@buffer.markRange(range, options).id)
markBufferRange: (bufferRange, options) ->
@defaultMarkerLayer.markBufferRange(bufferRange, options)
# Public: Constructs a new marker at the given screen position.
#
# range - The marker {Range} (representing the distance between the head and tail)
# options - Options to pass to the {Marker} constructor
# options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markScreenPosition: (screenPosition, options) ->
@markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options)
@defaultMarkerLayer.markScreenPosition(screenPosition, options)
# Public: Constructs a new marker at the given buffer position.
#
# range - The marker {Range} (representing the distance between the head and tail)
# options - Options to pass to the {Marker} constructor
# options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markBufferPosition: (bufferPosition, options) ->
@getMarker(@buffer.markPosition(bufferPosition, options).id)
# Public: Removes the marker with the given id.
#
# id - The {Number} of the ID to remove
destroyMarker: (id) ->
@buffer.destroyMarker(id)
delete @markers[id]
@defaultMarkerLayer.markBufferPosition(bufferPosition, options)
# Finds the first marker satisfying the given attributes
#
# Refer to {DisplayBuffer::findMarkers} for details.
#
# Returns a {Marker} or null
# Returns a {TextEditorMarker} or null
findMarker: (params) ->
@findMarkers(params)[0]
@defaultMarkerLayer.findMarkers(params)[0]
# Public: Find all markers satisfying a set of parameters.
#
@@ -888,69 +912,36 @@ class DisplayBuffer extends Model
# :containedInBufferRange - A {Range} or range-compatible {Array}. Only
# returns markers contained within this range.
#
# Returns an {Array} of {Marker}s
# Returns an {Array} of {TextEditorMarker}s
findMarkers: (params) ->
params = @translateToBufferMarkerParams(params)
@buffer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id)
@defaultMarkerLayer.findMarkers(params)
translateToBufferMarkerParams: (params) ->
bufferMarkerParams = {}
for key, value of params
switch key
when 'startBufferRow'
key = 'startRow'
when 'endBufferRow'
key = 'endRow'
when 'startScreenRow'
key = 'startRow'
value = @bufferRowForScreenRow(value)
when 'endScreenRow'
key = 'endRow'
value = @bufferRowForScreenRow(value)
when 'intersectsBufferRowRange'
key = 'intersectsRowRange'
when 'intersectsScreenRowRange'
key = 'intersectsRowRange'
[startRow, endRow] = value
value = [@bufferRowForScreenRow(startRow), @bufferRowForScreenRow(endRow)]
when 'containsBufferRange'
key = 'containsRange'
when 'containsBufferPosition'
key = 'containsPosition'
when 'containedInBufferRange'
key = 'containedInRange'
when 'containedInScreenRange'
key = 'containedInRange'
value = @bufferRangeForScreenRange(value)
when 'intersectsBufferRange'
key = 'intersectsRange'
when 'intersectsScreenRange'
key = 'intersectsRange'
value = @bufferRangeForScreenRange(value)
bufferMarkerParams[key] = value
addMarkerLayer: (options) ->
bufferLayer = @buffer.addMarkerLayer(options)
@getMarkerLayer(bufferLayer.id)
bufferMarkerParams
getMarkerLayer: (id) ->
if layer = @customMarkerLayersById[id]
layer
else if bufferLayer = @buffer.getMarkerLayer(id)
@customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer)
findFoldMarker: (attributes) ->
@findFoldMarkers(attributes)[0]
getDefaultMarkerLayer: -> @defaultMarkerLayer
findFoldMarkers: (attributes) ->
@buffer.findMarkers(@getFoldMarkerAttributes(attributes))
findFoldMarker: (params) ->
@findFoldMarkers(params)[0]
getFoldMarkerAttributes: (attributes) ->
if attributes
_.extend(attributes, @foldMarkerAttributes)
else
@foldMarkerAttributes
findFoldMarkers: (params) ->
@foldsMarkerLayer.findMarkers(params)
refreshMarkerScreenPositions: ->
for marker in @getMarkers()
marker.notifyObservers(textChanged: false)
@defaultMarkerLayer.refreshMarkerScreenPositions()
layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById
return
destroyed: ->
fold.destroy() for markerId, fold of @foldsByMarkerId
marker.disposables.dispose() for id, marker of @markers
@defaultMarkerLayer.destroy()
@foldsMarkerLayer.destroy()
@scopedConfigSubscriptions.dispose()
@disposables.dispose()
@tokenizedBuffer.destroy()
@@ -1072,17 +1063,23 @@ class DisplayBuffer extends Model
@longestScreenRow = screenRow
@maxLineLength = length
handleBufferMarkerCreated: (textBufferMarker) =>
if textBufferMarker.matchesParams(@getFoldMarkerAttributes())
fold = new Fold(this, textBufferMarker)
fold.updateDisplayBuffer()
@decorateFold(fold)
didCreateDefaultLayerMarker: (textBufferMarker) =>
if marker = @getMarker(textBufferMarker.id)
# The marker might have been removed in some other handler called before
# this one. Only emit when the marker still exists.
@emitter.emit 'did-create-marker', marker
scheduleUpdateDecorationsEvent: ->
if @updatedSynchronously
@emitter.emit 'did-update-decorations'
return
unless @didUpdateDecorationsEventScheduled
@didUpdateDecorationsEventScheduled = true
process.nextTick =>
@didUpdateDecorationsEventScheduled = false
@emitter.emit 'did-update-decorations'
decorateFold: (fold) ->
@decorateMarker(fold.marker, type: 'line-number', class: 'folded')
@@ -1095,6 +1092,42 @@ class DisplayBuffer extends Model
else
delete @overlayDecorationsById[decoration.id]
didDestroyDecoration: (decoration) ->
{marker} = decoration
return unless decorations = @decorationsByMarkerId[marker.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @decorationsById[decoration.id]
@emitter.emit 'did-remove-decoration', decoration
delete @decorationsByMarkerId[marker.id] if decorations.length is 0
delete @overlayDecorationsById[decoration.id]
@unobserveDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
didDestroyLayerDecoration: (decoration) ->
{markerLayer} = decoration
return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
@unobserveDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
observeDecoratedLayer: (layer) ->
@decorationCountsByLayerId[layer.id] ?= 0
if ++@decorationCountsByLayerId[layer.id] is 1
@layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
unobserveDecoratedLayer: (layer) ->
if --@decorationCountsByLayerId[layer.id] is 0
@layerUpdateDisposablesByLayerId[layer.id].dispose()
delete @decorationCountsByLayerId[layer.id]
delete @layerUpdateDisposablesByLayerId[layer.id]
checkScreenLinesInvariant: ->
return if @isSoftWrapped()
return if _.size(@foldsByMarkerId) > 0

View File

@@ -71,13 +71,13 @@ class Gutter
isVisible: ->
@visible
# Essential: Add a decoration that tracks a {Marker}. When the marker moves,
# Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
#
# ## Arguments
#
# * `marker` A {Marker} you want this decoration to follow.
# * `marker` A {TextEditorMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration. It is passed
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
# all options documented there.

View File

@@ -0,0 +1,61 @@
_ = require 'underscore-plus'
idCounter = 0
nextId = -> idCounter++
# Essential: Represents a decoration that applies to every marker on a given
# layer. Created via {TextEditor::decorateMarkerLayer}.
module.exports =
class LayerDecoration
constructor: (@markerLayer, @displayBuffer, @properties) ->
@id = nextId()
@destroyed = false
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
@overridePropertiesByMarkerId = {}
# Essential: Destroys the decoration.
destroy: ->
return if @destroyed
@markerLayerDestroyedDisposable.dispose()
@markerLayerDestroyedDisposable = null
@destroyed = true
@displayBuffer.didDestroyLayerDecoration(this)
# Essential: Determine whether this decoration is destroyed.
#
# Returns a {Boolean}.
isDestroyed: -> @destroyed
getId: -> @id
getMarkerLayer: -> @markerLayer
# Essential: Get this decoration's properties.
#
# Returns an {Object}.
getProperties: ->
@properties
# Essential: Set this decoration's properties.
#
# * `newProperties` See {TextEditor::decorateMarker} for more information on
# the properties. The `type` of `gutter` and `overlay` are not supported on
# layer decorations.
setProperties: (newProperties) ->
return if @destroyed
@properties = newProperties
@displayBuffer.scheduleUpdateDecorationsEvent()
# Essential: Override the decoration properties for a specific marker.
#
# * `marker` The {TextEditorMarker} or {Marker} for which to override
# properties.
# * `properties` An {Object} containing properties to apply to this marker.
# Pass `null` to clear the override.
setPropertiesForMarker: (marker, properties) ->
return if @destroyed
if properties?
@overridePropertiesByMarkerId[marker.id] = properties
else
delete @overridePropertiesByMarkerId[marker.id]
@displayBuffer.scheduleUpdateDecorationsEvent()

View File

@@ -220,7 +220,7 @@ class TextEditorComponent
@updatesPaused = false
if @updateRequestedWhilePaused and @canUpdate()
@updateRequestedWhilePaused = false
@updateSync()
@requestUpdate()
getTopmostDOMNode: ->
@hostElement

View File

@@ -103,6 +103,7 @@ class TextEditorElement extends HTMLElement
return if model.isDestroyed()
@model = model
@model.setUpdatedSynchronously(@isUpdatedSynchronously())
@initializeContent()
@mountComponent()
@addGrammarScopeAttribute()
@@ -194,7 +195,9 @@ class TextEditorElement extends HTMLElement
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
setUpdatedSynchronously: (@updatedSynchronously) -> @updatedSynchronously
setUpdatedSynchronously: (@updatedSynchronously) ->
@model?.setUpdatedSynchronously(@updatedSynchronously)
@updatedSynchronously
isUpdatedSynchronously: -> @updatedSynchronously

View File

@@ -0,0 +1,192 @@
TextEditorMarker = require './text-editor-marker'
# Public: *Experimental:* A container for a related set of markers at the
# {TextEditor} level. Wraps an underlying {MarkerLayer} on the editor's
# {TextBuffer}.
#
# This API is experimental and subject to change on any release.
module.exports =
class TextEditorMarkerLayer
constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) ->
@id = @bufferMarkerLayer.id
@markersById = {}
###
Section: Lifecycle
###
# Essential: Destroy this layer.
destroy: ->
if @isDefaultLayer
marker.destroy() for id, marker of @markersById
else
@bufferMarkerLayer.destroy()
###
Section: Querying
###
# Essential: Get an existing marker by its id.
#
# Returns a {TextEditorMarker}.
getMarker: (id) ->
if editorMarker = @markersById[id]
editorMarker
else if bufferMarker = @bufferMarkerLayer.getMarker(id)
@markersById[id] = new TextEditorMarker(this, bufferMarker)
# Essential: Get all markers in the layer.
#
# Returns an {Array} of {TextEditorMarker}s.
getMarkers: ->
@bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id)
# Public: Get the number of markers in the marker layer.
#
# Returns a {Number}.
getMarkerCount: ->
@bufferMarkerLayer.getMarkerCount()
# Public: Find markers in the layer conforming to the given parameters.
#
# See the documentation for {TextEditor::findMarkers}.
findMarkers: (params) ->
params = @translateToBufferMarkerParams(params)
@bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id)
###
Section: Marker creation
###
# Essential: Create a marker on this layer with the given range in buffer
# coordinates.
#
# See the documentation for {TextEditor::markBufferRange}
markBufferRange: (bufferRange, options) ->
@getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id)
# Essential: Create a marker on this layer with the given range in screen
# coordinates.
#
# See the documentation for {TextEditor::markScreenRange}
markScreenRange: (screenRange, options) ->
bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange)
@markBufferRange(bufferRange, options)
# Public: Create a marker on this layer with the given buffer position and no
# tail.
#
# See the documentation for {TextEditor::markBufferPosition}
markBufferPosition: (bufferPosition, options) ->
@getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id)
# Public: Create a marker on this layer with the given screen position and no
# tail.
#
# See the documentation for {TextEditor::markScreenPosition}
markScreenPosition: (screenPosition, options) ->
bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition)
@markBufferPosition(bufferPosition, options)
###
Section: Event Subscription
###
# Public: Subscribe to be notified asynchronously whenever markers are
# created, updated, or destroyed on this layer. *Prefer this method for
# optimal performance when interacting with layers that could contain large
# numbers of markers.*
#
# * `callback` A {Function} that will be called with no arguments when changes
# occur on this layer.
#
# Subscribers are notified once, asynchronously when any number of changes
# occur in a given tick of the event loop. You should re-query the layer
# to determine the state of markers in which you're interested in. It may
# be counter-intuitive, but this is much more efficient than subscribing to
# events on individual markers, which are expensive to deliver.
#
# Returns a {Disposable}.
onDidUpdate: (callback) ->
@bufferMarkerLayer.onDidUpdate(callback)
# Public: Subscribe to be notified synchronously whenever markers are created
# on this layer. *Avoid this method for optimal performance when interacting
# with layers that could contain large numbers of markers.*
#
# * `callback` A {Function} that will be called with a {TextEditorMarker}
# whenever a new marker is created.
#
# You should prefer {onDidUpdate} when synchronous notifications aren't
# absolutely necessary.
#
# Returns a {Disposable}.
onDidCreateMarker: (callback) ->
@bufferMarkerLayer.onDidCreateMarker (bufferMarker) =>
callback(@getMarker(bufferMarker.id))
# Public: Subscribe to be notified synchronously when this layer is destroyed.
#
# Returns a {Disposable}.
onDidDestroy: (callback) ->
@bufferMarkerLayer.onDidDestroy(callback)
###
Section: Private
###
refreshMarkerScreenPositions: ->
for marker in @getMarkers()
marker.notifyObservers(textChanged: false)
return
didDestroyMarker: (marker) ->
delete @markersById[marker.id]
translateToBufferMarkerParams: (params) ->
bufferMarkerParams = {}
for key, value of params
switch key
when 'startBufferPosition'
key = 'startPosition'
when 'endBufferPosition'
key = 'endPosition'
when 'startScreenPosition'
key = 'startPosition'
value = @displayBuffer.bufferPositionForScreenPosition(value)
when 'endScreenPosition'
key = 'endPosition'
value = @displayBuffer.bufferPositionForScreenPosition(value)
when 'startBufferRow'
key = 'startRow'
when 'endBufferRow'
key = 'endRow'
when 'startScreenRow'
key = 'startRow'
value = @displayBuffer.bufferRowForScreenRow(value)
when 'endScreenRow'
key = 'endRow'
value = @displayBuffer.bufferRowForScreenRow(value)
when 'intersectsBufferRowRange'
key = 'intersectsRowRange'
when 'intersectsScreenRowRange'
key = 'intersectsRowRange'
[startRow, endRow] = value
value = [@displayBuffer.bufferRowForScreenRow(startRow), @displayBuffer.bufferRowForScreenRow(endRow)]
when 'containsBufferRange'
key = 'containsRange'
when 'containsBufferPosition'
key = 'containsPosition'
when 'containedInBufferRange'
key = 'containedInRange'
when 'containedInScreenRange'
key = 'containedInRange'
value = @displayBuffer.bufferRangeForScreenRange(value)
when 'intersectsBufferRange'
key = 'intersectsRange'
when 'intersectsScreenRange'
key = 'intersectsRange'
value = @displayBuffer.bufferRangeForScreenRange(value)
bufferMarkerParams[key] = value
bufferMarkerParams

View File

@@ -6,7 +6,7 @@ _ = require 'underscore-plus'
# targets, misspelled words, and anything else that needs to track a logical
# location in the buffer over time.
#
# ### Marker Creation
# ### TextEditorMarker Creation
#
# Use {TextEditor::markBufferRange} rather than creating Markers directly.
#
@@ -40,7 +40,7 @@ _ = require 'underscore-plus'
#
# See {TextEditor::markBufferRange} for usage.
module.exports =
class Marker
class TextEditorMarker
bufferMarkerSubscription: null
oldHeadBufferPosition: null
oldHeadScreenPosition: null
@@ -53,7 +53,8 @@ class Marker
Section: Construction and Destruction
###
constructor: ({@bufferMarker, @displayBuffer}) ->
constructor: (@layer, @bufferMarker) ->
{@displayBuffer} = @layer
@emitter = new Emitter
@disposables = new CompositeDisposable
@id = @bufferMarker.id
@@ -66,7 +67,7 @@ class Marker
@bufferMarker.destroy()
@disposables.dispose()
# Essential: Creates and returns a new {Marker} with the same properties as
# Essential: Creates and returns a new {TextEditorMarker} with the same properties as
# this marker.
#
# {Selection} markers (markers with a custom property `type: "selection"`)
@@ -79,9 +80,9 @@ class Marker
# marker. The new marker's properties are computed by extending this marker's
# properties with `properties`.
#
# Returns a {Marker}.
# Returns a {TextEditorMarker}.
copy: (properties) ->
@displayBuffer.getMarker(@bufferMarker.copy(properties).id)
@layer.getMarker(@bufferMarker.copy(properties).id)
###
Section: Event Subscription
@@ -129,7 +130,7 @@ class Marker
@emitter.on 'did-destroy', callback
###
Section: Marker Details
Section: TextEditorMarker Details
###
# Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be
@@ -140,7 +141,7 @@ class Marker
# Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker
# can be invalid without being destroyed, in which case undoing the invalidating
# operation would restore the marker. Once a marker is destroyed by calling
# {Marker::destroy}, no undo/redo operation can ever bring it back.
# {TextEditorMarker::destroy}, no undo/redo operation can ever bring it back.
isDestroyed: ->
@bufferMarker.isDestroyed()
@@ -169,7 +170,7 @@ class Marker
@bufferMarker.setProperties(properties)
matchesProperties: (attributes) ->
attributes = @displayBuffer.translateToBufferMarkerParams(attributes)
attributes = @layer.translateToBufferMarkerParams(attributes)
@bufferMarker.matchesParams(attributes)
###
@@ -179,14 +180,14 @@ class Marker
# Essential: Returns a {Boolean} indicating whether this marker is equivalent to
# another marker, meaning they have the same range and options.
#
# * `other` {Marker} other marker
# * `other` {TextEditorMarker} other marker
isEqual: (other) ->
return false unless other instanceof @constructor
@bufferMarker.isEqual(other.bufferMarker)
# Essential: Compares this marker to another based on their ranges.
#
# * `other` {Marker}
# * `other` {TextEditorMarker}
#
# Returns a {Number}
compare: (other) ->
@@ -225,28 +226,28 @@ class Marker
@setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options)
# Essential: Retrieves the buffer position of the marker's start. This will always be
# less than or equal to the result of {Marker::getEndBufferPosition}.
# less than or equal to the result of {TextEditorMarker::getEndBufferPosition}.
#
# Returns a {Point}.
getStartBufferPosition: ->
@bufferMarker.getStartPosition()
# Essential: Retrieves the screen position of the marker's start. This will always be
# less than or equal to the result of {Marker::getEndScreenPosition}.
# less than or equal to the result of {TextEditorMarker::getEndScreenPosition}.
#
# Returns a {Point}.
getStartScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true)
# Essential: Retrieves the buffer position of the marker's end. This will always be
# greater than or equal to the result of {Marker::getStartBufferPosition}.
# greater than or equal to the result of {TextEditorMarker::getStartBufferPosition}.
#
# Returns a {Point}.
getEndBufferPosition: ->
@bufferMarker.getEndPosition()
# Essential: Retrieves the screen position of the marker's end. This will always be
# greater than or equal to the result of {Marker::getStartScreenPosition}.
# greater than or equal to the result of {TextEditorMarker::getStartScreenPosition}.
#
# Returns a {Point}.
getEndScreenPosition: ->
@@ -330,10 +331,10 @@ class Marker
# Returns a {String} representation of the marker
inspect: ->
"Marker(id: #{@id}, bufferRange: #{@getBufferRange()})"
"TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})"
destroyed: ->
delete @displayBuffer.markers[@id]
@layer.didDestroyMarker(this)
@emitter.emit 'did-destroy'
@emitter.dispose()

View File

@@ -28,10 +28,9 @@ class TextEditorPresenter
@emitter = new Emitter
@visibleHighlights = {}
@characterWidthsByScope = {}
@rangesByDecorationId = {}
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterNameAndScreenRow = {}
@customGutterDecorationsByGutterName = {}
@screenRowsToMeasure = []
@transferMeasurementsToModel()
@transferMeasurementsFromModel()
@@ -49,6 +48,9 @@ class TextEditorPresenter
destroy: ->
@disposables.dispose()
clearTimeout(@stoppedScrollingTimeoutId) if @stoppedScrollingTimeoutId?
clearInterval(@reflowingInterval) if @reflowingInterval?
@stopBlinkingCursors()
# Calls your `callback` when some changes in the model occurred and the current state has been updated.
onDidUpdateState: (callback) ->
@@ -185,7 +187,7 @@ class TextEditorPresenter
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
@disposables.add @model.onDidUpdateMarkers =>
@disposables.add @model.onDidUpdateDecorations =>
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateDecorations = true
@@ -214,10 +216,8 @@ class TextEditorPresenter
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
@disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
@disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
@disposables.add @model.onDidRequestAutoscroll(@requestAutoscroll.bind(this))
@observeDecoration(decoration) for decoration in @model.getDecorations()
@observeCursor(cursor) for cursor in @model.getCursors()
@disposables.add @model.onDidAddGutter(@didAddGutter.bind(this))
return
@@ -626,16 +626,14 @@ class TextEditorPresenter
@clearDecorationsForCustomGutterName(gutterName)
else
@customGutterDecorations[gutterName] = {}
continue if not @gutterIsVisible(gutter)
relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
relevantDecorations.forEach (decoration) =>
decorationRange = decoration.getMarker().getScreenRange()
@customGutterDecorations[gutterName][decoration.id] =
top: @lineHeight * decorationRange.start.row
height: @lineHeight * decorationRange.getRowCount()
item: decoration.getProperties().item
class: decoration.getProperties().class
continue unless @gutterIsVisible(gutter)
for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName]
@customGutterDecorations[gutterName][decorationId] =
top: @lineHeight * screenRange.start.row
height: @lineHeight * screenRange.getRowCount()
item: properties.item
class: properties.class
clearAllCustomGutterDecorations: ->
allGutterNames = Object.keys(@customGutterDecorations)
@@ -850,32 +848,20 @@ class TextEditorPresenter
return null if @model.isMini()
decorationClasses = null
for id, decoration of @lineDecorationsByScreenRow[row]
for id, properties of @lineDecorationsByScreenRow[row]
decorationClasses ?= []
decorationClasses.push(decoration.getProperties().class)
decorationClasses.push(properties.class)
decorationClasses
lineNumberDecorationClassesForRow: (row) ->
return null if @model.isMini()
decorationClasses = null
for id, decoration of @lineNumberDecorationsByScreenRow[row]
for id, properties of @lineNumberDecorationsByScreenRow[row]
decorationClasses ?= []
decorationClasses.push(decoration.getProperties().class)
decorationClasses.push(properties.class)
decorationClasses
# Returns a {Set} of {Decoration}s on the given custom gutter from startRow to endRow (inclusive).
customGutterDecorationsInRange: (gutterName, startRow, endRow) ->
decorations = new Set
return decorations if @model.isMini() or gutterName is 'line-number' or
not @customGutterDecorationsByGutterNameAndScreenRow[gutterName]
for screenRow in [@startRow..@endRow - 1]
for id, decoration of @customGutterDecorationsByGutterNameAndScreenRow[gutterName][screenRow]
decorations.add(decoration)
decorations
getCursorBlinkPeriod: -> @cursorBlinkPeriod
getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay
@@ -1183,93 +1169,32 @@ class TextEditorPresenter
rect
observeDecoration: (decoration) ->
decorationDisposables = new CompositeDisposable
if decoration.isType('highlight')
decorationDisposables.add decoration.onDidFlash =>
@shouldUpdateDecorations = true
@emitDidUpdateState()
decorationDisposables.add decoration.onDidChangeProperties (event) =>
@decorationPropertiesDidChange(decoration, event)
decorationDisposables.add decoration.onDidDestroy =>
@disposables.remove(decorationDisposables)
decorationDisposables.dispose()
@didDestroyDecoration(decoration)
@disposables.add(decorationDisposables)
decorationPropertiesDidChange: (decoration, {oldProperties}) ->
@shouldUpdateDecorations = true
if decoration.isType('line') or decoration.isType('gutter')
if decoration.isType('line') or Decoration.isType(oldProperties, 'line')
@shouldUpdateLinesState = true
if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number')
@shouldUpdateLineNumbersState = true
if (decoration.isType('gutter') and not decoration.isType('line-number')) or
(Decoration.isType(oldProperties, 'gutter') and not Decoration.isType(oldProperties, 'line-number'))
@shouldUpdateCustomGutterDecorationState = true
else if decoration.isType('overlay')
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
didDestroyDecoration: (decoration) ->
@shouldUpdateDecorations = true
if decoration.isType('line') or decoration.isType('gutter')
@shouldUpdateLinesState = true if decoration.isType('line')
if decoration.isType('line-number')
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@shouldUpdateCustomGutterDecorationState = true
if decoration.isType('overlay')
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
didAddDecoration: (decoration) ->
@observeDecoration(decoration)
if decoration.isType('line') or decoration.isType('gutter')
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true if decoration.isType('line')
if decoration.isType('line-number')
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@shouldUpdateCustomGutterDecorationState = true
else if decoration.isType('highlight')
@shouldUpdateDecorations = true
else if decoration.isType('overlay')
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
fetchDecorations: ->
@decorations = []
return unless 0 <= @startRow <= @endRow <= Infinity
for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1)
range = @model.getMarker(markerId).getScreenRange()
for decoration in decorations
@decorations.push({decoration, range})
@decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1)
updateLineDecorations: ->
@rangesByDecorationId = {}
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterNameAndScreenRow = {}
@customGutterDecorationsByGutterName = {}
for {decoration, range} in @decorations
if decoration.isType('line') or decoration.isType('gutter')
@addToLineDecorationCaches(decoration, range)
for decorationId, decorationState of @decorations
{properties, screenRange, rangeIsReversed} = decorationState
if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number')
@addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed)
else if Decoration.isType(properties, 'gutter') and properties.gutterName?
@customGutterDecorationsByGutterName[properties.gutterName] ?= {}
@customGutterDecorationsByGutterName[properties.gutterName][decorationId] = decorationState
return
updateHighlightDecorations: ->
@visibleHighlights = {}
for {decoration, range} in @decorations
if decoration.isType('highlight')
@updateHighlightState(decoration, range)
for decorationId, {properties, screenRange} of @decorations
if Decoration.isType(properties, 'highlight')
@updateHighlightState(decorationId, properties, screenRange)
for tileId, tileState of @state.content.tiles
for id, highlight of tileState.highlights
@@ -1277,50 +1202,29 @@ class TextEditorPresenter
return
removeFromLineDecorationCaches: (decoration) ->
@removePropertiesFromLineDecorationCaches(decoration.id, decoration.getProperties())
removePropertiesFromLineDecorationCaches: (decorationId, decorationProperties) ->
if range = @rangesByDecorationId[decorationId]
delete @rangesByDecorationId[decorationId]
gutterName = decorationProperties.gutterName
for row in [range.start.row..range.end.row] by 1
delete @lineDecorationsByScreenRow[row]?[decorationId]
delete @lineNumberDecorationsByScreenRow[row]?[decorationId]
delete @customGutterDecorationsByGutterNameAndScreenRow[gutterName]?[row]?[decorationId] if gutterName
return
addToLineDecorationCaches: (decoration, range) ->
marker = decoration.getMarker()
properties = decoration.getProperties()
return unless marker.isValid()
if range.isEmpty()
addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) ->
if screenRange.isEmpty()
return if properties.onlyNonEmpty
else
return if properties.onlyEmpty
omitLastRow = range.end.column is 0
omitLastRow = screenRange.end.column is 0
@rangesByDecorationId[decoration.id] = range
if rangeIsReversed
headPosition = screenRange.start
else
headPosition = screenRange.end
for row in [range.start.row..range.end.row] by 1
continue if properties.onlyHead and row isnt marker.getHeadScreenPosition().row
continue if omitLastRow and row is range.end.row
for row in [screenRange.start.row..screenRange.end.row] by 1
continue if properties.onlyHead and row isnt headPosition.row
continue if omitLastRow and row is screenRange.end.row
if decoration.isType('line')
if Decoration.isType(properties, 'line')
@lineDecorationsByScreenRow[row] ?= {}
@lineDecorationsByScreenRow[row][decoration.id] = decoration
@lineDecorationsByScreenRow[row][decorationId] = properties
if decoration.isType('line-number')
if Decoration.isType(properties, 'line-number')
@lineNumberDecorationsByScreenRow[row] ?= {}
@lineNumberDecorationsByScreenRow[row][decoration.id] = decoration
else if decoration.isType('gutter')
gutterName = decoration.getProperties().gutterName
@customGutterDecorationsByGutterNameAndScreenRow[gutterName] ?= {}
@customGutterDecorationsByGutterNameAndScreenRow[gutterName][row] ?= {}
@customGutterDecorationsByGutterNameAndScreenRow[gutterName][row][decoration.id] = decoration
@lineNumberDecorationsByScreenRow[row][decorationId] = properties
return
@@ -1340,46 +1244,34 @@ class TextEditorPresenter
intersectingRange
updateHighlightState: (decoration, range) ->
updateHighlightState: (decorationId, properties, screenRange) ->
return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements()
properties = decoration.getProperties()
marker = decoration.getMarker()
return if screenRange.isEmpty()
if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(@startRow, @endRow - 1)
return
if screenRange.start.row < @startRow
screenRange.start.row = @startRow
screenRange.start.column = 0
if screenRange.end.row >= @endRow
screenRange.end.row = @endRow
screenRange.end.column = 0
if range.start.row < @startRow
range.start.row = @startRow
range.start.column = 0
if range.end.row >= @endRow
range.end.row = @endRow
range.end.column = 0
return if screenRange.isEmpty()
return if range.isEmpty()
flash = decoration.consumeNextFlash()
startTile = @tileForRow(range.start.row)
endTile = @tileForRow(range.end.row)
startTile = @tileForRow(screenRange.start.row)
endTile = @tileForRow(screenRange.end.row)
for tileStartRow in [startTile..endTile] by @tileSize
rangeWithinTile = @intersectRangeWithTile(range, tileStartRow)
rangeWithinTile = @intersectRangeWithTile(screenRange, tileStartRow)
continue if rangeWithinTile.isEmpty()
tileState = @state.content.tiles[tileStartRow] ?= {highlights: {}}
highlightState = tileState.highlights[decoration.id] ?= {
flashCount: 0
flashDuration: null
flashClass: null
}
if flash?
highlightState.flashCount++
highlightState.flashClass = flash.class
highlightState.flashDuration = flash.duration
highlightState = tileState.highlights[decorationId] ?= {}
highlightState.flashCount = properties.flashCount
highlightState.flashClass = properties.flashClass
highlightState.flashDuration = properties.flashDuration
highlightState.class = properties.class
highlightState.deprecatedRegionClass = properties.deprecatedRegionClass
highlightState.regions = @buildHighlightRegions(rangeWithinTile)
@@ -1388,7 +1280,7 @@ class TextEditorPresenter
@repositionRegionWithinTile(region, tileStartRow)
@visibleHighlights[tileStartRow] ?= {}
@visibleHighlights[tileStartRow][decoration.id] = true
@visibleHighlights[tileStartRow][decorationId] = true
true

View File

@@ -74,6 +74,7 @@ class TextEditor extends Model
throw error
state.displayBuffer = displayBuffer
state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId)
state.config = atomEnvironment.config
state.notificationManager = atomEnvironment.notifications
state.packageManager = atomEnvironment.packages
@@ -90,9 +91,10 @@ class TextEditor extends Model
{
@softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength,
softWrapped, @displayBuffer, buffer, suppressCursorCreation, @mini, @placeholderText,
lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager,
@clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
@mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
@notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
@project, @assert, @applicationDelegate
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
@@ -115,8 +117,9 @@ class TextEditor extends Model
@config, @assert, @grammarRegistry, @packageManager
})
@buffer = @displayBuffer.buffer
@selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true)
for marker in @findMarkers(@getSelectionMarkerAttributes())
for marker in @selectionsMarkerLayer.getMarkers()
marker.setProperties(preserveFolds: true)
@addSelection(marker)
@@ -146,6 +149,7 @@ class TextEditor extends Model
scrollRow: @getScrollRow()
scrollColumn: @getScrollColumn()
displayBuffer: @displayBuffer.serialize()
selectionsMarkerLayerId: @selectionsMarkerLayer.id
subscribeToBuffer: ->
@buffer.retain()
@@ -161,9 +165,9 @@ class TextEditor extends Model
@preserveCursorPositionOnBufferReload()
subscribeToDisplayBuffer: ->
@disposables.add @displayBuffer.onDidCreateMarker @handleMarkerCreated
@disposables.add @displayBuffer.onDidChangeGrammar => @handleGrammarChange()
@disposables.add @displayBuffer.onDidTokenize => @handleTokenization()
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
@disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
@disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this)
@disposables.add @displayBuffer.onDidChange (e) =>
@mergeIntersectingSelections()
@emitter.emit 'did-change', e
@@ -177,6 +181,7 @@ class TextEditor extends Model
@disposables.dispose()
@tabTypeSubscription.dispose()
selection.destroy() for selection in @selections.slice()
@selectionsMarkerLayer.destroy()
@buffer.release()
@displayBuffer.destroy()
@languageMode.destroy()
@@ -468,6 +473,9 @@ class TextEditor extends Model
onDidUpdateMarkers: (callback) ->
@displayBuffer.onDidUpdateMarkers(callback)
onDidUpdateDecorations: (callback) ->
@displayBuffer.onDidUpdateDecorations(callback)
# Essential: Retrieves the current {TextBuffer}.
getBuffer: -> @buffer
@@ -477,14 +485,13 @@ class TextEditor extends Model
# Create an {TextEditor} with its initial state based on this object
copy: ->
displayBuffer = @displayBuffer.copy()
selectionsMarkerLayer = displayBuffer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
softTabs = @getSoftTabs()
newEditor = new TextEditor({
@buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true,
@config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
@grammarRegistry, @project, @assert, @applicationDelegate
@buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs,
suppressCursorCreation: true, @config, @notificationManager, @packageManager,
@clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
})
for marker in @findMarkers(editorId: @id)
marker.copy(editorId: newEditor.id, preserveFolds: true)
newEditor
# Controls visibility based on the given {Boolean}.
@@ -499,6 +506,9 @@ class TextEditor extends Model
isMini: -> @mini
setUpdatedSynchronously: (updatedSynchronously) ->
@displayBuffer.setUpdatedSynchronously(updatedSynchronously)
onDidChangeMini: (callback) ->
@emitter.on 'did-change-mini', callback
@@ -1393,9 +1403,9 @@ class TextEditor extends Model
Section: Decorations
###
# Essential: Adds a decoration that tracks a {Marker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
# Essential: Add a decoration that tracks a {TextEditorMarker}. When the
# marker moves, is invalidated, or is destroyed, the decoration will be
# updated to reflect the marker's state.
#
# The following are the supported decorations types:
#
@@ -1414,28 +1424,28 @@ class TextEditor extends Model
# </div>
# ```
# * __overlay__: Positions the view associated with the given item at the head
# or tail of the given `Marker`.
# * __gutter__: A decoration that tracks a {Marker} in a {Gutter}. Gutter
# or tail of the given `TextEditorMarker`.
# * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter
# decorations are created by calling {Gutter::decorateMarker} on the
# desired `Gutter` instance.
#
# ## Arguments
#
# * `marker` A {Marker} you want this decoration to follow.
# * `marker` A {TextEditorMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration e.g.
# `{type: 'line-number', class: 'linter-error'}`
# * `type` There are several supported decoration types. The behavior of the
# types are as follows:
# * `line` Adds the given `class` to the lines overlapping the rows
# spanned by the `Marker`.
# spanned by the `TextEditorMarker`.
# * `line-number` Adds the given `class` to the line numbers overlapping
# the rows spanned by the `Marker`.
# the rows spanned by the `TextEditorMarker`.
# * `highlight` Creates a `.highlight` div with the nested class with up
# to 3 nested regions that fill the area spanned by the `Marker`.
# to 3 nested regions that fill the area spanned by the `TextEditorMarker`.
# * `overlay` Positions the view associated with the given item at the
# head or tail of the given `Marker`, depending on the `position`
# head or tail of the given `TextEditorMarker`, depending on the `position`
# property.
# * `gutter` Tracks a {Marker} in a {Gutter}. Created by calling
# * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling
# {Gutter::decorateMarker} on the desired `Gutter` instance.
# * `class` This CSS class will be applied to the decorated line number,
# line, highlight, or overlay.
@@ -1443,35 +1453,53 @@ class TextEditor extends Model
# corresponding view registered. Only applicable to the `gutter` and
# `overlay` types.
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
# the head of the `Marker`. Only applicable to the `line` and
# the head of the `TextEditorMarker`. Only applicable to the `line` and
# `line-number` types.
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
# the associated `Marker` is empty. Only applicable to the `gutter`,
# the associated `TextEditorMarker` is empty. Only applicable to the `gutter`,
# `line`, and `line-number` types.
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
# if the associated `Marker` is non-empty. Only applicable to the
# if the associated `TextEditorMarker` is non-empty. Only applicable to the
# `gutter`, `line`, and `line-number` types.
# * `position` (optional) Only applicable to decorations of type `overlay`,
# controls where the overlay view is positioned relative to the `Marker`.
# controls where the overlay view is positioned relative to the `TextEditorMarker`.
# Values can be `'head'` (the default), or `'tail'`.
#
# Returns a {Decoration} object
decorateMarker: (marker, decorationParams) ->
@displayBuffer.decorateMarker(marker, decorationParams)
# Essential: Get all the decorations within a screen row range.
# Essential: *Experimental:* Add a decoration to every marker in the given
# marker layer. Can be used to decorate a large number of markers without
# having to create and manage many individual decorations.
#
# * `markerLayer` A {TextEditorMarkerLayer} or {MarkerLayer} to decorate.
# * `decorationParams` The same parameters that are passed to
# {decorateMarker}, except the `type` cannot be `overlay` or `gutter`.
#
# This API is experimental and subject to change on any release.
#
# Returns a {LayerDecoration}.
decorateMarkerLayer: (markerLayer, decorationParams) ->
@displayBuffer.decorateMarkerLayer(markerLayer, decorationParams)
# Deprecated: Get all the decorations within a screen row range on the default
# layer.
#
# * `startScreenRow` the {Number} beginning screen row
# * `endScreenRow` the {Number} end screen row (inclusive)
#
# Returns an {Object} of decorations in the form
# `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}`
# where the keys are {Marker} IDs, and the values are an array of decoration
# where the keys are {TextEditorMarker} IDs, and the values are an array of decoration
# params objects attached to the marker.
# Returns an empty object when no decorations are found
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
@displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow)
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
@displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
# Extended: Get all decorations.
#
# * `propertyFilter` (optional) An {Object} containing key value pairs that
@@ -1527,10 +1555,10 @@ class TextEditor extends Model
Section: Markers
###
# Essential: Create a marker with the given range in buffer coordinates. This
# marker will maintain its logical location as the buffer is changed, so if
# you mark a particular word, the marker will remain over that word even if
# the word's location in the buffer changes.
# Essential: Create a marker on the default marker layer with the given range
# in buffer coordinates. This marker will maintain its logical location as the
# buffer is changed, so if you mark a particular word, the marker will remain
# over that word even if the word's location in the buffer changes.
#
# * `range` A {Range} or range-compatible {Array}
# * `properties` A hash of key-value pairs to associate with the marker. There
@@ -1558,14 +1586,14 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
# Returns a {Marker}.
# Returns a {TextEditorMarker}.
markBufferRange: (args...) ->
@displayBuffer.markBufferRange(args...)
# Essential: Create a marker with the given range in screen coordinates. This
# marker will maintain its logical location as the buffer is changed, so if
# you mark a particular word, the marker will remain over that word even if
# the word's location in the buffer changes.
# Essential: Create a marker on the default marker layer with the given range
# in screen coordinates. This marker will maintain its logical location as the
# buffer is changed, so if you mark a particular word, the marker will remain
# over that word even if the word's location in the buffer changes.
#
# * `range` A {Range} or range-compatible {Array}
# * `properties` A hash of key-value pairs to associate with the marker. There
@@ -1593,29 +1621,32 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
# Returns a {Marker}.
# Returns a {TextEditorMarker}.
markScreenRange: (args...) ->
@displayBuffer.markScreenRange(args...)
# Essential: Mark the given position in buffer coordinates.
# Essential: Mark the given position in buffer coordinates on the default
# marker layer.
#
# * `position` A {Point} or {Array} of `[row, column]`.
# * `options` (optional) See {TextBuffer::markRange}.
#
# Returns a {Marker}.
# Returns a {TextEditorMarker}.
markBufferPosition: (args...) ->
@displayBuffer.markBufferPosition(args...)
# Essential: Mark the given position in screen coordinates.
# Essential: Mark the given position in screen coordinates on the default
# marker layer.
#
# * `position` A {Point} or {Array} of `[row, column]`.
# * `options` (optional) See {TextBuffer::markRange}.
#
# Returns a {Marker}.
# Returns a {TextEditorMarker}.
markScreenPosition: (args...) ->
@displayBuffer.markScreenPosition(args...)
# Essential: Find all {Marker}s that match the given properties.
# Essential: Find all {TextEditorMarker}s on the default marker layer that
# match the given properties.
#
# This method finds markers based on the given properties. Markers can be
# associated with custom properties that will be compared with basic equality.
@@ -1637,44 +1668,60 @@ class TextEditor extends Model
findMarkers: (properties) ->
@displayBuffer.findMarkers(properties)
# Extended: Observe changes in the set of markers that intersect a particular
# region of the editor.
#
# * `callback` A {Function} to call whenever one or more {Marker}s appears,
# disappears, or moves within the given region.
# * `event` An {Object} with the following keys:
# * `insert` A {Set} containing the ids of all markers that appeared
# in the range.
# * `update` A {Set} containing the ids of all markers that moved within
# the region.
# * `remove` A {Set} containing the ids of all markers that disappeared
# from the region.
#
# Returns a {MarkerObservationWindow}, which allows you to specify the region
# of interest by calling {MarkerObservationWindow::setBufferRange} or
# {MarkerObservationWindow::setScreenRange}.
observeMarkers: (callback) ->
@displayBuffer.observeMarkers(callback)
# Extended: Get the {Marker} for the given marker id.
# Extended: Get the {TextEditorMarker} on the default layer for the given
# marker id.
#
# * `id` {Number} id of the marker
getMarker: (id) ->
@displayBuffer.getMarker(id)
# Extended: Get all {Marker}s. Consider using {::findMarkers}
# Extended: Get all {TextEditorMarker}s on the default marker layer. Consider
# using {::findMarkers}
getMarkers: ->
@displayBuffer.getMarkers()
# Extended: Get the number of markers in this editor's buffer.
# Extended: Get the number of markers in the default marker layer.
#
# Returns a {Number}.
getMarkerCount: ->
@buffer.getMarkerCount()
# {Delegates to: DisplayBuffer.destroyMarker}
destroyMarker: (args...) ->
@displayBuffer.destroyMarker(args...)
destroyMarker: (id) ->
@getMarker(id)?.destroy()
# Extended: *Experimental:* Create a marker layer to group related markers.
#
# * `options` An {Object} containing the following keys:
# * `maintainHistory` A {Boolean} indicating whether marker state should be
# restored on undo/redo. Defaults to `false`.
#
# This API is experimental and subject to change on any release.
#
# Returns a {TextEditorMarkerLayer}.
addMarkerLayer: (options) ->
@displayBuffer.addMarkerLayer(options)
# Public: *Experimental:* Get a {TextEditorMarkerLayer} by id.
#
# * `id` The id of the marker layer to retrieve.
#
# This API is experimental and subject to change on any release.
#
# Returns a {MarkerLayer} or `undefined` if no layer exists with the given
# id.
getMarkerLayer: (id) ->
@displayBuffer.getMarkerLayer(id)
# Public: *Experimental:* Get the default {TextEditorMarkerLayer}.
#
# All marker APIs not tied to an explicit layer interact with this default
# layer.
#
# This API is experimental and subject to change on any release.
#
# Returns a {TextEditorMarkerLayer}.
getDefaultMarkerLayer: ->
@displayBuffer.getDefaultMarkerLayer()
###
Section: Cursors
@@ -1744,7 +1791,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtBufferPosition: (bufferPosition, options) ->
@markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
@selectionsMarkerLayer.markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1754,7 +1801,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtScreenPosition: (screenPosition, options) ->
@markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
@selectionsMarkerLayer.markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1879,7 +1926,7 @@ class TextEditor extends Model
getCursorsOrderedByBufferPosition: ->
@getCursors().sort (a, b) -> a.compare(b)
# Add a cursor based on the given {Marker}.
# Add a cursor based on the given {TextEditorMarker}.
addCursor: (marker) ->
cursor = new Cursor(editor: this, marker: marker, config: @config)
@cursors.push(cursor)
@@ -2032,7 +2079,7 @@ class TextEditor extends Model
#
# Returns the added {Selection}.
addSelectionForBufferRange: (bufferRange, options={}) ->
@markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
@selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
@getLastSelection().autoscroll() unless options.autoscroll is false
@getLastSelection()
@@ -2045,7 +2092,7 @@ class TextEditor extends Model
#
# Returns the added {Selection}.
addSelectionForScreenRange: (screenRange, options={}) ->
@markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options))
@selectionsMarkerLayer.markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options))
@getLastSelection().autoscroll() unless options.autoscroll is false
@getLastSelection()
@@ -2228,7 +2275,7 @@ class TextEditor extends Model
# Extended: Select the range of the given marker if it is valid.
#
# * `marker` A {Marker}
# * `marker` A {TextEditorMarker}
#
# Returns the selected {Range} or `undefined` if the marker is invalid.
selectMarker: (marker) ->
@@ -2354,9 +2401,9 @@ class TextEditor extends Model
_.reduce(tail, reducer, [head])
return result if fn?
# Add a {Selection} based on the given {Marker}.
# Add a {Selection} based on the given {TextEditorMarker}.
#
# * `marker` The {Marker} to highlight
# * `marker` The {TextEditorMarker} to highlight
# * `options` (optional) An {Object} that pertains to the {Selection} constructor.
#
# Returns the new {Selection}.
@@ -3064,10 +3111,6 @@ class TextEditor extends Model
@subscribeToTabTypeConfig()
@emitter.emit 'did-change-grammar', @getGrammar()
handleMarkerCreated: (marker) =>
if marker.matchesProperties(@getSelectionMarkerAttributes())
@addSelection(marker)
###
Section: TextEditor Rendering
###
@@ -3104,7 +3147,7 @@ class TextEditor extends Model
@viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition)
getSelectionMarkerAttributes: ->
{type: 'selection', editorId: @id, invalidate: 'never', maintainHistory: true}
{type: 'selection', invalidate: 'never'}
getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)

View File

@@ -43,7 +43,7 @@ _ = require 'underscore-plus'
# ```
module.exports =
class ViewRegistry
documentUpdateRequested: false
animationFrameRequest: null
documentReadInProgress: false
performDocumentPollAfterUpdate: false
debouncedPerformDocumentPoll: null
@@ -195,20 +195,30 @@ class ViewRegistry
pollAfterNextUpdate: ->
@performDocumentPollAfterUpdate = true
getNextUpdatePromise: ->
@nextUpdatePromise ?= new Promise (resolve) =>
@resolveNextUpdatePromise = resolve
clearDocumentRequests: ->
@documentReaders = []
@documentWriters = []
@documentPollers = []
@documentUpdateRequested = false
@nextUpdatePromise = null
@resolveNextUpdatePromise = null
if @animationFrameRequest?
cancelAnimationFrame(@animationFrameRequest)
@animationFrameRequest = null
@stopPollingDocument()
requestDocumentUpdate: ->
unless @documentUpdateRequested
@documentUpdateRequested = true
requestAnimationFrame(@performDocumentUpdate)
@animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate)
performDocumentUpdate: =>
@documentUpdateRequested = false
resolveNextUpdatePromise = @resolveNextUpdatePromise
@animationFrameRequest = null
@nextUpdatePromise = null
@resolveNextUpdatePromise = null
writer() while writer = @documentWriters.shift()
@documentReadInProgress = true
@@ -220,6 +230,8 @@ class ViewRegistry
# process updates requested as a result of reads
writer() while writer = @documentWriters.shift()
resolveNextUpdatePromise?()
startPollingDocument: ->
window.addEventListener('resize', @requestDocumentPoll)
@observer.observe(document, {subtree: true, childList: true, attributes: true})
@@ -229,7 +241,7 @@ class ViewRegistry
@observer.disconnect()
requestDocumentPoll: =>
if @documentUpdateRequested
if @animationFrameRequest?
@performDocumentPollAfterUpdate = true
else
@debouncedPerformDocumentPoll()