Add highlights state to TextEditorPresenter

Signed-off-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Esse commit está contido em:
Nathan Sobo
2015-01-23 12:18:27 -08:00
commit 2f526c59c5
2 arquivos alterados com 319 adições e 29 exclusões
+226 -1
Ver Arquivo
@@ -20,7 +20,7 @@ describe "TextEditorPresenter", ->
expectValues = (actual, expected) ->
for key, value of expected
expect(actual[key]).toBe value
expect(actual[key]).toEqual value
expectStateUpdate = (presenter, fn) ->
updatedState = false
@@ -578,3 +578,228 @@ describe "TextEditorPresenter", ->
advanceClock(cursorBlinkResumeDelay)
advanceClock(cursorBlinkPeriod / 2)
expect(presenter.state.content.blinkCursorsOff).toBe true
describe ".highlights", ->
stateForHighlight = (presenter, decoration) ->
presenter.state.content.highlights[decoration.id]
stateForSelection = (presenter, selectionIndex) ->
selection = presenter.model.getSelections()[selectionIndex]
stateForHighlight(presenter, selection.decoration)
it "contains states for highlights that are visible on screen", ->
# off-screen above
marker1 = editor.markBufferRange([[0, 0], [1, 0]])
highlight1 = editor.decorateMarker(marker1, type: 'highlight', class: 'a')
# partially off-screen above, 1 of 2 regions on screen
marker2 = editor.markBufferRange([[1, 6], [2, 6]])
highlight2 = editor.decorateMarker(marker2, type: 'highlight', class: 'b')
# partially off-screen above, 2 of 3 regions on screen
marker3 = editor.markBufferRange([[0, 6], [3, 6]])
highlight3 = editor.decorateMarker(marker3, type: 'highlight', class: 'c')
# on-screen
marker4 = editor.markBufferRange([[2, 6], [4, 6]])
highlight4 = editor.decorateMarker(marker4, type: 'highlight', class: 'd')
# partially off-screen below, 2 of 3 regions on screen
marker5 = editor.markBufferRange([[3, 6], [6, 6]])
highlight5 = editor.decorateMarker(marker5, type: 'highlight', class: 'e')
# partially off-screen below, 1 of 3 regions on screen
marker6 = editor.markBufferRange([[5, 6], [7, 6]])
highlight6 = editor.decorateMarker(marker6, type: 'highlight', class: 'f')
# off-screen below
marker7 = editor.markBufferRange([[6, 6], [7, 6]])
highlight7 = editor.decorateMarker(marker7, type: 'highlight', class: 'g')
# on-screen, empty
marker8 = editor.markBufferRange([[2, 2], [2, 2]])
highlight8 = editor.decorateMarker(marker8, type: 'highlight', class: 'h')
presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10)
expect(stateForHighlight(presenter, highlight1)).toBeUndefined()
expectValues stateForHighlight(presenter, highlight2), {
class: 'b'
regions: [
{top: 2 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10}
]
}
expectValues stateForHighlight(presenter, highlight3), {
class: 'c'
regions: [
{top: 2 * 10, left: 0 * 10, right: 0, height: 1 * 10}
{top: 3 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10}
]
}
expectValues stateForHighlight(presenter, highlight4), {
class: 'd'
regions: [
{top: 2 * 10, left: 6 * 10, right: 0, height: 1 * 10}
{top: 3 * 10, left: 0, right: 0, height: 1 * 10}
{top: 4 * 10, left: 0, width: 6 * 10, height: 1 * 10}
]
}
expectValues stateForHighlight(presenter, highlight5), {
class: 'e'
regions: [
{top: 3 * 10, left: 6 * 10, right: 0, height: 1 * 10}
{top: 4 * 10, left: 0 * 10, right: 0, height: 2 * 10}
]
}
expectValues stateForHighlight(presenter, highlight6), {
class: 'f'
regions: [
{top: 5 * 10, left: 6 * 10, right: 0, height: 1 * 10}
]
}
expect(stateForHighlight(presenter, highlight7)).toBeUndefined()
expect(stateForHighlight(presenter, highlight8)).toBeUndefined()
it "updates when ::scrollTop changes", ->
editor.setSelectedBufferRanges([
[[6, 2], [6, 4]],
])
presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10)
expect(stateForSelection(presenter, 0)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setScrollTop(5 * 10)
expect(stateForSelection(presenter, 0)).toBeDefined()
expectStateUpdate presenter, -> presenter.setScrollTop(2 * 10)
expect(stateForSelection(presenter, 0)).toBeUndefined()
it "updates when ::clientHeight changes", ->
editor.setSelectedBufferRanges([
[[6, 2], [6, 4]],
])
presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10)
expect(stateForSelection(presenter, 0)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setClientHeight(60)
expect(stateForSelection(presenter, 0)).toBeDefined()
expectStateUpdate presenter, -> presenter.setClientHeight(20)
expect(stateForSelection(presenter, 0)).toBeUndefined()
it "updates when ::lineHeight changes", ->
editor.setSelectedBufferRanges([
[[2, 2], [2, 4]],
[[3, 4], [3, 6]],
])
presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10)
expectValues stateForSelection(presenter, 0), {
regions: [
{top: 2 * 10, left: 2 * 10, width: 2 * 10, height: 10}
]
}
expect(stateForSelection(presenter, 1)).toBeUndefined()
expectStateUpdate presenter, -> presenter.setLineHeight(5)
expectValues stateForSelection(presenter, 0), {
regions: [
{top: 2 * 5, left: 2 * 10, width: 2 * 10, height: 5}
]
}
expectValues stateForSelection(presenter, 1), {
regions: [
{top: 3 * 5, left: 4 * 10, width: 2 * 10, height: 5}
]
}
it "updates when ::baseCharacterWidth changes", ->
editor.setSelectedBufferRanges([
[[2, 2], [2, 4]],
])
presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10)
expectValues stateForSelection(presenter, 0), {
regions: [{top: 2 * 10, left: 2 * 10, width: 2 * 10, height: 10}]
}
expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20)
expectValues stateForSelection(presenter, 0), {
regions: [{top: 2 * 10, left: 2 * 20, width: 2 * 20, height: 10}]
}
it "updates when scoped character widths change", ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
runs ->
editor.setSelectedBufferRanges([
[[2, 4], [2, 6]],
])
presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10)
expectValues stateForSelection(presenter, 0), {
regions: [{top: 2 * 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
expectStateUpdate presenter, -> presenter.setScopedCharWidth(['source.js', 'keyword.control.js'], 'i', 20)
expectValues stateForSelection(presenter, 0), {
regions: [{top: 2 * 10, left: 4 * 10, width: 20 + 10, height: 10}]
}
it "updates when highlight decorations are added, moved, hidden, shown, or destroyed", ->
editor.setSelectedBufferRanges([
[[1, 2], [1, 4]],
[[3, 4], [3, 6]]
])
presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10)
expectValues stateForSelection(presenter, 0), {
regions: [{top: 1 * 10, left: 2 * 10, width: 2 * 10, height: 10}]
}
expect(stateForSelection(presenter, 1)).toBeUndefined()
# moving into view
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]])
expectValues stateForSelection(presenter, 1), {
regions: [{top: 2 * 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
# becoming empty
expectStateUpdate presenter, -> editor.getSelections()[1].clear()
expect(stateForSelection(presenter, 1)).toBeUndefined()
# becoming non-empty
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]])
expectValues stateForSelection(presenter, 1), {
regions: [{top: 2 * 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving out of view
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]])
expect(stateForSelection(presenter, 1)).toBeUndefined()
# adding
expectStateUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]])
expectValues stateForSelection(presenter, 2), {
regions: [{top: 1 * 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving added selection
expectStateUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]])
expectValues stateForSelection(presenter, 2), {
regions: [{top: 1 * 10, left: 4 * 10, width: 4 * 10, height: 10}]
}
# destroying
destroyedSelection = editor.getSelections()[2]
expectStateUpdate presenter, -> destroyedSelection.destroy()
expect(stateForHighlight(presenter, destroyedSelection.decoration)).toBeUndefined()
+93 -28
Ver Arquivo
@@ -29,34 +29,25 @@ class TextEditorPresenter
@disposables.add @model.onDidChangeMini(@updateLinesState.bind(this))
@disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
@disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
@observeDecoration(decoration) for decoration in @model.getLineDecorations()
@observeLineDecoration(decoration) for decoration in @model.getLineDecorations()
@observeHighlightDecoration(decoration) for decoration in @model.getHighlightDecorations()
@observeCursor(cursor) for cursor in @model.getCursors()
observeConfig: ->
@disposables.add atom.config.onDidChange 'editor.showIndentGuide', scope: @model.getRootScopeDescriptor(), @updateContentState.bind(this)
buildState: ->
@state = {}
@buildContentState()
@buildLinesState()
@buildCursorsState()
buildContentState: ->
@state.content = {}
@updateContentState()
buildLinesState: ->
@state.content.lines = {}
@updateLinesState()
buildCursorsState: ->
@state.content.blinkCursorsOff = false
@state.content.cursors = {}
@updateCursorsState()
@state =
content:
lines: {}
blinkCursorsOff: false
@updateState()
updateState: ->
@updateContentState()
@updateLinesState()
@updateCursorsState()
@updateHighlightsState()
updateContentState: ->
@state.content.scrollWidth = @computeScrollWidth()
@@ -108,20 +99,79 @@ class TextEditorPresenter
updateCursorsState: ->
startRow = @getStartRow()
endRow = @getEndRow()
visibleCursors = {}
@state.content.cursors = {}
for cursor in @model.getCursors()
if cursor.isVisible() and startRow <= cursor.getScreenRow() < endRow
pixelRect = @pixelRectForScreenRange(cursor.getScreenRange())
pixelRect.width = @getBaseCharacterWidth() if pixelRect.width is 0
@state.content.cursors[cursor.id] = pixelRect
visibleCursors[cursor.id] = true
for id of @state.content.cursors
delete @state.content.cursors[id] unless visibleCursors.hasOwnProperty(id)
@emitter.emit 'did-update-state'
updateHighlightsState: ->
startRow = @getStartRow()
endRow = @getEndRow()
@state.content.highlights = {}
for decoration in @model.getHighlightDecorations()
screenRange = decoration.getMarker().getScreenRange()
if screenRange.intersectsRowRange(startRow, endRow - 1)
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
continue if screenRange.isEmpty()
@state.content.highlights[decoration.id] =
class: decoration.getProperties().class
regions: @buildHighlightRegions(screenRange)
buildHighlightRegions: (screenRange) ->
lineHeightInPixels = @getLineHeight()
startPixelPosition = @pixelPositionForScreenPosition(screenRange.start, true)
endPixelPosition = @pixelPositionForScreenPosition(screenRange.end, true)
spannedRows = screenRange.end.row - screenRange.start.row + 1
if spannedRows is 1
[
top: startPixelPosition.top
height: lineHeightInPixels
left: startPixelPosition.left
width: endPixelPosition.left - startPixelPosition.left
]
else
regions = []
# First row, extending from selection start to the right side of screen
regions.push(
top: startPixelPosition.top
left: startPixelPosition.left
height: lineHeightInPixels
right: 0
)
# Middle rows, extending from left side to right side of screen
if spannedRows > 2
regions.push(
top: startPixelPosition.top + lineHeightInPixels
height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels
left: 0
right: 0
)
# Last row, extending from left side of screen to selection end
if screenRange.end.column > 0
regions.push(
top: endPixelPosition.top
height: lineHeightInPixels
left: 0
width: endPixelPosition.left
)
regions
getStartRow: ->
startRow = Math.floor(@getScrollTop() / @getLineHeight()) - @lineOverdrawMargin
Math.max(0, startRow)
@@ -168,6 +218,7 @@ class TextEditorPresenter
@updateContentState()
@updateLinesState()
@updateCursorsState()
@updateHighlightsState()
getScrollTop: -> @scrollTop
@@ -179,6 +230,7 @@ class TextEditorPresenter
setClientHeight: (@clientHeight) ->
@updateLinesState()
@updateCursorsState()
@updateHighlightsState()
getClientHeight: ->
@clientHeight ? @model.getScreenLineCount() * @getLineHeight()
@@ -193,13 +245,12 @@ class TextEditorPresenter
@updateContentState()
@updateLinesState()
@updateCursorsState()
@updateHighlightsState()
getLineHeight: -> @lineHeight
setBaseCharacterWidth: (@baseCharacterWidth) ->
@updateContentState()
@updateLinesState()
@updateCursorsState()
@characterWidthsChanged()
getBaseCharacterWidth: -> @baseCharacterWidth
@@ -230,6 +281,7 @@ class TextEditorPresenter
@updateContentState()
@updateLinesState()
@updateCursorsState()
@updateHighlightsState()
clearScopedCharWidths: ->
@charWidthsByScope = {}
@@ -278,7 +330,7 @@ class TextEditorPresenter
{top, left, width, height}
observeDecoration: (decoration) ->
observeLineDecoration: (decoration) ->
markerDidChangeDisposable = decoration.getMarker().onDidChange(@updateLinesState.bind(this))
didDestroyDisposable = decoration.onDidDestroy =>
@disposables.remove(markerDidChangeDisposable)
@@ -288,10 +340,23 @@ class TextEditorPresenter
@disposables.add(markerDidChangeDisposable)
@disposables.add(didDestroyDisposable)
observeHighlightDecoration: (decoration) ->
markerDidChangeDisposable = decoration.getMarker().onDidChange(@updateHighlightsState.bind(this))
didDestroyDisposable = decoration.onDidDestroy =>
@disposables.remove(markerDidChangeDisposable)
@disposables.remove(didDestroyDisposable)
@updateHighlightsState()
@disposables.add(markerDidChangeDisposable)
@disposables.add(didDestroyDisposable)
didAddDecoration: (decoration) ->
if decoration.isType('line')
@observeDecoration(decoration)
@observeLineDecoration(decoration)
@updateLinesState()
else if decoration.isType('highlight')
@observeHighlightDecoration(decoration)
@updateHighlightsState()
observeCursor: (cursor) ->
didChangePositionDisposable = cursor.onDidChangePosition =>