diff --git a/package.json b/package.json index 791f63daf..159eea698 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.22", "marked": "^0.3.4", + "marker-index": "^3.0.4", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 6b4ac80ba..cf8d37a74 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -14,7 +14,6 @@ cloneObject = (object) -> module.exports = class LinesTileComponent constructor: ({@presenter, @id, @domElementPool, @assert, grammars}) -> - @tokenIterator = new TokenIterator(grammarRegistry: grammars) @measuredLines = new Set @lineNodesByLineId = {} @screenRowsByLineId = {} @@ -125,8 +124,7 @@ class LinesTileComponent screenRowForNode: (node) -> parseInt(node.dataset.screenRow) buildLineNode: (id) -> - {width} = @newState - {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id] + {screenRow, words, decorationClasses} = @newTileState.lines[id] lineNode = @domElementPool.buildElement("div", "line") lineNode.dataset.screenRow = screenRow @@ -136,13 +134,13 @@ class LinesTileComponent lineNode.classList.add(decorationClass) @currentLineTextNodes = [] - if text is "" - @setEmptyLineInnerNodes(id, lineNode) - else - @setLineInnerNodes(id, lineNode) + # if words.length is 0 + # @setEmptyLineInnerNodes(id, lineNode) + + @setLineInnerNodes(id, lineNode) @textNodesByLineId[id] = @currentLineTextNodes - lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold + # lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold lineNode setEmptyLineInnerNodes: (id, lineNode) -> @@ -184,48 +182,61 @@ class LinesTileComponent @currentLineTextNodes.push(textNode) setLineInnerNodes: (id, lineNode) -> - lineState = @newTileState.lines[id] - {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState - lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0 + {words} = @newTileState.lines[id] + lineLength = 0 + for word in words + lineLength += word.length + textNode = @domElementPool.buildText(word) + lineNode.appendChild(textNode) + @currentLineTextNodes.push(textNode) - @tokenIterator.reset(lineState) - openScopeNode = lineNode + if lineLength is 0 + textNode = @domElementPool.buildText('\u00a0') + lineNode.appendChild(textNode) + @currentLineTextNodes.push(textNode) - while @tokenIterator.next() - for scope in @tokenIterator.getScopeEnds() - openScopeNode = openScopeNode.parentElement - - for scope in @tokenIterator.getScopeStarts() - newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' ')) - openScopeNode.appendChild(newScopeNode) - openScopeNode = newScopeNode - - tokenStart = @tokenIterator.getScreenStart() - tokenEnd = @tokenIterator.getScreenEnd() - tokenText = @tokenIterator.getText() - isHardTab = @tokenIterator.isHardTab() - - if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex - tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart - else - tokenFirstNonWhitespaceIndex = null - - if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex - tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart) - else - tokenFirstTrailingWhitespaceIndex = null - - hasIndentGuide = - @newState.indentGuidesVisible and - (hasLeadingWhitespace or lineIsWhitespaceOnly) - - hasInvisibleCharacters = - (invisibles?.tab and isHardTab) or - (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace)) - - @appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode) - - @appendEndOfLineNodes(id, lineNode) + # lineState = @newTileState.lines[id] + # {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState + # lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0 + # + # @tokenIterator.reset(lineState) + # openScopeNode = lineNode + # + # while @tokenIterator.next() + # for scope in @tokenIterator.getScopeEnds() + # openScopeNode = openScopeNode.parentElement + # + # for scope in @tokenIterator.getScopeStarts() + # newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' ')) + # openScopeNode.appendChild(newScopeNode) + # openScopeNode = newScopeNode + # + # tokenStart = @tokenIterator.getScreenStart() + # tokenEnd = @tokenIterator.getScreenEnd() + # tokenText = @tokenIterator.getText() + # isHardTab = @tokenIterator.isHardTab() + # + # if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex + # tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart + # else + # tokenFirstNonWhitespaceIndex = null + # + # if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex + # tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart) + # else + # tokenFirstTrailingWhitespaceIndex = null + # + # hasIndentGuide = + # @newState.indentGuidesVisible and + # (hasLeadingWhitespace or lineIsWhitespaceOnly) + # + # hasInvisibleCharacters = + # (invisibles?.tab and isHardTab) or + # (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace)) + # + # @appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode) + # + # @appendEndOfLineNodes(id, lineNode) appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) -> if isHardTab diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 40ea95514..5d2b2c335 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1,5 +1,6 @@ {CompositeDisposable, Disposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' +MarkerIndex = require 'marker-index' _ = require 'underscore-plus' Decoration = require './decoration' @@ -16,6 +17,7 @@ class TextEditorPresenter {@model, @config} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params {@contentFrameWidth} = params + @tokenIterator = @model.displayLayer.buildTokenIterator() @gutterWidth = 0 @tileSize ?= 6 @@ -23,6 +25,9 @@ class TextEditorPresenter @realScrollLeft = @scrollLeft @disposables = new CompositeDisposable @emitter = new Emitter + @lineIdCounter = 1 + @linesById = new Map + @lineMarkerIndex = new MarkerIndex @visibleHighlights = {} @characterWidthsByScope = {} @lineDecorationsByScreenRow = {} @@ -82,6 +87,8 @@ class TextEditorPresenter @updateCommonGutterState() @updateReflowState() + @updateLines() + if @shouldUpdateDecorations @fetchDecorations() @updateLineDecorations() @@ -126,7 +133,8 @@ class TextEditorPresenter @shouldUpdateDecorations = true observeModel: -> - @disposables.add @model.onDidChange => + @disposables.add @model.displayLayer.onDidChangeTextSync (change) => + @invalidateLines(change) @shouldUpdateDecorations = true @emitDidUpdateState() @@ -375,7 +383,7 @@ class TextEditorPresenter tileState.lines ?= {} visibleLineIds = {} for screenRow in screenRows - line = @model.tokenizedLineForScreenRow(screenRow) + line = @lineForScreenRow(screenRow) unless line? throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") @@ -387,18 +395,7 @@ class TextEditorPresenter else tileState.lines[line.id] = screenRow: screenRow - text: line.text - openScopes: line.openScopes - tags: line.tags - specialTokens: line.specialTokens - firstNonWhitespaceIndex: line.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex - invisibles: line.invisibles - endOfLineInvisibles: line.endOfLineInvisibles - isOnlyWhitespace: line.isOnlyWhitespace() - indentLevel: line.indentLevel - tabLength: line.tabLength - fold: line.fold + words: line.words decorationClasses: @lineDecorationClassesForRow(screenRow) for id, line of tileState.lines @@ -1010,6 +1007,46 @@ class TextEditorPresenter rect.height = Math.round(rect.height) rect + updateLines: -> + visibleLineIds = new Set + + for screenRow in @getScreenRows() + screenRowStart = Point(screenRow, 0) + lineIds = @lineMarkerIndex.findStartingAt(screenRowStart) + if lineIds.size is 0 + line = @buildLine(screenRow) + @lineMarkerIndex.insert(line.id, screenRowStart, Point(screenRow, Infinity)) + @linesById.set(line.id, line) + visibleLineIds.add(line.id) + else + lineIds.forEach (id) -> + visibleLineIds.add(id) + + @linesById.forEach (line, lineId) => + unless visibleLineIds.has(lineId) + @lineMarkerIndex.delete(lineId) + @linesById.delete(lineId) + + buildLine: (screenRow) -> + line = {id: @lineIdCounter++, words: []} + @tokenIterator.seekToScreenRow(screenRow) + loop + line.words.push(@tokenIterator.getText()) + break unless @tokenIterator.moveToSuccessor() + break unless @tokenIterator.getStartScreenPosition().row is screenRow + line + + invalidateLines: ({start, replacedExtent, replacementExtent}) -> + {touch} = @lineMarkerIndex.splice(start, replacedExtent, replacementExtent) + touch.forEach (lineId) => + @lineMarkerIndex.delete(lineId) + @linesById.delete(lineId) + + lineForScreenRow: (screenRow) -> + lineIds = @lineMarkerIndex.findStartingAt(Point(screenRow, 0)) + lineId = lineIds.values().next().value + @linesById.get(lineId) + fetchDecorations: -> return unless 0 <= @startRow <= @endRow <= Infinity @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) @@ -1236,7 +1273,7 @@ class TextEditorPresenter startBlinkingCursors: -> unless @isCursorBlinking() @state.content.cursorsVisible = true - @toggleCursorBlinkHandle = setInterval(@toggleCursorBlink.bind(this), @getCursorBlinkPeriod() / 2) + # @toggleCursorBlinkHandle = setInterval(@toggleCursorBlink.bind(this), @getCursorBlinkPeriod() / 2) isCursorBlinking: -> @toggleCursorBlinkHandle? diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 983669e28..6a13439c8 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -117,6 +117,7 @@ class TextEditor extends Model @config, @assert, @grammarRegistry, @packageManager }) @buffer = @displayBuffer.buffer + @displayLayer = buffer.addDisplayLayer({tabLength}) @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) for marker in @selectionsMarkerLayer.getMarkers()