From c8b58761babbe0d65f7a6326eee3f7bdb1b00332 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 22 Jan 2015 10:55:01 -0700 Subject: [PATCH] Add TextEditorPresenter::state.content.cursors --- spec/text-editor-presenter-spec.coffee | 129 +++++++++++++++++++++++++ src/text-editor-presenter.coffee | 73 ++++++++++++-- 2 files changed, 193 insertions(+), 9 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index fb94116a6..a63579c56 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -385,3 +385,132 @@ describe "TextEditorPresenter", -> editor.setMini(true) expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull() + + describe ".cursors", -> + stateForCursor = (presenter, cursorIndex) -> + presenter.state.content.cursors[presenter.model.getCursors()[cursorIndex].id] + + it "contains pixelRects for empty selections that are visible on screen", -> + editor.setSelectedBufferRanges([ + [[1, 2], [1, 2]], + [[2, 4], [2, 4]], + [[3, 4], [3, 5]] + [[5, 12], [5, 12]], + [[8, 4], [8, 4]] + ]) + presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + expect(stateForCursor(presenter, 0)).toBeUndefined() + expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 4)).toBeUndefined() + + it "updates when ::scrollTop changes", -> + editor.setSelectedBufferRanges([ + [[1, 2], [1, 2]], + [[2, 4], [2, 4]], + [[3, 4], [3, 5]] + [[5, 12], [5, 12]], + [[8, 4], [8, 4]] + ]) + presenter = new TextEditorPresenter(model: editor, clientHeight: 30, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + presenter.setScrollTop(5 * 10) + expect(stateForCursor(presenter, 0)).toBeUndefined() + expect(stateForCursor(presenter, 1)).toBeUndefined() + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10} + + it "updates when ::clientHeight changes", -> + editor.setSelectedBufferRanges([ + [[1, 2], [1, 2]], + [[2, 4], [2, 4]], + [[3, 4], [3, 5]] + [[5, 12], [5, 12]], + [[8, 4], [8, 4]] + ]) + presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + presenter.setClientHeight(30) + expect(stateForCursor(presenter, 0)).toBeUndefined() + expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 4)).toBeUndefined() + + it "updates when ::lineHeight changes", -> + editor.setSelectedBufferRanges([ + [[1, 2], [1, 2]], + [[2, 4], [2, 4]], + [[3, 4], [3, 5]] + [[5, 12], [5, 12]], + [[8, 4], [8, 4]] + ]) + presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + presenter.setLineHeight(5) + expect(stateForCursor(presenter, 0)).toBeUndefined() + expect(stateForCursor(presenter, 1)).toBeUndefined() + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 5, left: 12 * 10, width: 10, height: 5} + expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5, left: 4 * 10, width: 10, height: 5} + + it "updates when ::baseCharacterWidth changes", -> + editor.setCursorBufferPosition([2, 4]) + presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + presenter.setBaseCharacterWidth(20) + expect(stateForCursor(presenter, 0)).toEqual {top: 2 * 10, left: 4 * 20, width: 20, height: 10} + + it "updates when scoped character widths change", -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + runs -> + editor.setCursorBufferPosition([1, 4]) + presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + presenter.setScopedCharWidth(['source.js', 'storage.modifier.js'], 'v', 20) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} + + presenter.setScopedCharWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} + + it "updates when cursors are added, moved, hidden, shown, or destroyed", -> + editor.setSelectedBufferRanges([ + [[1, 2], [1, 2]], + [[3, 4], [3, 5]] + ]) + presenter = new TextEditorPresenter(model: editor, clientHeight: 20, scrollTop: 20, lineHeight: 10, lineOverdrawMargin: 0, baseCharacterWidth: 10) + + # moving into view + expect(stateForCursor(presenter, 0)).toBeUndefined() + editor.getCursors()[0].setBufferPosition([2, 4]) + expect(stateForCursor(presenter, 0)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + + # showing + editor.getSelections()[1].clear() + expect(stateForCursor(presenter, 1)).toEqual {top: 3 * 10, left: 5 * 10, width: 10, height: 10} + + # hiding + editor.getSelections()[1].setBufferRange([[3, 4], [3, 5]]) + expect(stateForCursor(presenter, 1)).toBeUndefined() + + # moving out of view + editor.getCursors()[0].setBufferPosition([10, 4]) + expect(stateForCursor(presenter, 0)).toBeUndefined() + + # adding + editor.addCursorAtBufferPosition([4, 4]) + expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 4 * 10, width: 10, height: 10} + + # moving added cursor + editor.getCursors()[2].setBufferPosition([4, 6]) + expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 6 * 10, width: 10, height: 10} + + # destroying + destroyedCursor = editor.getCursors()[2] + destroyedCursor.destroy() + expect(presenter.state.content.cursors[destroyedCursor.id]).toBeUndefined() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 0e38845d4..78f8a0b3a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -17,9 +17,11 @@ class TextEditorPresenter @disposables.add @model.onDidChange(@updateState.bind(this)) @disposables.add @model.onDidChangeSoftWrapped(@updateState.bind(this)) @disposables.add @model.onDidChangeGrammar(@updateContentState.bind(this)) - @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this)) @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() + @observeCursor(cursor) for cursor in @model.getCursors() observeConfig: -> @disposables.add atom.config.onDidChange 'editor.showIndentGuide', scope: @model.getRootScopeDescriptor(), @updateContentState.bind(this) @@ -28,6 +30,7 @@ class TextEditorPresenter @state = {} @buildContentState() @buildLinesState() + @buildCursorsState() buildContentState: -> @state.content = {} @@ -37,6 +40,10 @@ class TextEditorPresenter @state.content.lines = {} @updateLinesState() + buildCursorsState: -> + @state.content.cursors = {} + @updateCursorsState() + updateState: -> @updateContentState() @updateLinesState() @@ -85,6 +92,19 @@ class TextEditorPresenter top: row * @getLineHeight() decorationClasses: @lineDecorationClassesForRow(row) + updateCursorsState: -> + startRow = @getStartRow() + endRow = @getEndRow() + visibleCursors = {} + + for cursor in @model.getCursors() + if cursor.isVisible() and startRow <= cursor.getScreenRow() < endRow + @state.content.cursors[cursor.id] = @pixelRectForScreenRange(cursor.getScreenRange()) + visibleCursors[cursor.id] = true + + for id of @state.content.cursors + delete @state.content.cursors[id] unless visibleCursors.hasOwnProperty(id) + getStartRow: -> startRow = Math.floor(@getScrollTop() / @getLineHeight()) - @lineOverdrawMargin Math.max(0, startRow) @@ -126,6 +146,7 @@ class TextEditorPresenter setScrollTop: (@scrollTop) -> @updateContentState() @updateLinesState() + @updateCursorsState() getScrollTop: -> @scrollTop @@ -136,6 +157,7 @@ class TextEditorPresenter setClientHeight: (@clientHeight) -> @updateLinesState() + @updateCursorsState() getClientHeight: -> @clientHeight ? @model.getScreenLineCount() * @getLineHeight() @@ -149,12 +171,14 @@ class TextEditorPresenter setLineHeight: (@lineHeight) -> @updateContentState() @updateLinesState() + @updateCursorsState() getLineHeight: -> @lineHeight setBaseCharacterWidth: (@baseCharacterWidth) -> @updateContentState() @updateLinesState() + @updateCursorsState() getBaseCharacterWidth: -> @baseCharacterWidth @@ -184,6 +208,7 @@ class TextEditorPresenter characterWidthsChanged: -> @updateContentState() @updateLinesState() + @updateCursorsState() clearScopedCharWidths: -> @charWidthsByScope = {} @@ -194,9 +219,9 @@ class TextEditorPresenter targetRow = screenPosition.row targetColumn = screenPosition.column - baseCharacterWidth = @baseCharacterWidth + baseCharacterWidth = @getBaseCharacterWidth() - top = targetRow * @lineHeightInPixels + top = targetRow * @getLineHeight() left = 0 column = 0 for token in @model.tokenizedLineForScreenRow(targetRow).tokens @@ -219,17 +244,47 @@ class TextEditorPresenter column += charLength {top, left} + pixelRectForScreenRange: (screenRange) -> + if screenRange.end.row > screenRange.start.row + top = @pixelPositionForScreenPosition(screenRange.start).top + left = 0 + height = (screenRange.end.row - screenRange.start.row + 1) * @getLineHeight() + width = @getScrollWidth() + else + {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) + height = @getLineHeight() + width = @pixelPositionForScreenPosition(screenRange.end, false).left - left + + {top, left, width, height} + observeDecoration: (decoration) -> - markerChangeDisposable = decoration.getMarker().onDidChange(@updateLinesState.bind(this)) - destroyDisposable = decoration.onDidDestroy => - @disposables.remove(markerChangeDisposable) - @disposables.remove(destroyDisposable) + markerDidChangeDisposable = decoration.getMarker().onDidChange(@updateLinesState.bind(this)) + didDestroyDisposable = decoration.onDidDestroy => + @disposables.remove(markerDidChangeDisposable) + @disposables.remove(didDestroyDisposable) @updateLinesState() - @disposables.add(markerChangeDisposable) - @disposables.add(destroyDisposable) + @disposables.add(markerDidChangeDisposable) + @disposables.add(didDestroyDisposable) didAddDecoration: (decoration) -> if decoration.isType('line') @observeDecoration(decoration) @updateLinesState() + + observeCursor: (cursor) -> + didChangePositionDisposable = cursor.onDidChangePosition(@updateCursorsState.bind(this)) + didChangeVisibilityDisposable = cursor.onDidChangeVisibility(@updateCursorsState.bind(this)) + didDestroyDisposable = cursor.onDidDestroy => + @disposables.remove(didChangePositionDisposable) + @disposables.remove(didChangeVisibilityDisposable) + @disposables.remove(didDestroyDisposable) + @updateCursorsState() + + @disposables.add(didChangePositionDisposable) + @disposables.add(didChangeVisibilityDisposable) + @disposables.add(didDestroyDisposable) + + didAddCursor: (cursor) -> + @observeCursor(cursor) + @updateCursorsState()