From b5c9a90ae00ec44e91782b0d6e30be7ca2fea11c Mon Sep 17 00:00:00 2001 From: Einar Boson Date: Sun, 5 Jul 2015 01:04:15 +0200 Subject: [PATCH 001/262] :bug: Ask user to 'save as' if save fails when closing tab, fixes #7708 --- spec/pane-spec.coffee | 98 +++++++++++++++++++++++++++++++++++++++++++ src/pane.coffee | 58 +++++++++++++++++-------- 2 files changed, 139 insertions(+), 17 deletions(-) diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 3453ac44d..bdeb9615a 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -671,6 +671,104 @@ describe "Pane", -> expect(item1.save).not.toHaveBeenCalled() expect(pane.isDestroyed()).toBe false + it "does not destroy the pane if save fails and user clicks cancel", -> + pane = new Pane(items: [new Item("A"), new Item("B")]) + [item1, item2] = pane.getItems() + + item1.shouldPromptToSave = -> true + item1.getURI = -> "/test/path" + + item1.save = jasmine.createSpy("save").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + confirmations = 0 + spyOn(atom, 'confirm').andCallFake -> + confirmations++ + if confirmations is 1 + return 0 + else + return 1 + + pane.close() + + expect(atom.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(item1.save).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe false + + it "does destroy the pane if the user saves the file under a new name", -> + pane = new Pane(items: [new Item("A"), new Item("B")]) + [item1, item2] = pane.getItems() + + item1.shouldPromptToSave = -> true + item1.getURI = -> "/test/path" + + item1.save = jasmine.createSpy("save").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + item1.saveAs = jasmine.createSpy("saveAs").andReturn(true) + + confirmations = 0 + spyOn(atom, 'confirm').andCallFake -> + confirmations++ + return 0 + + spyOn(atom, 'showSaveDialogSync').andReturn("new/path") + + pane.close() + + expect(atom.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(atom.showSaveDialogSync).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true + + it "asks again if the saveAs also fails", -> + pane = new Pane(items: [new Item("A"), new Item("B")]) + [item1, item2] = pane.getItems() + + item1.shouldPromptToSave = -> true + item1.getURI = -> "/test/path" + + item1.save = jasmine.createSpy("save").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + item1.saveAs = jasmine.createSpy("saveAs").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + + confirmations = 0 + spyOn(atom, 'confirm').andCallFake -> + confirmations++ + if confirmations < 3 + return 0 + return 2 + + spyOn(atom, 'showSaveDialogSync').andReturn("new/path") + + pane.close() + + expect(atom.confirm).toHaveBeenCalled() + expect(confirmations).toBe(3) + expect(atom.showSaveDialogSync).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true + + describe "::destroy()", -> [container, pane1, pane2] = [] diff --git a/src/pane.coffee b/src/pane.coffee index f77da9d58..759146564 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -454,13 +454,26 @@ class Pane extends Model else return true + saveError = (error) => + if error + chosen = atom.confirm + message: @getSaveErrorMessage(error) + detailedMessage: "Your changes will be lost if you close this item without saving." + buttons: ["Save as", "Cancel", "Don't save"] + switch chosen + when 0 then @saveItemAs item, saveError + when 1 then false + when 2 then true + else + true + chosen = atom.confirm message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?" detailedMessage: "Your changes will be lost if you close this item without saving." buttons: ["Save", "Cancel", "Don't Save"] switch chosen - when 0 then @saveItem(item, -> true) + when 0 then @saveItem(item, saveError) when 1 then false when 2 then true @@ -479,8 +492,10 @@ class Pane extends Model # Public: Save the given item. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is - # successfully saved. + # * `nextAction` (optional) {Function} which will be called with no argument + # after the item is successfully saved, or with the error if it failed. + # The return value will be that of `nextAction` or `undefined` if it was not + # provided saveItem: (item, nextAction) -> if typeof item?.getURI is 'function' itemURI = item.getURI() @@ -490,9 +505,9 @@ class Pane extends Model if itemURI? try item.save?() + nextAction?() catch error - @handleSaveError(error) - nextAction?() + (nextAction ? @handleSaveError)(error) else @saveItemAs(item, nextAction) @@ -500,8 +515,10 @@ class Pane extends Model # path they select. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is - # successfully saved. + # * `nextAction` (optional) {Function} which will be called with no argument + # after the item is successfully saved, or with the error if it failed. + # The return value will be that of `nextAction` or `undefined` if it was not + # provided saveItemAs: (item, nextAction) -> return unless item?.saveAs? @@ -511,9 +528,9 @@ class Pane extends Model if newItemPath try item.saveAs(newItemPath) + nextAction?() catch error - @handleSaveError(error) - nextAction?() + (nextAction ? @handleSaveError)(error) # Public: Save all items. saveItems: -> @@ -676,22 +693,29 @@ class Pane extends Model return false unless @promptToSaveItem(item) true - handleSaveError: (error) -> + # Translate an error object to a human readable string + getSaveErrorMessage: (error) -> if error.code is 'EISDIR' or error.message.endsWith('is a directory') - atom.notifications.addWarning("Unable to save file: #{error.message}") + "Unable to save file: #{error.message}." else if error.code is 'EACCES' and error.path? - atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'") + "Unable to save file: Permission denied '#{error.path}'." else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] and error.path? - atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message) + "Unable to save file '#{error.path}': #{error.message}." else if error.code is 'EROFS' and error.path? - atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'") + "Unable to save file: Read-only file system '#{error.path}'." else if error.code is 'ENOSPC' and error.path? - atom.notifications.addWarning("Unable to save file: No space left on device '#{error.path}'") + "Unable to save file: No space left on device '#{error.path}'." else if error.code is 'ENXIO' and error.path? - atom.notifications.addWarning("Unable to save file: No such device or address '#{error.path}'") + "Unable to save file: No such device or address '#{error.path}'." else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) fileName = errorMatch[1] - atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") + "Unable to save file: A directory in the path '#{fileName}' could not be written to." + + # Display a popup warning to the user + handleSaveError: (error) -> + errorMessage = Pane::getSaveErrorMessage(error) + if errorMessage? + atom.notifications.addWarning(errorMessage) else throw error From 924d880fa804560ae98859039910d8edcede1475 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Dec 2015 13:55:29 -0700 Subject: [PATCH 002/262] WIP: Start rendering lines from DisplayLayers --- package.json | 1 + src/lines-tile-component.coffee | 107 +++++++++++++++++-------------- src/text-editor-presenter.coffee | 67 ++++++++++++++----- src/text-editor.coffee | 1 + 4 files changed, 113 insertions(+), 63 deletions(-) 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() From b5f9ed2b0e447d5f42f2ac043697e95c86a09def Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 16 Dec 2015 17:28:06 -0700 Subject: [PATCH 003/262] Fix pixelPositionForScreenPosition --- src/lines-component.coffee | 12 ++++++ src/lines-tile-component.coffee | 9 +++++ src/lines-yardstick.coffee | 69 ++++++++++----------------------- 3 files changed, 41 insertions(+), 49 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index b5af56885..824655579 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -104,3 +104,15 @@ class LinesComponent extends TiledComponent textNodesForLineIdAndScreenRow: (lineId, screenRow) -> tile = @presenter.tileForRow(screenRow) @getComponentForTile(tile)?.textNodesForLineId(lineId) + + lineIdForScreenRow: (screenRow) -> + tile = @presenter.tileForRow(screenRow) + @getComponentForTile(tile)?.lineIdForScreenRow(screenRow) + + lineNodeForScreenRow: (screenRow) -> + tile = @presenter.tileForRow(screenRow) + @getComponentForTile(tile)?.lineNodeForScreenRow(screenRow) + + textNodesForScreenRow: (screenRow) -> + tile = @presenter.tileForRow(screenRow) + @getComponentForTile(tile)?.textNodesForScreenRow(screenRow) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index cf8d37a74..92d020456 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -361,3 +361,12 @@ class LinesTileComponent textNodesForLineId: (lineId) -> @textNodesByLineId[lineId].slice() + + lineIdForScreenRow: (screenRow) -> + @lineIdsByScreenRow[screenRow] + + lineNodeForScreenRow: (screenRow) -> + @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] + + textNodesForScreenRow: (screenRow) -> + @textNodesByLineId[@lineIdsByScreenRow[screenRow]]?.slice() diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 9edbbe17a..85d1e23d4 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -9,7 +9,7 @@ class LinesYardstick @invalidateCache() invalidateCache: -> - @pixelPositionsByLineIdAndColumn = {} + @leftPixelPositionCache = {} measuredRowForPixelPosition: (pixelPosition) -> targetTop = pixelPosition.top @@ -87,61 +87,32 @@ class LinesYardstick {top, left} leftPixelPositionForScreenPosition: (row, column) -> - line = @model.tokenizedLineForScreenRow(row) - lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) + lineNode = @lineNodesProvider.lineNodeForScreenRow(row) + lineId = @lineNodesProvider.lineIdForScreenRow(row) - return 0 unless line? and lineNode? + return 0 unless lineNode? - if cachedPosition = @pixelPositionsByLineIdAndColumn[line.id]?[column] + if cachedPosition = @leftPixelPositionCache[lineId]?[column] return cachedPosition - textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row) - indexWithinTextNode = null - charIndex = 0 + textNodes = @lineNodesProvider.textNodesForScreenRow(row) + textNodeStartIndex = 0 - @tokenIterator.reset(line, false) - while @tokenIterator.next() - break if foundIndexWithinTextNode? - - text = @tokenIterator.getText() - - textIndex = 0 - while textIndex < text.length - if @tokenIterator.isPairedCharacter() - char = text - charLength = 2 - textIndex += 2 - else - char = text[textIndex] - charLength = 1 - textIndex++ - - unless textNode? - textNode = textNodes.shift() - textNodeLength = textNode.textContent.length - textNodeIndex = 0 - nextTextNodeIndex = textNodeLength - - while nextTextNodeIndex <= charIndex - textNode = textNodes.shift() - textNodeLength = textNode.textContent.length - textNodeIndex = nextTextNodeIndex - nextTextNodeIndex = textNodeIndex + textNodeLength - - if charIndex is column - foundIndexWithinTextNode = charIndex - textNodeIndex - break - - charIndex += charLength + for textNode in textNodes + textNodeEndIndex = textNodeStartIndex + textNode.textContent.length + if textNodeEndIndex > column + indexInTextNode = column - textNodeStartIndex + break + else + textNodeStartIndex = textNodeEndIndex if textNode? - foundIndexWithinTextNode ?= textNode.textContent.length - position = @leftPixelPositionForCharInTextNode( - lineNode, textNode, foundIndexWithinTextNode - ) - @pixelPositionsByLineIdAndColumn[line.id] ?= {} - @pixelPositionsByLineIdAndColumn[line.id][column] = position - position + indexInTextNode ?= textNode.textContent.length + leftPixelPosition = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexInTextNode) + + @leftPixelPositionCache[lineId] ?= {} + @leftPixelPositionCache[lineId][column] = leftPixelPosition + leftPixelPosition else 0 From d5b204226feb83b1aabcb92d432477cc8c5b8aae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 16 Dec 2015 19:12:31 -0700 Subject: [PATCH 004/262] Correctly pass tabLength to addDisplayLayer --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6a13439c8..ccec60c85 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -117,7 +117,7 @@ class TextEditor extends Model @config, @assert, @grammarRegistry, @packageManager }) @buffer = @displayBuffer.buffer - @displayLayer = buffer.addDisplayLayer({tabLength}) + @displayLayer = buffer.addDisplayLayer({tabLength: @displayBuffer.getTabLength()}) @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) for marker in @selectionsMarkerLayer.getMarkers() From 8e1a772a2452f613adad193c6ec7aa4839d21509 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 16 Dec 2015 19:12:54 -0700 Subject: [PATCH 005/262] Replace spaces w/ non-breaking spaces when rendering text nodes --- src/lines-tile-component.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 92d020456..f4712dfb6 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -4,6 +4,7 @@ HighlightsComponent = require './highlights-component' TokenIterator = require './token-iterator' AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} TokenTextEscapeRegex = /[&"'<>]/g +NBSPCharacter = '\u00a0' MaxTokenLength = 20000 cloneObject = (object) -> @@ -184,14 +185,14 @@ class LinesTileComponent setLineInnerNodes: (id, lineNode) -> {words} = @newTileState.lines[id] lineLength = 0 - for word in words + for word in words when word.length > 0 lineLength += word.length - textNode = @domElementPool.buildText(word) + textNode = @domElementPool.buildText(word.replace(/\s/g, NBSPCharacter)) lineNode.appendChild(textNode) @currentLineTextNodes.push(textNode) if lineLength is 0 - textNode = @domElementPool.buildText('\u00a0') + textNode = @domElementPool.buildText(NBSPCharacter) lineNode.appendChild(textNode) @currentLineTextNodes.push(textNode) From 9fd9c0c621c773da4416951ee0d32d3bd06289bd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 16 Dec 2015 19:13:26 -0700 Subject: [PATCH 006/262] Fix screenPositionForPixelPosition with DisplayLayers --- src/lines-yardstick.coffee | 107 +++++++++++++++---------------------- 1 file changed, 42 insertions(+), 65 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 85d1e23d4..b78a7f317 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -1,5 +1,6 @@ TokenIterator = require './token-iterator' {Point} = require 'text-buffer' +{isPairedCharacter} = require './text-utils' module.exports = class LinesYardstick @@ -19,63 +20,45 @@ class LinesYardstick screenPositionForPixelPosition: (pixelPosition) -> targetTop = pixelPosition.top targetLeft = pixelPosition.left - defaultCharWidth = @model.getDefaultCharWidth() row = Math.floor(targetTop / @model.getLineHeightInPixels()) targetLeft = 0 if row < 0 targetLeft = Infinity if row > @model.getLastScreenRow() row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) - line = @model.tokenizedLineForScreenRow(row) - lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) + lineNode = @lineNodesProvider.lineNodeForScreenRow(row) + return Point(row, 0) unless lineNode - return Point(row, 0) unless lineNode? and line? + textNodes = @lineNodesProvider.textNodesForScreenRow(row) + lineOffset = lineNode.getBoundingClientRect().left + targetLeft += lineOffset - textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row) - column = 0 - previousColumn = 0 - previousLeft = 0 + textNodeStartColumn = 0 + for textNode in textNodes + {length: textNodeLength, textContent: textNodeContent} = textNode + textNodeRight = @clientRectForRange(textNode, 0, textNodeLength).right - @tokenIterator.reset(line, false) - while @tokenIterator.next() - text = @tokenIterator.getText() - textIndex = 0 - while textIndex < text.length - if @tokenIterator.isPairedCharacter() - char = text - charLength = 2 - textIndex += 2 - else - char = text[textIndex] - charLength = 1 - textIndex++ + if textNodeRight > targetLeft + characterIndex = 0 + while characterIndex < textNodeLength + if isPairedCharacter(textNodeContent, characterIndex) + nextCharacterIndex = characterIndex + 2 + else + nextCharacterIndex = characterIndex + 1 - unless textNode? - textNode = textNodes.shift() - textNodeLength = textNode.textContent.length - textNodeIndex = 0 - nextTextNodeIndex = textNodeLength + rangeRect = @clientRectForRange(textNode, characterIndex, nextCharacterIndex) - while nextTextNodeIndex <= column - textNode = textNodes.shift() - textNodeLength = textNode.textContent.length - textNodeIndex = nextTextNodeIndex - nextTextNodeIndex = textNodeIndex + textNodeLength + if rangeRect.right > targetLeft + if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) + return Point(row, textNodeStartColumn + characterIndex) + else + return Point(row, textNodeStartColumn + nextCharacterIndex) + else + characterIndex = nextCharacterIndex - indexWithinTextNode = column - textNodeIndex - left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode) - charWidth = left - previousLeft + textNodeStartColumn += textNodeLength - return Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2) - - previousLeft = left - previousColumn = column - column += charLength - - if targetLeft <= previousLeft + (charWidth / 2) - Point(row, previousColumn) - else - Point(row, column) + Point(row, textNodeStartColumn) pixelPositionForScreenPosition: (screenPosition) -> targetRow = screenPosition.row @@ -96,19 +79,24 @@ class LinesYardstick return cachedPosition textNodes = @lineNodesProvider.textNodesForScreenRow(row) - textNodeStartIndex = 0 + textNodeStartColumn = 0 for textNode in textNodes - textNodeEndIndex = textNodeStartIndex + textNode.textContent.length - if textNodeEndIndex > column - indexInTextNode = column - textNodeStartIndex + textNodeEndColumn = textNodeStartColumn + textNode.textContent.length + if textNodeEndColumn > column + indexInTextNode = column - textNodeStartColumn break else - textNodeStartIndex = textNodeEndIndex + textNodeStartColumn = textNodeEndColumn if textNode? indexInTextNode ?= textNode.textContent.length - leftPixelPosition = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexInTextNode) + lineOffset = lineNode.getBoundingClientRect().left + if indexInTextNode is 0 + leftPixelPosition = @clientRectForRange(textNode, 0, 1).left + else + leftPixelPosition = @clientRectForRange(textNode, 0, indexInTextNode).right + leftPixelPosition -= lineOffset @leftPixelPositionCache[lineId] ?= {} @leftPixelPositionCache[lineId][column] = leftPixelPosition @@ -116,18 +104,7 @@ class LinesYardstick else 0 - leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) -> - if charIndex is 0 - width = 0 - else - @rangeForMeasurement.setStart(textNode, 0) - @rangeForMeasurement.setEnd(textNode, charIndex) - width = @rangeForMeasurement.getBoundingClientRect().width - - @rangeForMeasurement.setStart(textNode, 0) - @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) - left = @rangeForMeasurement.getBoundingClientRect().left - - offset = lineNode.getBoundingClientRect().left - - left + width - offset + clientRectForRange: (textNode, startIndex, endIndex) -> + @rangeForMeasurement.setStart(textNode, startIndex) + @rangeForMeasurement.setEnd(textNode, endIndex) + @rangeForMeasurement.getBoundingClientRect() From 5292767084d3c7e95caa56e7282adb4cd2ffe370 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 17 Dec 2015 16:33:09 -0700 Subject: [PATCH 007/262] Use DisplayLayers for position translation and clipping in editor Markers are still translating via the DisplayBuffer, but that will change when display markers are moved into DisplayLayers. --- src/text-editor.coffee | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index ccec60c85..18fcad9c6 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1321,7 +1321,7 @@ class TextEditor extends Model # * `options` (optional) An options hash for {::clipScreenPosition}. # # Returns a {Point}. - screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) + screenPositionForBufferPosition: (bufferPosition, options) -> @displayLayer.translateBufferPosition(bufferPosition, options) # Essential: Convert a position in screen-coordinates to buffer-coordinates. # @@ -1331,21 +1331,29 @@ class TextEditor extends Model # * `options` (optional) An options hash for {::clipScreenPosition}. # # Returns a {Point}. - bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) + bufferPositionForScreenPosition: (screenPosition, options) -> @displayLayer.translateScreenPosition(screenPosition, options) # Essential: Convert a range in buffer-coordinates to screen-coordinates. # # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. # # Returns a {Range}. - screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) + screenRangeForBufferRange: (bufferRange) -> + bufferRange = Range.fromObject(bufferRange) + start = @displayLayer.translateBufferPosition(bufferRange.start) + end = @displayLayer.translateBufferPosition(bufferRange.end) + Range(start, end) # Essential: Convert a range in screen-coordinates to buffer-coordinates. # # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. # # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) + bufferRangeForScreenRange: (screenRange) -> + screenRange = Range.fromObject(screenRange) + start = @displayLayer.translateScreenPosition(screenRange.start) + end = @displayLayer.translateScreenPosition(screenRange.end) + Range(start, end) # Extended: Clip the given {Point} to a valid position in the buffer. # @@ -1394,20 +1402,26 @@ class TextEditor extends Model # # * `screenPosition` The {Point} representing the position to clip. # * `options` (optional) {Object} - # * `wrapBeyondNewlines` {Boolean} if `true`, continues wrapping past newlines - # * `wrapAtSoftNewlines` {Boolean} if `true`, continues wrapping past soft newlines - # * `screenLine` {Boolean} if `true`, indicates that you're using a line number, not a row number + # * `clipDirection` {String} If `'backward'`, returns the first valid + # position preceding an invalid position. If `'forward'`, returns the + # first valid position following a valid position. Defaults to + # `'backward'`. # # Returns a {Point}. - clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) + clipScreenPosition: (screenPosition, options) -> @displayLayer.clipScreenPosition(screenPosition, options) # Extended: Clip the start and end of the given range to valid positions on screen. # See {::clipScreenPosition} for more information. # # * `range` The {Range} to clip. # * `options` (optional) See {::clipScreenPosition} `options`. + # # Returns a {Range}. - clipScreenRange: (range, options) -> @displayBuffer.clipScreenRange(range, options) + clipScreenRange: (screenRange, options) -> + screenRange = Range.fromObject(screenRange) + start = @displayLayer.clipScreenPosition(screenRange.start, options) + end = @displayLayer.clipScreenPosition(screenRange.end, options) + Range(start, end) ### Section: Decorations From caf6d7f473b7ccf0064100e9e9aa73cb1e124b2b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 18 Dec 2015 19:20:09 -0700 Subject: [PATCH 008/262] Use DisplayMarkerLayers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’m creating the DisplayLayer in the DisplayBuffer. In places where our API deviates from DisplayBuffer, I’ll use the DisplayLayer directly from as a property of TextEditor. Otherwise I’ll continue to delegate from the DisplayLayer into the DisplayLayer to minimize impact until the DisplayBuffer can be removed entirely. --- src/cursor.coffee | 8 +- src/decoration.coffee | 6 +- src/display-buffer.coffee | 31 +-- src/gutter.coffee | 4 +- src/layer-decoration.coffee | 2 +- src/text-editor-marker-layer.coffee | 192 -------------- src/text-editor-marker.coffee | 371 ---------------------------- src/text-editor.coffee | 63 +++-- 8 files changed, 54 insertions(+), 623 deletions(-) delete mode 100644 src/text-editor-marker-layer.coffee delete mode 100644 src/text-editor-marker.coffee diff --git a/src/cursor.coffee b/src/cursor.coffee index 5b3b23b73..f91a7bfd9 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -9,7 +9,7 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g # where text can be inserted. # # Cursors belong to {TextEditor}s and have some metadata attached in the form -# of a {TextEditorMarker}. +# of a {DisplayMarker}. module.exports = class Cursor extends Model screenPosition: null @@ -129,7 +129,7 @@ class Cursor extends Model Section: Cursor Position Details ### - # Public: Returns the underlying {TextEditorMarker} for the cursor. + # Public: Returns the underlying {DisplayMarker} for the cursor. # Useful with overlay {Decoration}s. getMarker: -> @marker @@ -265,7 +265,7 @@ class Cursor extends Model columnCount-- # subtract 1 for the row move column = column - columnCount - @setScreenPosition({row, column}, clip: 'backward') + @setScreenPosition({row, column}, clipDirection: 'backward') # Public: Moves the cursor right one screen column. # @@ -292,7 +292,7 @@ class Cursor extends Model columnsRemainingInLine = rowLength column = column + columnCount - @setScreenPosition({row, column}, clip: 'forward', wrapBeyondNewlines: true, wrapAtSoftNewlines: true) + @setScreenPosition({row, column}, clipDirection: 'forward') # Public: Moves the cursor to the top of the buffer. moveToTop: -> diff --git a/src/decoration.coffee b/src/decoration.coffee index f57d234d1..9387913d4 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) -> decorationParams.gutterName = 'line-number' decorationParams -# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is +# Essential: Represents a decoration that follows a {DisplayMarker}. 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 {TextEditorMarker}. +# Best practice for destroying the decoration is by destroying the {DisplayMarker}. # # ```coffee # marker.destroy() @@ -72,7 +72,7 @@ class Decoration # Essential: Destroy this marker. # - # If you own the marker, you should use {TextEditorMarker::destroy} which will destroy + # If you own the marker, you should use {DisplayMarker::destroy} which will destroy # this decoration. destroy: -> return if @destroyed diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 8b95656f9..1f8c3cc26 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -8,7 +8,6 @@ Model = require './model' Token = require './token' Decoration = require './decoration' LayerDecoration = require './layer-decoration' -TextEditorMarkerLayer = require './text-editor-marker-layer' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> @@ -54,9 +53,9 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer + @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength()}) @charWidthsByScope = {} - @defaultMarkerLayer = new TextEditorMarkerLayer(this, @buffer.getDefaultMarkerLayer(), true) - @customMarkerLayersById = {} + @defaultMarkerLayer = @displayLayer.addMarkerLayer() @foldsByMarkerId = {} @decorationsById = {} @decorationsByMarkerId = {} @@ -835,17 +834,17 @@ class DisplayBuffer extends Model decorationsForMarkerId: (markerId) -> @decorationsByMarkerId[markerId] - # Retrieves a {TextEditorMarker} based on its id. + # Retrieves a {DisplayMarker} based on its id. # # id - A {Number} representing a marker id # - # Returns the {TextEditorMarker} (if it exists). + # Returns the {DisplayMarker} (if it exists). getMarker: (id) -> @defaultMarkerLayer.getMarker(id) # Retrieves the active markers in the buffer. # - # Returns an {Array} of existing {TextEditorMarker}s. + # Returns an {Array} of existing {DisplayMarker}s. getMarkers: -> @defaultMarkerLayer.getMarkers() @@ -855,7 +854,7 @@ 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 {TextEditorMarker} constructor + # options - Options to pass to the {DisplayMarker} constructor # # Returns a {Number} representing the new marker's ID. markScreenRange: (screenRange, options) -> @@ -864,7 +863,7 @@ class DisplayBuffer extends Model # 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 {TextEditorMarker} constructor + # options - Options to pass to the {DisplayMarker} constructor # # Returns a {Number} representing the new marker's ID. markBufferRange: (bufferRange, options) -> @@ -873,7 +872,7 @@ class DisplayBuffer extends Model # 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 {TextEditorMarker} constructor + # options - Options to pass to the {DisplayMarker} constructor # # Returns a {Number} representing the new marker's ID. markScreenPosition: (screenPosition, options) -> @@ -882,7 +881,7 @@ class DisplayBuffer extends Model # 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 {TextEditorMarker} constructor + # options - Options to pass to the {DisplayMarker} constructor # # Returns a {Number} representing the new marker's ID. markBufferPosition: (bufferPosition, options) -> @@ -892,7 +891,7 @@ class DisplayBuffer extends Model # # Refer to {DisplayBuffer::findMarkers} for details. # - # Returns a {TextEditorMarker} or null + # Returns a {DisplayMarker} or null findMarker: (params) -> @defaultMarkerLayer.findMarkers(params)[0] @@ -913,19 +912,15 @@ class DisplayBuffer extends Model # :containedInBufferRange - A {Range} or range-compatible {Array}. Only # returns markers contained within this range. # - # Returns an {Array} of {TextEditorMarker}s + # Returns an {Array} of {DisplayMarker}s findMarkers: (params) -> @defaultMarkerLayer.findMarkers(params) addMarkerLayer: (options) -> - bufferLayer = @buffer.addMarkerLayer(options) - @getMarkerLayer(bufferLayer.id) + @displayLayer.addMarkerLayer(options) getMarkerLayer: (id) -> - if layer = @customMarkerLayersById[id] - layer - else if bufferLayer = @buffer.getMarkerLayer(id) - @customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer) + @displayLayer.getMarkerLayer(id) getDefaultMarkerLayer: -> @defaultMarkerLayer diff --git a/src/gutter.coffee b/src/gutter.coffee index f59fa7b6e..2fc362cbd 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -71,13 +71,13 @@ class Gutter isVisible: -> @visible - # Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves, + # Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves, # is invalidated, or is destroyed, the decoration will be updated to reflect # the marker's state. # # ## Arguments # - # * `marker` A {TextEditorMarker} you want this decoration to follow. + # * `marker` A {DisplayMarker} 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. diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee index 1f76140a3..7d2396e0d 100644 --- a/src/layer-decoration.coffee +++ b/src/layer-decoration.coffee @@ -48,7 +48,7 @@ class LayerDecoration # Essential: Override the decoration properties for a specific marker. # - # * `marker` The {TextEditorMarker} or {Marker} for which to override + # * `marker` The {DisplayMarker} or {Marker} for which to override # properties. # * `properties` An {Object} containing properties to apply to this marker. # Pass `null` to clear the override. diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee deleted file mode 100644 index e99ad7323..000000000 --- a/src/text-editor-marker-layer.coffee +++ /dev/null @@ -1,192 +0,0 @@ -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 diff --git a/src/text-editor-marker.coffee b/src/text-editor-marker.coffee deleted file mode 100644 index df84700ee..000000000 --- a/src/text-editor-marker.coffee +++ /dev/null @@ -1,371 +0,0 @@ -_ = require 'underscore-plus' -{CompositeDisposable, Emitter} = require 'event-kit' - -# Essential: Represents a buffer annotation that remains logically stationary -# even as the buffer changes. This is used to represent cursors, folds, snippet -# targets, misspelled words, and anything else that needs to track a logical -# location in the buffer over time. -# -# ### TextEditorMarker Creation -# -# Use {TextEditor::markBufferRange} rather than creating Markers directly. -# -# ### Head and Tail -# -# Markers always have a *head* and sometimes have a *tail*. If you think of a -# marker as an editor selection, the tail is the part that's stationary and the -# head is the part that moves when the mouse is moved. A marker without a tail -# always reports an empty range at the head position. A marker with a head position -# greater than the tail is in a "normal" orientation. If the head precedes the -# tail the marker is in a "reversed" orientation. -# -# ### Validity -# -# Markers are considered *valid* when they are first created. Depending on the -# invalidation strategy you choose, certain changes to the buffer can cause a -# marker to become invalid, for example if the text surrounding the marker is -# deleted. The strategies, in order of descending fragility: -# -# * __never__: The marker is never marked as invalid. This is a good choice for -# markers representing selections in an editor. -# * __surround__: The marker is invalidated by changes that completely surround it. -# * __overlap__: The marker is invalidated by changes that surround the -# start or end of the marker. This is the default. -# * __inside__: The marker is invalidated by changes that extend into the -# inside of the marker. Changes that end at the marker's start or -# start at the marker's end do not invalidate the marker. -# * __touch__: The marker is invalidated by a change that touches the marked -# 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. -# -# See {TextEditor::markBufferRange} for usage. -module.exports = -class TextEditorMarker - bufferMarkerSubscription: null - oldHeadBufferPosition: null - oldHeadScreenPosition: null - oldTailBufferPosition: null - oldTailScreenPosition: null - wasValid: true - hasChangeObservers: false - - ### - Section: Construction and Destruction - ### - - constructor: (@layer, @bufferMarker) -> - {@displayBuffer} = @layer - @emitter = new Emitter - @disposables = new CompositeDisposable - @id = @bufferMarker.id - - @disposables.add @bufferMarker.onDidDestroy => @destroyed() - - # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once - # destroyed, a marker cannot be restored by undo/redo operations. - destroy: -> - @bufferMarker.destroy() - @disposables.dispose() - - # Essential: Creates and returns a new {TextEditorMarker} with the same properties as - # this marker. - # - # {Selection} markers (markers with a custom property `type: "selection"`) - # should be copied with a different `type` value, for example with - # `marker.copy({type: null})`. Otherwise, the new marker's selection will - # be merged with this marker's selection, and a `null` value will be - # returned. - # - # * `properties` (optional) {Object} properties to associate with the new - # marker. The new marker's properties are computed by extending this marker's - # properties with `properties`. - # - # Returns a {TextEditorMarker}. - copy: (properties) -> - @layer.getMarker(@bufferMarker.copy(properties).id) - - ### - Section: Event Subscription - ### - - # Essential: Invoke the given callback when the state of the marker changes. - # - # * `callback` {Function} to be called when the marker changes. - # * `event` {Object} with the following keys: - # * `oldHeadBufferPosition` {Point} representing the former head buffer position - # * `newHeadBufferPosition` {Point} representing the new head buffer position - # * `oldTailBufferPosition` {Point} representing the former tail buffer position - # * `newTailBufferPosition` {Point} representing the new tail buffer position - # * `oldHeadScreenPosition` {Point} representing the former head screen position - # * `newHeadScreenPosition` {Point} representing the new head screen position - # * `oldTailScreenPosition` {Point} representing the former tail screen position - # * `newTailScreenPosition` {Point} representing the new tail screen position - # * `wasValid` {Boolean} indicating whether the marker was valid before the change - # * `isValid` {Boolean} indicating whether the marker is now valid - # * `hadTail` {Boolean} indicating whether the marker had a tail before the change - # * `hasTail` {Boolean} indicating whether the marker now has a tail - # * `oldProperties` {Object} containing the marker's custom properties before the change. - # * `newProperties` {Object} containing the marker's custom properties after the change. - # * `textChanged` {Boolean} indicating whether this change was caused by a textual change - # to the buffer or whether the marker was manipulated directly via its public API. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - unless @hasChangeObservers - @oldHeadBufferPosition = @getHeadBufferPosition() - @oldHeadScreenPosition = @getHeadScreenPosition() - @oldTailBufferPosition = @getTailBufferPosition() - @oldTailScreenPosition = @getTailScreenPosition() - @wasValid = @isValid() - @disposables.add @bufferMarker.onDidChange (event) => @notifyObservers(event) - @hasChangeObservers = true - @emitter.on 'did-change', callback - - # Essential: Invoke the given callback when the marker is destroyed. - # - # * `callback` {Function} to be called when the marker is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback - - ### - Section: TextEditorMarker Details - ### - - # Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be - # invalidated when a region surrounding them in the buffer is changed. - isValid: -> - @bufferMarker.isValid() - - # 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 - # {TextEditorMarker::destroy}, no undo/redo operation can ever bring it back. - isDestroyed: -> - @bufferMarker.isDestroyed() - - # Essential: Returns a {Boolean} indicating whether the head precedes the tail. - isReversed: -> - @bufferMarker.isReversed() - - # Essential: Get the invalidation strategy for this marker. - # - # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. - # - # Returns a {String}. - getInvalidationStrategy: -> - @bufferMarker.getInvalidationStrategy() - - # Essential: Returns an {Object} containing any custom properties associated with - # the marker. - getProperties: -> - @bufferMarker.getProperties() - - # Essential: Merges an {Object} containing new properties into the marker's - # existing properties. - # - # * `properties` {Object} - setProperties: (properties) -> - @bufferMarker.setProperties(properties) - - matchesProperties: (attributes) -> - attributes = @layer.translateToBufferMarkerParams(attributes) - @bufferMarker.matchesParams(attributes) - - ### - Section: Comparing to other markers - ### - - # Essential: Returns a {Boolean} indicating whether this marker is equivalent to - # another marker, meaning they have the same range and options. - # - # * `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` {TextEditorMarker} - # - # Returns a {Number} - compare: (other) -> - @bufferMarker.compare(other.bufferMarker) - - ### - Section: Managing the marker's range - ### - - # Essential: Gets the buffer range of the display marker. - # - # Returns a {Range}. - getBufferRange: -> - @bufferMarker.getRange() - - # Essential: Modifies the buffer range of the display marker. - # - # * `bufferRange` The new {Range} to use - # * `properties` (optional) {Object} properties to associate with the marker. - # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. - setBufferRange: (bufferRange, properties) -> - @bufferMarker.setRange(bufferRange, properties) - - # Essential: Gets the screen range of the display marker. - # - # Returns a {Range}. - getScreenRange: -> - @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true) - - # Essential: Modifies the screen range of the display marker. - # - # * `screenRange` The new {Range} to use - # * `properties` (optional) {Object} properties to associate with the marker. - # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. - setScreenRange: (screenRange, options) -> - @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 {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 {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 {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 {TextEditorMarker::getStartScreenPosition}. - # - # Returns a {Point}. - getEndScreenPosition: -> - @displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true) - - # Extended: Retrieves the buffer position of the marker's head. - # - # Returns a {Point}. - getHeadBufferPosition: -> - @bufferMarker.getHeadPosition() - - # Extended: Sets the buffer position of the marker's head. - # - # * `bufferPosition` The new {Point} to use - # * `properties` (optional) {Object} properties to associate with the marker. - setHeadBufferPosition: (bufferPosition, properties) -> - @bufferMarker.setHeadPosition(bufferPosition, properties) - - # Extended: Retrieves the screen position of the marker's head. - # - # Returns a {Point}. - getHeadScreenPosition: -> - @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true) - - # Extended: Sets the screen position of the marker's head. - # - # * `screenPosition` The new {Point} to use - # * `properties` (optional) {Object} properties to associate with the marker. - setHeadScreenPosition: (screenPosition, properties) -> - @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties)) - - # Extended: Retrieves the buffer position of the marker's tail. - # - # Returns a {Point}. - getTailBufferPosition: -> - @bufferMarker.getTailPosition() - - # Extended: Sets the buffer position of the marker's tail. - # - # * `bufferPosition` The new {Point} to use - # * `properties` (optional) {Object} properties to associate with the marker. - setTailBufferPosition: (bufferPosition) -> - @bufferMarker.setTailPosition(bufferPosition) - - # Extended: Retrieves the screen position of the marker's tail. - # - # Returns a {Point}. - getTailScreenPosition: -> - @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true) - - # Extended: Sets the screen position of the marker's tail. - # - # * `screenPosition` The new {Point} to use - # * `properties` (optional) {Object} properties to associate with the marker. - setTailScreenPosition: (screenPosition, options) -> - @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) - - # Extended: Returns a {Boolean} indicating whether the marker has a tail. - hasTail: -> - @bufferMarker.hasTail() - - # Extended: Plants the marker's tail at the current head position. After calling - # the marker's tail position will be its head position at the time of the - # call, regardless of where the marker's head is moved. - # - # * `properties` (optional) {Object} properties to associate with the marker. - plantTail: -> - @bufferMarker.plantTail() - - # Extended: Removes the marker's tail. After calling the marker's head position - # will be reported as its current tail position until the tail is planted - # again. - # - # * `properties` (optional) {Object} properties to associate with the marker. - clearTail: (properties) -> - @bufferMarker.clearTail(properties) - - ### - Section: Private utility methods - ### - - # Returns a {String} representation of the marker - inspect: -> - "TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})" - - destroyed: -> - @layer.didDestroyMarker(this) - @emitter.emit 'did-destroy' - @emitter.dispose() - - notifyObservers: ({textChanged}) -> - textChanged ?= false - - newHeadBufferPosition = @getHeadBufferPosition() - newHeadScreenPosition = @getHeadScreenPosition() - newTailBufferPosition = @getTailBufferPosition() - newTailScreenPosition = @getTailScreenPosition() - isValid = @isValid() - - return if isValid is @wasValid and - newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and - newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and - newTailBufferPosition.isEqual(@oldTailBufferPosition) and - newTailScreenPosition.isEqual(@oldTailScreenPosition) - - changeEvent = { - @oldHeadScreenPosition, newHeadScreenPosition, - @oldTailScreenPosition, newTailScreenPosition, - @oldHeadBufferPosition, newHeadBufferPosition, - @oldTailBufferPosition, newTailBufferPosition, - textChanged, - isValid - } - - @oldHeadBufferPosition = newHeadBufferPosition - @oldHeadScreenPosition = newHeadScreenPosition - @oldTailBufferPosition = newTailBufferPosition - @oldTailScreenPosition = newTailScreenPosition - @wasValid = isValid - - @emitter.emit 'did-change', changeEvent diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 18fcad9c6..e5757a6e6 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -116,8 +116,7 @@ class TextEditor extends Model buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode, @config, @assert, @grammarRegistry, @packageManager }) - @buffer = @displayBuffer.buffer - @displayLayer = buffer.addDisplayLayer({tabLength: @displayBuffer.getTabLength()}) + {@buffer, @displayLayer} = @displayBuffer @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) for marker in @selectionsMarkerLayer.getMarkers() @@ -1427,7 +1426,7 @@ class TextEditor extends Model Section: Decorations ### - # Essential: Add a decoration that tracks a {TextEditorMarker}. When the + # Essential: Add a decoration that tracks a {DisplayMarker}. When the # marker moves, is invalidated, or is destroyed, the decoration will be # updated to reflect the marker's state. # @@ -1448,28 +1447,28 @@ class TextEditor extends Model # # ``` # * __overlay__: Positions the view associated with the given item at the head - # or tail of the given `TextEditorMarker`. - # * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter + # or tail of the given `DisplayMarker`. + # * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter # decorations are created by calling {Gutter::decorateMarker} on the # desired `Gutter` instance. # # ## Arguments # - # * `marker` A {TextEditorMarker} you want this decoration to follow. + # * `marker` A {DisplayMarker} 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 `TextEditorMarker`. + # spanned by the `DisplayMarker`. # * `line-number` Adds the given `class` to the line numbers overlapping - # the rows spanned by the `TextEditorMarker`. + # the rows spanned by the `DisplayMarker`. # * `highlight` Creates a `.highlight` div with the nested class with up - # to 3 nested regions that fill the area spanned by the `TextEditorMarker`. + # to 3 nested regions that fill the area spanned by the `DisplayMarker`. # * `overlay` Positions the view associated with the given item at the - # head or tail of the given `TextEditorMarker`, depending on the `position` + # head or tail of the given `DisplayMarker`, depending on the `position` # property. - # * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling + # * `gutter` Tracks a {DisplayMarker} 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. @@ -1477,16 +1476,16 @@ 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 `TextEditorMarker`. Only applicable to the `line` and + # the head of the `DisplayMarker`. Only applicable to the `line` and # `line-number` types. # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if - # the associated `TextEditorMarker` is empty. Only applicable to the `gutter`, + # the associated `DisplayMarker` 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 `TextEditorMarker` is non-empty. Only applicable to the + # if the associated `DisplayMarker` 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 `TextEditorMarker`. + # controls where the overlay view is positioned relative to the `DisplayMarker`. # Values can be `'head'` (the default), or `'tail'`. # # Returns a {Decoration} object @@ -1497,7 +1496,7 @@ class TextEditor extends Model # 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. + # * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. # * `decorationParams` The same parameters that are passed to # {decorateMarker}, except the `type` cannot be `overlay` or `gutter`. # @@ -1515,7 +1514,7 @@ class TextEditor extends Model # # Returns an {Object} of decorations in the form # `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` - # where the keys are {TextEditorMarker} IDs, and the values are an array of decoration + # where the keys are {DisplayMarker} 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) -> @@ -1610,7 +1609,7 @@ 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 {TextEditorMarker}. + # Returns a {DisplayMarker}. markBufferRange: (args...) -> @displayBuffer.markBufferRange(args...) @@ -1645,7 +1644,7 @@ 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 {TextEditorMarker}. + # Returns a {DisplayMarker}. markScreenRange: (args...) -> @displayBuffer.markScreenRange(args...) @@ -1655,7 +1654,7 @@ class TextEditor extends Model # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. # - # Returns a {TextEditorMarker}. + # Returns a {DisplayMarker}. markBufferPosition: (args...) -> @displayBuffer.markBufferPosition(args...) @@ -1665,11 +1664,11 @@ class TextEditor extends Model # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. # - # Returns a {TextEditorMarker}. + # Returns a {DisplayMarker}. markScreenPosition: (args...) -> @displayBuffer.markScreenPosition(args...) - # Essential: Find all {TextEditorMarker}s on the default marker layer that + # Essential: Find all {DisplayMarker}s on the default marker layer that # match the given properties. # # This method finds markers based on the given properties. Markers can be @@ -1692,14 +1691,14 @@ class TextEditor extends Model findMarkers: (properties) -> @displayBuffer.findMarkers(properties) - # Extended: Get the {TextEditorMarker} on the default layer for the given + # Extended: Get the {DisplayMarker} on the default layer for the given # marker id. # # * `id` {Number} id of the marker getMarker: (id) -> @displayBuffer.getMarker(id) - # Extended: Get all {TextEditorMarker}s on the default marker layer. Consider + # Extended: Get all {DisplayMarker}s on the default marker layer. Consider # using {::findMarkers} getMarkers: -> @displayBuffer.getMarkers() @@ -1721,11 +1720,11 @@ class TextEditor extends Model # # This API is experimental and subject to change on any release. # - # Returns a {TextEditorMarkerLayer}. + # Returns a {DisplayMarkerLayer}. addMarkerLayer: (options) -> @displayBuffer.addMarkerLayer(options) - # Public: *Experimental:* Get a {TextEditorMarkerLayer} by id. + # Public: *Experimental:* Get a {DisplayMarkerLayer} by id. # # * `id` The id of the marker layer to retrieve. # @@ -1736,14 +1735,14 @@ class TextEditor extends Model getMarkerLayer: (id) -> @displayBuffer.getMarkerLayer(id) - # Public: *Experimental:* Get the default {TextEditorMarkerLayer}. + # Public: *Experimental:* Get the default {DisplayMarkerLayer}. # # 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}. + # Returns a {DisplayMarkerLayer}. getDefaultMarkerLayer: -> @displayBuffer.getDefaultMarkerLayer() @@ -1950,7 +1949,7 @@ class TextEditor extends Model getCursorsOrderedByBufferPosition: -> @getCursors().sort (a, b) -> a.compare(b) - # Add a cursor based on the given {TextEditorMarker}. + # Add a cursor based on the given {DisplayMarker}. addCursor: (marker) -> cursor = new Cursor(editor: this, marker: marker, config: @config) @cursors.push(cursor) @@ -2299,7 +2298,7 @@ class TextEditor extends Model # Extended: Select the range of the given marker if it is valid. # - # * `marker` A {TextEditorMarker} + # * `marker` A {DisplayMarker} # # Returns the selected {Range} or `undefined` if the marker is invalid. selectMarker: (marker) -> @@ -2425,9 +2424,9 @@ class TextEditor extends Model _.reduce(tail, reducer, [head]) return result if fn? - # Add a {Selection} based on the given {TextEditorMarker}. + # Add a {Selection} based on the given {DisplayMarker}. # - # * `marker` The {TextEditorMarker} to highlight + # * `marker` The {DisplayMarker} to highlight # * `options` (optional) An {Object} that pertains to the {Selection} constructor. # # Returns the new {Selection}. From 972fda4ef7280423b5537e4910c53f16bcc108d4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 31 Dec 2015 15:23:05 -0700 Subject: [PATCH 009/262] Start using DisplayLayer for folds --- spec/display-buffer-spec.coffee | 78 +++++++------- spec/text-editor-spec.coffee | 58 +++++------ src/display-buffer.coffee | 177 ++++++++------------------------ src/fold.coffee | 83 --------------- src/language-mode.coffee | 12 +-- src/selection.coffee | 14 +-- src/text-editor.coffee | 53 +++------- 7 files changed, 133 insertions(+), 342 deletions(-) delete mode 100644 src/fold.coffee diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 0246008a4..48103f3a7 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -26,7 +26,7 @@ describe "DisplayBuffer", -> marker1 = displayBuffer.markBufferRange([[1, 2], [3, 4]], id: 1) marker2 = displayBuffer.markBufferRange([[2, 3], [4, 5]], reversed: true, id: 2) marker3 = displayBuffer.markBufferPosition([5, 6], id: 3) - displayBuffer.createFold(3, 5) + displayBuffer.foldBufferRowRange(3, 5) displayBuffer2 = displayBuffer.copy() expect(displayBuffer2.id).not.toBe displayBuffer.id @@ -361,7 +361,7 @@ describe "DisplayBuffer", -> describe "when folds are created and destroyed", -> describe "when a fold spans multiple lines", -> it "replaces the lines spanned by the fold with a placeholder that references the fold object", -> - fold = displayBuffer.createFold(4, 7) + fold = displayBuffer.foldBufferRowRange(4, 7) expect(fold).toBeDefined() [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) @@ -382,7 +382,7 @@ describe "DisplayBuffer", -> describe "when a fold spans a single line", -> it "renders a fold placeholder for the folded line but does not skip any lines", -> - fold = displayBuffer.createFold(4, 4) + fold = displayBuffer.foldBufferRowRange(4, 4) [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) expect(line4.fold).toBe fold @@ -406,8 +406,8 @@ describe "DisplayBuffer", -> describe "when a fold is nested within another fold", -> it "does not render the placeholder for the inner fold until the outer fold is destroyed", -> - innerFold = displayBuffer.createFold(6, 7) - outerFold = displayBuffer.createFold(4, 8) + innerFold = displayBuffer.foldBufferRowRange(6, 7) + outerFold = displayBuffer.foldBufferRowRange(4, 8) [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) expect(line4.fold).toBe outerFold @@ -424,8 +424,8 @@ describe "DisplayBuffer", -> expect(line7.text).toBe '8' it "allows the outer fold to start at the same location as the inner fold", -> - innerFold = displayBuffer.createFold(4, 6) - outerFold = displayBuffer.createFold(4, 8) + innerFold = displayBuffer.foldBufferRowRange(4, 6) + outerFold = displayBuffer.foldBufferRowRange(4, 8) [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) expect(line4.fold).toBe outerFold @@ -434,19 +434,19 @@ 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) + fold = displayBuffer.foldBufferRowRange(0, 10) expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1 - newFold = displayBuffer.createFold(0, 10) + newFold = displayBuffer.foldBufferRowRange(0, 10) expect(newFold).toBe fold 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", -> - outerFold = displayBuffer.createFold(0, 10) + outerFold = displayBuffer.foldBufferRowRange(0, 10) changeHandler.reset() - innerFold = displayBuffer.createFold(2, 5) + innerFold = displayBuffer.foldBufferRowRange(2, 5) expect(changeHandler).not.toHaveBeenCalled() [line0, line1] = displayBuffer.tokenizedLinesForScreenRows(0, 1) expect(line0.fold).toBe outerFold @@ -461,8 +461,8 @@ describe "DisplayBuffer", -> describe "when a fold ends where another fold begins", -> it "continues to hide the lines inside the second fold", -> - fold2 = displayBuffer.createFold(4, 9) - fold1 = displayBuffer.createFold(0, 4) + fold2 = displayBuffer.foldBufferRowRange(4, 9) + fold1 = displayBuffer.foldBufferRowRange(0, 4) expect(displayBuffer.tokenizedLineForScreenRow(0).text).toMatch /^0/ expect(displayBuffer.tokenizedLineForScreenRow(1).text).toMatch /^10/ @@ -473,9 +473,9 @@ describe "DisplayBuffer", -> buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: -> }) - otherDisplayBuffer.createFold(1, 5) + otherDisplayBuffer.foldBufferRowRange(1, 5) - displayBuffer.createFold(2, 4) + displayBuffer.foldBufferRowRange(2, 4) expect(otherDisplayBuffer.foldsStartingAtBufferRow(2).length).toBe 0 expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2' @@ -484,8 +484,8 @@ describe "DisplayBuffer", -> describe "when the buffer changes", -> [fold1, fold2] = [] beforeEach -> - fold1 = displayBuffer.createFold(2, 4) - fold2 = displayBuffer.createFold(6, 8) + fold1 = displayBuffer.foldBufferRowRange(2, 4) + fold2 = displayBuffer.foldBufferRowRange(6, 8) changeHandler.reset() describe "when the old range surrounds a fold", -> @@ -509,7 +509,7 @@ describe "DisplayBuffer", -> describe "when the old range surrounds two nested folds", -> it "removes both folds and replaces the selection with the new text", -> - displayBuffer.createFold(2, 9) + displayBuffer.foldBufferRowRange(2, 9) changeHandler.reset() buffer.setTextInRange([[1, 0], [10, 0]], 'goodbye') @@ -634,7 +634,7 @@ describe "DisplayBuffer", -> describe "position translation", -> it "translates positions to account for folded lines and characters and the placeholder", -> - fold = displayBuffer.createFold(4, 7) + fold = displayBuffer.foldBufferRowRange(4, 7) # preceding fold: identity expect(displayBuffer.screenPositionForBufferPosition([3, 0])).toEqual [3, 0] @@ -669,11 +669,11 @@ describe "DisplayBuffer", -> describe ".unfoldBufferRow(row)", -> it "destroys all folds containing the given row", -> - displayBuffer.createFold(2, 4) - displayBuffer.createFold(2, 6) - displayBuffer.createFold(7, 8) - displayBuffer.createFold(1, 9) - displayBuffer.createFold(11, 12) + displayBuffer.foldBufferRowRange(2, 4) + displayBuffer.foldBufferRowRange(2, 6) + displayBuffer.foldBufferRowRange(7, 8) + displayBuffer.foldBufferRowRange(1, 9) + displayBuffer.foldBufferRowRange(11, 12) expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1' expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '10' @@ -687,11 +687,11 @@ describe "DisplayBuffer", -> describe ".outermostFoldsInBufferRowRange(startRow, endRow)", -> it "returns the outermost folds entirely contained in the given row range, exclusive of end row", -> - fold1 = displayBuffer.createFold(4, 7) - fold2 = displayBuffer.createFold(5, 6) - fold3 = displayBuffer.createFold(11, 15) - fold4 = displayBuffer.createFold(12, 13) - fold5 = displayBuffer.createFold(16, 17) + fold1 = displayBuffer.foldBufferRowRange(4, 7) + fold2 = displayBuffer.foldBufferRowRange(5, 6) + fold3 = displayBuffer.foldBufferRowRange(11, 15) + fold4 = displayBuffer.foldBufferRowRange(12, 13) + fold5 = displayBuffer.foldBufferRowRange(16, 17) expect(displayBuffer.outermostFoldsInBufferRowRange(3, 18)).toEqual [fold1, fold3, fold5] expect(displayBuffer.outermostFoldsInBufferRowRange(5, 16)).toEqual [fold3] @@ -731,7 +731,7 @@ describe "DisplayBuffer", -> expect(displayBuffer.clipScreenPosition([0, 1000], wrapBeyondNewlines: true)).toEqual [1, 0] it "wraps positions in the middle of fold lines to the next screen line", -> - displayBuffer.createFold(3, 5) + displayBuffer.foldBufferRowRange(3, 5) expect(displayBuffer.clipScreenPosition([3, 5], wrapBeyondNewlines: true)).toEqual [4, 0] describe "when skipSoftWrapIndentation is false (the default)", -> @@ -850,7 +850,7 @@ describe "DisplayBuffer", -> describe "markers", -> beforeEach -> - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) describe "marker creation and manipulation", -> it "allows markers to be created in terms of both screen and buffer coordinates", -> @@ -947,7 +947,7 @@ describe "DisplayBuffer", -> } markerChangedHandler.reset() - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) expect(markerChangedHandler).toHaveBeenCalled() expect(markerChangedHandler.argsForCall[0][0]).toEqual { oldHeadScreenPosition: [11, 23] @@ -1028,7 +1028,7 @@ describe "DisplayBuffer", -> } it "does not call the callback for screen changes that don't change the position of the marker", -> - displayBuffer.createFold(10, 11) + displayBuffer.foldBufferRowRange(10, 11) expect(markerChangedHandler).not.toHaveBeenCalled() it "updates markers before emitting buffer change events, but does not notify their observers until the change event", -> @@ -1151,37 +1151,37 @@ describe "DisplayBuffer", -> it "allows the startScreenRow and endScreenRow to be specified", -> marker1 = displayBuffer.markBufferRange([[6, 0], [7, 0]], class: 'a') marker2 = displayBuffer.markBufferRange([[9, 0], [10, 0]], class: 'a') - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) expect(displayBuffer.findMarkers(class: 'a', startScreenRow: 6, endScreenRow: 7)).toEqual [marker2] it "allows intersectsBufferRowRange to be specified", -> marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRowRange: [5, 6])).toEqual [marker1] it "allows intersectsScreenRowRange to be specified", -> marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 10])).toEqual [marker2] it "allows containedInScreenRange to be specified", -> marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) expect(displayBuffer.findMarkers(class: 'a', containedInScreenRange: [[5, 0], [7, 0]])).toEqual [marker2] it "allows intersectsBufferRange to be specified", -> marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRange: [[5, 0], [6, 0]])).toEqual [marker1] it "allows intersectsScreenRange to be specified", -> marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.createFold(4, 7) + displayBuffer.foldBufferRowRange(4, 7) expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRange: [[5, 0], [10, 0]])).toEqual [marker2] describe "marker destruction", -> diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index ce84d2c50..76fd1a979 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -299,7 +299,7 @@ describe "TextEditor", -> editor.setSoftWrapped(true) editor.setDefaultCharWidth(1) editor.setEditorWidthInChars(50) - editor.createFold(2, 3) + editor.foldBufferRowRange(2, 3) it "positions the cursor at the buffer position that corresponds to the given screen position", -> editor.setCursorScreenPosition([9, 0]) @@ -1787,10 +1787,10 @@ describe "TextEditor", -> describe "when the 'preserveFolds' option is false (the default)", -> it "removes folds that contain the selections", -> editor.setSelectedBufferRange([[0, 0], [0, 0]]) - editor.createFold(1, 4) - editor.createFold(2, 3) - editor.createFold(6, 8) - editor.createFold(10, 11) + editor.foldBufferRowRange(1, 4) + editor.foldBufferRowRange(2, 3) + editor.foldBufferRowRange(6, 8) + editor.foldBufferRowRange(10, 11) editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) expect(editor.tokenizedLineForScreenRow(1).fold).toBeUndefined() @@ -1801,8 +1801,8 @@ describe "TextEditor", -> describe "when the 'preserveFolds' option is true", -> it "does not remove folds that contain the selections", -> editor.setSelectedBufferRange([[0, 0], [0, 0]]) - editor.createFold(1, 4) - editor.createFold(6, 8) + editor.foldBufferRowRange(1, 4) + editor.foldBufferRowRange(6, 8) editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true) expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() @@ -2203,7 +2203,7 @@ describe "TextEditor", -> it "moves the line to the previous row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]] @@ -2231,7 +2231,7 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" expect(editor.lineTextForBufferRow(9)).toBe " };" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() @@ -2269,7 +2269,7 @@ describe "TextEditor", -> it "moves the lines to the previous row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() @@ -2297,7 +2297,7 @@ describe "TextEditor", -> it "moves the lines to the previous row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() @@ -2341,7 +2341,7 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" expect(editor.lineTextForBufferRow(9)).toBe " };" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() @@ -2381,7 +2381,7 @@ describe "TextEditor", -> it "moves the lines to the previous row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRanges([ [[2, 2], [2, 9]], [[4, 2], [4, 9]] @@ -2419,7 +2419,7 @@ describe "TextEditor", -> describe "when there is a fold", -> it "moves all lines that spanned by a selection to preceding row, preserving all folds", -> - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() @@ -2446,8 +2446,8 @@ describe "TextEditor", -> describe 'and many selections intersects folded rows', -> it 'moves and preserves all the folds', -> - editor.createFold(2, 4) - editor.createFold(7, 9) + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) editor.setSelectedBufferRanges([ [[1, 0], [5, 4]], @@ -2531,7 +2531,7 @@ describe "TextEditor", -> it "moves the line to the following row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() @@ -2557,7 +2557,7 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() @@ -2611,7 +2611,7 @@ describe "TextEditor", -> it "moves the lines to the following row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() @@ -2639,7 +2639,7 @@ describe "TextEditor", -> it "moves the lines to the following row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() @@ -2669,7 +2669,7 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() @@ -2711,8 +2711,8 @@ describe "TextEditor", -> describe 'and many selections intersects folded rows', -> it 'moves and preserves all the folds', -> - editor.createFold(2, 4) - editor.createFold(7, 9) + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) editor.setSelectedBufferRanges([ [[2, 0], [2, 4]], @@ -2741,7 +2741,7 @@ describe "TextEditor", -> describe "when there is a fold below one of the selected row", -> it "moves all lines spanned by a selection to the following row, preserving the fold", -> - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() @@ -2764,7 +2764,7 @@ describe "TextEditor", -> describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", -> it "moves all the lines below the fold, preserving the fold", -> - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() @@ -2789,7 +2789,7 @@ describe "TextEditor", -> it "moves the lines to the previous row without breaking the fold", -> expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - editor.createFold(4, 7) + editor.foldBufferRowRange(4, 7) editor.setSelectedBufferRanges([ [[2, 2], [2, 9]], [[4, 2], [4, 9]] @@ -2927,7 +2927,7 @@ describe "TextEditor", -> describe "when there is a selection that ends on a folded line", -> it "destroys the selection", -> - editor.createFold(2, 4) + editor.foldBufferRowRange(2, 4) editor.setSelectedBufferRange([[1, 0], [2, 0]]) editor.insertText('holy cow') expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined() @@ -3519,8 +3519,8 @@ describe "TextEditor", -> describe "when the cursor is on a folded line", -> it "removes the lines contained by the fold", -> editor.setSelectedBufferRange([[2, 0], [2, 0]]) - editor.createFold(2, 4) - editor.createFold(2, 6) + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(2, 6) oldLine7 = buffer.lineForRow(7) oldLine8 = buffer.lineForRow(8) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 1f8c3cc26..5cae0a23a 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -3,7 +3,6 @@ _ = require 'underscore-plus' {Point, Range} = require 'text-buffer' TokenizedBuffer = require './tokenized-buffer' RowMap = require './row-map' -Fold = require './fold' Model = require './model' Token = require './token' Decoration = require './decoration' @@ -30,7 +29,6 @@ class DisplayBuffer extends Model @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 @@ -41,8 +39,8 @@ class DisplayBuffer extends Model super { - tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer, - ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager + tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, + @largeFileMode, @config, @assert, @grammarRegistry, @packageManager } = params @emitter = new Emitter @@ -56,7 +54,6 @@ class DisplayBuffer extends Model @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength()}) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() - @foldsByMarkerId = {} @decorationsById = {} @decorationsByMarkerId = {} @overlayDecorationsById = {} @@ -68,10 +65,7 @@ class DisplayBuffer extends Model @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange @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 subscribeToScopedConfigSettings: => @scopedConfigSubscriptions?.dispose() @@ -115,13 +109,11 @@ class DisplayBuffer extends Model editorWidthInChars: @editorWidthInChars tokenizedBuffer: @tokenizedBuffer.serialize() largeFileMode: @largeFileMode - foldsMarkerLayerId: @foldsMarkerLayer.id copy: -> - foldsMarkerLayer = @foldsMarkerLayer.copy() new DisplayBuffer({ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, - @grammarRegistry, @packageManager, foldsMarkerLayer + @grammarRegistry, @packageManager }) updateAllScreenLines: -> @@ -395,75 +387,20 @@ class DisplayBuffer extends Model # endRow - The row {Number} to end the fold # # Returns the new {Fold}. - createFold: (startRow, endRow) -> - unless @largeFileMode - 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 + foldBufferRowRange: (startRow, endRow) -> + @displayLayer.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) isFoldedAtBufferRow: (bufferRow) -> - @largestFoldContainingBufferRow(bufferRow)? + @displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0 isFoldedAtScreenRow: (screenRow) -> - @largestFoldContainingBufferRow(@bufferRowForScreenRow(screenRow))? - - # Destroys the fold with the given id - destroyFoldWithId: (id) -> - @foldsByMarkerId[id]?.destroy() + @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))? # Removes any folds found that contain the given buffer row. # # bufferRow - The buffer row {Number} to check against unfoldBufferRow: (bufferRow) -> - fold.destroy() for fold in @foldsContainingBufferRow(bufferRow) - return - - # Given a buffer row, this returns the largest fold that starts there. - # - # Largest is defined as the fold whose difference between its start and end points - # are the greatest. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns a {Fold} or null if none exists. - largestFoldStartingAtBufferRow: (bufferRow) -> - @foldsStartingAtBufferRow(bufferRow)[0] - - # Public: Given a buffer row, this returns all folds that start there. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns an {Array} of {Fold}s. - foldsStartingAtBufferRow: (bufferRow) -> - for marker in @findFoldMarkers(startRow: bufferRow) - @foldForMarker(marker) - - # Given a screen row, this returns the largest fold that starts there. - # - # Largest is defined as the fold whose difference between its start and end points - # are the greatest. - # - # screenRow - A {Number} indicating the screen row - # - # Returns a {Fold}. - largestFoldStartingAtScreenRow: (screenRow) -> - @largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow)) - - # Given a buffer row, this returns the largest fold that includes it. - # - # Largest is defined as the fold whose difference between its start and end rows - # is the greatest. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns a {Fold}. - largestFoldContainingBufferRow: (bufferRow) -> - @foldsContainingBufferRow(bufferRow)[0] + @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))) # Returns the folds in the given row range (exclusive of end row) that are # not contained by any other folds. @@ -480,20 +417,6 @@ class DisplayBuffer extends Model folds - # Returns all the folds that intersect the given row range. - foldsIntersectingBufferRowRange: (startRow, endRow) -> - @foldForMarker(marker) for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow]) - - # Public: Given a buffer row, this returns folds that include it. - # - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns an {Array} of {Fold}s. - foldsContainingBufferRow: (bufferRow) -> - for marker in @findFoldMarkers(intersectsRow: bufferRow) - @foldForMarker(marker) - # Given a buffer row, this converts it into a screen row. # # bufferRow - A {Number} representing a buffer row @@ -548,10 +471,7 @@ class DisplayBuffer extends Model # # Returns a {Number}. getLineCount: -> - if @largeFileMode - @tokenizedBuffer.getLineCount() - else - @screenLines.length + @displayLayer.getScreenLineCount() # Gets the number of the last screen line. # @@ -591,7 +511,6 @@ class DisplayBuffer extends Model unless screenLine? throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row", softWrapEnabled: @isSoftWrapped() - foldCount: @findFoldMarkers().length lastBufferRow: @buffer.getLastRow() lastScreenRow: @getLastRow() bufferRow: row @@ -924,12 +843,6 @@ class DisplayBuffer extends Model getDefaultMarkerLayer: -> @defaultMarkerLayer - findFoldMarker: (params) -> - @findFoldMarkers(params)[0] - - findFoldMarkers: (params) -> - @foldsMarkerLayer.findMarkers(params) - refreshMarkerScreenPositions: -> @defaultMarkerLayer.refreshMarkerScreenPositions() layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById @@ -937,7 +850,6 @@ class DisplayBuffer extends Model destroyed: -> @defaultMarkerLayer.destroy() - @foldsMarkerLayer.destroy() @scopedConfigSubscriptions.dispose() @disposables.dispose() @tokenizedBuffer.destroy() @@ -990,49 +902,49 @@ class DisplayBuffer extends Model rectangularRegion = null foldsByStartRow = {} - for fold in @outermostFoldsInBufferRowRange(startBufferRow, endBufferRow) - foldsByStartRow[fold.getStartRow()] = fold + # for fold in @outermostFoldsInBufferRowRange(startBufferRow, endBufferRow) + # foldsByStartRow[fold.getStartRow()] = fold bufferRow = startBufferRow while bufferRow < endBufferRow tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(bufferRow) - if fold = foldsByStartRow[bufferRow] - foldLine = tokenizedLine.copy() - foldLine.fold = fold - screenLines.push(foldLine) + # if fold = foldsByStartRow[bufferRow] + # foldLine = tokenizedLine.copy() + # foldLine.fold = fold + # screenLines.push(foldLine) + # + # if rectangularRegion? + # regions.push(rectangularRegion) + # rectangularRegion = null + # + # foldedRowCount = fold.getBufferRowCount() + # regions.push(bufferRows: foldedRowCount, screenRows: 1) + # bufferRow += foldedRowCount + # else + softWraps = 0 + if @isSoftWrapped() + while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumnForTokenizedLine(tokenizedLine)) + [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt( + wrapScreenColumn, + @configSettings.softWrapHangingIndent + ) + break if wrappedLine.hasOnlySoftWrapIndentation() + screenLines.push(wrappedLine) + softWraps++ + screenLines.push(tokenizedLine) + if softWraps > 0 if rectangularRegion? regions.push(rectangularRegion) rectangularRegion = null - - foldedRowCount = fold.getBufferRowCount() - regions.push(bufferRows: foldedRowCount, screenRows: 1) - bufferRow += foldedRowCount + regions.push(bufferRows: 1, screenRows: softWraps + 1) else - softWraps = 0 - if @isSoftWrapped() - while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumnForTokenizedLine(tokenizedLine)) - [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt( - wrapScreenColumn, - @configSettings.softWrapHangingIndent - ) - break if wrappedLine.hasOnlySoftWrapIndentation() - screenLines.push(wrappedLine) - softWraps++ - screenLines.push(tokenizedLine) + rectangularRegion ?= {bufferRows: 0, screenRows: 0} + rectangularRegion.bufferRows++ + rectangularRegion.screenRows++ - if softWraps > 0 - if rectangularRegion? - regions.push(rectangularRegion) - rectangularRegion = null - regions.push(bufferRows: 1, screenRows: softWraps + 1) - else - rectangularRegion ?= {bufferRows: 0, screenRows: 0} - rectangularRegion.bufferRows++ - rectangularRegion.screenRows++ - - bufferRow++ + bufferRow++ if rectangularRegion? regions.push(rectangularRegion) @@ -1076,12 +988,6 @@ class DisplayBuffer extends Model @didUpdateDecorationsEventScheduled = false @emitter.emit 'did-update-decorations' - decorateFold: (fold) -> - @decorateMarker(fold.marker, type: 'line-number', class: 'folded') - - foldForMarker: (marker) -> - @foldsByMarkerId[marker.id] - decorationDidChangeType: (decoration) -> if decoration.isType('overlay') @overlayDecorationsById[decoration.id] = decoration @@ -1126,7 +1032,6 @@ class DisplayBuffer extends Model checkScreenLinesInvariant: -> return if @isSoftWrapped() - return if _.size(@foldsByMarkerId) > 0 screenLinesCount = @screenLines.length tokenizedLinesCount = @tokenizedBuffer.getLineCount() diff --git a/src/fold.coffee b/src/fold.coffee deleted file mode 100644 index 051be9f4c..000000000 --- a/src/fold.coffee +++ /dev/null @@ -1,83 +0,0 @@ -{Point, Range} = require 'text-buffer' - -# Represents a fold that collapses multiple buffer lines into a single -# line on the screen. -# -# Their creation is managed by the {DisplayBuffer}. -module.exports = -class Fold - id: null - displayBuffer: null - marker: null - - constructor: (@displayBuffer, @marker) -> - @id = @marker.id - @displayBuffer.foldsByMarkerId[@marker.id] = this - @marker.onDidDestroy => @destroyed() - @marker.onDidChange ({isValid}) => @destroy() unless isValid - - # Returns whether this fold is contained within another fold - isInsideLargerFold: -> - largestContainingFoldMarker = @displayBuffer.findFoldMarker(containsRange: @getBufferRange()) - largestContainingFoldMarker and - not largestContainingFoldMarker.getRange().isEqual(@getBufferRange()) - - # Destroys this fold - destroy: -> - @marker.destroy() - - # Returns the fold's {Range} in buffer coordinates - # - # includeNewline - A {Boolean} which, if `true`, includes the trailing newline - # - # Returns a {Range}. - getBufferRange: ({includeNewline}={}) -> - range = @marker.getRange() - - if range.end.row > range.start.row and nextFold = @displayBuffer.largestFoldStartingAtBufferRow(range.end.row) - nextRange = nextFold.getBufferRange() - range = new Range(range.start, nextRange.end) - - if includeNewline - range = range.copy() - range.end.row++ - range.end.column = 0 - range - - getBufferRowRange: -> - {start, end} = @getBufferRange() - [start.row, end.row] - - # Returns the fold's start row as a {Number}. - getStartRow: -> - @getBufferRange().start.row - - # Returns the fold's end row as a {Number}. - getEndRow: -> - @getBufferRange().end.row - - # Returns a {String} representation of the fold. - inspect: -> - "Fold(#{@getStartRow()}, #{@getEndRow()})" - - # Retrieves the number of buffer rows spanned by the fold. - # - # Returns a {Number}. - getBufferRowCount: -> - @getEndRow() - @getStartRow() + 1 - - # Identifies if a fold is nested within a fold. - # - # fold - A {Fold} to check - # - # Returns a {Boolean}. - isContainedByFold: (fold) -> - @isContainedByRange(fold.getBufferRange()) - - updateDisplayBuffer: -> - unless @isInsideLargerFold() - @displayBuffer.updateScreenLines(@getStartRow(), @getEndRow() + 1, 0, updateMarkers: true) - - destroyed: -> - delete @displayBuffer.foldsByMarkerId[@marker.id] - @updateDisplayBuffer() diff --git a/src/language-mode.coffee b/src/language-mode.coffee index ac8a5af76..b58744558 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -92,14 +92,12 @@ class LanguageMode for currentRow in [0..@buffer.getLastRow()] by 1 [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? - @editor.createFold(startRow, endRow) + @editor.foldBufferRowRange(startRow, endRow) return # Unfolds all the foldable lines in the buffer. unfoldAll: -> - for fold in @editor.displayBuffer.foldsIntersectingBufferRowRange(0, @buffer.getLastRow()) by -1 - fold.destroy() - return + @editor.displayLayer.destroyAllFolds() # Fold all comment and code blocks at a given indentLevel # @@ -112,7 +110,7 @@ class LanguageMode # assumption: startRow will always be the min indent level for the entire range if @editor.indentationForBufferRow(startRow) is indentLevel - @editor.createFold(startRow, endRow) + @editor.foldBufferRowRange(startRow, endRow) return # Given a buffer row, creates a fold at it. @@ -124,8 +122,8 @@ class LanguageMode for currentRow in [bufferRow..0] by -1 [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? and startRow <= bufferRow <= endRow - fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow) - return @editor.createFold(startRow, endRow) unless fold + unless @editor.displayBuffer.isFoldedAtBufferRow(startRow) + return @editor.foldBufferRowRange(startRow, endRow) # Find the row range for a fold at a given bufferRow. Will handle comments # and code. diff --git a/src/selection.coffee b/src/selection.coffee index 2ba66ebb0..7afa10767 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -87,7 +87,7 @@ class Selection extends Model setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) options.reversed ?= @isReversed() - @editor.destroyFoldsContainingBufferRange(bufferRange) unless options.preserveFolds + @editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds @modifySelection => needsFlash = options.flash delete options.flash if options.flash? @@ -410,7 +410,7 @@ class Selection extends Model # Public: Removes the first character before the selection if the selection # is empty otherwise it deletes the selection. backspace: -> - @selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow()) + @selectLeft() if @isEmpty() @deleteSelectedText() # Public: Removes the selection or, if nothing is selected, then all @@ -445,11 +445,7 @@ class Selection extends Model # Public: Removes the selection or the next character after the start of the # selection if the selection is empty. delete: -> - if @isEmpty() - if @cursor.isAtEndOfLine() and fold = @editor.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1) - @selectToBufferPosition(fold.getBufferRange().end) - else - @selectRight() + @selectRight() if @isEmpty() @deleteSelectedText() # Public: If the selection is empty, removes all text from the cursor to the @@ -482,8 +478,6 @@ class Selection extends Model # Public: Removes only the selected text. deleteSelectedText: -> bufferRange = @getBufferRange() - if bufferRange.isEmpty() and fold = @editor.largestFoldContainingBufferRow(bufferRange.start.row) - bufferRange = bufferRange.union(fold.getBufferRange(includeNewline: true)) @editor.buffer.delete(bufferRange) unless bufferRange.isEmpty() @cursor?.setBufferPosition(bufferRange.start) @@ -634,7 +628,7 @@ class Selection extends Model # Public: Creates a fold containing the current selection. fold: -> range = @getBufferRange() - @editor.createFold(range.start.row, range.end.row) + @editor.foldBufferRowRange(range.start.row, range.end.row) @cursor.setBufferPosition([range.end.row + 1, 0]) # Private: Increase the indentation level of the given text by given number diff --git a/src/text-editor.coffee b/src/text-editor.coffee index e5757a6e6..ef8265e62 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -948,10 +948,10 @@ class TextEditor extends Model range ).map (range) -> range.translate([-insertDelta, 0]) - # Make sure the inserted text doesn't go into an existing fold - if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow) - rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0])) - fold.destroy() + # # Make sure the inserted text doesn't go into an existing fold + # if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow) + # rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0])) + # fold.destroy() # Delete lines spanned by selection and insert them on the preceding buffer row lines = @buffer.getTextInRange(linesRange) @@ -961,7 +961,7 @@ class TextEditor extends Model # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold - @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row) + @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row) for selection in selectionsToMove newSelectionRanges.push(selection.translate([-insertDelta, 0])) @@ -1032,10 +1032,10 @@ class TextEditor extends Model range ).map (range) -> range.translate([insertDelta, 0]) - # Make sure the inserted text doesn't go into an existing fold - if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow) - rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0])) - fold.destroy() + # # Make sure the inserted text doesn't go into an existing fold + # if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow) + # rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0])) + # fold.destroy() # Delete lines spanned by selection and insert them on the following correct buffer row insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0) @@ -1048,7 +1048,7 @@ class TextEditor extends Model # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold - @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row) + @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row) for selection in selectionsToMove newSelectionRanges.push(selection.translate([insertDelta, 0])) @@ -1081,7 +1081,7 @@ class TextEditor extends Model delta = endRow - startRow selection.setBufferRange(selectedBufferRange.translate([delta, 0])) for [foldStartRow, foldEndRow] in foldedRowRanges - @createFold(foldStartRow + delta, foldEndRow + delta) + @foldBufferRowRange(foldStartRow + delta, foldEndRow + delta) return replaceSelectedText: (options={}, fn) -> @@ -2432,7 +2432,7 @@ class TextEditor extends Model # Returns the new {Selection}. addSelection: (marker, options={}) -> unless marker.getProperties().preserveFolds - @destroyFoldsContainingBufferRange(marker.getBufferRange()) + @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) selection = new Selection(_.extend({editor: this, marker, cursor, @clipboard}, options)) @selections.push(selection) @@ -2978,35 +2978,12 @@ class TextEditor extends Model isFoldedAtScreenRow: (screenRow) -> @displayBuffer.isFoldedAtScreenRow(screenRow) - # TODO: Rename to foldRowRange? - createFold: (startRow, endRow) -> - @displayBuffer.createFold(startRow, endRow) - - # {Delegates to: DisplayBuffer.destroyFoldWithId} - destroyFoldWithId: (id) -> - @displayBuffer.destroyFoldWithId(id) + foldBufferRowRange: (startRow, endRow) -> + @displayBuffer.foldBufferRowRange(startRow, endRow) # Remove any {Fold}s found that intersect the given buffer range. destroyFoldsIntersectingBufferRange: (bufferRange) -> - @destroyFoldsContainingBufferRange(bufferRange) - - for row in [bufferRange.end.row..bufferRange.start.row] - fold.destroy() for fold in @displayBuffer.foldsStartingAtBufferRow(row) - - return - - # Remove any {Fold}s found that contain the given buffer range. - destroyFoldsContainingBufferRange: (bufferRange) -> - @unfoldBufferRow(bufferRange.start.row) - @unfoldBufferRow(bufferRange.end.row) - - # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} - largestFoldContainingBufferRow: (bufferRow) -> - @displayBuffer.largestFoldContainingBufferRow(bufferRow) - - # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow} - largestFoldStartingAtScreenRow: (screenRow) -> - @displayBuffer.largestFoldStartingAtScreenRow(screenRow) + @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange} outermostFoldsInBufferRowRange: (startRow, endRow) -> From 0d55a0bd76e1e97b18c96aafeb86646c744bec04 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 13 Jan 2016 17:54:18 -0700 Subject: [PATCH 010/262] Make TokenizedBuffer conform to text decoration layer interface --- spec/tokenized-buffer-spec.coffee | 54 ++++++++++- src/tokenized-buffer-iterator.coffee | 78 +++++++++++++++ src/tokenized-buffer.coffee | 8 ++ src/tokenized-line.coffee | 137 --------------------------- 4 files changed, 139 insertions(+), 138 deletions(-) create mode 100644 src/tokenized-buffer-iterator.coffee diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 9c53b3c07..4fc0d1618 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -1,5 +1,5 @@ TokenizedBuffer = require '../src/tokenized-buffer' -TextBuffer = require 'text-buffer' +{Point} = TextBuffer = require 'text-buffer' _ = require 'underscore-plus' describe "TokenizedBuffer", -> @@ -1061,3 +1061,55 @@ describe "TokenizedBuffer", -> runs -> expect(coffeeCalled).toBe true + + describe "text decoration layer API", -> + describe "iterator", -> + it "iterates over the syntactic scope boundaries", -> + buffer = new TextBuffer(text: "var foo = 1 /*\nhello*/var bar = 2\n") + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) + tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".js")) + fullyTokenize(tokenizedBuffer) + + iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + + boundaries = [] + loop + boundaries.push({ + position: iterator.getPosition(), + closeTags: iterator.getCloseTags(), + openTags: iterator.getOpenTags() + }) + break unless iterator.moveToSuccessor() + + expect(boundaries).toEqual([ + {position: Point(0, 0), closeTags: [], openTags: ["source.js", "storage.type.var.js"]} + {position: Point(0, 3), closeTags: ["storage.type.var.js"], openTags: []} + {position: Point(0, 8), closeTags: [], openTags: ["keyword.operator.assignment.js"]} + {position: Point(0, 9), closeTags: ["keyword.operator.assignment.js"], openTags: []} + {position: Point(0, 10), closeTags: [], openTags: ["constant.numeric.js"]} + {position: Point(0, 11), closeTags: ["constant.numeric.js"], openTags: []} + {position: Point(0, 12), closeTags: [], openTags: ["comment.block.js", "punctuation.definition.comment.js"]} + {position: Point(0, 14), closeTags: ["punctuation.definition.comment.js"], openTags: []} + {position: Point(1, 5), closeTags: [], openTags: ["punctuation.definition.comment.js"]} + {position: Point(1, 7), closeTags: ["punctuation.definition.comment.js", "comment.block.js"], openTags: ["storage.type.var.js"]} + {position: Point(1, 10), closeTags: ["storage.type.var.js"], openTags: []} + {position: Point(1, 15), closeTags: [], openTags: ["keyword.operator.assignment.js"]} + {position: Point(1, 16), closeTags: ["keyword.operator.assignment.js"], openTags: []} + {position: Point(1, 17), closeTags: [], openTags: ["constant.numeric.js"]} + {position: Point(1, 18), closeTags: ["constant.numeric.js"], openTags: []} + ]) + + expect(iterator.seek(Point(0, 1))).toEqual(["source.js", "storage.type.var.js"]) + expect(iterator.getPosition()).toEqual(Point(0, 3)) + expect(iterator.seek(Point(0, 8))).toEqual(["source.js"]) + expect(iterator.getPosition()).toEqual(Point(0, 8)) + expect(iterator.seek(Point(1, 0))).toEqual(["source.js", "comment.block.js"]) + expect(iterator.getPosition()).toEqual(Point(1, 5)) + expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.js"]) + expect(iterator.getPosition()).toEqual(Point(1, 18)) + + expect(iterator.seek(Point(2, 0))).toEqual(["source.js"]) + iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test) diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee new file mode 100644 index 000000000..acafb8f4b --- /dev/null +++ b/src/tokenized-buffer-iterator.coffee @@ -0,0 +1,78 @@ +{Point} = require 'text-buffer' + +module.exports = +class TokenizedBufferIterator + constructor: (@tokenizedBuffer, @grammarRegistry) -> + @openTags = null + @closeTags = null + + seek: (position) -> + @openTags = [] + @closeTags = [] + @tagIndex = null + + currentLine = @tokenizedBuffer.tokenizedLineForRow(position.row) + containingTags = currentLine.openScopes.map (id) => @grammarRegistry.scopeForId(id) + @currentTags = currentLine.tags + currentColumn = 0 + for tag, index in @currentTags + if tag >= 0 + if currentColumn >= position.column and @isAtTagBoundary() + @tagIndex = index + break + else + currentColumn += tag + containingTags.pop() while @closeTags.shift() + containingTags.push(tag) while tag = @openTags.shift() + else + scopeName = @grammarRegistry.scopeForId(tag) + if tag % 2 is 0 + @closeTags.push(scopeName) + else + @openTags.push(scopeName) + + @tagIndex ?= @currentTags.length + @position = Point(position.row, currentColumn) + containingTags + + moveToSuccessor: -> + if @tagIndex is @currentTags.length + @position = Point(@position.row + 1, 0) + @currentTags = @tokenizedBuffer.tokenizedLineForRow(@position.row)?.tags + return false unless @currentTags? + @tagIndex = 0 + else + @position = Point(@position.row, @position.column + @currentTags[@tagIndex]) + @tagIndex++ + + @openTags = [] + @closeTags = [] + + loop + tag = @currentTags[@tagIndex] + if tag >= 0 or @tagIndex is @currentTags.length + if @isAtTagBoundary() + break + else + return @moveToSuccessor() + else + scopeName = @grammarRegistry.scopeForId(tag) + if tag % 2 is 0 + @closeTags.push(scopeName) + else + @openTags.push(scopeName) + @tagIndex++ + + true + + getPosition: -> + @position + + getCloseTags: -> + @closeTags.slice() + + getOpenTags: -> + @openTags.slice() + + isAtTagBoundary: -> + @closeTags.length > 0 or @openTags.length > 0 diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 0fdf4eea8..0267e02dd 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -7,6 +7,7 @@ TokenizedLine = require './tokenized-line' TokenIterator = require './token-iterator' Token = require './token' ScopeDescriptor = require './scope-descriptor' +TokenizedBufferIterator = require './tokenized-buffer-iterator' module.exports = class TokenizedBuffer extends Model @@ -58,6 +59,12 @@ class TokenizedBuffer extends Model destroyed: -> @disposables.dispose() + buildIterator: -> + new TokenizedBufferIterator(this, @grammarRegistry) + + getInvalidatedRanges: -> + [@invalidatedRange] + serialize: -> state = { deserializer: 'TokenizedBuffer' @@ -274,6 +281,7 @@ class TokenizedBuffer extends Model [start, end] = @updateFoldableStatus(start, end + delta) end -= delta + @invalidatedRange = Range(start, end) event = {start, end, delta, bufferChange: e} @emitter.emit 'did-change', event diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index c97a621ac..e063387eb 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -47,143 +47,6 @@ class TokenizedLine @startBufferColumn ?= 0 @bufferDelta = @text.length - @transformContent() - @buildEndOfLineInvisibles() if @invisibles? and @lineEnding? - - transformContent: -> - text = '' - bufferColumn = 0 - screenColumn = 0 - tokenIndex = 0 - tokenOffset = 0 - firstNonWhitespaceColumn = null - lastNonWhitespaceColumn = null - - substringStart = 0 - substringEnd = 0 - - while bufferColumn < @text.length - # advance to next token if we've iterated over its length - if tokenOffset is @tags[tokenIndex] - tokenIndex++ - tokenOffset = 0 - - # advance to next token tag - tokenIndex++ while @tags[tokenIndex] < 0 - - charCode = @text.charCodeAt(bufferColumn) - - # split out unicode surrogate pairs - if isPairedCharacter(@text, bufferColumn) - prefix = tokenOffset - suffix = @tags[tokenIndex] - tokenOffset - 2 - - i = tokenIndex - @tags.splice(i, 1) - @tags.splice(i++, 0, prefix) if prefix > 0 - @tags.splice(i++, 0, 2) - @tags.splice(i, 0, suffix) if suffix > 0 - - firstNonWhitespaceColumn ?= screenColumn - lastNonWhitespaceColumn = screenColumn + 1 - - substringEnd += 2 - screenColumn += 2 - bufferColumn += 2 - - tokenIndex++ if prefix > 0 - @specialTokens[tokenIndex] = PairedCharacter - tokenIndex++ - tokenOffset = 0 - - # split out leading soft tabs - else if charCode is SpaceCharCode - if firstNonWhitespaceColumn? - substringEnd += 1 - else - if (screenColumn + 1) % @tabLength is 0 - suffix = @tags[tokenIndex] - @tabLength - if suffix >= 0 - @specialTokens[tokenIndex] = SoftTab - @tags.splice(tokenIndex, 1, @tabLength) - @tags.splice(tokenIndex + 1, 0, suffix) if suffix > 0 - - if @invisibles?.space - if substringEnd > substringStart - text += @text.substring(substringStart, substringEnd) - substringStart = substringEnd - text += @invisibles.space - substringStart += 1 - - substringEnd += 1 - - screenColumn++ - bufferColumn++ - tokenOffset++ - - # expand hard tabs to the next tab stop - else if charCode is TabCharCode - if substringEnd > substringStart - text += @text.substring(substringStart, substringEnd) - substringStart = substringEnd - - tabLength = @tabLength - (screenColumn % @tabLength) - if @invisibles?.tab - text += @invisibles.tab - text += getTabString(tabLength - 1) if tabLength > 1 - else - text += getTabString(tabLength) - - substringStart += 1 - substringEnd += 1 - - prefix = tokenOffset - suffix = @tags[tokenIndex] - tokenOffset - 1 - - i = tokenIndex - @tags.splice(i, 1) - @tags.splice(i++, 0, prefix) if prefix > 0 - @tags.splice(i++, 0, tabLength) - @tags.splice(i, 0, suffix) if suffix > 0 - - screenColumn += tabLength - bufferColumn++ - - tokenIndex++ if prefix > 0 - @specialTokens[tokenIndex] = HardTab - tokenIndex++ - tokenOffset = 0 - - # continue past any other character - else - firstNonWhitespaceColumn ?= screenColumn - lastNonWhitespaceColumn = screenColumn - - substringEnd += 1 - screenColumn++ - bufferColumn++ - tokenOffset++ - - if substringEnd > substringStart - unless substringStart is 0 and substringEnd is @text.length - text += @text.substring(substringStart, substringEnd) - @text = text - else - @text = text - - @firstNonWhitespaceIndex = firstNonWhitespaceColumn - if lastNonWhitespaceColumn? - if lastNonWhitespaceColumn + 1 < @text.length - @firstTrailingWhitespaceIndex = lastNonWhitespaceColumn + 1 - if @invisibles?.space - @text = - @text.substring(0, @firstTrailingWhitespaceIndex) + - @text.substring(@firstTrailingWhitespaceIndex) - .replace(RepeatedSpaceRegex, @invisibles.space) - else - @lineIsWhitespaceOnly = true - @firstTrailingWhitespaceIndex = 0 - getTokenIterator: -> @tokenIterator.reset(this, arguments...) Object.defineProperty @prototype, 'tokens', get: -> From acbacae6d50b7a44cf712a170cb9216955b5476b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 13 Jan 2016 18:23:22 -0700 Subject: [PATCH 011/262] Use TokenizedBuffer as a text decoration layer and render tags --- src/display-buffer.coffee | 1 + src/lines-tile-component.coffee | 23 +++++++++++++++++------ src/text-editor-presenter.coffee | 15 ++++++++++----- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 5cae0a23a..096afaa60 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -52,6 +52,7 @@ class DisplayBuffer extends Model }) @buffer = @tokenizedBuffer.buffer @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength()}) + @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() @decorationsById = {} diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index f4712dfb6..7d106f9b2 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -125,7 +125,7 @@ class LinesTileComponent screenRowForNode: (node) -> parseInt(node.dataset.screenRow) buildLineNode: (id) -> - {screenRow, words, decorationClasses} = @newTileState.lines[id] + {screenRow, decorationClasses} = @newTileState.lines[id] lineNode = @domElementPool.buildElement("div", "line") lineNode.dataset.screenRow = screenRow @@ -183,12 +183,23 @@ class LinesTileComponent @currentLineTextNodes.push(textNode) setLineInnerNodes: (id, lineNode) -> - {words} = @newTileState.lines[id] + {tokens} = @newTileState.lines[id] lineLength = 0 - for word in words when word.length > 0 - lineLength += word.length - textNode = @domElementPool.buildText(word.replace(/\s/g, NBSPCharacter)) - lineNode.appendChild(textNode) + openScopeNode = lineNode + for token in tokens when token.text.length > 0 + {closeTags, openTags, text} = token + + for scope in closeTags + openScopeNode = openScopeNode.parentElement + + for scope in openTags + newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' ')) + openScopeNode.appendChild(newScopeNode) + openScopeNode = newScopeNode + + lineLength += text.length + textNode = @domElementPool.buildText(text.replace(/\s/g, NBSPCharacter)) + openScopeNode.appendChild(textNode) @currentLineTextNodes.push(textNode) if lineLength is 0 diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 5d2b2c335..54ea000bd 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -133,8 +133,9 @@ class TextEditorPresenter @shouldUpdateDecorations = true observeModel: -> - @disposables.add @model.displayLayer.onDidChangeTextSync (change) => - @invalidateLines(change) + @disposables.add @model.displayLayer.onDidChangeSync (changes) => + for change in changes + @invalidateLines(change) @shouldUpdateDecorations = true @emitDidUpdateState() @@ -395,7 +396,7 @@ class TextEditorPresenter else tileState.lines[line.id] = screenRow: screenRow - words: line.words + tokens: line.tokens decorationClasses: @lineDecorationClassesForRow(screenRow) for id, line of tileState.lines @@ -1028,10 +1029,14 @@ class TextEditorPresenter @linesById.delete(lineId) buildLine: (screenRow) -> - line = {id: @lineIdCounter++, words: []} + line = {id: @lineIdCounter++, tokens: []} @tokenIterator.seekToScreenRow(screenRow) loop - line.words.push(@tokenIterator.getText()) + line.tokens.push({ + text: @tokenIterator.getText(), + closeTags: @tokenIterator.getCloseTags(), + openTags: @tokenIterator.getOpenTags() + }) break unless @tokenIterator.moveToSuccessor() break unless @tokenIterator.getStartScreenPosition().row is screenRow line From a90a2e65b5c4618b25cabeeecd4e201defa6b320 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 14 Jan 2016 14:45:52 -0700 Subject: [PATCH 012/262] Implement TokenizedBuffer.prototype.onDidInvalidateRange This causes DisplayLayer to emit change events when syntax is updated asynchronously. --- src/tokenized-buffer.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 0267e02dd..96d1f4649 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -65,6 +65,9 @@ class TokenizedBuffer extends Model getInvalidatedRanges: -> [@invalidatedRange] + onDidInvalidateRange: (fn) -> + @emitter.on 'did-invalidate-range', fn + serialize: -> state = { deserializer: 'TokenizedBuffer' @@ -221,6 +224,7 @@ class TokenizedBuffer extends Model event = {start: startRow, end: endRow, delta: 0} @emitter.emit 'did-change', event + @emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0)) if @firstInvalidRow()? @tokenizeInBackground() From ed79413de18e2f840e955f014c5cab7e3fa80731 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 19 Jan 2016 14:39:37 -0700 Subject: [PATCH 013/262] Depend on DisplayLayer for more things --- src/cursor.coffee | 6 +++--- src/text-editor-presenter.coffee | 18 ++++++++++-------- src/text-editor.coffee | 31 +++++++++++++------------------ 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/cursor.coffee b/src/cursor.coffee index f91a7bfd9..87cd73dc8 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -261,7 +261,7 @@ class Cursor extends Model while columnCount > column and row > 0 columnCount -= column - column = @editor.lineTextForScreenRow(--row).length + column = @editor.lineLengthForScreenRow(--row) columnCount-- # subtract 1 for the row move column = column - columnCount @@ -280,7 +280,7 @@ class Cursor extends Model else {row, column} = @getScreenPosition() maxLines = @editor.getScreenLineCount() - rowLength = @editor.lineTextForScreenRow(row).length + rowLength = @editor.lineLengthForScreenRow(row) columnsRemainingInLine = rowLength - column while columnCount > columnsRemainingInLine and row < maxLines - 1 @@ -288,7 +288,7 @@ class Cursor extends Model columnCount-- # subtract 1 for the row move column = 0 - rowLength = @editor.lineTextForScreenRow(++row).length + rowLength = @editor.lineLengthForScreenRow(++row) columnsRemainingInLine = rowLength column = column + columnCount diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 54ea000bd..e251fb0da 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -366,7 +366,7 @@ class TextEditorPresenter visibleTiles[tileStartRow] = true zIndex++ - if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? + if @mouseWheelScreenRow? and 0 <= @mouseWheelScreenRow < @model.getScreenLineCount() mouseWheelTile = @tileForRow(@mouseWheelScreenRow) unless visibleTiles[mouseWheelTile]? @@ -581,12 +581,12 @@ class TextEditorPresenter softWrapped = false screenRow = startRow + i - line = @model.tokenizedLineForScreenRow(screenRow) + lineId = @lineIdForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} - visibleLineNumberIds[line.id] = true + tileState.lineNumbers[lineId] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} + visibleLineNumberIds[lineId] = true for id of tileState.lineNumbers delete tileState.lineNumbers[id] unless visibleLineNumberIds[id] @@ -647,9 +647,10 @@ class TextEditorPresenter updateHorizontalDimensions: -> if @baseCharacterWidth? oldContentWidth = @contentWidth - rightmostPosition = Point(@model.getLongestScreenRow(), @model.getMaxScreenLineLength()) - if @model.tokenizedLineForScreenRow(rightmostPosition.row)?.isSoftWrapped() - rightmostPosition = @model.clipScreenPosition(rightmostPosition) + rightmostPosition = @model.getRightmostScreenPosition() + # TODO: Add some version of this back once softwrap is reintroduced + # if @model.tokenizedLineForScreenRow(rightmostPosition.row)?.isSoftWrapped() + # rightmostPosition = @model.clipScreenPosition(rightmostPosition) @contentWidth = @pixelPositionForScreenPosition(rightmostPosition).left @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width @@ -1421,4 +1422,5 @@ class TextEditorPresenter @startRow <= row < @endRow lineIdForScreenRow: (screenRow) -> - @model.tokenizedLineForScreenRow(screenRow)?.id + ids = @lineMarkerIndex.findStartingAt(Point(screenRow, 0)) + ids.values().next().value if ids.size > 0 diff --git a/src/text-editor.coffee b/src/text-editor.coffee index ef8265e62..9aab56bca 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -738,7 +738,7 @@ class TextEditor extends Model # Essential: Returns a {Number} representing the number of screen lines in the # editor. This accounts for folds. - getScreenLineCount: -> @displayBuffer.getLineCount() + getScreenLineCount: -> @displayLayer.getScreenLineCount() # Essential: Returns a {Number} representing the last zero-indexed buffer row # number of the editor. @@ -746,7 +746,7 @@ class TextEditor extends Model # Essential: Returns a {Number} representing the last zero-indexed screen row # number of the editor. - getLastScreenRow: -> @displayBuffer.getLastRow() + getLastScreenRow: -> @getScreenLineCount() - 1 # Essential: Returns a {String} representing the contents of the line at the # given buffer row. @@ -770,17 +770,21 @@ class TextEditor extends Model # {Delegates to: DisplayBuffer.tokenizedLinesForScreenRows} tokenizedLinesForScreenRows: (start, end) -> @displayBuffer.tokenizedLinesForScreenRows(start, end) - bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row) + bufferRowForScreenRow: (row) -> @displayLayer.translateScreenPosition(Point(row, 0)).row # {Delegates to: DisplayBuffer.bufferRowsForScreenRows} bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) - screenRowForBufferRow: (row) -> @displayBuffer.screenRowForBufferRow(row) + screenRowForBufferRow: (row) -> @displayLayer.translateBufferPosition(Point(row, 0)).row + + getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() # {Delegates to: DisplayBuffer.getMaxLineLength} - getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength() + getMaxScreenLineLength: -> @getRightmostScreenPosition().column - getLongestScreenRow: -> @displayBuffer.getLongestScreenRow() + getLongestScreenRow: -> @getRightmostScreenPosition().row + + lineLengthForScreenRow: (screenRow) -> @displayLayer.lineLengthForScreenRow(screenRow) # Returns the range for the given buffer row. # @@ -1337,22 +1341,14 @@ class TextEditor extends Model # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. # # Returns a {Range}. - screenRangeForBufferRange: (bufferRange) -> - bufferRange = Range.fromObject(bufferRange) - start = @displayLayer.translateBufferPosition(bufferRange.start) - end = @displayLayer.translateBufferPosition(bufferRange.end) - Range(start, end) + screenRangeForBufferRange: (bufferRange) -> @displayLayer.translateBufferRange(bufferRange) # Essential: Convert a range in screen-coordinates to buffer-coordinates. # # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. # # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> - screenRange = Range.fromObject(screenRange) - start = @displayLayer.translateScreenPosition(screenRange.start) - end = @displayLayer.translateScreenPosition(screenRange.end) - Range(start, end) + bufferRangeForScreenRange: (screenRange) -> @displayLayer.translateScreenRange(screenRange) # Extended: Clip the given {Point} to a valid position in the buffer. # @@ -2945,8 +2941,7 @@ class TextEditor extends Model # # Returns a {Boolean}. isFoldableAtScreenRow: (screenRow) -> - bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow) - @isFoldableAtBufferRow(bufferRow) + @isFoldableAtBufferRow(@bufferRowForScreenRow(screenRow)) # Extended: Fold the given buffer row if it isn't currently folded, and unfold # it otherwise. From 21b1f79fabe7b31f668b245a6005ac5acbc52104 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 19 Jan 2016 14:51:24 -0700 Subject: [PATCH 014/262] Avoid updating screen lines in DisplayBuffer --- src/display-buffer.coffee | 15 ++++++--------- src/text-editor.coffee | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 096afaa60..51e0f6f52 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -118,6 +118,7 @@ class DisplayBuffer extends Model }) updateAllScreenLines: -> + return # TODO: After DisplayLayer is finished, delete these code paths @maxLineLength = 0 @screenLines = [] @rowMap = new RowMap @@ -376,11 +377,8 @@ class DisplayBuffer extends Model # # Returns an {Array} of buffer rows as {Numbers}s. bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> - if @largeFileMode - [startScreenRow..endScreenRow] - else - for screenRow in [startScreenRow..endScreenRow] - @rowMap.bufferRowRangeForScreenRow(screenRow)[0] + for screenRow in [startScreenRow..endScreenRow] + @bufferRowForScreenRow(screenRow) # Creates a new fold between two row numbers. # @@ -441,10 +439,7 @@ class DisplayBuffer extends Model # # Returns a {Number}. bufferRowForScreenRow: (screenRow) -> - if @largeFileMode - screenRow - else - @rowMap.bufferRowRangeForScreenRow(screenRow)[0] + @displayLayer.translateScreenPosition(Point(screenRow, 0)).row # Given a buffer range, this converts it into a screen position. # @@ -870,6 +865,8 @@ class DisplayBuffer extends Model @updateScreenLines(start, end + 1, delta, refreshMarkers: false) updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) -> + return # TODO: After DisplayLayer is finished, delete these code paths + return if @largeFileMode return if @isDestroyed() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 9aab56bca..50b3196db 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -171,7 +171,7 @@ class TextEditor extends Model @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) => + @disposables.add @displayLayer.onDidChangeSync (e) => @mergeIntersectingSelections() @emitter.emit 'did-change', e From 47338253dbb3a5a445f3a97c4ec6d764a8de10f8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Jan 2016 12:24:48 -0700 Subject: [PATCH 015/262] Adapt to new text-buffer API --- src/text-editor-presenter.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e251fb0da..d6112bdfe 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1042,8 +1042,8 @@ class TextEditorPresenter break unless @tokenIterator.getStartScreenPosition().row is screenRow line - invalidateLines: ({start, replacedExtent, replacementExtent}) -> - {touch} = @lineMarkerIndex.splice(start, replacedExtent, replacementExtent) + invalidateLines: ({start, oldExtent, newExtent}) -> + {touch} = @lineMarkerIndex.splice(start, oldExtent, newExtent) touch.forEach (lineId) => @lineMarkerIndex.delete(lineId) @linesById.delete(lineId) From d0e29bfc2c2dda47752a134896e4958c0f83e0cc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Jan 2016 17:42:28 -0700 Subject: [PATCH 016/262] Avoid recursive call to moveToSuccessor, which is blowing the stack --- spec/tokenized-buffer-spec.coffee | 27 +++++++++++++++++ src/tokenized-buffer-iterator.coffee | 44 ++++++++++++++++------------ 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 4fc0d1618..39d03330a 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -1113,3 +1113,30 @@ describe "TokenizedBuffer", -> expect(iterator.seek(Point(2, 0))).toEqual(["source.js"]) iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test) + + it "does not report columns beyond the length of the line", -> + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + + runs -> + buffer = new TextBuffer(text: "# hello\n# world") + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) + tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".coffee")) + fullyTokenize(tokenizedBuffer) + + iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + iterator.moveToSuccessor() + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(7) + + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(0) + + iterator.seek(Point(0, 7)) + expect(iterator.getPosition().column).toBe(7) + + iterator.seek(Point(0, 8)) + expect(iterator.getPosition().column).toBe(7) diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee index acafb8f4b..e6e129d7a 100644 --- a/src/tokenized-buffer-iterator.coffee +++ b/src/tokenized-buffer-iterator.coffee @@ -14,6 +14,7 @@ class TokenizedBufferIterator currentLine = @tokenizedBuffer.tokenizedLineForRow(position.row) containingTags = currentLine.openScopes.map (id) => @grammarRegistry.scopeForId(id) @currentTags = currentLine.tags + @currentLineLength = currentLine.text.length currentColumn = 0 for tag, index in @currentTags if tag >= 0 @@ -32,39 +33,46 @@ class TokenizedBufferIterator @openTags.push(scopeName) @tagIndex ?= @currentTags.length - @position = Point(position.row, currentColumn) + @position = Point(position.row, Math.min(@currentLineLength, currentColumn)) containingTags moveToSuccessor: -> - if @tagIndex is @currentTags.length - @position = Point(@position.row + 1, 0) - @currentTags = @tokenizedBuffer.tokenizedLineForRow(@position.row)?.tags - return false unless @currentTags? - @tagIndex = 0 - else - @position = Point(@position.row, @position.column + @currentTags[@tagIndex]) - @tagIndex++ - @openTags = [] @closeTags = [] loop - tag = @currentTags[@tagIndex] - if tag >= 0 or @tagIndex is @currentTags.length + if @tagIndex is @currentTags.length if @isAtTagBoundary() break else - return @moveToSuccessor() + return false unless @moveToNextLine() else - scopeName = @grammarRegistry.scopeForId(tag) - if tag % 2 is 0 - @closeTags.push(scopeName) + tag = @currentTags[@tagIndex] + if tag >= 0 + if @isAtTagBoundary() + break + else + @position = Point(@position.row, Math.min(@currentLineLength, @position.column + @currentTags[@tagIndex])) else - @openTags.push(scopeName) - @tagIndex++ + scopeName = @grammarRegistry.scopeForId(tag) + if tag % 2 is 0 + @closeTags.push(scopeName) + else + @openTags.push(scopeName) + @tagIndex++ true + # Private + moveToNextLine: -> + @position = Point(@position.row + 1, 0) + tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(@position.row) + return false unless tokenizedLine? + @currentTags = tokenizedLine.tags + @currentLineLength = tokenizedLine.text.length + @tagIndex = 0 + true + getPosition: -> @position From 07f35818f8358a9b5767bc5a8b7779d4accc82b8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Jan 2016 17:43:36 -0700 Subject: [PATCH 017/262] Pass invisibles configuration into DisplayLayer --- src/display-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 51e0f6f52..980148229 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -51,7 +51,7 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer - @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength()}) + @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength(), invisibles: @config.get('editor.invisibles')}) @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() From 40beb0bd1e4a65ec9fea84b3674b471b197b3796 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Mar 2016 16:00:56 +0100 Subject: [PATCH 018/262] Use new TextBuffer APIs --- src/display-buffer.coffee | 164 ++++++++++++++++--------------- src/text-editor-presenter.coffee | 80 +++++---------- 2 files changed, 111 insertions(+), 133 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index cba8a6551..500cffcfb 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -428,13 +428,13 @@ class DisplayBuffer extends Model if @largeFileMode bufferRow else - @rowMap.screenRowRangeForBufferRow(bufferRow)[0] + @displayLayer.translateScreenPosition(Point(screenRow, 0)).row lastScreenRowForBufferRow: (bufferRow) -> if @largeFileMode bufferRow else - @rowMap.screenRowRangeForBufferRow(bufferRow)[1] - 1 + @displayLayer.translateScreenPosition(Point(screenRow, 0), clip: 'forward').row # Given a screen row, this converts it into a buffer row. # @@ -502,33 +502,35 @@ class DisplayBuffer extends Model screenPositionForBufferPosition: (bufferPosition, options) -> throw new Error("This TextEditor has been destroyed") if @isDestroyed() - {row, column} = @buffer.clipPosition(bufferPosition) - [startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row) - for screenRow in [startScreenRow...endScreenRow] - screenLine = @tokenizedLineForScreenRow(screenRow) - - unless screenLine? - throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row", - softWrapEnabled: @isSoftWrapped() - lastBufferRow: @buffer.getLastRow() - lastScreenRow: @getLastRow() - bufferRow: row - screenRow: screenRow - displayBufferChangeCount: @changeCount - tokenizedBufferChangeCount: @tokenizedBuffer.changeCount - bufferChangeCount: @buffer.changeCount - - maxBufferColumn = screenLine.getMaxBufferColumn() - if screenLine.isSoftWrapped() and column > maxBufferColumn - continue - else - if column <= maxBufferColumn - screenColumn = screenLine.screenColumnForBufferColumn(column) - else - screenColumn = Infinity - break - - @clipScreenPosition([screenRow, screenColumn], options) + return @displayLayer.translateBufferPosition(bufferPosition, options) + # TODO: should DisplayLayer deal with options.wrapBeyondNewlines / options.wrapAtSoftNewlines? + # {row, column} = @buffer.clipPosition(bufferPosition) + # [startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row) + # for screenRow in [startScreenRow...endScreenRow] + # screenLine = @tokenizedLineForScreenRow(screenRow) + # + # unless screenLine? + # throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row", + # softWrapEnabled: @isSoftWrapped() + # lastBufferRow: @buffer.getLastRow() + # lastScreenRow: @getLastRow() + # bufferRow: row + # screenRow: screenRow + # displayBufferChangeCount: @changeCount + # tokenizedBufferChangeCount: @tokenizedBuffer.changeCount + # bufferChangeCount: @buffer.changeCount + # + # maxBufferColumn = screenLine.getMaxBufferColumn() + # if screenLine.isSoftWrapped() and column > maxBufferColumn + # continue + # else + # if column <= maxBufferColumn + # screenColumn = screenLine.screenColumnForBufferColumn(column) + # else + # screenColumn = Infinity + # break + # + # @clipScreenPosition([screenRow, screenColumn], options) # Given a buffer position, this converts it into a screen position. # @@ -540,9 +542,11 @@ class DisplayBuffer extends Model # # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> - {row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options) - [bufferRow] = @rowMap.bufferRowRangeForScreenRow(row) - new Point(bufferRow, @tokenizedLineForScreenRow(row).bufferColumnForScreenColumn(column)) + return @displayLayer.translateScreenPosition(screenPosition, options) + # TODO: should DisplayLayer deal with options.wrapBeyondNewlines / options.wrapAtSoftNewlines? + # {row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options) + # [bufferRow] = @rowMap.bufferRowRangeForScreenRow(row) + # new Point(bufferRow, @tokenizedLineForScreenRow(row).bufferColumnForScreenColumn(column)) # Retrieves the grammar's token scopeDescriptor for a buffer position. # @@ -594,53 +598,55 @@ class DisplayBuffer extends Model # # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options={}) -> - {wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options - {row, column} = Point.fromObject(screenPosition) - - if row < 0 - row = 0 - column = 0 - else if row > @getLastRow() - row = @getLastRow() - column = Infinity - else if column < 0 - column = 0 - - screenLine = @tokenizedLineForScreenRow(row) - unless screenLine? - error = new Error("Undefined screen line when clipping screen position") - Error.captureStackTrace(error) - error.metadata = { - screenRow: row - screenColumn: column - maxScreenRow: @getLastRow() - screenLinesDefined: @screenLines.map (sl) -> sl? - displayBufferChangeCount: @changeCount - tokenizedBufferChangeCount: @tokenizedBuffer.changeCount - bufferChangeCount: @buffer.changeCount - } - throw error - - maxScreenColumn = screenLine.getMaxScreenColumn() - - if screenLine.isSoftWrapped() and column >= maxScreenColumn - if wrapAtSoftNewlines - row++ - column = @tokenizedLineForScreenRow(row).clipScreenColumn(0) - else - column = screenLine.clipScreenColumn(maxScreenColumn - 1) - else if screenLine.isColumnInsideSoftWrapIndentation(column) - if skipSoftWrapIndentation - column = screenLine.clipScreenColumn(0) - else - row-- - column = @tokenizedLineForScreenRow(row).getMaxScreenColumn() - 1 - else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow() - row++ - column = 0 - else - column = screenLine.clipScreenColumn(column, options) - new Point(row, column) + return @displayLayer.clipScreenPosition(screenPosition, options) + # TODO: should DisplayLayer deal with options.wrapBeyondNewlines / options.wrapAtSoftNewlines? + # {wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options + # {row, column} = Point.fromObject(screenPosition) + # + # if row < 0 + # row = 0 + # column = 0 + # else if row > @getLastRow() + # row = @getLastRow() + # column = Infinity + # else if column < 0 + # column = 0 + # + # screenLine = @tokenizedLineForScreenRow(row) + # unless screenLine? + # error = new Error("Undefined screen line when clipping screen position") + # Error.captureStackTrace(error) + # error.metadata = { + # screenRow: row + # screenColumn: column + # maxScreenRow: @getLastRow() + # screenLinesDefined: @screenLines.map (sl) -> sl? + # displayBufferChangeCount: @changeCount + # tokenizedBufferChangeCount: @tokenizedBuffer.changeCount + # bufferChangeCount: @buffer.changeCount + # } + # throw error + # + # maxScreenColumn = screenLine.getMaxScreenColumn() + # + # if screenLine.isSoftWrapped() and column >= maxScreenColumn + # if wrapAtSoftNewlines + # row++ + # column = @tokenizedLineForScreenRow(row).clipScreenColumn(0) + # else + # column = screenLine.clipScreenColumn(maxScreenColumn - 1) + # else if screenLine.isColumnInsideSoftWrapIndentation(column) + # if skipSoftWrapIndentation + # column = screenLine.clipScreenColumn(0) + # else + # row-- + # column = @tokenizedLineForScreenRow(row).getMaxScreenColumn() - 1 + # else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow() + # row++ + # column = 0 + # else + # column = screenLine.clipScreenColumn(column, options) + # new Point(row, column) # Clip the start and end of the given range to valid positions on screen. # See {::clipScreenPosition} for more information. diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ab471684e..2fef4ca14 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -17,7 +17,7 @@ class TextEditorPresenter {@model, @config, @lineTopIndex, scrollPastEnd} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params {@contentFrameWidth} = params - @tokenIterator = @model.displayLayer.buildTokenIterator() + {@displayLayer} = @model @gutterWidth = 0 @tileSize ?= 6 @@ -25,9 +25,7 @@ class TextEditorPresenter @realScrollLeft = @scrollLeft @disposables = new CompositeDisposable @emitter = new Emitter - @lineIdCounter = 1 - @linesById = new Map - @lineMarkerIndex = new MarkerIndex + @linesByScreenRow = new Map @visibleHighlights = {} @characterWidthsByScope = {} @lineDecorationsByScreenRow = {} @@ -141,7 +139,6 @@ class TextEditorPresenter observeModel: -> @disposables.add @model.displayLayer.onDidChangeSync (changes) => for change in changes - @invalidateLines(change) startRow = change.start.row endRow = startRow + change.oldExtent.row @spliceBlockDecorationsInRange(startRow, endRow, change.newExtent.row - change.oldExtent.row) @@ -316,7 +313,7 @@ class TextEditorPresenter isValidScreenRow: (screenRow) -> screenRow >= 0 and screenRow < @model.getScreenLineCount() - getScreenRows: -> + getScreenRowsToRender: -> startRow = @getStartTileRow() endRow = @constrainRow(@getEndTileRow() + @tileSize) @@ -331,6 +328,22 @@ class TextEditorPresenter screenRows.sort (a, b) -> a - b _.uniq(screenRows, true) + getScreenRangesToRender: -> + screenRows = @getScreenRowsToRender() + screenRows.push(Infinity) # makes the loop below inclusive + + startRow = screenRows[0] + endRow = startRow - 1 + screenRanges = [] + for row in screenRows + if row is endRow + 1 + endRow++ + else + screenRanges.push([startRow, endRow]) + startRow = endRow = row + + screenRanges + setScreenRowsToMeasure: (screenRows) -> return if not screenRows? or screenRows.length is 0 @@ -343,7 +356,7 @@ class TextEditorPresenter updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - screenRows = @getScreenRows() + screenRows = @getScreenRowsToRender() visibleTiles = {} startRow = screenRows[0] endRow = screenRows[screenRows.length - 1] @@ -404,7 +417,7 @@ class TextEditorPresenter tileState.lines ?= {} visibleLineIds = {} for screenRow in screenRows - line = @lineForScreenRow(screenRow) + line = @linesByScreenRow.get(screenRow) unless line? throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") @@ -616,7 +629,7 @@ class TextEditorPresenter softWrapped = false screenRow = startRow + i - lineId = @lineIdForScreenRow(screenRow) + lineId = @linesByScreenRow.get(screenRow).id decorationClasses = @lineNumberDecorationClassesForRow(screenRow) blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight @@ -1057,48 +1070,11 @@ class TextEditorPresenter rect updateLines: -> - visibleLineIds = new Set + @linesByScreenRow.clear() - 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++, tokens: []} - @tokenIterator.seekToScreenRow(screenRow) - loop - line.tokens.push({ - text: @tokenIterator.getText(), - closeTags: @tokenIterator.getCloseTags(), - openTags: @tokenIterator.getOpenTags() - }) - break unless @tokenIterator.moveToSuccessor() - break unless @tokenIterator.getStartScreenPosition().row is screenRow - line - - invalidateLines: ({start, oldExtent, newExtent}) -> - {touch} = @lineMarkerIndex.splice(start, oldExtent, newExtent) - 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) + for [startRow, endRow] in @getScreenRangesToRender() + for line, index in @displayLayer.getScreenLines(startRow, endRow + 1) + @linesByScreenRow.set(startRow + index, line) fetchDecorations: -> return unless 0 <= @startRow <= @endRow <= Infinity @@ -1571,7 +1547,3 @@ class TextEditorPresenter isRowVisible: (row) -> @startRow <= row < @endRow - - lineIdForScreenRow: (screenRow) -> - ids = @lineMarkerIndex.findStartingAt(Point(screenRow, 0)) - ids.values().next().value if ids.size > 0 From 8c3ab52b6436ec22aa515d3b1ff32e349339f46d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Mar 2016 19:17:28 +0100 Subject: [PATCH 019/262] Pass showIndentGuides config to DisplayLayer --- src/display-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 500cffcfb..ee4e9be77 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -51,7 +51,7 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer - @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength(), invisibles: @config.get('editor.invisibles')}) + @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength(), invisibles: @config.get('editor.invisibles'), showIndentGuides: @config.get('editor.showIndentGuide')}) @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() From e513ed3a118baa99acf96f4516f7297ce01167b6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 14 Mar 2016 18:43:27 +0100 Subject: [PATCH 020/262] WIP: Always enable soft-wrapping --- src/display-buffer.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index ee4e9be77..b2b00f009 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -51,7 +51,11 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer - @displayLayer = @buffer.addDisplayLayer({tabLength: @getTabLength(), invisibles: @config.get('editor.invisibles'), showIndentGuides: @config.get('editor.showIndentGuide')}) + @displayLayer = @buffer.addDisplayLayer({ + tabLength: @getTabLength(), invisibles: @config.get('editor.invisibles'), + showIndentGuides: @config.get('editor.showIndentGuide'), + softWrapColumn: @config.get('editor.preferredLineLength') + }) @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() From 88882030059bc9503dbf3b9e07ef1529a9841294 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Mar 2016 17:14:27 -0600 Subject: [PATCH 021/262] Drop marker-index dependency --- build/tasks/build-task.coffee | 1 - package.json | 1 - src/text-editor-presenter.coffee | 1 - 3 files changed, 3 deletions(-) diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index 9164f8dab..94fb00e2c 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -133,7 +133,6 @@ module.exports = (grunt) -> ignoredPaths.push "#{_.escapeRegExp(path.join('scrollbar-style', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('spellchecker', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('cached-run-in-this-context', 'src') + path.sep)}.*\\.(cc|h)?" - ignoredPaths.push "#{_.escapeRegExp(path.join('marker-index', 'src') + path.sep)}.*\\.(cc|h)?" ignoredPaths.push "#{_.escapeRegExp(path.join('keyboard-layout', 'src') + path.sep)}.*\\.(cc|h|mm)*" # Ignore build files diff --git a/package.json b/package.json index 4093ee7d0..6b449d0de 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "less-cache": "0.23", "line-top-index": "0.2.0", "marked": "^0.3.4", - "marker-index": "^3.0.4", "nodegit": "0.11.9", "normalize-package-data": "^2.0.0", "nslog": "^3", diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 2fef4ca14..8192442dd 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1,6 +1,5 @@ {CompositeDisposable, Disposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' -MarkerIndex = require 'marker-index' _ = require 'underscore-plus' Decoration = require './decoration' From e56c2addea76694d0143789f2edba9d68d3080c7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Mar 2016 20:13:00 -0600 Subject: [PATCH 022/262] Pass softWrapHangingIndent option --- src/display-buffer.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b2b00f009..37dbd81b1 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -55,6 +55,7 @@ class DisplayBuffer extends Model tabLength: @getTabLength(), invisibles: @config.get('editor.invisibles'), showIndentGuides: @config.get('editor.showIndentGuide'), softWrapColumn: @config.get('editor.preferredLineLength') + softWrapHangingIndent: @config.get('editor.softWrapHangingIndent') }) @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} From 4c8f43f41b37361e92f158cbad05553225dcad74 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Mar 2016 15:15:48 +0100 Subject: [PATCH 023/262] Use new APIs in FakeLinesYardstick --- spec/fake-lines-yardstick.coffee | 38 ++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 38716ab3e..288032d84 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -1,8 +1,10 @@ {Point} = require 'text-buffer' +{isPairedCharacter} = require '../src/text-utils' module.exports = class FakeLinesYardstick constructor: (@model, @lineTopIndex) -> + {@displayLayer} = @model @characterWidthsByScope = {} getScopedCharacterWidth: (scopeNames, char) -> @@ -30,25 +32,27 @@ class FakeLinesYardstick left = 0 column = 0 - iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() - while iterator.next() - characterWidths = @getScopedCharacterWidths(iterator.getScopes()) + for {tokens} in @displayLayer.getScreenLines(targetRow, targetRow + 1)[0] + scopes = [] + for {text, closeTags, openTags} in tokens + scopes.splice(scopes.lastIndexOf(closeTag), 1) for closeTag in closeTags + scopes.push(openTag) for openTag in openTags - valueIndex = 0 - text = iterator.getText() - while valueIndex < text.length - if iterator.isPairedCharacter() - char = text - charLength = 2 - valueIndex += 2 - else - char = text[valueIndex] - charLength = 1 - valueIndex++ + characterWidths = @getScopedCharacterWidths(iterator.getScopes()) + valueIndex = 0 + while valueIndex < text.length + if isPairedCharacter(text, valueIndex) + char = text[valueIndex...valueIndex + 2] + charLength = 2 + valueIndex += 2 + else + char = text[valueIndex] + charLength = 1 + valueIndex++ - break if column is targetColumn + break if column is targetColumn - left += characterWidths[char] ? baseCharacterWidth unless char is '\0' - column += charLength + left += characterWidths[char] ? baseCharacterWidth unless char is '\0' + column += charLength {top, left} From 2e41e9ead47850acf87bce546f4bdb2d89a55db5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Mar 2016 19:19:00 +0100 Subject: [PATCH 024/262] Reset DisplayLayer every time config changes --- src/display-buffer.coffee | 80 ++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 37dbd81b1..4db1c922e 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -51,12 +51,7 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer - @displayLayer = @buffer.addDisplayLayer({ - tabLength: @getTabLength(), invisibles: @config.get('editor.invisibles'), - showIndentGuides: @config.get('editor.showIndentGuide'), - softWrapColumn: @config.get('editor.preferredLineLength') - softWrapHangingIndent: @config.get('editor.softWrapHangingIndent') - }) + @displayLayer = @buffer.addDisplayLayer() @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() @@ -78,35 +73,16 @@ class DisplayBuffer extends Model @scopedConfigSubscriptions = subscriptions = new CompositeDisposable scopeDescriptor = @getRootScopeDescriptor() + subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - oldConfigSettings = @configSettings - @configSettings = - scrollPastEnd: @config.get('editor.scrollPastEnd', scope: scopeDescriptor) - softWrap: @config.get('editor.softWrap', scope: scopeDescriptor) - softWrapAtPreferredLineLength: @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) - softWrapHangingIndent: @config.get('editor.softWrapHangingIndent', scope: scopeDescriptor) - preferredLineLength: @config.get('editor.preferredLineLength', scope: scopeDescriptor) - - subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) => - @configSettings.softWrap = newValue - @updateWrappedScreenLines() - - subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) => - @configSettings.softWrapHangingIndent = newValue - @updateWrappedScreenLines() - - subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) => - @configSettings.softWrapAtPreferredLineLength = newValue - @updateWrappedScreenLines() if @isSoftWrapped() - - subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) => - @configSettings.preferredLineLength = newValue - @updateWrappedScreenLines() if @isSoftWrapped() and @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) - - subscriptions.add @config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) => - @configSettings.scrollPastEnd = value - - @updateWrappedScreenLines() if oldConfigSettings? and not _.isEqual(oldConfigSettings, @configSettings) + @resetDisplayLayer() serialize: -> deserializer: 'DisplayBuffer' @@ -122,6 +98,31 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager }) + resetDisplayLayer: -> + scopeDescriptor = @getRootScopeDescriptor() + invisibles = + if @config.get('editor.showInvisibles', scope: scopeDescriptor) + @config.get('editor.invisibles', scope: scopeDescriptor) + else + {} + + softWrapColumn = + if @isSoftWrapped() + if @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) + @config.get('editor.preferredLineLength', scope: scopeDescriptor) + else + @getEditorWidthInChars() + else + Infinity + + @displayLayer.reset({ + invisibles: invisibles + softWrapColumn: softWrapColumn + showIndentGuides: @config.get('editor.showIndentGuide', scope: scopeDescriptor) + tabLength: @config.get('editor.tabLength', scope: scopeDescriptor), + ratioForCharacter: -> 1.0 # TODO: replace this with korean/double/half width characters. + }) + updateAllScreenLines: -> return # TODO: After DisplayLayer is finished, delete these code paths @maxLineLength = 0 @@ -208,7 +209,7 @@ class DisplayBuffer extends Model setWidth: (newWidth) -> oldWidth = @width @width = newWidth - @updateWrappedScreenLines() if newWidth isnt oldWidth and @isSoftWrapped() + @resetDisplayLayer() if newWidth isnt oldWidth and @isSoftWrapped() @width getLineHeightInPixels: -> @lineHeightInPixels @@ -231,7 +232,7 @@ class DisplayBuffer extends Model @doubleWidthCharWidth = doubleWidthCharWidth @halfWidthCharWidth = halfWidthCharWidth @koreanCharWidth = koreanCharWidth - @updateWrappedScreenLines() if @isSoftWrapped() and @getEditorWidthInChars()? + @resetDisplayLayer() if @isSoftWrapped() and @getEditorWidthInChars()? defaultCharWidth getCursorWidth: -> 1 @@ -264,7 +265,7 @@ class DisplayBuffer extends Model setSoftWrapped: (softWrapped) -> if softWrapped isnt @softWrapped @softWrapped = softWrapped - @updateWrappedScreenLines() + @resetDisplayLayer() softWrapped = @isSoftWrapped() @emitter.emit 'did-change-soft-wrapped', softWrapped softWrapped @@ -275,7 +276,8 @@ class DisplayBuffer extends Model if @largeFileMode false else - @softWrapped ? @configSettings.softWrap ? false + scopeDescriptor = @getRootScopeDescriptor() + @softWrapped ? @config.get('editor.softWrap', scope: scopeDescriptor) ? false # Set the number of characters that fit horizontally in the editor. # @@ -285,7 +287,7 @@ class DisplayBuffer extends Model previousWidthInChars = @editorWidthInChars @editorWidthInChars = editorWidthInChars if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped() - @updateWrappedScreenLines() + @resetDisplayLayer() # Returns the editor width in characters for soft wrap. getEditorWidthInChars: -> From 670123a9f75a5e59ebf65b35a8a8d8894be4a60f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 09:28:48 +0100 Subject: [PATCH 025/262] More fixes in FakeLinesYardstick --- spec/fake-lines-yardstick.coffee | 38 +++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 288032d84..085ab4bde 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -26,33 +26,31 @@ class FakeLinesYardstick targetRow = screenPosition.row targetColumn = screenPosition.column - baseCharacterWidth = @model.getDefaultCharWidth() top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow) left = 0 column = 0 - for {tokens} in @displayLayer.getScreenLines(targetRow, targetRow + 1)[0] - scopes = [] - for {text, closeTags, openTags} in tokens - scopes.splice(scopes.lastIndexOf(closeTag), 1) for closeTag in closeTags - scopes.push(openTag) for openTag in openTags + scopes = [] + for {text, closeTags, openTags} in @displayLayer.getScreenLines(targetRow, targetRow + 1)[0].tokens + scopes.splice(scopes.lastIndexOf(closeTag), 1) for closeTag in closeTags + scopes.push(openTag) for openTag in openTags + characterWidths = @getScopedCharacterWidths(scopes) - characterWidths = @getScopedCharacterWidths(iterator.getScopes()) - valueIndex = 0 - while valueIndex < text.length - if isPairedCharacter(text, valueIndex) - char = text[valueIndex...valueIndex + 2] - charLength = 2 - valueIndex += 2 - else - char = text[valueIndex] - charLength = 1 - valueIndex++ + valueIndex = 0 + while valueIndex < text.length + if isPairedCharacter(text, valueIndex) + char = text[valueIndex...valueIndex + 2] + charLength = 2 + valueIndex += 2 + else + char = text[valueIndex] + charLength = 1 + valueIndex++ - break if column is targetColumn + break if column is targetColumn - left += characterWidths[char] ? baseCharacterWidth unless char is '\0' - column += charLength + left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0' + column += charLength {top, left} From 661417e3627afe30d090829ae99f9920581b3127 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 09:42:36 +0100 Subject: [PATCH 026/262] Update lines in TextEditorPresenter.prototype.getPostMeasurementState() Calling ::updateHorizontalDimensions might cause the editor vertical coordinates (e.g. height, scroll top) to change, so we need to fetch lines again from `DisplayLayer`. --- src/text-editor-presenter.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 8192442dd..7381369f9 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -110,6 +110,8 @@ class TextEditorPresenter @clearPendingScrollPosition() @updateRowsPerPage() + @updateLines() + @updateFocusedState() @updateHeightState() @updateVerticalScrollState() From 708da39355ded0c79e907ffbdda4caebc4530e2f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 10:35:54 +0100 Subject: [PATCH 027/262] Avoid using tokenizedLineForScreenRow in TextEditorPresenter specs --- spec/text-editor-presenter-spec.coffee | 149 ++++++++++++------------- 1 file changed, 69 insertions(+), 80 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index f8117af09..d40828d77 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1229,9 +1229,16 @@ describe "TextEditorPresenter", -> describe ".tiles", -> lineStateForScreenRow = (presenter, row) -> - lineId = presenter.model.tokenizedLineForScreenRow(row).id - tileRow = presenter.tileForRow(row) - getState(presenter).content.tiles[tileRow]?.lines[lineId] + tilesState = getState(presenter).content.tiles + lineId = presenter.linesByScreenRow.get(row)?.id + tilesState[presenter.tileForRow(row)]?.lines[lineId] + + tokensIncludeTag = (tokens, tag) -> + includeTag = false + for {openTags, closeTags} in tokens + includeTag = true for openTag in openTags when openTag.indexOf(tag) isnt -1 + includeTag = true for closeTag in closeTags when closeTag.indexOf(tag) isnt -1 + includeTag tiledContentContract (presenter) -> getState(presenter).content @@ -1241,73 +1248,56 @@ describe "TextEditorPresenter", -> presenter.setExplicitHeight(3) expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() - - line3 = editor.tokenizedLineForScreenRow(3) expectValues lineStateForScreenRow(presenter, 3), { - screenRow: 3 - text: line3.text - tags: line3.tags - specialTokens: line3.specialTokens - firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex - invisibles: line3.invisibles + screenRow: 3, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: [], text: 'var pivot = items.shift(), current, left = [], right = [];'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } - - line4 = editor.tokenizedLineForScreenRow(4) expectValues lineStateForScreenRow(presenter, 4), { - screenRow: 4 - text: line4.text - tags: line4.tags - specialTokens: line4.specialTokens - firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex - invisibles: line4.invisibles + screenRow: 4, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: [], text: 'while(items.length > 0) {'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } - - line5 = editor.tokenizedLineForScreenRow(5) expectValues lineStateForScreenRow(presenter, 5), { - screenRow: 5 - text: line5.text - tags: line5.tags - specialTokens: line5.specialTokens - firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex - invisibles: line5.invisibles + screenRow: 5, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: [], text: 'current = items.shift();'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } - - line6 = editor.tokenizedLineForScreenRow(6) expectValues lineStateForScreenRow(presenter, 6), { - screenRow: 6 - text: line6.text - tags: line6.tags - specialTokens: line6.specialTokens - firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex - invisibles: line6.invisibles + screenRow: 6, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: [], text: 'current < pivot ? left.push(current) : right.push(current);'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } - - line7 = editor.tokenizedLineForScreenRow(7) expectValues lineStateForScreenRow(presenter, 7), { - screenRow: 7 - text: line7.text - tags: line7.tags - specialTokens: line7.specialTokens - firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex - invisibles: line7.invisibles + screenRow: 7, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: [], text: '}'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } - - line8 = editor.tokenizedLineForScreenRow(8) expectValues lineStateForScreenRow(presenter, 8), { - screenRow: 8 - text: line8.text - tags: line8.tags - specialTokens: line8.specialTokens - firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex - invisibles: line8.invisibles + screenRow: 8, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: [], text: 'return sort(left).concat(pivot).concat(sort(right));'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } - expect(lineStateForScreenRow(presenter, 9)).toBeUndefined() it "updates when the editor's content changes", -> @@ -1315,34 +1305,36 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n") - line1 = editor.tokenizedLineForScreenRow(1) expectValues lineStateForScreenRow(presenter, 1), { - text: line1.text - tags: line1.tags + screenRow: 1, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, + {closeTags: ['leading-whitespace'], openTags: [], text: 'var sort = function(items) {'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text : ''} + ] } - - line2 = editor.tokenizedLineForScreenRow(2) expectValues lineStateForScreenRow(presenter, 2), { - text: line2.text - tags: line2.tags + screenRow: 2, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar'], text: 'hello'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } - - line3 = editor.tokenizedLineForScreenRow(3) expectValues lineStateForScreenRow(presenter, 3), { - text: line3.text - tags: line3.tags + screenRow: 3, tokens: [ + {closeTags: [], openTags: ['text.plain.null-grammar'], text: 'world'}, + {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} + ] } it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", -> editor.setText("hello\nworld\r\n") presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) - expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toBeNull() - expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toBeNull() + expect(tokensIncludeTag(lineStateForScreenRow(presenter, 0).tokens, 'eol')).toBe(false) + expect(tokensIncludeTag(lineStateForScreenRow(presenter, 1).tokens, 'eol')).toBe(false) atom.config.set('editor.showInvisibles', true) presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) - expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')] - expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')] + expect(tokensIncludeTag(lineStateForScreenRow(presenter, 0).tokens, 'eol')).toBe(true) + expect(tokensIncludeTag(lineStateForScreenRow(presenter, 1).tokens, 'eol')).toBe(true) describe ".blockDecorations", -> it "contains all block decorations that are present before/after a line, both initially and when decorations change", -> @@ -2905,12 +2897,9 @@ describe "TextEditorPresenter", -> describe ".content.tiles", -> lineNumberStateForScreenRow = (presenter, screenRow) -> - editor = presenter.model - tileRow = presenter.tileForRow(screenRow) - line = editor.tokenizedLineForScreenRow(screenRow) - - gutterState = getLineNumberGutterState(presenter) - gutterState.content.tiles[tileRow]?.lineNumbers[line?.id] + tilesState = getLineNumberGutterState(presenter).content.tiles + line = presenter.linesByScreenRow.get(screenRow) + tilesState[presenter.tileForRow(screenRow)]?.lineNumbers[line?.id] tiledContentContract (presenter) -> getLineNumberGutterState(presenter).content @@ -2919,7 +2908,7 @@ describe "TextEditorPresenter", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) + editor.setEditorWidthInChars(51) presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 2) expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() From 23ddeb7f087b7c2a4da7ea7493ff7df7ae5b9a1f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 10:38:23 +0100 Subject: [PATCH 028/262] :fire: Put back commented out LOC --- src/text-editor-presenter.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 7381369f9..a6cd0d0b4 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1407,7 +1407,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? From f57fb3176ae1b6b21e615687f7df59030accf609 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 11:19:05 +0100 Subject: [PATCH 029/262] Add back `lineIdForScreenRow` --- src/text-editor-presenter.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a6cd0d0b4..6240f4a37 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -700,9 +700,6 @@ class TextEditorPresenter if @baseCharacterWidth? oldContentWidth = @contentWidth rightmostPosition = @model.getRightmostScreenPosition() - # TODO: Add some version of this back once softwrap is reintroduced - # if @model.tokenizedLineForScreenRow(rightmostPosition.row)?.isSoftWrapped() - # rightmostPosition = @model.clipScreenPosition(rightmostPosition) @contentWidth = @pixelPositionForScreenPosition(rightmostPosition).left @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width @@ -1077,6 +1074,9 @@ class TextEditorPresenter for line, index in @displayLayer.getScreenLines(startRow, endRow + 1) @linesByScreenRow.set(startRow + index, line) + lineIdForScreenRow: (screenRow) -> + @linesByScreenRow.get(screenRow).id + fetchDecorations: -> return unless 0 <= @startRow <= @endRow <= Infinity @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) From 5efb969a633e13c647c77f53b6ab2eb212e12f4c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 11:19:54 +0100 Subject: [PATCH 030/262] :green_heart: Start fixing TextEditorComponent specs --- spec/text-editor-component-spec.js | 15 +++++++-------- src/text-editor.coffee | 4 +++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 37a9751e1..d806b872e 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -69,13 +69,12 @@ describe('TextEditorComponent', function () { describe('line rendering', async function () { function expectTileContainsRow (tileNode, screenRow, {top}) { let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]') - let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) - + let text = editor.lineTextForScreenRow(screenRow) expect(lineNode.offsetTop).toBe(top) - if (tokenizedLine.text === '') { + if (text === '') { expect(lineNode.innerHTML).toBe(' ') } else { - expect(lineNode.textContent).toBe(tokenizedLine.text) + expect(lineNode.textContent).toBe(text) } } @@ -294,12 +293,12 @@ describe('TextEditorComponent', function () { await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) + expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3)) buffer.delete([[0, 0], [3, 0]]) await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) + expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3)) }) it('updates the top position of lines when the line height changes', async function () { @@ -550,8 +549,8 @@ describe('TextEditorComponent', function () { }) it('does not show end of line invisibles at the end of wrapped lines', function () { - expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ') - expect(component.lineNodeForScreenRow(1).textContent).toBe('wraps' + invisibles.space + invisibles.eol) + expect(component.lineNodeForScreenRow(0).textContent).toBe('a line ') + expect(component.lineNodeForScreenRow(1).textContent).toBe('that wraps' + invisibles.space + invisibles.eol) }) }) }) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b2ea7985e..52d5ffd47 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -773,7 +773,9 @@ class TextEditor extends Model # given screen row. # # * `screenRow` A {Number} representing a zero-indexed screen row. - lineTextForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow)?.text + lineTextForScreenRow: (screenRow) -> + line = @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] + line?.tokens.map((t) -> t.text).join('') # Gets the screen line for the given screen row. # From 43f27780fd25f7cd7e00c61a057402a3d91742ad Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 14:56:23 +0100 Subject: [PATCH 031/262] Fix specs related to the produced HTML line output This verifies that with the new DisplayLayer the produced output is cleaner when tags interleave. --- spec/text-editor-component-spec.js | 18 +++++++++--------- src/text-editor-presenter.coffee | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index d806b872e..ffcf38ea2 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -483,7 +483,7 @@ describe('TextEditorComponent', function () { it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () { editor.setText('let\n') await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') + expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') }) it('displays trailing carriage returns using a visible, non-empty value', async function () { @@ -522,19 +522,19 @@ describe('TextEditorComponent', function () { }) await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTabLength(3) await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTabLength(1) await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') }) describe('when soft wrapping is enabled', function () { @@ -1198,10 +1198,10 @@ describe('TextEditorComponent', function () { let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() - let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2] - let range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) + let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[0] + let range = document.createRange(cursorLocationTextNode) + range.setStart(cursorLocationTextNode, 3) + range.setEnd(cursorLocationTextNode, 4) let rangeRect = range.getBoundingClientRect() expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6240f4a37..9ff1e5970 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1075,7 +1075,7 @@ class TextEditorPresenter @linesByScreenRow.set(startRow + index, line) lineIdForScreenRow: (screenRow) -> - @linesByScreenRow.get(screenRow).id + @linesByScreenRow.get(screenRow)?.id fetchDecorations: -> return unless 0 <= @startRow <= @endRow <= Infinity From bf5a0d8c8cca9c4c2d51006ef0727c64accf60d8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2016 15:11:27 +0100 Subject: [PATCH 032/262] Adjust assertions based on the new soft-wrap logic --- spec/text-editor-component-spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index ffcf38ea2..ea908a64b 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3095,7 +3095,7 @@ describe('TextEditorComponent', function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), { shiftKey: true })) - expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [16, 0]]) + expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [17, 0]]) }) }) }) @@ -3169,7 +3169,7 @@ describe('TextEditorComponent', function () { gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), { metaKey: true })) - expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [19, 0]]]) + expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [20, 0]]]) }) it('merges overlapping selections on mouseup', async function () { @@ -3183,7 +3183,7 @@ describe('TextEditorComponent', function () { gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), { metaKey: true })) - expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [19, 0]]]) + expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [20, 0]]]) }) }) }) @@ -3198,7 +3198,7 @@ describe('TextEditorComponent', function () { })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11))) await nextAnimationFramePromise() - expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 14]]) + expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 5]]) }) }) From 1d1bdf587200e91f531a089305827a4d7760d927 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 10:51:45 +0100 Subject: [PATCH 033/262] Improve folds behavior when duplicating lines Now we will select the entire screen line (which could contain some free-form fold), and we duplicate its contents preserving the structure of the existing folds. --- src/text-editor.coffee | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 52d5ffd47..d64b7107c 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1085,15 +1085,12 @@ class TextEditor extends Model selectedBufferRange = selection.getBufferRange() if selection.isEmpty() {start} = selection.getScreenRange() - selection.selectToScreenPosition([start.row + 1, 0]) + selection.setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true) [startRow, endRow] = selection.getBufferRowRange() endRow++ - foldedRowRanges = - @outermostFoldsInBufferRowRange(startRow, endRow) - .map (fold) -> fold.getBufferRowRange() - + outermostFolds = @displayLayer.outermostFoldsInBufferRowRange(startRow, endRow) rangeToDuplicate = [[startRow, 0], [endRow, 0]] textToDuplicate = @getTextInBufferRange(rangeToDuplicate) textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow() @@ -1101,8 +1098,9 @@ class TextEditor extends Model delta = endRow - startRow selection.setBufferRange(selectedBufferRange.translate([delta, 0])) - for [foldStartRow, foldEndRow] in foldedRowRanges - @foldBufferRowRange(foldStartRow + delta, foldEndRow + delta) + for fold in outermostFolds + foldRange = @displayLayer.bufferRangeForFold(fold) + @displayLayer.foldBufferRange(foldRange.translate([delta, 0])) return replaceSelectedText: (options={}, fn) -> @@ -3013,9 +3011,9 @@ class TextEditor extends Model destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) - # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange} + # {Delegates to: DisplayLayer.outermostFoldsForBufferRowRange} outermostFoldsInBufferRowRange: (startRow, endRow) -> - @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow) + @displayLayer.outermostFoldsInBufferRowRange(startRow, endRow) ### Section: Gutters From 83da3ca440afa6e3f9e39f04dabdaa68587d34cc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 11:24:22 +0100 Subject: [PATCH 034/262] Fix moveLineUp and moveLineDown --- src/text-editor.coffee | 46 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index d64b7107c..f0f4ee12f 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -945,14 +945,14 @@ class TextEditor extends Model # insert delta is know. selectionFoldRanges = [] foldAtSelectionStart = - @displayBuffer.largestFoldContainingBufferRow(selection.start.row) + @displayLayer.largestFoldContainingBufferRow(selection.start.row) foldAtSelectionEnd = - @displayBuffer.largestFoldContainingBufferRow(selection.end.row) + @displayLayer.largestFoldContainingBufferRow(selection.end.row) if fold = foldAtSelectionStart ? foldAtSelectionEnd - selectionFoldRanges.push range = fold.getBufferRange() + selectionFoldRanges.push range = @displayLayer.bufferRangeForFold(fold) newEndRow = range.end.row + 1 linesRange.end.row = newEndRow if newEndRow > linesRange.end.row - fold.destroy() + @displayLayer.destroyFold(fold) # If selected line range is preceded by a fold, one line above on screen # could be multiple lines in the buffer. @@ -963,16 +963,18 @@ class TextEditor extends Model # Any folds in the text that is moved will need to be re-created. # It includes the folds that were intersecting with the selection. rangesToRefold = selectionFoldRanges.concat( - @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> - range = fold.getBufferRange() - fold.destroy() + @displayLayer.outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> + range = @displayLayer.bufferRangeForFold(fold) + @displayLayer.destroyFold(fold) range ).map (range) -> range.translate([-insertDelta, 0]) - # # Make sure the inserted text doesn't go into an existing fold - # if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow) - # rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0])) - # fold.destroy() + # Make sure the inserted text doesn't go into an existing fold + if fold = @displayLayer.largestFoldStartingAtBufferRow(precedingBufferRow) + rangesToRefold.push( + @displayLayer.bufferRangeForFold(fold).translate([linesRange.getRowCount() - 1, 0]) + ) + @displayLayer.destroyFold(fold) # Delete lines spanned by selection and insert them on the preceding buffer row lines = @buffer.getTextInRange(linesRange) @@ -982,7 +984,7 @@ class TextEditor extends Model # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold - @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row) + @displayLayer.foldBufferRange(rangeToRefold) for selection in selectionsToMove newSelectionRanges.push(selection.translate([-insertDelta, 0])) @@ -1027,14 +1029,14 @@ class TextEditor extends Model # insert delta is know. selectionFoldRanges = [] foldAtSelectionStart = - @displayBuffer.largestFoldContainingBufferRow(selection.start.row) + @displayLayer.largestFoldContainingBufferRow(selection.start.row) foldAtSelectionEnd = - @displayBuffer.largestFoldContainingBufferRow(selection.end.row) + @displayLayer.largestFoldContainingBufferRow(selection.end.row) if fold = foldAtSelectionStart ? foldAtSelectionEnd - selectionFoldRanges.push range = fold.getBufferRange() + selectionFoldRanges.push range = @displayLayer.bufferRangeForFold(fold) newEndRow = range.end.row + 1 linesRange.end.row = newEndRow if newEndRow > linesRange.end.row - fold.destroy() + @displayLayer.destroyFold(fold) # If selected line range is followed by a fold, one line below on screen # could be multiple lines in the buffer. But at the same time, if the @@ -1047,16 +1049,16 @@ class TextEditor extends Model # Any folds in the text that is moved will need to be re-created. # It includes the folds that were intersecting with the selection. rangesToRefold = selectionFoldRanges.concat( - @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> + @displayLayer.outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> range = fold.getBufferRange() - fold.destroy() + @displayLayer.destroyFold(fold) range ).map (range) -> range.translate([insertDelta, 0]) # # Make sure the inserted text doesn't go into an existing fold - # if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow) - # rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0])) - # fold.destroy() + if fold = @displayLayer.largestFoldStartingAtBufferRow(followingBufferRow) + rangesToRefold.push(@displayLayer.bufferRangeForFold(fold).translate([insertDelta - 1, 0])) + @displayLayer.destroyFold(fold) # Delete lines spanned by selection and insert them on the following correct buffer row insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0) @@ -1069,7 +1071,7 @@ class TextEditor extends Model # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold - @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row) + @displayLayer.foldBufferRange(rangeToRefold) for selection in selectionsToMove newSelectionRanges.push(selection.translate([insertDelta, 0])) From d86309e46b835ce41f7e5d650acb45a695465311 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 11:33:29 +0100 Subject: [PATCH 035/262] Use DisplayLayer.prototype.foldBufferRange in Selection.prototype.fold --- src/selection.coffee | 4 ++-- src/text-editor.coffee | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/selection.coffee b/src/selection.coffee index faee09742..b90bb6854 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -628,8 +628,8 @@ class Selection extends Model # Public: Creates a fold containing the current selection. fold: -> range = @getBufferRange() - @editor.foldBufferRowRange(range.start.row, range.end.row) - @cursor.setBufferPosition([range.end.row + 1, 0]) + @editor.foldBufferRange(range) + @cursor.setBufferPosition(range.end) # Private: Increase the indentation level of the given text by given number # of levels. Leaves the first line unchanged. diff --git a/src/text-editor.coffee b/src/text-editor.coffee index f0f4ee12f..44eab56ea 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3009,6 +3009,9 @@ class TextEditor extends Model foldBufferRowRange: (startRow, endRow) -> @displayBuffer.foldBufferRowRange(startRow, endRow) + foldBufferRange: (range) -> + @displayLayer.foldBufferRange(range) + # Remove any {Fold}s found that intersect the given buffer range. destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) From 5fc699f791b3474531a8b8502310b956c225059c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 11:44:23 +0100 Subject: [PATCH 036/262] Use intersecting folds when duplicating lines --- src/text-editor.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 44eab56ea..8220e09c2 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1092,7 +1092,7 @@ class TextEditor extends Model [startRow, endRow] = selection.getBufferRowRange() endRow++ - outermostFolds = @displayLayer.outermostFoldsInBufferRowRange(startRow, endRow) + intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) rangeToDuplicate = [[startRow, 0], [endRow, 0]] textToDuplicate = @getTextInBufferRange(rangeToDuplicate) textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow() @@ -1100,7 +1100,7 @@ class TextEditor extends Model delta = endRow - startRow selection.setBufferRange(selectedBufferRange.translate([delta, 0])) - for fold in outermostFolds + for fold in intersectingFolds foldRange = @displayLayer.bufferRangeForFold(fold) @displayLayer.foldBufferRange(foldRange.translate([delta, 0])) return From d666e4c008f44ad95b5423aa9ab6cca0c0a6d5a0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 11:48:52 +0100 Subject: [PATCH 037/262] Fix lastScreenRowForBufferRow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …which was mistakenly translating screen positions to buffer positions, and not vice versa. --- src/display-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 4db1c922e..b16bc9e24 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -441,7 +441,7 @@ class DisplayBuffer extends Model if @largeFileMode bufferRow else - @displayLayer.translateScreenPosition(Point(screenRow, 0), clip: 'forward').row + @displayLayer.translateBufferPosition(Point(bufferRow, 0), clip: 'forward').row # Given a screen row, this converts it into a buffer row. # From d1306ae9442b0140bc3d54ceb2efeaeeaed39cdf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 14:06:46 +0100 Subject: [PATCH 038/262] Remove invisibles handling from TokenizedBuffer --- spec/text-editor-spec.coffee | 3 +-- src/display-buffer.coffee | 11 +++++++---- src/text-editor.coffee | 2 +- src/tokenized-buffer.coffee | 24 +++--------------------- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 852e7b50a..f52bb1bb3 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5837,8 +5837,7 @@ describe "TextEditor", -> it "ignores invisibles even if editor.showInvisibles is true", -> atom.config.set('editor.showInvisibles', true) - invisibles = editor.tokenizedLineForScreenRow(0).invisibles - expect(invisibles).toBe(null) + expect(editor.lineTextForScreenRow(0).indexOf(atom.config.get('editor.invisibles.eol'))).toBe(-1) describe "when the editor is constructed with the grammar option set", -> beforeEach -> diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b16bc9e24..a96aba529 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -39,7 +39,7 @@ class DisplayBuffer extends Model super { - tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, + tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager } = params @@ -47,7 +47,7 @@ class DisplayBuffer extends Model @disposables = new CompositeDisposable @tokenizedBuffer ?= new TokenizedBuffer({ - tabLength, buffer, ignoreInvisibles, @largeFileMode, @config, + tabLength, buffer, @largeFileMode, @config, @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer @@ -101,7 +101,7 @@ class DisplayBuffer extends Model resetDisplayLayer: -> scopeDescriptor = @getRootScopeDescriptor() invisibles = - if @config.get('editor.showInvisibles', scope: scopeDescriptor) + if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles @config.get('editor.invisibles', scope: scopeDescriptor) else {} @@ -260,7 +260,10 @@ class DisplayBuffer extends Model @tokenizedBuffer.setTabLength(tabLength) setIgnoreInvisibles: (ignoreInvisibles) -> - @tokenizedBuffer.setIgnoreInvisibles(ignoreInvisibles) + return if ignoreInvisibles is @ignoreInvisibles + + @ignoreInvisibles = ignoreInvisibles + @resetDisplayLayer() setSoftWrapped: (softWrapped) -> if softWrapped isnt @softWrapped diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 8220e09c2..f3082814e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1055,7 +1055,7 @@ class TextEditor extends Model range ).map (range) -> range.translate([insertDelta, 0]) - # # Make sure the inserted text doesn't go into an existing fold + # Make sure the inserted text doesn't go into an existing fold if fold = @displayLayer.largestFoldStartingAtBufferRow(followingBufferRow) rangesToRefold.push(@displayLayer.bufferRangeForFold(fold).translate([insertDelta - 1, 0])) @displayLayer.destroyFold(fold) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index eb2230e37..3bb0f8bfc 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -36,7 +36,7 @@ class TokenizedBuffer extends Model constructor: (params) -> { - @buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config, + @buffer, @tabLength, @largeFileMode, @config, @grammarRegistry, @packageManager, @assert, grammarScopeName } = params @@ -74,7 +74,6 @@ class TokenizedBuffer extends Model bufferPath: @buffer.getPath() bufferId: @buffer.getId() tabLength: @tabLength - ignoreInvisibles: @ignoreInvisibles largeFileMode: @largeFileMode } state.grammarScopeName = @grammar?.scopeName unless @buffer.getPath() @@ -128,11 +127,6 @@ class TokenizedBuffer extends Model @configSubscriptions.add @config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) => @configSettings.tabLength = newValue @retokenizeLines() - ['invisibles', 'showInvisibles'].forEach (key) => - @configSubscriptions.add @config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) => - oldInvisibles = @getInvisiblesToShow() - @configSettings[key] = newValue - @retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles) @disposables.add(@configSubscriptions) @retokenizeLines() @@ -175,12 +169,6 @@ class TokenizedBuffer extends Model @tabLength = tabLength @retokenizeLines() - setIgnoreInvisibles: (ignoreInvisibles) -> - if ignoreInvisibles isnt @ignoreInvisibles - @ignoreInvisibles = ignoreInvisibles - if @configSettings.showInvisibles and @configSettings.invisibles? - @retokenizeLines() - tokenizeInBackground: -> return if not @visible or @pendingChunk or not @isAlive() @@ -362,7 +350,7 @@ class TokenizedBuffer extends Model tabLength = @getTabLength() indentLevel = @indentLevelForRow(row) lineEnding = @buffer.lineEndingForRow(row) - new TokenizedLine({openScopes, text, tags, tabLength, indentLevel, invisibles: @getInvisiblesToShow(), lineEnding, @tokenIterator}) + new TokenizedLine({openScopes, text, tags, tabLength, indentLevel, lineEnding, @tokenIterator}) buildTokenizedLineForRow: (row, ruleStack, openScopes) -> @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes) @@ -372,13 +360,7 @@ class TokenizedBuffer extends Model tabLength = @getTabLength() indentLevel = @indentLevelForRow(row) {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) - new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, indentLevel, invisibles: @getInvisiblesToShow(), @tokenIterator}) - - getInvisiblesToShow: -> - if @configSettings.showInvisibles and not @ignoreInvisibles - @configSettings.invisibles - else - null + new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, indentLevel, @tokenIterator}) tokenizedLineForRow: (bufferRow) -> if 0 <= bufferRow < @tokenizedLines.length From 3c70ff92f4acfe173511210502642e20d81b5bcf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 14:25:33 +0100 Subject: [PATCH 039/262] :green_heart: Add TextEditor.prototype.screenLineForScreenRow This is going to supplant our internal usage of TextEditor.prototype.tokenizedLineForScreenRow(). --- spec/text-editor-spec.coffee | 21 ++++++++++++--------- src/text-editor.coffee | 14 +++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index f52bb1bb3..d4f28d9cc 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5436,25 +5436,28 @@ describe "TextEditor", -> runs -> editor.setText("// SELECT * FROM OCTOCATS") - {tokens} = editor.tokenizedLineForScreenRow(0) - expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS" - expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] + {tokens} = editor.screenLineForScreenRow(0) + expect(tokens[2].closeTags).toEqual ['comment.line.double-slash.js', 'source.js'] + expect(tokens[2].openTags).toEqual [] + expect(tokens[2].text).toBe "" waitsForPromise -> atom.packages.activatePackage('package-with-injection-selector') runs -> - {tokens} = editor.tokenizedLineForScreenRow(0) - expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS" - expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] + {tokens} = editor.screenLineForScreenRow(0) + expect(tokens[2].closeTags).toEqual ['comment.line.double-slash.js', 'source.js'] + expect(tokens[2].openTags).toEqual [] + expect(tokens[2].text).toBe "" waitsForPromise -> atom.packages.activatePackage('language-sql') runs -> - {tokens} = editor.tokenizedLineForScreenRow(0) - expect(tokens[2].value).toBe "SELECT" - expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"] + {tokens} = editor.screenLineForScreenRow(2) + expect(tokens[2].closeTags).toEqual [] + expect(tokens[2].openTags).toEqual ["keyword.other.DML.sql"] + expect(tokens[2].text).toBe "SELECT" describe ".normalizeTabsInBufferRange()", -> it "normalizes tabs depending on the editor's soft tab/tab length settings", -> diff --git a/src/text-editor.coffee b/src/text-editor.coffee index f3082814e..a27d62f5c 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -774,18 +774,10 @@ class TextEditor extends Model # # * `screenRow` A {Number} representing a zero-indexed screen row. lineTextForScreenRow: (screenRow) -> - line = @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] - line?.tokens.map((t) -> t.text).join('') + @screenLineForScreenRow(screenRow)?.tokens.map((t) -> t.text).join('') - # Gets the screen line for the given screen row. - # - # * `screenRow` - A {Number} indicating the screen row. - # - # Returns {TokenizedLine} - tokenizedLineForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow) - - # {Delegates to: DisplayBuffer.tokenizedLinesForScreenRows} - tokenizedLinesForScreenRows: (start, end) -> @displayBuffer.tokenizedLinesForScreenRows(start, end) + screenLineForScreenRow: (screenRow) -> + @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] bufferRowForScreenRow: (row) -> @displayLayer.translateScreenPosition(Point(row, 0)).row From bbcbe9e809826ae45cbfb7ca0aa695ff459dcb3b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 14:58:13 +0100 Subject: [PATCH 040/262] Implement ratioForCharacter --- src/display-buffer.coffee | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index a96aba529..4b869233a 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -7,6 +7,7 @@ Model = require './model' Token = require './token' Decoration = require './decoration' LayerDecoration = require './layer-decoration' +{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter} = require './text-utils' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> @@ -120,7 +121,7 @@ class DisplayBuffer extends Model softWrapColumn: softWrapColumn showIndentGuides: @config.get('editor.showIndentGuide', scope: scopeDescriptor) tabLength: @config.get('editor.tabLength', scope: scopeDescriptor), - ratioForCharacter: -> 1.0 # TODO: replace this with korean/double/half width characters. + ratioForCharacter: @ratioForCharacter.bind(this) }) updateAllScreenLines: -> @@ -215,6 +216,16 @@ class DisplayBuffer extends Model getLineHeightInPixels: -> @lineHeightInPixels setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels + ratioForCharacter: (character) -> + if isKoreanCharacter(character) + @getKoreanCharWidth() / @getDefaultCharWidth() + else if isHalfWidthCharacter(character) + @getHalfWidthCharWidth() / @getDefaultCharWidth() + else if isDoubleWidthCharacter(character) + @getDoubleWidthCharWidth() / @getDefaultCharWidth() + else + 1 + getKoreanCharWidth: -> @koreanCharWidth getHalfWidthCharWidth: -> @halfWidthCharWidth From 0cd6bd19af144b53fac95be7e588e836e596fdef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2016 18:30:35 +0100 Subject: [PATCH 041/262] Implement isWrapBoundary for DisplayLayer So that we can correctly soft wrap CJK characters. --- spec/text-utils-spec.coffee | 37 +++++++++++++++++++------------------ src/display-buffer.coffee | 3 ++- src/text-utils.coffee | 13 ++++++++++++- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/spec/text-utils-spec.coffee b/spec/text-utils-spec.coffee index aa36c5003..bae7f5997 100644 --- a/spec/text-utils-spec.coffee +++ b/spec/text-utils-spec.coffee @@ -75,22 +75,23 @@ describe 'text utilities', -> expect(textUtils.isKoreanCharacter("O")).toBe(false) - describe ".isCJKCharacter(character)", -> - it "returns true when the character is either a korean, half-width or double-width character", -> - expect(textUtils.isCJKCharacter("我")).toBe(true) - expect(textUtils.isCJKCharacter("私")).toBe(true) - expect(textUtils.isCJKCharacter("B")).toBe(true) - expect(textUtils.isCJKCharacter(",")).toBe(true) - expect(textUtils.isCJKCharacter("¢")).toBe(true) - expect(textUtils.isCJKCharacter("ハ")).toBe(true) - expect(textUtils.isCJKCharacter("ヒ")).toBe(true) - expect(textUtils.isCJKCharacter("ᆲ")).toBe(true) - expect(textUtils.isCJKCharacter("■")).toBe(true) - expect(textUtils.isCJKCharacter("우")).toBe(true) - expect(textUtils.isCJKCharacter("가")).toBe(true) - expect(textUtils.isCJKCharacter("ㅢ")).toBe(true) - expect(textUtils.isCJKCharacter("ㄼ")).toBe(true) + describe ".isWrapBoundary(previousCharacter, character)", -> + it "returns true when the character is CJK or when the previous character is a space/tab", -> + anyCharacter = 'x' + expect(textUtils.isWrapBoundary(anyCharacter, "我")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "私")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "B")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, ",")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "¢")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "ハ")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "ヒ")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "ᆲ")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "■")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "우")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "가")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "ㅢ")).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, "ㄼ")).toBe(true) - expect(textUtils.isDoubleWidthCharacter("a")).toBe(false) - expect(textUtils.isDoubleWidthCharacter("O")).toBe(false) - expect(textUtils.isDoubleWidthCharacter("z")).toBe(false) + expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true) + expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true) + expect(textUtils.isWrapBoundary('a', 'h')).toBe(false) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 4b869233a..eb4faa1a5 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -7,7 +7,7 @@ Model = require './model' Token = require './token' Decoration = require './decoration' LayerDecoration = require './layer-decoration' -{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter} = require './text-utils' +{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> @@ -122,6 +122,7 @@ class DisplayBuffer extends Model showIndentGuides: @config.get('editor.showIndentGuide', scope: scopeDescriptor) tabLength: @config.get('editor.tabLength', scope: scopeDescriptor), ratioForCharacter: @ratioForCharacter.bind(this) + isWrapBoundary: isWrapBoundary }) updateAllScreenLines: -> diff --git a/src/text-utils.coffee b/src/text-utils.coffee index af17335aa..f4d62772e 100644 --- a/src/text-utils.coffee +++ b/src/text-utils.coffee @@ -94,6 +94,13 @@ isCJKCharacter = (character) -> isHalfWidthCharacter(character) or isKoreanCharacter(character) +isWordStart = (previousCharacter, character) -> + (previousCharacter is ' ' or previousCharacter is '\t') and + (character isnt ' ' and character isnt '\t') + +isWrapBoundary = (previousCharacter, character) -> + isWordStart(previousCharacter, character) or isCJKCharacter(character) + # Does the given string contain at least surrogate pair, variation sequence, # or combined character? # @@ -107,4 +114,8 @@ hasPairedCharacter = (string) -> index++ false -module.exports = {isPairedCharacter, hasPairedCharacter, isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isCJKCharacter} +module.exports = { + isPairedCharacter, hasPairedCharacter, + isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, + isWrapBoundary +} From 1a2f306db3225fb22b7b1f088c5340cc876f83c3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 18 Mar 2016 15:57:03 -0600 Subject: [PATCH 042/262] :shower: Remove commented code --- src/lines-tile-component.coffee | 43 --------------------------------- 1 file changed, 43 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 13a267055..03af7333b 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -277,49 +277,6 @@ class LinesTileComponent lineNode.appendChild(textNode) @currentLineTextNodes.push(textNode) - # 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 textNode = @domElementPool.buildText(tokenText) From d62ef599cd5f543d65e3e62163692696d3a2a687 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 18 Mar 2016 15:57:49 -0600 Subject: [PATCH 043/262] Replace tokens with tagCodes in DisplayLayer.prototype.getScreenLines --- src/lines-tile-component.coffee | 26 +++++++++++++------------- src/text-editor-presenter.coffee | 12 +++++++++++- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 03af7333b..a815c5be5 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -253,26 +253,26 @@ class LinesTileComponent @currentLineTextNodes.push(textNode) setLineInnerNodes: (id, lineNode) -> - {tokens} = @newTileState.lines[id] + {lineText, tagCodes} = @newTileState.lines[id] + lineLength = 0 + startIndex = 0 openScopeNode = lineNode - for token in tokens when token.text.length > 0 - {closeTags, openTags, text} = token - - for scope in closeTags + for tagCode in tagCodes when tagCode isnt 0 + if @presenter.isCloseTagCode(tagCode) openScopeNode = openScopeNode.parentElement - - for scope in openTags + else if @presenter.isOpenTagCode(tagCode) + scope = @presenter.tagForCode(tagCode) newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' ')) openScopeNode.appendChild(newScopeNode) openScopeNode = newScopeNode + else + textNode = @domElementPool.buildText(lineText.substr(startIndex, tagCode).replace(/\s/g, NBSPCharacter)) + startIndex += tagCode + openScopeNode.appendChild(textNode) + @currentLineTextNodes.push(textNode) - lineLength += text.length - textNode = @domElementPool.buildText(text.replace(/\s/g, NBSPCharacter)) - openScopeNode.appendChild(textNode) - @currentLineTextNodes.push(textNode) - - if lineLength is 0 + if startIndex is 0 textNode = @domElementPool.buildText(NBSPCharacter) lineNode.appendChild(textNode) @currentLineTextNodes.push(textNode) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 9ff1e5970..3d69606aa 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -436,7 +436,8 @@ class TextEditorPresenter else tileState.lines[line.id] = screenRow: screenRow - tokens: line.tokens + lineText: line.lineText + tagCodes: line.tagCodes decorationClasses: @lineDecorationClassesForRow(screenRow) precedingBlockDecorations: precedingBlockDecorations followingBlockDecorations: followingBlockDecorations @@ -1548,3 +1549,12 @@ class TextEditorPresenter isRowVisible: (row) -> @startRow <= row < @endRow + + isOpenTagCode: (tagCode) -> + @displayLayer.isOpenTagCode(tagCode) + + isCloseTagCode: (tagCode) -> + @displayLayer.isCloseTagCode(tagCode) + + tagForCode: (tagCode) -> + @displayLayer.tagForCode(tagCode) From 462157039b5f8d8f63f860095901021cbb3c1abe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 18 Mar 2016 18:09:23 -0600 Subject: [PATCH 044/262] Drop indentLevel and soft wrap support from TokenizedLine --- src/tokenized-buffer.coffee | 19 +---- src/tokenized-line.coffee | 154 +----------------------------------- 2 files changed, 3 insertions(+), 170 deletions(-) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 3bb0f8bfc..7a943d78c 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -261,9 +261,6 @@ class TokenizedBuffer extends Model newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start)) _.spliceWithArray(@tokenizedLines, start, end - start + 1, newTokenizedLines) - start = @retokenizeWhitespaceRowsIfIndentLevelChanged(start - 1, -1) - end = @retokenizeWhitespaceRowsIfIndentLevelChanged(newRange.end.row + 1, 1) - delta - newEndStack = @stackForRow(end + delta) if newEndStack and not _.isEqual(newEndStack, previousEndStack) @invalidateRow(end + delta + 1) @@ -273,16 +270,6 @@ class TokenizedBuffer extends Model event = {start, end, delta, bufferChange: e} @emitter.emit 'did-change', event - retokenizeWhitespaceRowsIfIndentLevelChanged: (row, increment) -> - line = @tokenizedLineForRow(row) - if line?.isOnlyWhitespace() and @indentLevelForRow(row) isnt line.indentLevel - while line?.isOnlyWhitespace() - @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row)) - row += increment - line = @tokenizedLineForRow(row) - - row - increment - isFoldableAtRow: (row) -> if @largeFileMode false @@ -348,9 +335,8 @@ class TokenizedBuffer extends Model text = @buffer.lineForRow(row) tags = [text.length] tabLength = @getTabLength() - indentLevel = @indentLevelForRow(row) lineEnding = @buffer.lineEndingForRow(row) - new TokenizedLine({openScopes, text, tags, tabLength, indentLevel, lineEnding, @tokenIterator}) + new TokenizedLine({openScopes, text, tags, tabLength, lineEnding, @tokenIterator}) buildTokenizedLineForRow: (row, ruleStack, openScopes) -> @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes) @@ -358,9 +344,8 @@ class TokenizedBuffer extends Model buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> lineEnding = @buffer.lineEndingForRow(row) tabLength = @getTabLength() - indentLevel = @indentLevelForRow(row) {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) - new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, indentLevel, @tokenIterator}) + new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, @tokenIterator}) tokenizedLineForRow: (bufferRow) -> if 0 <= bufferRow < @tokenizedLines.length diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 5d092ab82..cca1a3543 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -41,7 +41,7 @@ class TokenizedLine @specialTokens = {} {@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties - {@startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles} = properties + {@startBufferColumn, @fold, @tabLength, @invisibles} = properties @startBufferColumn ?= 0 @bufferDelta = @text.length @@ -93,7 +93,6 @@ class TokenizedLine copy.lineEnding = @lineEnding copy.invisibles = @invisibles copy.endOfLineInvisibles = @endOfLineInvisibles - copy.indentLevel = @indentLevel copy.tabLength = @tabLength copy.firstNonWhitespaceIndex = @firstNonWhitespaceIndex copy.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex @@ -173,157 +172,6 @@ class TokenizedLine getMaxBufferColumn: -> @startBufferColumn + @bufferDelta - # Given a boundary column, finds the point where this line would wrap. - # - # maxColumn - The {Number} where you want soft wrapping to occur - # - # Returns a {Number} representing the `line` position where the wrap would take place. - # Returns `null` if a wrap wouldn't occur. - findWrapColumn: (maxColumn) -> - return unless maxColumn? - return unless @text.length > maxColumn - - if /\s/.test(@text[maxColumn]) - # search forward for the start of a word past the boundary - for column in [maxColumn..@text.length] - return column if /\S/.test(@text[column]) - - return @text.length - else if isCJKCharacter(@text[maxColumn]) - maxColumn - else - # search backward for the start of the word on the boundary - for column in [maxColumn..@firstNonWhitespaceIndex] - if /\s/.test(@text[column]) or isCJKCharacter(@text[column]) - return column + 1 - - return maxColumn - - softWrapAt: (column, hangingIndent) -> - return [null, this] if column is 0 - - leftText = @text.substring(0, column) - rightText = @text.substring(column) - - leftTags = [] - rightTags = [] - - leftSpecialTokens = {} - rightSpecialTokens = {} - - rightOpenScopes = @openScopes.slice() - - screenColumn = 0 - - for tag, index in @tags - # tag represents a token - if tag >= 0 - # token ends before the soft wrap column - if screenColumn + tag <= column - if specialToken = @specialTokens[index] - leftSpecialTokens[index] = specialToken - leftTags.push(tag) - screenColumn += tag - - # token starts before and ends after the split column - else if screenColumn <= column - leftSuffix = column - screenColumn - rightPrefix = screenColumn + tag - column - - leftTags.push(leftSuffix) if leftSuffix > 0 - - softWrapIndent = @indentLevel * @tabLength + (hangingIndent ? 0) - for i in [0...softWrapIndent] by 1 - rightText = ' ' + rightText - remainingSoftWrapIndent = softWrapIndent - while remainingSoftWrapIndent > 0 - indentToken = Math.min(remainingSoftWrapIndent, @tabLength) - rightSpecialTokens[rightTags.length] = SoftWrapIndent - rightTags.push(indentToken) - remainingSoftWrapIndent -= indentToken - - rightTags.push(rightPrefix) if rightPrefix > 0 - - screenColumn += tag - - # token is after split column - else - if specialToken = @specialTokens[index] - rightSpecialTokens[rightTags.length] = specialToken - rightTags.push(tag) - - # tag represents the start of a scope - else if (tag % 2) is -1 - if screenColumn < column - leftTags.push(tag) - rightOpenScopes.push(tag) - else - rightTags.push(tag) - - # tag represents the end of a scope - else - if screenColumn <= column - leftTags.push(tag) - rightOpenScopes.pop() - else - rightTags.push(tag) - - splitBufferColumn = @bufferColumnForScreenColumn(column) - - leftFragment = new TokenizedLine - leftFragment.tokenIterator = @tokenIterator - leftFragment.openScopes = @openScopes - leftFragment.text = leftText - leftFragment.tags = leftTags - leftFragment.specialTokens = leftSpecialTokens - leftFragment.startBufferColumn = @startBufferColumn - leftFragment.bufferDelta = splitBufferColumn - @startBufferColumn - leftFragment.ruleStack = @ruleStack - leftFragment.invisibles = @invisibles - leftFragment.lineEnding = null - leftFragment.indentLevel = @indentLevel - leftFragment.tabLength = @tabLength - leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex) - leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex) - - rightFragment = new TokenizedLine - rightFragment.tokenIterator = @tokenIterator - rightFragment.openScopes = rightOpenScopes - rightFragment.text = rightText - rightFragment.tags = rightTags - rightFragment.specialTokens = rightSpecialTokens - rightFragment.startBufferColumn = splitBufferColumn - rightFragment.bufferDelta = @startBufferColumn + @bufferDelta - splitBufferColumn - rightFragment.ruleStack = @ruleStack - rightFragment.invisibles = @invisibles - rightFragment.lineEnding = @lineEnding - rightFragment.indentLevel = @indentLevel - rightFragment.tabLength = @tabLength - rightFragment.endOfLineInvisibles = @endOfLineInvisibles - rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent) - rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent) - - [leftFragment, rightFragment] - - isSoftWrapped: -> - @lineEnding is null - - isColumnInsideSoftWrapIndentation: (targetColumn) -> - targetColumn < @getSoftWrapIndentationDelta() - - getSoftWrapIndentationDelta: -> - delta = 0 - for tag, index in @tags - if tag >= 0 - if @specialTokens[index] is SoftWrapIndent - delta += tag - else - break - delta - - hasOnlySoftWrapIndentation: -> - @getSoftWrapIndentationDelta() is @text.length - tokenAtBufferColumn: (bufferColumn) -> @tokens[@tokenIndexAtBufferColumn(bufferColumn)] From 1994e3b404a7221c6db6a1a765e03ddc720b6fa3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 18 Mar 2016 18:09:57 -0600 Subject: [PATCH 045/262] Remove TokenizedLine::copy --- src/tokenized-line.coffee | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index cca1a3543..402c95eb2 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -80,25 +80,6 @@ class TokenizedLine tokens - copy: -> - copy = new TokenizedLine - copy.tokenIterator = @tokenIterator - copy.openScopes = @openScopes - copy.text = @text - copy.tags = @tags - copy.specialTokens = @specialTokens - copy.startBufferColumn = @startBufferColumn - copy.bufferDelta = @bufferDelta - copy.ruleStack = @ruleStack - copy.lineEnding = @lineEnding - copy.invisibles = @invisibles - copy.endOfLineInvisibles = @endOfLineInvisibles - copy.tabLength = @tabLength - copy.firstNonWhitespaceIndex = @firstNonWhitespaceIndex - copy.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex - copy.fold = @fold - copy - # This clips a given screen column to a valid column that's within the line # and not in the middle of any atomic tokens. # From b0c5870425d5e56cf3e010d848ff362b58ca013d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 18 Mar 2016 18:15:41 -0600 Subject: [PATCH 046/262] Remove dead code from TokenizedLine --- src/tokenized-line.coffee | 92 --------------------------------------- 1 file changed, 92 deletions(-) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 402c95eb2..62dadc836 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -20,14 +20,6 @@ TabStringsByLength = { idCounter = 1 -getTabString = (length) -> - TabStringsByLength[length] ?= buildTabString(length) - -buildTabString = (length) -> - string = SpaceString - string += SpaceString for i in [1...length] by 1 - string - module.exports = class TokenizedLine endOfLineInvisibles: null @@ -80,79 +72,6 @@ class TokenizedLine tokens - # This clips a given screen column to a valid column that's within the line - # and not in the middle of any atomic tokens. - # - # column - A {Number} representing the column to clip - # options - A hash with the key clip. Valid values for this key: - # 'closest' (default): clip to the closest edge of an atomic token. - # 'forward': clip to the forward edge. - # 'backward': clip to the backward edge. - # - # Returns a {Number} representing the clipped column. - clipScreenColumn: (column, options={}) -> - return 0 if @tags.length is 0 - - {clip} = options - column = Math.min(column, @getMaxScreenColumn()) - - tokenStartColumn = 0 - - iterator = @getTokenIterator() - while iterator.next() - break if iterator.getScreenEnd() > column - - if iterator.isSoftWrapIndentation() - iterator.next() while iterator.isSoftWrapIndentation() - iterator.getScreenStart() - else if iterator.isAtomic() and iterator.getScreenStart() < column - if clip is 'forward' - iterator.getScreenEnd() - else if clip is 'backward' - iterator.getScreenStart() - else #'closest' - if column > ((iterator.getScreenStart() + iterator.getScreenEnd()) / 2) - iterator.getScreenEnd() - else - iterator.getScreenStart() - else - column - - screenColumnForBufferColumn: (targetBufferColumn, options) -> - iterator = @getTokenIterator() - while iterator.next() - tokenBufferStart = iterator.getBufferStart() - tokenBufferEnd = iterator.getBufferEnd() - if tokenBufferStart <= targetBufferColumn < tokenBufferEnd - overshoot = targetBufferColumn - tokenBufferStart - return Math.min( - iterator.getScreenStart() + overshoot, - iterator.getScreenEnd() - ) - iterator.getScreenEnd() - - bufferColumnForScreenColumn: (targetScreenColumn) -> - iterator = @getTokenIterator() - while iterator.next() - tokenScreenStart = iterator.getScreenStart() - tokenScreenEnd = iterator.getScreenEnd() - if tokenScreenStart <= targetScreenColumn < tokenScreenEnd - overshoot = targetScreenColumn - tokenScreenStart - return Math.min( - iterator.getBufferStart() + overshoot, - iterator.getBufferEnd() - ) - iterator.getBufferEnd() - - getMaxScreenColumn: -> - if @fold - 0 - else - @text.length - - getMaxBufferColumn: -> - @startBufferColumn + @bufferDelta - tokenAtBufferColumn: (bufferColumn) -> @tokens[@tokenIndexAtBufferColumn(bufferColumn)] @@ -171,17 +90,6 @@ class TokenizedLine delta = nextDelta delta - buildEndOfLineInvisibles: -> - @endOfLineInvisibles = [] - {cr, eol} = @invisibles - - switch @lineEnding - when '\r\n' - @endOfLineInvisibles.push(cr) if cr - @endOfLineInvisibles.push(eol) if eol - when '\n' - @endOfLineInvisibles.push(eol) if eol - isComment: -> return @isCommentLine if @isCommentLine? From 5aba734a412e484d573f52daa4f7dc187a489dc3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 18 Mar 2016 19:19:38 -0600 Subject: [PATCH 047/262] Remove specialTokens object from TokenizedLine --- src/token-iterator.coffee | 38 ++------------------------------------ src/tokenized-line.coffee | 16 ---------------- 2 files changed, 2 insertions(+), 52 deletions(-) diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee index 8f0fe202f..259bfd346 100644 --- a/src/token-iterator.coffee +++ b/src/token-iterator.coffee @@ -1,6 +1,3 @@ -{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols' -{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter} = require './text-utils' - module.exports = class TokenIterator constructor: ({@grammarRegistry}, line, enableScopes) -> @@ -32,15 +29,8 @@ class TokenIterator @handleScopeForTag(tag) if @enableScopes @index++ else - if @isHardTab() - @screenEnd = @screenStart + tag - @bufferEnd = @bufferStart + 1 - else if @isSoftWrapIndentation() - @screenEnd = @screenStart + tag - @bufferEnd = @bufferStart + 0 - else - @screenEnd = @screenStart + tag - @bufferEnd = @bufferStart + tag + @screenEnd = @screenStart + tag + @bufferEnd = @bufferStart + tag @text = @line.text.substring(@screenStart, @screenEnd) return true @@ -80,27 +70,3 @@ class TokenIterator getScopes: -> @scopes getText: -> @text - - isSoftTab: -> - @line.specialTokens[@index] is SoftTab - - isHardTab: -> - @line.specialTokens[@index] is HardTab - - isSoftWrapIndentation: -> - @line.specialTokens[@index] is SoftWrapIndent - - isPairedCharacter: -> - @line.specialTokens[@index] is PairedCharacter - - hasDoubleWidthCharacterAt: (charIndex) -> - isDoubleWidthCharacter(@getText()[charIndex]) - - hasHalfWidthCharacterAt: (charIndex) -> - isHalfWidthCharacter(@getText()[charIndex]) - - hasKoreanCharacterAt: (charIndex) -> - isKoreanCharacter(@getText()[charIndex]) - - isAtomic: -> - @isSoftTab() or @isHardTab() or @isSoftWrapIndentation() or @isPairedCharacter() diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 62dadc836..39a551586 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -1,22 +1,7 @@ _ = require 'underscore-plus' {isPairedCharacter, isCJKCharacter} = require './text-utils' Token = require './token' -{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols' - -NonWhitespaceRegex = /\S/ -LeadingWhitespaceRegex = /^\s*/ -TrailingWhitespaceRegex = /\s*$/ -RepeatedSpaceRegex = /[ ]/g CommentScopeRegex = /(\b|\.)comment/ -TabCharCode = 9 -SpaceCharCode = 32 -SpaceString = ' ' -TabStringsByLength = { - 1: ' ' - 2: ' ' - 3: ' ' - 4: ' ' -} idCounter = 1 @@ -31,7 +16,6 @@ class TokenizedLine return unless properties? - @specialTokens = {} {@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties {@startBufferColumn, @fold, @tabLength, @invisibles} = properties From 8f2ebe8b799324a54e2e193e6efa36ef4091b4b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2016 17:46:58 +0100 Subject: [PATCH 048/262] :fire: --- src/tokenized-line.coffee | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 39a551586..cbf64633b 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -29,30 +29,10 @@ class TokenizedLine tokens = [] while iterator.next() - properties = { + tokens.push(new Token({ value: iterator.getText() scopes: iterator.getScopes().slice() - isAtomic: iterator.isAtomic() - isHardTab: iterator.isHardTab() - hasPairedCharacter: iterator.isPairedCharacter() - isSoftWrapIndentation: iterator.isSoftWrapIndentation() - } - - if iterator.isHardTab() - properties.bufferDelta = 1 - properties.hasInvisibleCharacters = true if @invisibles?.tab - - if iterator.getScreenStart() < @firstNonWhitespaceIndex - properties.firstNonWhitespaceIndex = - Math.min(@firstNonWhitespaceIndex, iterator.getScreenEnd()) - iterator.getScreenStart() - properties.hasInvisibleCharacters = true if @invisibles?.space - - if @lineEnding? and iterator.getScreenEnd() > @firstTrailingWhitespaceIndex - properties.firstTrailingWhitespaceIndex = - Math.max(0, @firstTrailingWhitespaceIndex - iterator.getScreenStart()) - properties.hasInvisibleCharacters = true if @invisibles?.space - - tokens.push(new Token(properties)) + })) tokens From 4e237486601c3afddac2883834b87911c879554a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2016 14:02:13 +0100 Subject: [PATCH 049/262] Fix `moveLineUp` and `moveLineDown` to work with free-form folds --- src/text-editor.coffee | 99 ++++++++++++------------------------------ 1 file changed, 28 insertions(+), 71 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 578529ade..f9ebb80e3 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -903,8 +903,7 @@ class TextEditor extends Model # Move lines intersecting the most recent selection or multiple selections # up by one row in screen coordinates. moveLineUp: -> - selections = @getSelectedBufferRanges() - selections.sort (a, b) -> a.compare(b) + selections = @getSelectedBufferRanges().sort((a, b) -> a.compare(b)) if selections[0].start.row is 0 return @@ -925,56 +924,34 @@ class TextEditor extends Model selection.end.row = selections[0].end.row selections.shift() - # Compute the range spanned by all these selections... - linesRangeStart = [selection.start.row, 0] + # Compute the buffer range spanned by all these selections, expanding it + # so that it includes any folded region that intersects them. + startRow = selection.start.row + endRow = selection.end.row if selection.end.row > selection.start.row and selection.end.column is 0 # Don't move the last line of a multi-line selection if the selection ends at column 0 - linesRange = new Range(linesRangeStart, selection.end) - else - linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0]) + endRow-- - # If there's a fold containing either the starting row or the end row - # of the selection then the whole fold needs to be moved and restored. - # The initial fold range is stored and will be translated once the - # insert delta is know. - selectionFoldRanges = [] - foldAtSelectionStart = - @displayLayer.largestFoldContainingBufferRow(selection.start.row) - foldAtSelectionEnd = - @displayLayer.largestFoldContainingBufferRow(selection.end.row) - if fold = foldAtSelectionStart ? foldAtSelectionEnd - selectionFoldRanges.push range = @displayLayer.bufferRangeForFold(fold) - newEndRow = range.end.row + 1 - linesRange.end.row = newEndRow if newEndRow > linesRange.end.row - @displayLayer.destroyFold(fold) + {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow) + {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow) + linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) # If selected line range is preceded by a fold, one line above on screen # could be multiple lines in the buffer. - precedingScreenRow = @screenRowForBufferRow(linesRange.start.row) - 1 - precedingBufferRow = @bufferRowForScreenRow(precedingScreenRow) - insertDelta = linesRange.start.row - precedingBufferRow + {bufferRow: precedingRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow - 1) + insertDelta = linesRange.start.row - precedingRow # Any folds in the text that is moved will need to be re-created. # It includes the folds that were intersecting with the selection. - rangesToRefold = selectionFoldRanges.concat( - @displayLayer.outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> - range = @displayLayer.bufferRangeForFold(fold) - @displayLayer.destroyFold(fold) - range - ).map (range) -> range.translate([-insertDelta, 0]) - - # Make sure the inserted text doesn't go into an existing fold - if fold = @displayLayer.largestFoldStartingAtBufferRow(precedingBufferRow) - rangesToRefold.push( - @displayLayer.bufferRangeForFold(fold).translate([linesRange.getRowCount() - 1, 0]) - ) - @displayLayer.destroyFold(fold) + rangesToRefold = @displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map((range) -> range.translate([-insertDelta, 0])) # Delete lines spanned by selection and insert them on the preceding buffer row lines = @buffer.getTextInRange(linesRange) lines += @buffer.lineEndingForRow(linesRange.end.row - 1) unless lines[lines.length - 1] is '\n' @buffer.delete(linesRange) - @buffer.insert([precedingBufferRow, 0], lines) + @buffer.insert([precedingRow, 0], lines) # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold @@ -1009,50 +986,30 @@ class TextEditor extends Model selection.start.row = selections[0].start.row selections.shift() - # Compute the range spanned by all these selections... - linesRangeStart = [selection.start.row, 0] + # Compute the buffer range spanned by all these selections, expanding it + # so that it includes any folded region that intersects them. + startRow = selection.start.row + endRow = selection.end.row if selection.end.row > selection.start.row and selection.end.column is 0 # Don't move the last line of a multi-line selection if the selection ends at column 0 - linesRange = new Range(linesRangeStart, selection.end) - else - linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0]) + endRow-- - # If there's a fold containing either the starting row or the end row - # of the selection then the whole fold needs to be moved and restored. - # The initial fold range is stored and will be translated once the - # insert delta is know. - selectionFoldRanges = [] - foldAtSelectionStart = - @displayLayer.largestFoldContainingBufferRow(selection.start.row) - foldAtSelectionEnd = - @displayLayer.largestFoldContainingBufferRow(selection.end.row) - if fold = foldAtSelectionStart ? foldAtSelectionEnd - selectionFoldRanges.push range = @displayLayer.bufferRangeForFold(fold) - newEndRow = range.end.row + 1 - linesRange.end.row = newEndRow if newEndRow > linesRange.end.row - @displayLayer.destroyFold(fold) + {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow) + {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow) + linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) # If selected line range is followed by a fold, one line below on screen # could be multiple lines in the buffer. But at the same time, if the # next buffer row is wrapped, one line in the buffer can represent many # screen rows. - followingScreenRow = @displayBuffer.lastScreenRowForBufferRow(linesRange.end.row) + 1 - followingBufferRow = @bufferRowForScreenRow(followingScreenRow) - insertDelta = followingBufferRow - linesRange.end.row + {bufferRow: followingRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow) + insertDelta = followingRow - linesRange.end.row # Any folds in the text that is moved will need to be re-created. # It includes the folds that were intersecting with the selection. - rangesToRefold = selectionFoldRanges.concat( - @displayLayer.outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> - range = fold.getBufferRange() - @displayLayer.destroyFold(fold) - range - ).map (range) -> range.translate([insertDelta, 0]) - - # Make sure the inserted text doesn't go into an existing fold - if fold = @displayLayer.largestFoldStartingAtBufferRow(followingBufferRow) - rangesToRefold.push(@displayLayer.bufferRangeForFold(fold).translate([insertDelta - 1, 0])) - @displayLayer.destroyFold(fold) + rangesToRefold = @displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map((range) -> range.translate([insertDelta, 0])) # Delete lines spanned by selection and insert them on the following correct buffer row insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0) From 27799d30f624590245556f04e4720ac80ada48f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2016 14:27:23 +0100 Subject: [PATCH 050/262] Use deserialized display layer when possible --- src/display-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index eb4faa1a5..e025ce03d 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -52,7 +52,7 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer - @displayLayer = @buffer.addDisplayLayer() + @displayLayer = @buffer.getDisplayLayer() ? @buffer.addDisplayLayer() @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() From b1e07c0cfe0a38e2a7af499d0037db0cf1142822 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2016 14:36:28 +0100 Subject: [PATCH 051/262] Use the new `lineText` property --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index f9ebb80e3..b06ab5194 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -776,7 +776,7 @@ class TextEditor extends Model # # * `screenRow` A {Number} representing a zero-indexed screen row. lineTextForScreenRow: (screenRow) -> - @screenLineForScreenRow(screenRow)?.tokens.map((t) -> t.text).join('') + @screenLineForScreenRow(screenRow)?.lineText screenLineForScreenRow: (screenRow) -> @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] From 326f2c6a9e2cd3b2ccf39eeda4b3d9a13085daf8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2016 14:49:29 +0100 Subject: [PATCH 052/262] Add `TextEditor.prototype.tokensForScreenRow` for testing purposes --- spec/text-editor-spec.coffee | 25 +++++++++++-------------- src/text-editor.coffee | 4 ++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index d4f28d9cc..1526dfe3b 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -39,21 +39,19 @@ describe "TextEditor", -> it "preserves the invisibles setting", -> atom.config.set('editor.showInvisibles', true) - previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles - + previousLineText = editor.lineTextForScreenRow(0) editor2 = TextEditor.deserialize(editor.serialize(), atom) - - expect(previousInvisibles).toBeDefined() - expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles + expect(editor2.lineTextForScreenRow(0)).toBe(previousLineText) it "updates invisibles if the settings have changed between serialization and deserialization", -> atom.config.set('editor.showInvisibles', true) - + previousLineText = editor.lineTextForScreenRow(0) state = editor.serialize() atom.config.set('editor.invisibles', eol: '?') editor2 = TextEditor.deserialize(state, atom) - expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?' + expect(editor2.lineTextForScreenRow(0)).not.toBe(previousLineText) + expect(editor2.lineTextForScreenRow(0).endsWith('?')).toBe(true) describe "when the editor is constructed with the largeFileMode option set to true", -> it "loads the editor but doesn't tokenize", -> @@ -64,15 +62,14 @@ describe "TextEditor", -> runs -> buffer = editor.getBuffer() - expect(editor.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0) - expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1 - expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # soft tab - expect(editor.tokenizedLineForScreenRow(12).text).toBe buffer.lineForRow(12) - expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1 + expect(editor.lineTextForScreenRow(0)).toBe buffer.lineForRow(0) + expect(editor.tokensForScreenRow(0).length).toBe 1 + expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab + expect(editor.lineTextForScreenRow(12)).toBe buffer.lineForRow(12) expect(editor.getCursorScreenPosition()).toEqual [0, 0] editor.insertText('hey"') - expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1 - expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # sof tab + expect(editor.tokensForScreenRow(0).length).toBe 1 + expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab describe ".copy()", -> it "returns a different edit session with the same initial state", -> diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b06ab5194..1f9893132 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -778,6 +778,10 @@ class TextEditor extends Model lineTextForScreenRow: (screenRow) -> @screenLineForScreenRow(screenRow)?.lineText + tokensForScreenRow: (screenRow) -> + for tagCode in @screenLineForScreenRow(screenRow).tagCodes when @displayLayer.isOpenTagCode(tagCode) + @displayLayer.tagForCode(tagCode) + screenLineForScreenRow: (screenRow) -> @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] From 9abc547bfb7df52c0d967caa95ec85ce017b4d77 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2016 14:56:22 +0100 Subject: [PATCH 053/262] Copy also DisplayLayer --- src/display-buffer.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index e025ce03d..b10ffb0c4 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -52,7 +52,7 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer - @displayLayer = @buffer.getDisplayLayer() ? @buffer.addDisplayLayer() + @displayLayer ?= @buffer.getDisplayLayer() ? @buffer.addDisplayLayer() @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() @@ -96,7 +96,7 @@ class DisplayBuffer extends Model copy: -> new DisplayBuffer({ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, - @grammarRegistry, @packageManager + @grammarRegistry, @packageManager, displayLayer: @displayLayer.copy() }) resetDisplayLayer: -> From 756db7588b706996c71f61bbfb8d969c7b21798b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2016 14:59:26 +0100 Subject: [PATCH 054/262] Return an invalidated range only when TokenizedBuffer has one --- src/tokenized-buffer.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 7a943d78c..470aa59b9 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -63,7 +63,10 @@ class TokenizedBuffer extends Model new TokenizedBufferIterator(this, @grammarRegistry) getInvalidatedRanges: -> - [@invalidatedRange] + if @invalidatedRange? + [@invalidatedRange] + else + [] onDidInvalidateRange: (fn) -> @emitter.on 'did-invalidate-range', fn From 3d9835bbbd6b78f8d88fe4244ac810216d8ef508 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2016 16:05:38 +0100 Subject: [PATCH 055/262] :green_heart: WIP: Continue fixing TextEditor specs --- spec/text-editor-spec.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 1526dfe3b..cce47b926 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -490,7 +490,7 @@ describe "TextEditor", -> it "wraps to the end of the previous line", -> editor.setCursorScreenPosition([4, 4]) editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [3, 50] + expect(editor.getCursorScreenPosition()).toEqual [3, 46] describe "when the cursor is on the first line", -> it "remains in the same position (0,0)", -> @@ -678,7 +678,7 @@ describe "TextEditor", -> editor.setCursorScreenPosition([0, 2]) editor.moveToEndOfLine() cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [3, 4] + expect(cursor.getScreenPosition()).toEqual [4, 4] describe ".moveToFirstCharacterOfLine()", -> describe "when soft wrap is on", -> @@ -1803,10 +1803,10 @@ describe "TextEditor", -> editor.foldBufferRowRange(10, 11) editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) - expect(editor.tokenizedLineForScreenRow(1).fold).toBeUndefined() - expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined() - expect(editor.tokenizedLineForScreenRow(6).fold).toBeUndefined() - expect(editor.tokenizedLineForScreenRow(10).fold).toBeDefined() + expect(editor.isFoldedAtScreenRow(1)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(6)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() describe "when the 'preserveFolds' option is true", -> it "does not remove folds that contain the selections", -> @@ -2951,7 +2951,7 @@ describe "TextEditor", -> editor.foldBufferRowRange(2, 4) editor.setSelectedBufferRange([[1, 0], [2, 0]]) editor.insertText('holy cow') - expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined() + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() describe "when there are ::onWillInsertText and ::onDidInsertText observers", -> beforeEach -> From 21831a4e436203a6d0a9e213c11ca89ce1f41559 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2016 16:16:51 +0100 Subject: [PATCH 056/262] :bug: Fix inaccurate logic in moveLineDown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of deleting and then inserting, it’s better to do the opposite so that we don’t have to translate points at all. --- src/text-editor.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 1f9893132..2200ce827 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1016,13 +1016,12 @@ class TextEditor extends Model .map((range) -> range.translate([insertDelta, 0])) # Delete lines spanned by selection and insert them on the following correct buffer row - insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0) lines = @buffer.getTextInRange(linesRange) if linesRange.end.row is @buffer.getLastRow() lines = "\n#{lines}" + @buffer.insert([followingRow, 0], lines) @buffer.delete(linesRange) - @buffer.insert(insertPosition, lines) # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold From 6acbbb3a5c6002dc6136750be1be1a62ca11ea8b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2016 17:20:08 +0100 Subject: [PATCH 057/262] Improve DisplayLayer management --- src/display-buffer.coffee | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b10ffb0c4..b17600226 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -30,6 +30,7 @@ class DisplayBuffer extends Model @deserialize: (state, atomEnvironment) -> state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) + state.displayLayer = state.tokenizedBuffer.buffer.getDisplayLayer(state.displayLayerId) state.config = atomEnvironment.config state.assert = atomEnvironment.assert state.grammarRegistry = atomEnvironment.grammars @@ -41,7 +42,7 @@ class DisplayBuffer extends Model { tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @ignoreInvisibles, - @largeFileMode, @config, @assert, @grammarRegistry, @packageManager + @largeFileMode, @config, @assert, @grammarRegistry, @packageManager, @displayLayer } = params @emitter = new Emitter @@ -52,7 +53,7 @@ class DisplayBuffer extends Model @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer - @displayLayer ?= @buffer.getDisplayLayer() ? @buffer.addDisplayLayer() + @displayLayer ?= @buffer.addDisplayLayer() @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @charWidthsByScope = {} @defaultMarkerLayer = @displayLayer.addMarkerLayer() @@ -92,11 +93,12 @@ class DisplayBuffer extends Model editorWidthInChars: @editorWidthInChars tokenizedBuffer: @tokenizedBuffer.serialize() largeFileMode: @largeFileMode + displayLayerId: @displayLayer.id copy: -> new DisplayBuffer({ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, - @grammarRegistry, @packageManager, displayLayer: @displayLayer.copy() + @grammarRegistry, @packageManager, displayLayer: @buffer.copyDisplayLayer(@displayLayer.id) }) resetDisplayLayer: -> From a8d6a0036d08363c69a4df9eb8e31658cfffec86 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2016 13:40:24 +0100 Subject: [PATCH 058/262] =?UTF-8?q?:sparkles:=20Don=E2=80=99t=20unfold=20b?= =?UTF-8?q?uffer=20rows=20when=20editing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/selection.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/selection.coffee b/src/selection.coffee index b90bb6854..7f77ad6c0 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -365,7 +365,6 @@ class Selection extends Model # * `undo` if `skip`, skips the undo stack for this operation. insertText: (text, options={}) -> oldBufferRange = @getBufferRange() - @editor.unfoldBufferRow(oldBufferRange.end.row) wasReversed = @isReversed() @clear() From 4136e27d4439df8331fa1fe2515ddefadc873f9e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2016 13:43:42 +0100 Subject: [PATCH 059/262] Fix TextEditor backspace() and delete() specs --- spec/text-editor-spec.coffee | 53 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index cce47b926..51a8bc150 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -3245,15 +3245,14 @@ describe "TextEditor", -> editor.setCursorScreenPosition(row: 0, column: 0) editor.backspace() - describe "when the cursor is on the first column of a line below a fold", -> - it "deletes the folded lines", -> - editor.setCursorScreenPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorScreenPosition([5, 0]) + describe "when the cursor is after a fold", -> + it "deletes the folded range", -> + editor.foldBufferRange([[4, 7], [5, 8]]) + editor.setCursorBufferPosition([5, 8]) editor.backspace() - expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(buffer.lineForRow(4).fold).toBeUndefined() + expect(buffer.lineForRow(4)).toBe " whirrent = items.shift();" + expect(editor.isFoldedAtBufferRow(4)).toBe(false) describe "when the cursor is in the middle of a line below a fold", -> it "backspaces as normal", -> @@ -3266,14 +3265,13 @@ describe "TextEditor", -> expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));" describe "when the cursor is on a folded screen line", -> - it "deletes all of the folded lines along with the fold", -> + it "deletes the contents of the fold before the cursor", -> editor.setCursorBufferPosition([3, 0]) editor.foldCurrentRow() editor.backspace() - expect(buffer.lineForRow(1)).toBe "" - expect(buffer.lineForRow(2)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.getCursorScreenPosition()).toEqual [1, 0] + expect(buffer.lineForRow(1)).toBe " var sort = function(items) var pivot = items.shift(), current, left = [], right = [];" + expect(editor.getCursorScreenPosition()).toEqual [1, 29] describe "when there are multiple cursors", -> describe "when cursors are on the same line", -> @@ -3340,7 +3338,7 @@ describe "TextEditor", -> editor.backspace() expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {" - expect(editor.tokenizedLineForScreenRow(3).fold).toBeDefined() + expect(editor.isFoldedAtScreenRow(3)).toBe(true) describe "when there are multiple selections", -> it "removes all selected text", -> @@ -3513,16 +3511,16 @@ describe "TextEditor", -> editor.delete() expect(buffer.lineForRow(12)).toBe '};' - describe "when the cursor is on the end of a line above a fold", -> + describe "when the cursor is before a fold", -> it "only deletes the lines inside the fold", -> - editor.foldBufferRow(4) - editor.setCursorScreenPosition([3, Infinity]) + editor.foldBufferRange([[3, 6], [4, 8]]) + editor.setCursorScreenPosition([3, 6]) cursorPositionBefore = editor.getCursorScreenPosition() editor.delete() - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" + expect(buffer.lineForRow(3)).toBe " vae(items.length > 0) {" + expect(buffer.lineForRow(4)).toBe " current = items.shift();" expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore describe "when the cursor is in the middle a line above a fold", -> @@ -3534,20 +3532,21 @@ describe "TextEditor", -> editor.delete() expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];" - expect(editor.tokenizedLineForScreenRow(4).fold).toBeDefined() + expect(editor.isFoldedAtScreenRow(4)).toBe(true) expect(editor.getCursorScreenPosition()).toEqual [3, 4] - describe "when the cursor is on a folded line", -> - it "removes the lines contained by the fold", -> - editor.setSelectedBufferRange([[2, 0], [2, 0]]) - editor.foldBufferRowRange(2, 4) - editor.foldBufferRowRange(2, 6) - oldLine7 = buffer.lineForRow(7) - oldLine8 = buffer.lineForRow(8) + describe "when the cursor is inside a fold", -> + it "removes the folded content after the cursor", -> + editor.foldBufferRange([[2, 6], [6, 21]]) + editor.setCursorBufferPosition([4, 9]) editor.delete() - expect(editor.tokenizedLineForScreenRow(2).text).toBe oldLine7 - expect(editor.tokenizedLineForScreenRow(3).text).toBe oldLine8 + + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(4)).toBe ' while ? left.push(current) : right.push(current);' + expect(buffer.lineForRow(5)).toBe ' }' + expect(editor.getCursorBufferPosition()).toEqual [4, 9] describe "when there are multiple cursors", -> describe "when cursors are on the same line", -> From 87489d4b0b25a05e9df7f4c65744668798abe8cd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2016 14:15:06 +0100 Subject: [PATCH 060/262] Fix TextEditor cutToEndOfLine() test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …which was failing due to the different soft-wrapping rules. --- spec/text-editor-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 51a8bc150..9c9cf1475 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -3803,10 +3803,10 @@ describe "TextEditor", -> it "cuts up to the end of the line", -> editor.setSoftWrapped(true) editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([2, 2]) + editor.setEditorWidthInChars(25) + editor.setCursorScreenPosition([2, 6]) editor.cutToEndOfLine() - expect(editor.tokenizedLineForScreenRow(2).text).toBe '= () {' + expect(editor.lineTextForScreenRow(2)).toBe ' var function(items) {' describe "when soft wrap is off", -> describe "when nothing is selected", -> From e6cb5c8e89aab9ac347891d95fadc4137f1f9d69 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2016 15:57:24 +0100 Subject: [PATCH 061/262] :bug: Guard against unexisting screen rows --- spec/text-editor-spec.coffee | 3 ++- src/text-editor.coffee | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 9c9cf1475..f16298029 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4561,7 +4561,8 @@ describe "TextEditor", -> it '.lineTextForScreenRow(row)', -> editor.foldBufferRow(4) expect(editor.lineTextForScreenRow(5)).toEqual ' return sort(left).concat(pivot).concat(sort(right));' - expect(editor.lineTextForScreenRow(100)).not.toBeDefined() + expect(editor.lineTextForScreenRow(9)).toEqual '};' + expect(editor.lineTextForScreenRow(10)).toBeUndefined() describe ".deleteLine()", -> it "deletes the first line when the cursor is there", -> diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 2200ce827..7b77e4403 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -783,6 +783,7 @@ class TextEditor extends Model @displayLayer.tagForCode(tagCode) screenLineForScreenRow: (screenRow) -> + return if screenRow < 0 or screenRow > @getLastScreenRow() @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] bufferRowForScreenRow: (row) -> @displayLayer.translateScreenPosition(Point(row, 0)).row From 27aad426442e67a1a8ca124625a16823a0d71bc1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Mar 2016 10:33:12 +0100 Subject: [PATCH 062/262] Handle tab length retokenization in DisplayLayer We still want to keep the tab length in TokenizedBuffer, because we need it to understand if a certain buffer row is foldable or not (due to the indent level) --- spec/text-editor-spec.coffee | 18 ++++++++++++------ src/display-buffer.coffee | 13 ++++++++++--- src/tokenized-buffer.coffee | 16 ++++------------ src/tokenized-line.coffee | 6 +----- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index f16298029..138061a8f 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4919,11 +4919,13 @@ describe "TextEditor", -> it 'retokenizes when the tab length is updated via .setTabLength()', -> expect(editor.getTabLength()).toBe 2 - expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2 + leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace' + expect(leadingWhitespaceTokens.length).toBe(3) editor.setTabLength(6) expect(editor.getTabLength()).toBe 6 - expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6 + leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace' + expect(leadingWhitespaceTokens.length).toBe(1) changeHandler = jasmine.createSpy('changeHandler') editor.onDidChange(changeHandler) @@ -4932,21 +4934,25 @@ describe "TextEditor", -> it 'retokenizes when the editor.tabLength setting is updated', -> expect(editor.getTabLength()).toBe 2 - expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2 + leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace' + expect(leadingWhitespaceTokens.length).toBe(3) atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.js' expect(editor.getTabLength()).toBe 6 - expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6 + leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace' + expect(leadingWhitespaceTokens.length).toBe(1) it 'updates the tab length when the grammar changes', -> atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.coffee' expect(editor.getTabLength()).toBe 2 - expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2 + leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace' + expect(leadingWhitespaceTokens.length).toBe(3) editor.setGrammar(coffeeEditor.getGrammar()) expect(editor.getTabLength()).toBe 6 - expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6 + leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace' + expect(leadingWhitespaceTokens.length).toBe(1) describe ".indentLevelForLine(line)", -> it "returns the indent level when the line has only leading whitespace", -> diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b17600226..a59ccc8f5 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -122,7 +122,7 @@ class DisplayBuffer extends Model invisibles: invisibles softWrapColumn: softWrapColumn showIndentGuides: @config.get('editor.showIndentGuide', scope: scopeDescriptor) - tabLength: @config.get('editor.tabLength', scope: scopeDescriptor), + tabLength: @getTabLength(), ratioForCharacter: @ratioForCharacter.bind(this) isWrapBoundary: isWrapBoundary }) @@ -265,13 +265,20 @@ class DisplayBuffer extends Model # # Returns a {Number}. getTabLength: -> - @tokenizedBuffer.getTabLength() + if @tabLength? + @tabLength + else + @config.get('editor.tabLength', scope: @getRootScopeDescriptor()) # Specifies the tab length. # # tabLength - A {Number} that defines the new tab length. setTabLength: (tabLength) -> - @tokenizedBuffer.setTabLength(tabLength) + return if tabLength is @tabLength + + @tabLength = tabLength + @tokenizedBuffer.setTabLength(@tabLength) + @resetDisplayLayer() setIgnoreInvisibles: (ignoreInvisibles) -> return if ignoreInvisibles is @ignoreInvisibles diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 470aa59b9..00d4cdf63 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -117,19 +117,14 @@ class TokenizedBuffer extends Model @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines() @disposables.add(@grammarUpdateDisposable) - scopeOptions = {scope: @rootScopeDescriptor} - @configSettings = - tabLength: @config.get('editor.tabLength', scopeOptions) - invisibles: @config.get('editor.invisibles', scopeOptions) - showInvisibles: @config.get('editor.showInvisibles', scopeOptions) + @configSettings = {tabLength: @config.get('editor.tabLength', {scope: @rootScopeDescriptor})} if @configSubscriptions? @configSubscriptions.dispose() @disposables.remove(@configSubscriptions) @configSubscriptions = new CompositeDisposable - @configSubscriptions.add @config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) => + @configSubscriptions.add @config.onDidChange 'editor.tabLength', {scope: @rootScopeDescriptor}, ({newValue}) => @configSettings.tabLength = newValue - @retokenizeLines() @disposables.add(@configSubscriptions) @retokenizeLines() @@ -170,7 +165,6 @@ class TokenizedBuffer extends Model return if tabLength is @tabLength @tabLength = tabLength - @retokenizeLines() tokenizeInBackground: -> return if not @visible or @pendingChunk or not @isAlive() @@ -337,18 +331,16 @@ class TokenizedBuffer extends Model openScopes = [@grammar.startIdForScope(@grammar.scopeName)] text = @buffer.lineForRow(row) tags = [text.length] - tabLength = @getTabLength() lineEnding = @buffer.lineEndingForRow(row) - new TokenizedLine({openScopes, text, tags, tabLength, lineEnding, @tokenIterator}) + new TokenizedLine({openScopes, text, tags, lineEnding, @tokenIterator}) buildTokenizedLineForRow: (row, ruleStack, openScopes) -> @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes) buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> lineEnding = @buffer.lineEndingForRow(row) - tabLength = @getTabLength() {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) - new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, @tokenIterator}) + new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator}) tokenizedLineForRow: (bufferRow) -> if 0 <= bufferRow < @tokenizedLines.length diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index cbf64633b..8574cb960 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -16,11 +16,7 @@ class TokenizedLine return unless properties? - {@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties - {@startBufferColumn, @fold, @tabLength, @invisibles} = properties - - @startBufferColumn ?= 0 - @bufferDelta = @text.length + {@openScopes, @text, @tags, @ruleStack, @tokenIterator} = properties getTokenIterator: -> @tokenIterator.reset(this, arguments...) From 0a634a5870ee9302a6c39f824e57efd9892c8e21 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Mar 2016 10:49:32 +0100 Subject: [PATCH 063/262] :green_heart: Fix more tests using tokenizedLineForScreenRow --- spec/text-editor-spec.coffee | 40 +++++++++++++++--------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 138061a8f..2121bb2da 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4988,11 +4988,11 @@ describe "TextEditor", -> runs -> expect(editor.getGrammar()).toBe atom.grammars.nullGrammar - expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1 + expect(editor.tokensForScreenRow(0).length).toBe(1) atom.grammars.addGrammar(jsGrammar) expect(editor.getGrammar()).toBe jsGrammar - expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBeGreaterThan 1 + expect(editor.tokensForScreenRow(0).length).toBeGreaterThan 1 describe "editor.autoIndent", -> describe "when editor.autoIndent is false (default)", -> @@ -5220,10 +5220,10 @@ describe "TextEditor", -> expect(editor.getSelectedBufferRanges()).toEqual [[[3, 5], [3, 5]], [[9, 0], [14, 0]]] # folds are also duplicated - expect(editor.tokenizedLineForScreenRow(5).fold).toBeDefined() - expect(editor.tokenizedLineForScreenRow(7).fold).toBeDefined() - expect(editor.tokenizedLineForScreenRow(7).text).toBe " while(items.length > 0) {" - expect(editor.tokenizedLineForScreenRow(8).text).toBe " return sort(left).concat(pivot).concat(sort(right));" + expect(editor.isFoldedAtScreenRow(5)).toBe(true) + expect(editor.isFoldedAtScreenRow(7)).toBe(true) + expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {⋯" + expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" it "duplicates all folded lines for empty selections on folded lines", -> editor.foldBufferRow(4) @@ -5419,17 +5419,15 @@ describe "TextEditor", -> runs -> editor.setText("// http://github.com") - {tokens} = editor.tokenizedLineForScreenRow(0) - expect(tokens[1].value).toBe " http://github.com" - expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js'] waitsForPromise -> atom.packages.activatePackage('language-hyperlink') runs -> - {tokens} = editor.tokenizedLineForScreenRow(0) - expect(tokens[2].value).toBe "http://github.com" - expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'markup.underline.link.http.hyperlink'] describe "when the grammar is updated", -> it "retokenizes existing buffers that contain tokens that match the injection selector", -> @@ -5439,28 +5437,22 @@ describe "TextEditor", -> runs -> editor.setText("// SELECT * FROM OCTOCATS") - {tokens} = editor.screenLineForScreenRow(0) - expect(tokens[2].closeTags).toEqual ['comment.line.double-slash.js', 'source.js'] - expect(tokens[2].openTags).toEqual [] - expect(tokens[2].text).toBe "" + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js'] waitsForPromise -> atom.packages.activatePackage('package-with-injection-selector') runs -> - {tokens} = editor.screenLineForScreenRow(0) - expect(tokens[2].closeTags).toEqual ['comment.line.double-slash.js', 'source.js'] - expect(tokens[2].openTags).toEqual [] - expect(tokens[2].text).toBe "" + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js'] waitsForPromise -> atom.packages.activatePackage('language-sql') runs -> - {tokens} = editor.screenLineForScreenRow(2) - expect(tokens[2].closeTags).toEqual [] - expect(tokens[2].openTags).toEqual ["keyword.other.DML.sql"] - expect(tokens[2].text).toBe "SELECT" + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'keyword.other.DML.sql', 'keyword.operator.star.sql', 'keyword.other.DML.sql'] describe ".normalizeTabsInBufferRange()", -> it "normalizes tabs depending on the editor's soft tab/tab length settings", -> From c609f6c967c5434b34b4549121a486e8d6e3c53c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Mar 2016 10:53:02 +0100 Subject: [PATCH 064/262] Destroy `DisplayLayer` upon `DisplayBuffer` destruction --- spec/text-editor-spec.coffee | 2 +- src/display-buffer.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 2121bb2da..8c8fdf2e2 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5136,7 +5136,7 @@ describe "TextEditor", -> describe ".destroy()", -> it "destroys marker layers associated with the text editor", -> selectionsMarkerLayerId = editor.selectionsMarkerLayer.id - foldsMarkerLayerId = editor.displayBuffer.foldsMarkerLayer.id + foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id editor.destroy() expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index a59ccc8f5..560aefb20 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -885,6 +885,7 @@ class DisplayBuffer extends Model return destroyed: -> + @displayLayer.destroy() @defaultMarkerLayer.destroy() @scopedConfigSubscriptions.dispose() @disposables.dispose() From f9fb93f214b11db61dccdfabde0b25f24d3d7a18 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Mar 2016 16:37:01 +0100 Subject: [PATCH 065/262] :art: --- spec/fake-lines-yardstick.coffee | 41 +++++++++++++++++++------------- src/text-editor.coffee | 4 ---- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 085ab4bde..c3396ff9f 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -32,25 +32,32 @@ class FakeLinesYardstick column = 0 scopes = [] - for {text, closeTags, openTags} in @displayLayer.getScreenLines(targetRow, targetRow + 1)[0].tokens - scopes.splice(scopes.lastIndexOf(closeTag), 1) for closeTag in closeTags - scopes.push(openTag) for openTag in openTags - characterWidths = @getScopedCharacterWidths(scopes) + startIndex = 0 + {tagCodes, lineText} = @model.screenLineForScreenRow(targetRow) + for tagCode in tagCodes + if @displayLayer.isOpenTagCode(tagCode) + scopes.push(@displayLayer.tagForCode(tagCode)) + else if @displayLayer.isCloseTagCode(tagCode) + scopes.splice(scopes.lastIndexOf(@displayLayer.tagForCode(tagCode)), 1) + else + text = lineText.substr(startIndex, tagCode) + startIndex += tagCode + characterWidths = @getScopedCharacterWidths(scopes) - valueIndex = 0 - while valueIndex < text.length - if isPairedCharacter(text, valueIndex) - char = text[valueIndex...valueIndex + 2] - charLength = 2 - valueIndex += 2 - else - char = text[valueIndex] - charLength = 1 - valueIndex++ + valueIndex = 0 + while valueIndex < text.length + if isPairedCharacter(text, valueIndex) + char = text[valueIndex...valueIndex + 2] + charLength = 2 + valueIndex += 2 + else + char = text[valueIndex] + charLength = 1 + valueIndex++ - break if column is targetColumn + break if column is targetColumn - left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0' - column += charLength + left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0' + column += charLength {top, left} diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 7b77e4403..958c1d8ec 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2971,10 +2971,6 @@ class TextEditor extends Model destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) - # {Delegates to: DisplayLayer.outermostFoldsForBufferRowRange} - outermostFoldsInBufferRowRange: (startRow, endRow) -> - @displayLayer.outermostFoldsInBufferRowRange(startRow, endRow) - ### Section: Gutters ### From 8b6940c46352d56f84b47026d5beca1cde8ad8a8 Mon Sep 17 00:00:00 2001 From: Nick Smith Date: Wed, 30 Mar 2016 22:53:42 -0400 Subject: [PATCH 066/262] Update text-editor-element.coffee --- src/text-editor-element.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index a0ec1b7fa..0c9fa6123 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -96,7 +96,7 @@ class TextEditorElement extends HTMLElement throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config? throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes? throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace? - throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert? + throw new Error("Must pass an assert parameter when initializing TextEditorElements") unless @assert? throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles? throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars? From 6f6b14dbf11bd57e225cad9238c059eb43008088 Mon Sep 17 00:00:00 2001 From: Nick Smith Date: Wed, 30 Mar 2016 22:56:34 -0400 Subject: [PATCH 067/262] Update text-editor-spec.coffee Grammar Update --- spec/text-editor-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 9d2a2a58c..7a5b6041e 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -3176,7 +3176,7 @@ describe "TextEditor", -> expect(editor.indentationForBufferRow(0)).toBe 1 expect(editor.indentationForBufferRow(1)).toBe 1 - it "indents the new line to the correct level when editor.autoIndent is true and using a off-side rule language", -> + it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", -> waitsForPromise -> atom.packages.activatePackage('language-coffee-script') @@ -5025,7 +5025,7 @@ describe "TextEditor", -> expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 describe "when the line preceding the newline does't add a level of indentation", -> - it "indents the new line to the same level a as the preceding line", -> + it "indents the new line to the same level as the preceding line", -> editor.setCursorBufferPosition([5, 14]) editor.insertText('\n') expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5) From bc2e6742347c444f3e99cba025e57af269cfe734 Mon Sep 17 00:00:00 2001 From: Drew Noel Date: Fri, 26 Feb 2016 12:32:55 -0500 Subject: [PATCH 068/262] :arrow_up: Electron --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be1bb2f51..98afe224f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "0.36.8", + "electronVersion": "0.36.9", "dependencies": { "async": "0.2.6", "atom-keymap": "6.3.2", From 81d0874fa5433c033ed276c02d7a3bcb2c7fed7e Mon Sep 17 00:00:00 2001 From: Drew Noel Date: Sat, 12 Mar 2016 23:57:24 -0500 Subject: [PATCH 069/262] :arrow_up: electron v0.36.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98afe224f..89b79fc45 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "0.36.9", + "electronVersion": "0.36.11", "dependencies": { "async": "0.2.6", "atom-keymap": "6.3.2", From fb5bfe1b3a30a0643cf020aa1daee7700d780fbc Mon Sep 17 00:00:00 2001 From: Drew Noel Date: Sun, 13 Mar 2016 13:25:27 -0400 Subject: [PATCH 070/262] Fix old electron require syntax in specs --- spec/atom-environment-spec.coffee | 2 +- spec/window-event-handler-spec.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 5fd4b11f1..2d431cc26 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -365,7 +365,7 @@ describe "AtomEnvironment", -> updateAvailableHandler = jasmine.createSpy("update-available-handler") subscription = atom.onUpdateAvailable updateAvailableHandler - autoUpdater = require('remote').require('auto-updater') + autoUpdater = require('electron').remote.require('auto-updater') autoUpdater.emit 'update-downloaded', null, "notes", "version" waitsFor -> diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee index bb7e1665b..22f43c90f 100644 --- a/spec/window-event-handler-spec.coffee +++ b/spec/window-event-handler-spec.coffee @@ -75,7 +75,7 @@ describe "WindowEventHandler", -> describe "when a link is clicked", -> it "opens the http/https links in an external application", -> - shell = require 'shell' + {shell} = require 'electron' spyOn(shell, 'openExternal') link = document.createElement('a') From 8694ea3c94225dda985e50c69804a916da76bd14 Mon Sep 17 00:00:00 2001 From: Wliu Date: Fri, 1 Apr 2016 22:21:39 -0400 Subject: [PATCH 071/262] Update require paths for deprecated Electron syntax --- src/module-cache.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/module-cache.coffee b/src/module-cache.coffee index a2840a864..c8e3b9105 100644 --- a/src/module-cache.coffee +++ b/src/module-cache.coffee @@ -202,12 +202,12 @@ registerBuiltins = (devMode) -> atomShellRoot = path.join(process.resourcesPath, 'atom.asar') - commonRoot = path.join(atomShellRoot, 'common', 'api', 'lib') + commonRoot = path.join(atomShellRoot, 'lib', 'common', 'api') commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell'] for builtin in commonBuiltins cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js") - rendererRoot = path.join(atomShellRoot, 'renderer', 'api', 'lib') + rendererRoot = path.join(atomShellRoot, 'lib', 'renderer', 'api') rendererBuiltins = ['ipc-renderer', 'remote'] for builtin in rendererBuiltins cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js") From 37ea3c913f4acbc7af875bb7b64ec4e7aff93b94 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 2 Apr 2016 10:21:52 -0400 Subject: [PATCH 072/262] :arrow_up: electron@0.36.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89b79fc45..e23ab4b54 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "0.36.11", + "electronVersion": "0.36.12", "dependencies": { "async": "0.2.6", "atom-keymap": "6.3.2", From bbef4c67c3171569b249f236812577bce787021f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 4 Apr 2016 10:37:54 -0600 Subject: [PATCH 073/262] Get the TextEditorComponent specs green --- spec/text-editor-component-spec.js | 49 ++++++++++++++++-------------- src/lines-tile-component.coffee | 5 ++- src/text-editor.coffee | 2 ++ 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 79936311b..c7c9f9277 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -72,7 +72,7 @@ describe('TextEditorComponent', function () { let text = editor.lineTextForScreenRow(screenRow) expect(lineNode.offsetTop).toBe(top) if (text === '') { - expect(lineNode.innerHTML).toBe(' ') + expect(lineNode.textContent).toBe(' ') } else { expect(lineNode.textContent).toBe(text) } @@ -360,9 +360,9 @@ describe('TextEditorComponent', function () { } }) - it('renders an nbsp on empty lines when no line-ending character is defined', function () { + it('renders an placeholder space on empty lines when no line-ending character is defined', function () { atom.config.set('editor.showInvisibles', false) - expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) + expect(component.lineNodeForScreenRow(10).textContent).toBe(' ') }) it('gives the lines and tiles divs the same background color as the editor to improve GPU performance', async function () { @@ -428,13 +428,14 @@ describe('TextEditorComponent', function () { expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) }) - it('keeps rebuilding lines when continuous reflow is on', function () { + it('keeps rebuilding lines when continuous reflow is on', async function () { wrapperNode.setContinuousReflow(true) - let oldLineNode = componentNode.querySelector('.line') + let oldLineNode = componentNode.querySelectorAll('.line')[1] - waitsFor(function () { - return componentNode.querySelector('.line') !== oldLineNode - }) + while (true) { + await nextViewUpdatePromise() + if (componentNode.querySelectorAll('.line')[1] !== oldLineNode) break + } }) describe('when showInvisibles is enabled', function () { @@ -496,20 +497,20 @@ describe('TextEditorComponent', function () { expect(component.lineNodeForScreenRow(10).textContent).toBe(invisibles.eol) }) - it('renders an nbsp on empty lines when the line-ending character is an empty string', async function () { + it('renders a placeholder space on empty lines when the line-ending character is an empty string', async function () { atom.config.set('editor.invisibles', { eol: '' }) await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) + expect(component.lineNodeForScreenRow(10).textContent).toBe(' ') }) - it('renders an nbsp on empty lines when the line-ending character is false', async function () { + it('renders an placeholder space on empty lines when the line-ending character is false', async function () { atom.config.set('editor.invisibles', { eol: false }) await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) + expect(component.lineNodeForScreenRow(10).textContent).toBe(' ') }) it('interleaves invisible line-ending characters with indent guides on empty lines', async function () { @@ -517,24 +518,25 @@ describe('TextEditorComponent', function () { await nextViewUpdatePromise() + editor.setTabLength(2) editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', { normalizeLineEndings: false }) await nextViewUpdatePromise() + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTabLength(3) await nextViewUpdatePromise() + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTabLength(1) await nextViewUpdatePromise() + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') }) describe('when soft wrapping is enabled', function () { @@ -985,13 +987,14 @@ describe('TextEditorComponent', function () { expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true) }) - it('keeps rebuilding line numbers when continuous reflow is on', function () { + it('keeps rebuilding line numbers when continuous reflow is on', async function () { wrapperNode.setContinuousReflow(true) let oldLineNode = componentNode.querySelectorAll('.line-number')[1] - waitsFor(function () { - return componentNode.querySelectorAll('.line-number')[1] !== oldLineNode - }) + while (true) { + await nextViewUpdatePromise() + if (componentNode.querySelectorAll('.line-number')[1] !== oldLineNode) break + } }) describe('fold decorations', function () { @@ -1198,10 +1201,10 @@ describe('TextEditorComponent', function () { let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() - let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[0] + let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2] let range = document.createRange(cursorLocationTextNode) - range.setStart(cursorLocationTextNode, 3) - range.setEnd(cursorLocationTextNode, 4) + range.setStart(cursorLocationTextNode, 0) + range.setEnd(cursorLocationTextNode, 1) let rangeRect = range.getBoundingClientRect() expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 544a58040..6e9a15e6b 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -4,7 +4,6 @@ HighlightsComponent = require './highlights-component' TokenIterator = require './token-iterator' AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} TokenTextEscapeRegex = /[&"'<>]/g -NBSPCharacter = '\u00a0' MaxTokenLength = 20000 cloneObject = (object) -> @@ -267,13 +266,13 @@ class LinesTileComponent openScopeNode.appendChild(newScopeNode) openScopeNode = newScopeNode else - textNode = @domElementPool.buildText(lineText.substr(startIndex, tagCode).replace(/\s/g, NBSPCharacter)) + textNode = @domElementPool.buildText(lineText.substr(startIndex, tagCode)) startIndex += tagCode openScopeNode.appendChild(textNode) @currentLineTextNodes.push(textNode) if startIndex is 0 - textNode = @domElementPool.buildText(NBSPCharacter) + textNode = @domElementPool.buildText(' ') lineNode.appendChild(textNode) @currentLineTextNodes.push(textNode) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 06229bff2..ffb5a4706 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -132,6 +132,8 @@ class TextEditor extends Model @config, @assert, @grammarRegistry, @packageManager }) {@buffer, @displayLayer} = @displayBuffer + @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) + @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) for marker in @selectionsMarkerLayer.getMarkers() From 244f117d9504bbac439ce3cc44c475931c595493 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 4 Apr 2016 18:08:34 -0600 Subject: [PATCH 074/262] Handle empty client rects in LinesYardstick getBoundingClientRect returns garbage values if the range has zero width, which it does in the case of a fold placeholder or any other zero-width character. Sometimes getClientRects() returns an empty list, so we fall back to the bounding rect in these cases. --- src/lines-yardstick.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 4bdd43603..e90f267f1 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -108,4 +108,4 @@ class LinesYardstick clientRectForRange: (textNode, startIndex, endIndex) -> @rangeForMeasurement.setStart(textNode, startIndex) @rangeForMeasurement.setEnd(textNode, endIndex) - @rangeForMeasurement.getBoundingClientRect() + @rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect() From 108513f994d81ae611d43c5f2d81ac3e6933f951 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 10:47:25 +0200 Subject: [PATCH 075/262] Fix LinesYardstick specs to use the new tagCode-based scope structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, remove specs testing RTL behavior. They don’t work as of today, and I think we need a better approach to handle them, that doesn’t solely rely on the DOM, but actually takes into account that e.g. (0, 0) is the rightmost character on a right-to-left string. --- spec/lines-yardstick-spec.coffee | 69 ++++++++++++++------------------ src/lines-component.coffee | 8 ---- src/lines-yardstick.coffee | 1 - src/text-editor-component.coffee | 7 +--- 4 files changed, 32 insertions(+), 53 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 46935510f..15ec2a533 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -19,36 +19,44 @@ describe "LinesYardstick", -> screenRowsToMeasure = [] buildLineNode = (screenRow) -> - tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) - iterator = tokenizedLine.getTokenIterator() + startIndex = 0 + scopes = [] + screenLine = editor.screenLineForScreenRow(screenRow) lineNode = document.createElement("div") lineNode.style.whiteSpace = "pre" - while iterator.next() - span = document.createElement("span") - span.className = iterator.getScopes().join(' ').replace(/\.+/g, ' ') - span.textContent = iterator.getText() - lineNode.appendChild(span) + for tagCode in screenLine.tagCodes when tagCode isnt 0 + if editor.displayLayer.isCloseTagCode(tagCode) + scopes.pop() + else if editor.displayLayer.isOpenTagCode(tagCode) + scopes.push(editor.displayLayer.tagForCode(tagCode)) + else + text = screenLine.lineText.substr(startIndex, tagCode) + startIndex += tagCode + span = document.createElement("span") + span.className = scopes.join(' ').replace(/\.+/g, ' ') + span.textContent = text + lineNode.appendChild(span) jasmine.attachToDOM(lineNode) createdLineNodes.push(lineNode) lineNode mockLineNodesProvider = - lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> + lineIdForScreenRow: (screenRow) -> + editor.screenLineForScreenRow(screenRow).id + + lineNodeForScreenRow: (screenRow) -> buildLineNode(screenRow) - textNodesForLineIdAndScreenRow: (lineId, screenRow) -> - lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow) + textNodesForScreenRow: (screenRow) -> + lineNode = @lineNodeForScreenRow(screenRow) iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) textNodes = [] - while textNode = iterator.nextNode() - textNodes.push(textNode) + textNodes.push(textNode) while textNode = iterator.nextNode() textNodes editor.setLineHeightInPixels(14) - lineTopIndex = new LineTopIndex({ - defaultLineHeight: editor.getLineHeightInPixels() - }) + lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()}) linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) afterEach -> @@ -69,9 +77,9 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0))).toEqual({left: 0, top: 0}) expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1))).toEqual({left: 7, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 37.78125, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43.171875, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72.171875, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 38, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14}) expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.859375, top: 28}) it "reuses already computed pixel positions unless it is invalidated", -> @@ -82,9 +90,9 @@ describe "LinesYardstick", -> } """ - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14}) expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70}) atom.styles.addStyleSheet """ * { @@ -92,9 +100,9 @@ describe "LinesYardstick", -> } """ - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14}) expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70}) linesYardstick.invalidateCache() @@ -102,23 +110,6 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28}) expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70}) - it "correctly handles RTL characters", -> - atom.styles.addStyleSheet """ - * { - font-size: 14px; - font-family: monospace; - } - """ - - editor.setText("السلام عليكم") - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0)).left).toBe 0 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1)).left).toBe 8 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 2)).left).toBe 16 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5)).left).toBe 33 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 7)).left).toBe 50 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 9)).left).toBe 67 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 11)).left).toBe 84 - it "doesn't report a width greater than 0 when the character to measure is at the beginning of a text node", -> # This spec documents what seems to be a bug in Chromium, because we'd # expect that Range(0, 0).getBoundingClientRect().width to always be zero. diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 824655579..9a7ce7f44 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -97,14 +97,6 @@ class LinesComponent extends TiledComponent @presenter.setLineHeight(lineHeightInPixels) @presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) - lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> - tile = @presenter.tileForRow(screenRow) - @getComponentForTile(tile)?.lineNodeForLineId(lineId) - - textNodesForLineIdAndScreenRow: (lineId, screenRow) -> - tile = @presenter.tileForRow(screenRow) - @getComponentForTile(tile)?.textNodesForLineId(lineId) - lineIdForScreenRow: (screenRow) -> tile = @presenter.tileForRow(screenRow) @getComponentForTile(tile)?.lineIdForScreenRow(screenRow) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index e90f267f1..f53b6348e 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -5,7 +5,6 @@ TokenIterator = require './token-iterator' module.exports = class LinesYardstick constructor: (@model, @lineNodesProvider, @lineTopIndex, grammarRegistry) -> - @tokenIterator = new TokenIterator({grammarRegistry}) @rangeForMeasurement = document.createRange() @invalidateCache() diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 9b091100d..c5dc2929d 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -453,7 +453,7 @@ class TextEditorComponent unless @presenter.isRowVisible(screenPosition.row) @presenter.setScreenRowsToMeasure([screenPosition.row]) - unless @linesComponent.lineNodeForLineIdAndScreenRow(@presenter.lineIdForScreenRow(screenPosition.row), screenPosition.row)? + unless @linesComponent.lineNodeForScreenRow(screenPosition.row)? @updateSyncPreMeasurement() pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition) @@ -849,10 +849,7 @@ class TextEditorComponent e.abortKeyBinding() unless @editor.consolidateSelections() lineNodeForScreenRow: (screenRow) -> - tileRow = @presenter.tileForRow(screenRow) - tileComponent = @linesComponent.getComponentForTile(tileRow) - - tileComponent?.lineNodeForScreenRow(screenRow) + @linesComponent.lineNodeForScreenRow(screenRow) lineNumberNodeForScreenRow: (screenRow) -> tileRow = @presenter.tileForRow(screenRow) From a083a754a57d21851406a50c6237b78277fedb6d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 11:12:51 +0200 Subject: [PATCH 076/262] :green_heart: Fix TextEditorPresenter specs --- spec/text-editor-presenter-spec.coffee | 101 ++++++------------------- 1 file changed, 22 insertions(+), 79 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index d40828d77..a7beb3c57 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1233,12 +1233,15 @@ describe "TextEditorPresenter", -> lineId = presenter.linesByScreenRow.get(row)?.id tilesState[presenter.tileForRow(row)]?.lines[lineId] - tokensIncludeTag = (tokens, tag) -> - includeTag = false - for {openTags, closeTags} in tokens - includeTag = true for openTag in openTags when openTag.indexOf(tag) isnt -1 - includeTag = true for closeTag in closeTags when closeTag.indexOf(tag) isnt -1 - includeTag + tagsForCodes = (presenter, tagCodes) -> + openTags = [] + closeTags = [] + for tagCode in tagCodes when tagCode < 0 # skip text codes + if presenter.isOpenTagCode(tagCode) + openTags.push(presenter.tagForCode(tagCode)) + else + closeTags.push(presenter.tagForCode(tagCode)) + {openTags, closeTags} tiledContentContract (presenter) -> getState(presenter).content @@ -1248,56 +1251,12 @@ describe "TextEditorPresenter", -> presenter.setExplicitHeight(3) expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() - expectValues lineStateForScreenRow(presenter, 3), { - screenRow: 3, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: [], text: 'var pivot = items.shift(), current, left = [], right = [];'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } - expectValues lineStateForScreenRow(presenter, 4), { - screenRow: 4, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: [], text: 'while(items.length > 0) {'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } - expectValues lineStateForScreenRow(presenter, 5), { - screenRow: 5, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: [], text: 'current = items.shift();'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } - expectValues lineStateForScreenRow(presenter, 6), { - screenRow: 6, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: [], text: 'current < pivot ? left.push(current) : right.push(current);'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } - expectValues lineStateForScreenRow(presenter, 7), { - screenRow: 7, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: [], text: '}'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } - expectValues lineStateForScreenRow(presenter, 8), { - screenRow: 8, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: [], text: 'return sort(left).concat(pivot).concat(sort(right));'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } + expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes} + expectValues lineStateForScreenRow(presenter, 4), {screenRow: 4, tagCodes: editor.screenLineForScreenRow(4).tagCodes} + expectValues lineStateForScreenRow(presenter, 5), {screenRow: 5, tagCodes: editor.screenLineForScreenRow(5).tagCodes} + expectValues lineStateForScreenRow(presenter, 6), {screenRow: 6, tagCodes: editor.screenLineForScreenRow(6).tagCodes} + expectValues lineStateForScreenRow(presenter, 7), {screenRow: 7, tagCodes: editor.screenLineForScreenRow(7).tagCodes} + expectValues lineStateForScreenRow(presenter, 8), {screenRow: 8, tagCodes: editor.screenLineForScreenRow(8).tagCodes} expect(lineStateForScreenRow(presenter, 9)).toBeUndefined() it "updates when the editor's content changes", -> @@ -1305,36 +1264,20 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n") - expectValues lineStateForScreenRow(presenter, 1), { - screenRow: 1, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: ' '}, - {closeTags: ['leading-whitespace'], openTags: [], text: 'var sort = function(items) {'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text : ''} - ] - } - expectValues lineStateForScreenRow(presenter, 2), { - screenRow: 2, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar'], text: 'hello'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } - expectValues lineStateForScreenRow(presenter, 3), { - screenRow: 3, tokens: [ - {closeTags: [], openTags: ['text.plain.null-grammar'], text: 'world'}, - {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''} - ] - } + expectValues lineStateForScreenRow(presenter, 1), {screenRow: 1, tagCodes: editor.screenLineForScreenRow(1).tagCodes} + expectValues lineStateForScreenRow(presenter, 2), {screenRow: 2, tagCodes: editor.screenLineForScreenRow(2).tagCodes} + expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes} it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", -> editor.setText("hello\nworld\r\n") presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) - expect(tokensIncludeTag(lineStateForScreenRow(presenter, 0).tokens, 'eol')).toBe(false) - expect(tokensIncludeTag(lineStateForScreenRow(presenter, 1).tokens, 'eol')).toBe(false) + expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).not.toContain('invisible-character eol') + expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).not.toContain('invisible-character eol') atom.config.set('editor.showInvisibles', true) presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) - expect(tokensIncludeTag(lineStateForScreenRow(presenter, 0).tokens, 'eol')).toBe(true) - expect(tokensIncludeTag(lineStateForScreenRow(presenter, 1).tokens, 'eol')).toBe(true) + expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).toContain('invisible-character eol') + expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).toContain('invisible-character eol') describe ".blockDecorations", -> it "contains all block decorations that are present before/after a line, both initially and when decorations change", -> From e6cfb8d5874c375b4f8116ceba8fa766d1d80a15 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 11:28:16 +0200 Subject: [PATCH 077/262] Use a zero-width nbsp as our fold character --- spec/text-editor-spec.coffee | 2 +- src/display-buffer.coffee | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 6b551cf55..95003a498 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5352,7 +5352,7 @@ describe "TextEditor", -> # folds are also duplicated expect(editor.isFoldedAtScreenRow(5)).toBe(true) expect(editor.isFoldedAtScreenRow(7)).toBe(true) - expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {⋯" + expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {" + editor.displayLayer.foldCharacter expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" it "duplicates all folded lines for empty selections on folded lines", -> diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 560aefb20..cfc352baf 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -9,6 +9,8 @@ Decoration = require './decoration' LayerDecoration = require './layer-decoration' {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' +ZERO_WIDTH_NBSP = '\ufeff' + class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> super @@ -123,8 +125,9 @@ class DisplayBuffer extends Model softWrapColumn: softWrapColumn showIndentGuides: @config.get('editor.showIndentGuide', scope: scopeDescriptor) tabLength: @getTabLength(), - ratioForCharacter: @ratioForCharacter.bind(this) - isWrapBoundary: isWrapBoundary + ratioForCharacter: @ratioForCharacter.bind(this), + isWrapBoundary: isWrapBoundary, + foldCharacter: ZERO_WIDTH_NBSP }) updateAllScreenLines: -> From 80b956e996aa62937d4f241e6bb0d3ce7d8268b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 11:39:06 +0200 Subject: [PATCH 078/262] :green_heart: Fix TextDecorationLayer API specs in TokenizedBuffer --- spec/tokenized-buffer-spec.coffee | 33 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 4dec5bdf5..b90ae8779 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -1064,22 +1064,13 @@ describe "TokenizedBuffer", -> iterator = tokenizedBuffer.buildIterator() iterator.seek(Point(0, 0)) - boundaries = [] - loop - boundaries.push({ - position: iterator.getPosition(), - closeTags: iterator.getCloseTags(), - openTags: iterator.getOpenTags() - }) - break unless iterator.moveToSuccessor() - - expect(boundaries).toEqual([ + expectedBoundaries = [ {position: Point(0, 0), closeTags: [], openTags: ["source.js", "storage.type.var.js"]} {position: Point(0, 3), closeTags: ["storage.type.var.js"], openTags: []} {position: Point(0, 8), closeTags: [], openTags: ["keyword.operator.assignment.js"]} {position: Point(0, 9), closeTags: ["keyword.operator.assignment.js"], openTags: []} - {position: Point(0, 10), closeTags: [], openTags: ["constant.numeric.js"]} - {position: Point(0, 11), closeTags: ["constant.numeric.js"], openTags: []} + {position: Point(0, 10), closeTags: [], openTags: ["constant.numeric.decimal.js"]} + {position: Point(0, 11), closeTags: ["constant.numeric.decimal.js"], openTags: []} {position: Point(0, 12), closeTags: [], openTags: ["comment.block.js", "punctuation.definition.comment.js"]} {position: Point(0, 14), closeTags: ["punctuation.definition.comment.js"], openTags: []} {position: Point(1, 5), closeTags: [], openTags: ["punctuation.definition.comment.js"]} @@ -1087,9 +1078,19 @@ describe "TokenizedBuffer", -> {position: Point(1, 10), closeTags: ["storage.type.var.js"], openTags: []} {position: Point(1, 15), closeTags: [], openTags: ["keyword.operator.assignment.js"]} {position: Point(1, 16), closeTags: ["keyword.operator.assignment.js"], openTags: []} - {position: Point(1, 17), closeTags: [], openTags: ["constant.numeric.js"]} - {position: Point(1, 18), closeTags: ["constant.numeric.js"], openTags: []} - ]) + {position: Point(1, 17), closeTags: [], openTags: ["constant.numeric.decimal.js"]} + {position: Point(1, 18), closeTags: ["constant.numeric.decimal.js"], openTags: []} + ] + + loop + boundary = { + position: iterator.getPosition(), + closeTags: iterator.getCloseTags(), + openTags: iterator.getOpenTags() + } + + expect(boundary).toEqual(expectedBoundaries.shift()) + break unless iterator.moveToSuccessor() expect(iterator.seek(Point(0, 1))).toEqual(["source.js", "storage.type.var.js"]) expect(iterator.getPosition()).toEqual(Point(0, 3)) @@ -1097,7 +1098,7 @@ describe "TokenizedBuffer", -> expect(iterator.getPosition()).toEqual(Point(0, 8)) expect(iterator.seek(Point(1, 0))).toEqual(["source.js", "comment.block.js"]) expect(iterator.getPosition()).toEqual(Point(1, 5)) - expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.js"]) + expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.decimal.js"]) expect(iterator.getPosition()).toEqual(Point(1, 18)) expect(iterator.seek(Point(2, 0))).toEqual(["source.js"]) From 544b75c7b0829be2ee6ff5caeffc97ae54d12c2e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 12:19:45 +0200 Subject: [PATCH 079/262] :fire: :green_heart: Fix TokenizedBuffer specs --- spec/tokenized-buffer-spec.coffee | 359 ++++-------------------------- src/token.coffee | 23 +- src/tokenized-line.coffee | 13 +- 3 files changed, 43 insertions(+), 352 deletions(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index b90ae8779..f8746a511 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -134,13 +134,10 @@ describe "TokenizedBuffer", -> describe "on construction", -> it "initially creates un-tokenized screen lines, then tokenizes lines chunk at a time in the background", -> line0 = tokenizedBuffer.tokenizedLineForRow(0) - expect(line0.tokens.length).toBe 1 - expect(line0.tokens[0]).toEqual(value: line0.text, scopes: ['source.js']) + expect(line0.tokens).toEqual([value: line0.text, scopes: ['source.js']]) line11 = tokenizedBuffer.tokenizedLineForRow(11) - expect(line11.tokens.length).toBe 2 - expect(line11.tokens[0]).toEqual(value: " ", scopes: ['source.js'], isAtomic: true) - expect(line11.tokens[1]).toEqual(value: "return sort(Array.apply(this, arguments));", scopes: ['source.js']) + expect(line11.tokens).toEqual([value: " return sort(Array.apply(this, arguments));", scopes: ['source.js']]) # background tokenization has not begun expect(tokenizedBuffer.tokenizedLineForRow(0).ruleStack).toBeUndefined() @@ -236,7 +233,7 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']) expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']) # line 2 is unchanged - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js']) + expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js']) expect(changeHandler).toHaveBeenCalled() [event] = changeHandler.argsForCall[0] @@ -283,9 +280,9 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) # lines below deleted regions should be shifted upward - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js']) - expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) - expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']) + expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js']) + expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) + expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']) expect(changeHandler).toHaveBeenCalled() [event] = changeHandler.argsForCall[0] @@ -331,7 +328,7 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js']) # previous line 3 is pushed down to become line 5 - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) + expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) expect(changeHandler).toHaveBeenCalled() [event] = changeHandler.argsForCall[0] @@ -377,32 +374,6 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.tokenizedLineForRow(5).ruleStack?).toBeTruthy() expect(tokenizedBuffer.tokenizedLineForRow(6).ruleStack?).toBeTruthy() - it "tokenizes leading whitespace based on the new tab length", -> - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy() - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " " - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeTruthy() - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " " - - tokenizedBuffer.setTabLength(4) - fullyTokenize(tokenizedBuffer) - - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy() - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " " - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeFalsy() - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " current " - - it "does not tokenize whitespaces followed by combining characters as leading whitespace", -> - buffer.setText(" \u030b") - fullyTokenize(tokenizedBuffer) - - {tokens} = tokenizedBuffer.tokenizedLineForRow(0) - expect(tokens[0].value).toBe " " - expect(tokens[0].hasLeadingWhitespace()).toBe true - expect(tokens[1].value).toBe " " - expect(tokens[1].hasLeadingWhitespace()).toBe true - expect(tokens[2].value).toBe " \u030b" - expect(tokens[2].hasLeadingWhitespace()).toBe false - it "does not break out soft tabs across a scope boundary", -> waitsForPromise -> atom.packages.activatePackage('language-gfm') @@ -439,133 +410,6 @@ describe "TokenizedBuffer", -> beforeEach -> fullyTokenize(tokenizedBuffer) - it "renders each tab as its own atomic token with a value of size tabLength", -> - tabAsSpaces = _.multiplyString(' ', tokenizedBuffer.getTabLength()) - screenLine0 = tokenizedBuffer.tokenizedLineForRow(0) - expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}" - {tokens} = screenLine0 - - expect(tokens.length).toBe 4 - expect(tokens[0].value).toBe "#" - expect(tokens[1].value).toBe " Econ 101" - expect(tokens[2].value).toBe tabAsSpaces - expect(tokens[2].scopes).toEqual tokens[1].scopes - expect(tokens[2].isAtomic).toBeTruthy() - expect(tokens[3].value).toBe "" - - expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand" - - it "aligns the hard tabs to the correct tab stop column", -> - buffer.setText """ - 1\t2 \t3\t4 - 12\t3 \t4\t5 - 123\t4 \t5\t6 - """ - - tokenizedBuffer.setTabLength(4) - fullyTokenize(tokenizedBuffer) - - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4" - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 3 - - expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5" - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2 - - expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6" - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1 - - tokenizedBuffer.setTabLength(3) - fullyTokenize(tokenizedBuffer) - - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4" - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 2 - - expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5" - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1 - - expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6" - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 3 - - tokenizedBuffer.setTabLength(2) - fullyTokenize(tokenizedBuffer) - - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4" - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1 - - expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5" - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2 - - expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6" - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1 - - tokenizedBuffer.setTabLength(1) - fullyTokenize(tokenizedBuffer) - - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4" - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1 - - expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5" - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1 - - expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6" - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1 - - describe "when the buffer contains UTF-8 surrogate pairs", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - buffer = atom.project.bufferForPathSync 'sample-with-pairs.js' - buffer.setText """ - 'abc\uD835\uDF97def' - //\uD835\uDF97xyz - """ - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - fullyTokenize(tokenizedBuffer) - - afterEach -> - tokenizedBuffer.destroy() - buffer.release() - - it "renders each UTF-8 surrogate pair as its own atomic token", -> - screenLine0 = tokenizedBuffer.tokenizedLineForRow(0) - expect(screenLine0.text).toBe "'abc\uD835\uDF97def'" - {tokens} = screenLine0 - - expect(tokens.length).toBe 5 - expect(tokens[0].value).toBe "'" - expect(tokens[1].value).toBe "abc" - expect(tokens[2].value).toBe "\uD835\uDF97" - expect(tokens[2].isAtomic).toBeTruthy() - expect(tokens[3].value).toBe "def" - expect(tokens[4].value).toBe "'" - - screenLine1 = tokenizedBuffer.tokenizedLineForRow(1) - expect(screenLine1.text).toBe "//\uD835\uDF97xyz" - {tokens} = screenLine1 - - expect(tokens.length).toBe 4 - expect(tokens[0].value).toBe '//' - expect(tokens[1].value).toBe '\uD835\uDF97' - expect(tokens[1].value).toBeTruthy() - expect(tokens[2].value).toBe 'xyz' - expect(tokens[3].value).toBe '' - describe "when the grammar is tokenized", -> it "emits the `tokenized` event", -> editor = null @@ -683,132 +527,7 @@ describe "TokenizedBuffer", -> it "returns the range covered by all contigous tokens (within a single line)", -> expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]] - describe "when the editor.tabLength config value changes", -> - it "updates the tab length of the tokenized lines", -> - buffer = atom.project.bufferForPathSync('sample.js') - buffer.setText('\ttest') - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' ' - atom.config.set('editor.tabLength', 6) - expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' ' - - it "does not allow the tab length to be less than 1", -> - buffer = atom.project.bufferForPathSync('sample.js') - buffer.setText('\ttest') - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' ' - atom.config.set('editor.tabLength', 1) - expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' ' - atom.config.set('editor.tabLength', 0) - expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' ' - - describe "when the invisibles value changes", -> - beforeEach -> - - it "updates the tokens with the appropriate invisible characters", -> - buffer = new TextBuffer(text: " \t a line with tabs\tand \tspaces \t ") - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - fullyTokenize(tokenizedBuffer) - - atom.config.set("editor.showInvisibles", true) - atom.config.set("editor.invisibles", space: 'S', tab: 'T') - fullyTokenize(tokenizedBuffer) - - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "SST Sa line with tabsTand T spacesSTS" - # Also needs to work for copies - expect(tokenizedBuffer.tokenizedLineForRow(0).copy().text).toBe "SST Sa line with tabsTand T spacesSTS" - - it "assigns endOfLineInvisibles to tokenized lines", -> - buffer = new TextBuffer(text: "a line that ends in a carriage-return-line-feed \r\na line that ends in just a line-feed\na line with no ending") - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - - atom.config.set('editor.showInvisibles', true) - atom.config.set("editor.invisibles", cr: 'R', eol: 'N') - fullyTokenize(tokenizedBuffer) - - expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R', 'N'] - expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual ['N'] - - # Lines ending in soft wraps get no invisibles - [left, right] = tokenizedBuffer.tokenizedLineForRow(0).softWrapAt(20) - expect(left.endOfLineInvisibles).toBe null - expect(right.endOfLineInvisibles).toEqual ['R', 'N'] - - atom.config.set("editor.invisibles", cr: 'R', eol: false) - expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R'] - expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual [] - - describe "leading and trailing whitespace", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - fullyTokenize(tokenizedBuffer) - - it "assigns ::firstNonWhitespaceIndex on tokens that have leading whitespace", -> - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0].firstNonWhitespaceIndex).toBe null - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0].firstNonWhitespaceIndex).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].firstNonWhitespaceIndex).toBe null - - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[0].firstNonWhitespaceIndex).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].firstNonWhitespaceIndex).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2].firstNonWhitespaceIndex).toBe null - - # The 4th token *has* leading whitespace, but isn't entirely whitespace - buffer.insert([5, 0], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3].firstNonWhitespaceIndex).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4].firstNonWhitespaceIndex).toBe null - - # Lines that are *only* whitespace are not considered to have leading whitespace - buffer.insert([10, 0], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstNonWhitespaceIndex).toBe null - - it "assigns ::firstTrailingWhitespaceIndex on tokens that have trailing whitespace", -> - buffer.insert([0, Infinity], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[11].firstTrailingWhitespaceIndex).toBe null - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[12].firstTrailingWhitespaceIndex).toBe 0 - - # The last token *has* trailing whitespace, but isn't entirely whitespace - buffer.setTextInRange([[2, 39], [2, 40]], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[14].firstTrailingWhitespaceIndex).toBe null - expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[15].firstTrailingWhitespaceIndex).toBe 6 - - # Lines that are *only* whitespace are considered to have trailing whitespace - buffer.insert([10, 0], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstTrailingWhitespaceIndex).toBe 0 - - it "only marks trailing whitespace on the last segment of a soft-wrapped line", -> - buffer.insert([0, Infinity], ' ') - tokenizedLine = tokenizedBuffer.tokenizedLineForRow(0) - [segment1, segment2] = tokenizedLine.softWrapAt(16) - expect(segment1.tokens[5].value).toBe ' ' - expect(segment1.tokens[5].firstTrailingWhitespaceIndex).toBe null - expect(segment2.tokens[6].value).toBe ' ' - expect(segment2.tokens[6].firstTrailingWhitespaceIndex).toBe 0 - - it "sets leading and trailing whitespace correctly on a line with invisible characters that is copied", -> - buffer.setText(" \t a line with tabs\tand \tspaces \t ") - - atom.config.set("editor.showInvisibles", true) - atom.config.set("editor.invisibles", space: 'S', tab: 'T') - fullyTokenize(tokenizedBuffer) - - line = tokenizedBuffer.tokenizedLineForRow(0).copy() - expect(line.tokens[0].firstNonWhitespaceIndex).toBe 2 - expect(line.tokens[line.tokens.length - 1].firstTrailingWhitespaceIndex).toBe 0 - - describe ".indentLevel on tokenized lines", -> + describe ".indentLevelForRow(row)", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') tokenizedBuffer = new TokenizedBuffer({ @@ -818,43 +537,43 @@ describe "TokenizedBuffer", -> describe "when the line is non-empty", -> it "has an indent level based on the leading whitespace on the line", -> - expect(tokenizedBuffer.tokenizedLineForRow(0).indentLevel).toBe 0 - expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 1 - expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0 + expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1 + expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2 buffer.insert([2, 0], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2.5 + expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5 describe "when the line is empty", -> it "assumes the indentation level of the first non-empty line below or above if one exists", -> buffer.insert([12, 0], ' ') buffer.insert([12, Infinity], '\n\n') - expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(14).indentLevel).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2 buffer.insert([1, Infinity], '\n\n') - expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(3).indentLevel).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2 buffer.setText('\n\n\n') - expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 0 + expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0 describe "when the changed lines are surrounded by whitespace-only lines", -> it "updates the indentLevel of empty lines that precede the change", -> - expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 0 + expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0 buffer.insert([12, 0], '\n') buffer.insert([13, 0], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 1 + expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1 it "updates empty line indent guides when the empty line is the last line", -> buffer.insert([12, 2], '\n') # The newline and the tab need to be in two different operations to surface the bug buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 1 + expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1 buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 expect(tokenizedBuffer.tokenizedLineForRow(14)).not.toBeDefined() it "updates the indentLevel of empty lines surrounding a change that inserts lines", -> @@ -862,24 +581,24 @@ describe "TokenizedBuffer", -> buffer.insert([7, 0], '\n\n') buffer.insert([5, 0], '\n\n') - expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 3 - expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 3 - expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 3 - expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 3 - expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3 + expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3 + expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3 + expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3 + expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2 tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler') buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') delete changeHandler.argsForCall[0][0].bufferChange - expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: 2) + expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: 2) - expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 4 - expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 4 - expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 4 - expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 4 - expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4 + expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4 + expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4 + expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4 + expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 it "updates the indentLevel of empty lines surrounding a change that removes lines", -> # create some new lines @@ -891,14 +610,14 @@ describe "TokenizedBuffer", -> buffer.setTextInRange([[7, 0], [8, 65]], ' ok') delete changeHandler.argsForCall[0][0].bufferChange - expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: -1) + expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: -1) - expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(7).indentLevel).toBe 2 # new text - expect(tokenizedBuffer.tokenizedLineForRow(8).indentLevel).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 2 - expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 2 # } + expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text + expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2 + expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # } describe "::isFoldableAtRow(row)", -> changes = null diff --git a/src/token.coffee b/src/token.coffee index 60e8194f8..5e1f1e811 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -7,22 +7,10 @@ WhitespaceRegex = /\S/ module.exports = class Token value: null - hasPairedCharacter: false scopes: null - isAtomic: null - isHardTab: null - firstNonWhitespaceIndex: null - firstTrailingWhitespaceIndex: null - hasInvisibleCharacters: false constructor: (properties) -> - {@value, @scopes, @isAtomic, @isHardTab, @bufferDelta} = properties - {@hasInvisibleCharacters, @hasPairedCharacter, @isSoftWrapIndentation} = properties - @firstNonWhitespaceIndex = properties.firstNonWhitespaceIndex ? null - @firstTrailingWhitespaceIndex = properties.firstTrailingWhitespaceIndex ? null - - @screenDelta = @value.length - @bufferDelta ?= @screenDelta + {@value, @scopes} = properties isEqual: (other) -> # TODO: scopes is deprecated. This is here for the sake of lang package tests @@ -31,17 +19,8 @@ class Token isBracket: -> /^meta\.brace\b/.test(_.last(@scopes)) - isOnlyWhitespace: -> - not WhitespaceRegex.test(@value) - matchesScopeSelector: (selector) -> targetClasses = selector.replace(StartDotRegex, '').split('.') _.any @scopes, (scope) -> scopeClasses = scope.split('.') _.isSubset(targetClasses, scopeClasses) - - hasLeadingWhitespace: -> - @firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0 - - hasTrailingWhitespace: -> - @firstTrailingWhitespaceIndex? and @firstTrailingWhitespaceIndex < @value.length diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 8574cb960..f8faad865 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -7,10 +7,6 @@ idCounter = 1 module.exports = class TokenizedLine - endOfLineInvisibles: null - lineIsWhitespaceOnly: false - firstNonWhitespaceIndex: 0 - constructor: (properties) -> @id = idCounter++ @@ -36,10 +32,10 @@ class TokenizedLine @tokens[@tokenIndexAtBufferColumn(bufferColumn)] tokenIndexAtBufferColumn: (bufferColumn) -> - delta = 0 + column = 0 for token, index in @tokens - delta += token.bufferDelta - return index if delta > bufferColumn + column += token.value.length + return index if column > bufferColumn index - 1 tokenStartColumnForBufferColumn: (bufferColumn) -> @@ -65,9 +61,6 @@ class TokenizedLine break @isCommentLine - isOnlyWhitespace: -> - @lineIsWhitespaceOnly - tokenAtIndex: (index) -> @tokens[index] From 712b1f1f88d758486f83c17134f1f3cd0000b722 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 14:19:45 +0200 Subject: [PATCH 080/262] Fix LanguageMode specs Also, avoid creating folds twice for the same position when calling `foldAll`. --- spec/language-mode-spec.coffee | 108 +++++++++++---------------------- src/language-mode.coffee | 12 +++- 2 files changed, 47 insertions(+), 73 deletions(-) diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index 26bb19b0e..d8f545abe 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -334,66 +334,56 @@ describe "LanguageMode", -> it "folds every foldable line", -> languageMode.foldAll() - fold1 = editor.tokenizedLineForScreenRow(0).fold - expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 12] - fold1.destroy() - - fold2 = editor.tokenizedLineForScreenRow(1).fold - expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 9] - fold2.destroy() - - fold3 = editor.tokenizedLineForScreenRow(4).fold - expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [4, 7] + [fold1, fold2, fold3] = languageMode.unfoldAll() + expect([fold1.start.row, fold1.end.row]).toEqual [0, 12] + expect([fold2.start.row, fold2.end.row]).toEqual [1, 9] + expect([fold3.start.row, fold3.end.row]).toEqual [4, 7] describe ".foldBufferRow(bufferRow)", -> describe "when bufferRow can be folded", -> it "creates a fold based on the syntactic region starting at the given row", -> languageMode.foldBufferRow(1) - fold = editor.tokenizedLineForScreenRow(1).fold - expect(fold.getStartRow()).toBe 1 - expect(fold.getEndRow()).toBe 9 + [fold] = languageMode.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual [1, 9] describe "when bufferRow can't be folded", -> it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", -> languageMode.foldBufferRow(8) - fold = editor.tokenizedLineForScreenRow(1).fold - expect(fold.getStartRow()).toBe 1 - expect(fold.getEndRow()).toBe 9 + [fold] = languageMode.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual [1, 9] describe "when the bufferRow is already folded", -> it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", -> languageMode.foldBufferRow(2) - expect(editor.tokenizedLineForScreenRow(1).fold).toBeDefined() - expect(editor.tokenizedLineForScreenRow(0).fold).not.toBeDefined() + expect(editor.isFoldedAtBufferRow(0)).toBe(false) + expect(editor.isFoldedAtBufferRow(1)).toBe(true) languageMode.foldBufferRow(1) - expect(editor.tokenizedLineForScreenRow(0).fold).toBeDefined() + expect(editor.isFoldedAtBufferRow(0)).toBe(true) describe "when the bufferRow is in a multi-line comment", -> it "searches upward and downward for surrounding comment lines and folds them as a single fold", -> buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment") languageMode.foldBufferRow(1) - fold = editor.tokenizedLineForScreenRow(1).fold - expect(fold.getStartRow()).toBe 1 - expect(fold.getEndRow()).toBe 3 + [fold] = languageMode.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual [1, 3] describe "when the bufferRow is a single-line comment", -> it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", -> buffer.insert([1, 0], " //this is a single line comment\n") languageMode.foldBufferRow(1) - fold = editor.tokenizedLineForScreenRow(0).fold - expect(fold.getStartRow()).toBe 0 - expect(fold.getEndRow()).toBe 13 + [fold] = languageMode.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual [0, 13] describe ".foldAllAtIndentLevel(indentLevel)", -> it "folds blocks of text at the given indentation level", -> languageMode.foldAllAtIndentLevel(0) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter expect(editor.getLastScreenRow()).toBe 0 languageMode.foldAllAtIndentLevel(1) expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" - expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter expect(editor.getLastScreenRow()).toBe 4 languageMode.foldAllAtIndentLevel(2) @@ -429,59 +419,35 @@ describe "LanguageMode", -> it "folds every foldable line", -> languageMode.foldAll() - fold1 = editor.tokenizedLineForScreenRow(0).fold - expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30] - fold1.destroy() - - fold2 = editor.tokenizedLineForScreenRow(1).fold - expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 4] - - fold3 = editor.tokenizedLineForScreenRow(2).fold.destroy() - - fold4 = editor.tokenizedLineForScreenRow(3).fold - expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8] - - fold5 = editor.tokenizedLineForScreenRow(6).fold - expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [11, 16] - fold5.destroy() - - fold6 = editor.tokenizedLineForScreenRow(13).fold - expect([fold6.getStartRow(), fold6.getEndRow()]).toEqual [21, 22] - fold6.destroy() + folds = languageMode.unfoldAll() + expect(folds.length).toBe 8 + expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] + expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4] + expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27] + expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8] + expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16] + expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20] + expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22] + expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25] describe ".foldAllAtIndentLevel()", -> it "folds every foldable range at a given indentLevel", -> languageMode.foldAllAtIndentLevel(2) - fold1 = editor.tokenizedLineForScreenRow(6).fold - expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [6, 8] - fold1.destroy() - - fold2 = editor.tokenizedLineForScreenRow(11).fold - expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 16] - fold2.destroy() - - fold3 = editor.tokenizedLineForScreenRow(17).fold - expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [17, 20] - fold3.destroy() - - fold4 = editor.tokenizedLineForScreenRow(21).fold - expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [21, 22] - fold4.destroy() - - fold5 = editor.tokenizedLineForScreenRow(24).fold - expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [24, 25] - fold5.destroy() + folds = languageMode.unfoldAll() + expect(folds.length).toBe 5 + expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8] + expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16] + expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20] + expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22] + expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25] it "does not fold anything but the indentLevel", -> languageMode.foldAllAtIndentLevel(0) - fold1 = editor.tokenizedLineForScreenRow(0).fold - expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30] - fold1.destroy() - - fold2 = editor.tokenizedLineForScreenRow(5).fold - expect(fold2).toBeFalsy() + folds = languageMode.unfoldAll() + expect(folds.length).toBe 1 + expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] describe ".isFoldableAtBufferRow(bufferRow)", -> it "returns true if the line starts a multi-line comment", -> diff --git a/src/language-mode.coffee b/src/language-mode.coffee index e60c661b2..605f8454b 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -90,10 +90,15 @@ class LanguageMode # Folds all the foldable lines in the buffer. foldAll: -> + @unfoldAll() + foldedRowRanges = {} for currentRow in [0..@buffer.getLastRow()] by 1 - [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] + rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? + continue if foldedRowRanges[rowRange] + @editor.foldBufferRowRange(startRow, endRow) + foldedRowRanges[rowRange] = true return # Unfolds all the foldable lines in the buffer. @@ -105,13 +110,16 @@ class LanguageMode # indentLevel - A {Number} indicating indentLevel; 0 based. foldAllAtIndentLevel: (indentLevel) -> @unfoldAll() + foldedRowRanges = {} for currentRow in [0..@buffer.getLastRow()] by 1 - [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] + rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? + continue if foldedRowRanges[rowRange] # assumption: startRow will always be the min indent level for the entire range if @editor.indentationForBufferRow(startRow) is indentLevel @editor.foldBufferRowRange(startRow, endRow) + foldedRowRanges[rowRange] = true return # Given a buffer row, creates a fold at it. From 0cf0d6f587b9febf377b2caf596b161aab833514 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 15:38:52 +0200 Subject: [PATCH 081/262] :fire: Remove unused code Now that `DisplayLayer` was fully implemented, we can start deleting those codepaths in `DisplayBuffer` that are now covered by this new abstraction. --- src/display-buffer.coffee | 340 +------------------------------------- 1 file changed, 2 insertions(+), 338 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index cfc352baf..63508e4cb 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -67,11 +67,8 @@ class DisplayBuffer extends Model @layerUpdateDisposablesByLayerId = {} @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings - @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange @disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker - @updateAllScreenLines() - subscribeToScopedConfigSettings: => @scopedConfigSubscriptions?.dispose() @scopedConfigSubscriptions = subscriptions = new CompositeDisposable @@ -130,13 +127,6 @@ class DisplayBuffer extends Model foldCharacter: ZERO_WIDTH_NBSP }) - updateAllScreenLines: -> - return # TODO: After DisplayLayer is finished, delete these code paths - @maxLineLength = 0 - @screenLines = [] - @rowMap = new RowMap - @updateScreenLines(0, @buffer.getLineCount(), null, suppressChangeEvent: true) - onDidChangeSoftWrapped: (callback) -> @emitter.on 'did-change-soft-wrapped', callback @@ -174,20 +164,6 @@ class DisplayBuffer extends Model onDidUpdateDecorations: (callback) -> @emitter.on 'did-update-decorations', callback - emitDidChange: (eventProperties, refreshMarkers=true) -> - @emitter.emit 'did-change', eventProperties - if refreshMarkers - @refreshMarkerScreenPositions() - @emitter.emit 'did-update-markers' - - updateWrappedScreenLines: -> - start = 0 - end = @getLastRow() - @updateAllScreenLines() - screenDelta = @getLastRow() - end - bufferDelta = 0 - @emitDidChange({start, end, screenDelta, bufferDelta}) - # Sets the visibility of the tokenized buffer. # # visible - A {Boolean} indicating of the tokenized buffer is shown @@ -324,82 +300,6 @@ class DisplayBuffer extends Model else @editorWidthInChars - getSoftWrapColumn: -> - if @configSettings.softWrapAtPreferredLineLength - Math.min(@getEditorWidthInChars(), @configSettings.preferredLineLength) - else - @getEditorWidthInChars() - - getSoftWrapColumnForTokenizedLine: (tokenizedLine) -> - lineMaxWidth = @getSoftWrapColumn() * @getDefaultCharWidth() - - return if Number.isNaN(lineMaxWidth) - return 0 if lineMaxWidth is 0 - - iterator = tokenizedLine.getTokenIterator(false) - column = 0 - currentWidth = 0 - while iterator.next() - textIndex = 0 - text = iterator.getText() - while textIndex < text.length - if iterator.isPairedCharacter() - charLength = 2 - else - charLength = 1 - - if iterator.hasDoubleWidthCharacterAt(textIndex) - charWidth = @getDoubleWidthCharWidth() - else if iterator.hasHalfWidthCharacterAt(textIndex) - charWidth = @getHalfWidthCharWidth() - else if iterator.hasKoreanCharacterAt(textIndex) - charWidth = @getKoreanCharWidth() - else - charWidth = @getDefaultCharWidth() - - return column if currentWidth + charWidth > lineMaxWidth - - currentWidth += charWidth - column += charLength - textIndex += charLength - column - - # Gets the screen line for the given screen row. - # - # * `screenRow` - A {Number} indicating the screen row. - # - # Returns {TokenizedLine} - tokenizedLineForScreenRow: (screenRow) -> - if @largeFileMode - if line = @tokenizedBuffer.tokenizedLineForRow(screenRow) - if line.text.length > @maxLineLength - @maxLineLength = line.text.length - @longestScreenRow = screenRow - line - else - @screenLines[screenRow] - - # Gets the screen lines for the given screen row range. - # - # startRow - A {Number} indicating the beginning screen row. - # endRow - A {Number} indicating the ending screen row. - # - # Returns an {Array} of {TokenizedLine}s. - tokenizedLinesForScreenRows: (startRow, endRow) -> - if @largeFileMode - @tokenizedBuffer.tokenizedLinesForRows(startRow, endRow) - else - @screenLines[startRow..endRow] - - # Gets all the screen lines. - # - # Returns an {Array} of {TokenizedLine}s. - getTokenizedLines: -> - if @largeFileMode - @tokenizedBuffer.tokenizedLinesForRows(0, @getLastRow()) - else - new Array(@screenLines...) - indentLevelForLine: (line) -> @tokenizedBuffer.indentLevelForLine(line) @@ -438,21 +338,6 @@ class DisplayBuffer extends Model unfoldBufferRow: (bufferRow) -> @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))) - # Returns the folds in the given row range (exclusive of end row) that are - # not contained by any other folds. - outermostFoldsInBufferRowRange: (startRow, endRow) -> - folds = [] - lastFoldEndRow = -1 - - for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow]) - range = marker.getRange() - if range.start.row > lastFoldEndRow - lastFoldEndRow = range.end.row - if startRow <= range.start.row <= range.end.row < endRow - folds.push(@foldForMarker(marker)) - - folds - # Given a buffer row, this converts it into a screen row. # # bufferRow - A {Number} representing a buffer row @@ -512,18 +397,6 @@ class DisplayBuffer extends Model getLastRow: -> @getLineCount() - 1 - # Gets the length of the longest screen line. - # - # Returns a {Number}. - getMaxLineLength: -> - @maxLineLength - - # Gets the row number of the longest screen line. - # - # Return a {} - getLongestScreenRow: -> - @longestScreenRow - # Given a buffer position, this converts it into a screen position. # # bufferPosition - An object that represents a buffer position. It can be either @@ -536,35 +409,7 @@ class DisplayBuffer extends Model screenPositionForBufferPosition: (bufferPosition, options) -> throw new Error("This TextEditor has been destroyed") if @isDestroyed() - return @displayLayer.translateBufferPosition(bufferPosition, options) - # TODO: should DisplayLayer deal with options.wrapBeyondNewlines / options.wrapAtSoftNewlines? - # {row, column} = @buffer.clipPosition(bufferPosition) - # [startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row) - # for screenRow in [startScreenRow...endScreenRow] - # screenLine = @tokenizedLineForScreenRow(screenRow) - # - # unless screenLine? - # throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row", - # softWrapEnabled: @isSoftWrapped() - # lastBufferRow: @buffer.getLastRow() - # lastScreenRow: @getLastRow() - # bufferRow: row - # screenRow: screenRow - # displayBufferChangeCount: @changeCount - # tokenizedBufferChangeCount: @tokenizedBuffer.changeCount - # bufferChangeCount: @buffer.changeCount - # - # maxBufferColumn = screenLine.getMaxBufferColumn() - # if screenLine.isSoftWrapped() and column > maxBufferColumn - # continue - # else - # if column <= maxBufferColumn - # screenColumn = screenLine.screenColumnForBufferColumn(column) - # else - # screenColumn = Infinity - # break - # - # @clipScreenPosition([screenRow, screenColumn], options) + @displayLayer.translateBufferPosition(bufferPosition, options) # Given a buffer position, this converts it into a screen position. # @@ -577,10 +422,6 @@ class DisplayBuffer extends Model # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> return @displayLayer.translateScreenPosition(screenPosition, options) - # TODO: should DisplayLayer deal with options.wrapBeyondNewlines / options.wrapAtSoftNewlines? - # {row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options) - # [bufferRow] = @rowMap.bufferRowRangeForScreenRow(row) - # new Point(bufferRow, @tokenizedLineForScreenRow(row).bufferColumnForScreenColumn(column)) # Retrieves the grammar's token scopeDescriptor for a buffer position. # @@ -632,55 +473,7 @@ class DisplayBuffer extends Model # # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options={}) -> - return @displayLayer.clipScreenPosition(screenPosition, options) - # TODO: should DisplayLayer deal with options.wrapBeyondNewlines / options.wrapAtSoftNewlines? - # {wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options - # {row, column} = Point.fromObject(screenPosition) - # - # if row < 0 - # row = 0 - # column = 0 - # else if row > @getLastRow() - # row = @getLastRow() - # column = Infinity - # else if column < 0 - # column = 0 - # - # screenLine = @tokenizedLineForScreenRow(row) - # unless screenLine? - # error = new Error("Undefined screen line when clipping screen position") - # Error.captureStackTrace(error) - # error.metadata = { - # screenRow: row - # screenColumn: column - # maxScreenRow: @getLastRow() - # screenLinesDefined: @screenLines.map (sl) -> sl? - # displayBufferChangeCount: @changeCount - # tokenizedBufferChangeCount: @tokenizedBuffer.changeCount - # bufferChangeCount: @buffer.changeCount - # } - # throw error - # - # maxScreenColumn = screenLine.getMaxScreenColumn() - # - # if screenLine.isSoftWrapped() and column >= maxScreenColumn - # if wrapAtSoftNewlines - # row++ - # column = @tokenizedLineForScreenRow(row).clipScreenColumn(0) - # else - # column = screenLine.clipScreenColumn(maxScreenColumn - 1) - # else if screenLine.isColumnInsideSoftWrapIndentation(column) - # if skipSoftWrapIndentation - # column = screenLine.clipScreenColumn(0) - # else - # row-- - # column = @tokenizedLineForScreenRow(row).getMaxScreenColumn() - 1 - # else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow() - # row++ - # column = 0 - # else - # column = screenLine.clipScreenColumn(column, options) - # new Point(row, column) + @displayLayer.clipScreenPosition(screenPosition, options) # Clip the start and end of the given range to valid positions on screen. # See {::clipScreenPosition} for more information. @@ -894,125 +687,9 @@ class DisplayBuffer extends Model @disposables.dispose() @tokenizedBuffer.destroy() - logLines: (start=0, end=@getLastRow()) -> - for row in [start..end] - line = @tokenizedLineForScreenRow(row).text - console.log row, @bufferRowForScreenRow(row), line, line.length - return - getRootScopeDescriptor: -> @tokenizedBuffer.rootScopeDescriptor - handleTokenizedBufferChange: (tokenizedBufferChange) => - @changeCount = @tokenizedBuffer.changeCount - {start, end, delta, bufferChange} = tokenizedBufferChange - @updateScreenLines(start, end + 1, delta, refreshMarkers: false) - - updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) -> - return # TODO: After DisplayLayer is finished, delete these code paths - - return if @largeFileMode - return if @isDestroyed() - - startBufferRow = @rowMap.bufferRowRangeForBufferRow(startBufferRow)[0] - endBufferRow = @rowMap.bufferRowRangeForBufferRow(endBufferRow - 1)[1] - startScreenRow = @rowMap.screenRowRangeForBufferRow(startBufferRow)[0] - endScreenRow = @rowMap.screenRowRangeForBufferRow(endBufferRow - 1)[1] - {screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta) - screenDelta = screenLines.length - (endScreenRow - startScreenRow) - - _.spliceWithArray(@screenLines, startScreenRow, endScreenRow - startScreenRow, screenLines, 10000) - - @checkScreenLinesInvariant() - - @rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions) - @findMaxLineLength(startScreenRow, endScreenRow, screenLines, screenDelta) - - return if options.suppressChangeEvent - - changeEvent = - start: startScreenRow - end: endScreenRow - 1 - screenDelta: screenDelta - bufferDelta: bufferDelta - - @emitDidChange(changeEvent, options.refreshMarkers) - - buildScreenLines: (startBufferRow, endBufferRow) -> - screenLines = [] - regions = [] - rectangularRegion = null - - foldsByStartRow = {} - # for fold in @outermostFoldsInBufferRowRange(startBufferRow, endBufferRow) - # foldsByStartRow[fold.getStartRow()] = fold - - bufferRow = startBufferRow - while bufferRow < endBufferRow - tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(bufferRow) - - # if fold = foldsByStartRow[bufferRow] - # foldLine = tokenizedLine.copy() - # foldLine.fold = fold - # screenLines.push(foldLine) - # - # if rectangularRegion? - # regions.push(rectangularRegion) - # rectangularRegion = null - # - # foldedRowCount = fold.getBufferRowCount() - # regions.push(bufferRows: foldedRowCount, screenRows: 1) - # bufferRow += foldedRowCount - # else - softWraps = 0 - if @isSoftWrapped() - while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumnForTokenizedLine(tokenizedLine)) - [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt( - wrapScreenColumn, - @configSettings.softWrapHangingIndent - ) - break if wrappedLine.hasOnlySoftWrapIndentation() - screenLines.push(wrappedLine) - softWraps++ - screenLines.push(tokenizedLine) - - if softWraps > 0 - if rectangularRegion? - regions.push(rectangularRegion) - rectangularRegion = null - regions.push(bufferRows: 1, screenRows: softWraps + 1) - else - rectangularRegion ?= {bufferRows: 0, screenRows: 0} - rectangularRegion.bufferRows++ - rectangularRegion.screenRows++ - - bufferRow++ - - if rectangularRegion? - regions.push(rectangularRegion) - - {screenLines, regions} - - findMaxLineLength: (startScreenRow, endScreenRow, newScreenLines, screenDelta) -> - oldMaxLineLength = @maxLineLength - - if startScreenRow <= @longestScreenRow < endScreenRow - @longestScreenRow = 0 - @maxLineLength = 0 - maxLengthCandidatesStartRow = 0 - maxLengthCandidates = @screenLines - else - @longestScreenRow += screenDelta if endScreenRow <= @longestScreenRow - maxLengthCandidatesStartRow = startScreenRow - maxLengthCandidates = newScreenLines - - for screenLine, i in maxLengthCandidates - screenRow = maxLengthCandidatesStartRow + i - length = screenLine.text.length - if length > @maxLineLength - @longestScreenRow = screenRow - @maxLineLength = length - didCreateDefaultLayerMarker: (textBufferMarker) => if marker = @getMarker(textBufferMarker.id) # The marker might have been removed in some other handler called before @@ -1071,16 +748,3 @@ class DisplayBuffer extends Model @layerUpdateDisposablesByLayerId[layer.id].dispose() delete @decorationCountsByLayerId[layer.id] delete @layerUpdateDisposablesByLayerId[layer.id] - - checkScreenLinesInvariant: -> - return if @isSoftWrapped() - - screenLinesCount = @screenLines.length - tokenizedLinesCount = @tokenizedBuffer.getLineCount() - bufferLinesCount = @buffer.getLineCount() - - @assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) -> - error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount} - - @assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) -> - error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount} From bef7539e3453c049c91c052a3539784eb6fa2694 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 17:40:24 +0200 Subject: [PATCH 082/262] Refactor DisplayBuffer to DecorationManager This commit moves all the remaining concerns not related to decorations out of `DisplayBuffer` and into `TextEditor`. This means the `DisplayBuffer` is now free to be renamed to `DecorationManager`. --- spec/decoration-manager-spec.coffee | 85 ++ spec/display-buffer-spec.coffee | 1312 -------------------------- spec/random-editor-spec.coffee | 2 +- spec/text-editor-component-spec.js | 14 +- spec/text-editor-spec.coffee | 13 + spec/tokenized-buffer-spec.coffee | 6 +- spec/workspace-spec.coffee | 4 +- src/decoration-manager.coffee | 180 ++++ src/decoration.coffee | 10 +- src/display-buffer.coffee | 750 --------------- src/language-mode.coffee | 16 +- src/layer-decoration.coffee | 8 +- src/marker-observation-window.coffee | 4 +- src/text-editor.coffee | 401 +++++--- 14 files changed, 583 insertions(+), 2222 deletions(-) create mode 100644 spec/decoration-manager-spec.coffee delete mode 100644 spec/display-buffer-spec.coffee create mode 100644 src/decoration-manager.coffee delete mode 100644 src/display-buffer.coffee diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee new file mode 100644 index 000000000..c428df8cf --- /dev/null +++ b/spec/decoration-manager-spec.coffee @@ -0,0 +1,85 @@ +DecorationManager = require '../src/decoration-manager' +_ = require 'underscore-plus' + +describe "DecorationManager", -> + [decorationManager, buffer, defaultMarkerLayer] = [] + + beforeEach -> + buffer = atom.project.bufferForPathSync('sample.js') + displayLayer = buffer.addDisplayLayer() + defaultMarkerLayer = displayLayer.addMarkerLayer() + decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer) + + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + afterEach -> + decorationManager.destroy() + buffer.release() + + describe "decorations", -> + [marker, decoration, decorationProperties] = [] + beforeEach -> + marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]]) + decorationProperties = {type: 'line-number', class: 'one'} + decoration = decorationManager.decorateMarker(marker, decorationProperties) + + it "can add decorations associated with markers and remove them", -> + expect(decoration).toBeDefined() + expect(decoration.getProperties()).toBe decorationProperties + expect(decorationManager.decorationForId(decoration.id)).toBe decoration + expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration + + decoration.destroy() + expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined() + expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined() + + it "will not fail if the decoration is removed twice", -> + decoration.destroy() + decoration.destroy() + expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined() + + it "does not allow destroyed markers to be decorated", -> + marker.destroy() + expect(-> + decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')}) + ).toThrow("Cannot decorate a destroyed marker") + expect(decorationManager.getOverlayDecorations()).toEqual [] + + 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() + decoration.setProperties type: 'line-number', class: 'two' + + {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0] + expect(oldProperties).toEqual decorationProperties + expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'} + + describe "::getDecorations(properties)", -> + it "returns decorations matching the given optional properties", -> + expect(decorationManager.getDecorations()).toEqual [decoration] + expect(decorationManager.getDecorations(class: 'two').length).toEqual 0 + expect(decorationManager.getDecorations(class: 'one').length).toEqual 1 + + describe "::decorateMarker", -> + describe "when decorating gutters", -> + [marker] = [] + + beforeEach -> + marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]]) + + it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", -> + decorationProperties = {type: 'line-number', class: 'one'} + decoration = decorationManager.decorateMarker(marker, decorationProperties) + expect(decoration.isType('line-number')).toBe true + expect(decoration.isType('gutter')).toBe true + expect(decoration.getProperties().gutterName).toBe 'line-number' + expect(decoration.getProperties().class).toBe 'one' + + it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", -> + decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'} + decoration = decorationManager.decorateMarker(marker, decorationProperties) + expect(decoration.isType('gutter')).toBe true + expect(decoration.isType('line-number')).toBe false + expect(decoration.getProperties().gutterName).toBe 'test-gutter' + expect(decoration.getProperties().class).toBe 'one' diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee deleted file mode 100644 index 48103f3a7..000000000 --- a/spec/display-buffer-spec.coffee +++ /dev/null @@ -1,1312 +0,0 @@ -DisplayBuffer = require '../src/display-buffer' -_ = require 'underscore-plus' - -describe "DisplayBuffer", -> - [displayBuffer, buffer, changeHandler, tabLength] = [] - beforeEach -> - tabLength = 2 - - buffer = atom.project.bufferForPathSync('sample.js') - displayBuffer = new DisplayBuffer({ - buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, - packageManager: atom.packages, assert: -> - }) - changeHandler = jasmine.createSpy 'changeHandler' - displayBuffer.onDidChange changeHandler - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - displayBuffer.destroy() - buffer.release() - - describe "::copy()", -> - it "creates a new DisplayBuffer with the same initial state", -> - marker1 = displayBuffer.markBufferRange([[1, 2], [3, 4]], id: 1) - marker2 = displayBuffer.markBufferRange([[2, 3], [4, 5]], reversed: true, id: 2) - marker3 = displayBuffer.markBufferPosition([5, 6], id: 3) - displayBuffer.foldBufferRowRange(3, 5) - - displayBuffer2 = displayBuffer.copy() - expect(displayBuffer2.id).not.toBe displayBuffer.id - expect(displayBuffer2.buffer).toBe displayBuffer.buffer - expect(displayBuffer2.getTabLength()).toBe displayBuffer.getTabLength() - - expect(displayBuffer2.getMarkerCount()).toEqual displayBuffer.getMarkerCount() - expect(displayBuffer2.findMarker(id: 1)).toEqual marker1 - expect(displayBuffer2.findMarker(id: 2)).toEqual marker2 - expect(displayBuffer2.findMarker(id: 3)).toEqual marker3 - expect(displayBuffer2.isFoldedAtBufferRow(3)).toBeTruthy() - - # can diverge from origin - displayBuffer2.unfoldBufferRow(3) - expect(displayBuffer2.isFoldedAtBufferRow(3)).not.toBe displayBuffer.isFoldedAtBufferRow(3) - - describe "when the buffer changes", -> - it "renders line numbers correctly", -> - originalLineCount = displayBuffer.getLineCount() - oneHundredLines = [0..100].join("\n") - buffer.insert([0, 0], oneHundredLines) - expect(displayBuffer.getLineCount()).toBe 100 + originalLineCount - - it "updates the display buffer prior to invoking change handlers registered on the buffer", -> - buffer.onDidChange -> expect(displayBuffer2.tokenizedLineForScreenRow(0).text).toBe "testing" - displayBuffer2 = new DisplayBuffer({ - buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, - packageManager: atom.packages, assert: -> - }) - buffer.setText("testing") - - describe "soft wrapping", -> - beforeEach -> - displayBuffer.setEditorWidthInChars(50) - displayBuffer.setSoftWrapped(true) - displayBuffer.setDefaultCharWidth(1) - changeHandler.reset() - - describe "rendering of soft-wrapped lines", -> - describe "when there are double width characters", -> - it "takes them into account when finding the soft wrap column", -> - buffer.setText("私たちのフ是一个地方,数千名学生12345业余爱们的板作为hello world this is a pretty long latin line") - displayBuffer.setDefaultCharWidth(1, 5, 0, 0) - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("私たちのフ是一个地方") - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe(",数千名学生12345业余爱") - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("们的板作为hello world this is a ") - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe("pretty long latin line") - - describe "when there are half width characters", -> - it "takes them into account when finding the soft wrap column", -> - displayBuffer.setDefaultCharWidth(1, 0, 5, 0) - buffer.setText("abcᆰᆱᆲネヌネノハヒフヒフヌᄡ○○○hello world this is a pretty long line") - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("abcᆰᆱᆲネヌネノハヒ") - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe("フヒフヌᄡ○○○hello ") - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("world this is a pretty long line") - - describe "when there are korean characters", -> - it "takes them into account when finding the soft wrap column", -> - displayBuffer.setDefaultCharWidth(1, 0, 0, 10) - buffer.setText("1234세계를향한대화,유니코제10회유니코드국제") - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("1234세계를향") - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe("한대화,유") - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("니코제10회") - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe("유니코드국") - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe("제") - - describe "when editor.softWrapAtPreferredLineLength is set", -> - it "uses the preferred line length as the soft wrap column when it is less than the configured soft wrap column", -> - atom.config.set('editor.preferredLineLength', 100) - atom.config.set('editor.softWrapAtPreferredLineLength', true) - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return ' - - atom.config.set('editor.preferredLineLength', 5) - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' fun' - - atom.config.set('editor.softWrapAtPreferredLineLength', false) - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return ' - - describe "when editor width is negative", -> - it "does not hang while wrapping", -> - displayBuffer.setDefaultCharWidth(1) - displayBuffer.setWidth(-1) - - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe " " - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe " var sort = function(items) {" - - describe "when the line is shorter than the max line length", -> - it "renders the line unchanged", -> - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0) - - describe "when the line is empty", -> - it "renders the empty line", -> - expect(displayBuffer.tokenizedLineForScreenRow(13).text).toBe '' - - describe "when there is a non-whitespace character at the max length boundary", -> - describe "when there is whitespace before the boundary", -> - it "wraps the line at the end of the first whitespace preceding the boundary", -> - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return ' - expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe ' sort(left).concat(pivot).concat(sort(right));' - - it "wraps the line at the first CJK character before the boundary", -> - displayBuffer.setEditorWidthInChars(10) - - buffer.setTextInRange([[0, 0], [1, 0]], 'abcd efg유私フ业余爱\n') - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcd efg유私' - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'フ业余爱' - - buffer.setTextInRange([[0, 0], [1, 0]], 'abcd ef유gef业余爱\n') - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcd ef유' - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'gef业余爱' - - describe "when there is no whitespace before the boundary", -> - it "wraps the line at the first CJK character before the boundary", -> - buffer.setTextInRange([[0, 0], [1, 0]], '私たちのabcdefghij\n') - displayBuffer.setEditorWidthInChars(10) - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe '私たちの' - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'abcdefghij' - - it "wraps the line exactly at the boundary when no CJK character is found, since there's no more graceful place to wrap it", -> - buffer.setTextInRange([[0, 0], [1, 0]], 'abcdefghijklmnopqrstuvwxyz\n') - displayBuffer.setEditorWidthInChars(10) - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcdefghij' - expect(displayBuffer.tokenizedLineForScreenRow(0).bufferDelta).toBe 'abcdefghij'.length - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'klmnopqrst' - expect(displayBuffer.tokenizedLineForScreenRow(1).bufferDelta).toBe 'klmnopqrst'.length - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe 'uvwxyz' - expect(displayBuffer.tokenizedLineForScreenRow(2).bufferDelta).toBe 'uvwxyz'.length - - it "closes all scopes at the wrap boundary", -> - displayBuffer.setEditorWidthInChars(10) - buffer.setText("`aaa${1+2}aaa`") - iterator = displayBuffer.tokenizedLineForScreenRow(1).getTokenIterator() - scopes = iterator.getScopes() - expect(scopes[scopes.length - 1]).not.toBe 'punctuation.section.embedded.js' - - describe "when there is a whitespace character at the max length boundary", -> - it "wraps the line at the first non-whitespace character following the boundary", -> - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items.shift(), current, left = [], ' - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' right = [];' - - describe "when the only whitespace characters are at the beginning of the line", -> - beforeEach -> - displayBuffer.setEditorWidthInChars(10) - - it "wraps the line at the max length when indented with tabs", -> - buffer.setTextInRange([[0, 0], [1, 0]], '\t\tabcdefghijklmnopqrstuvwxyz') - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef' - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl' - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr' - - it "wraps the line at the max length when indented with spaces", -> - buffer.setTextInRange([[0, 0], [1, 0]], ' abcdefghijklmnopqrstuvwxyz') - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef' - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl' - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr' - - describe "when there are hard tabs", -> - beforeEach -> - buffer.setText(buffer.getText().replace(new RegExp(' ', 'g'), '\t')) - - it "correctly tokenizes the hard tabs", -> - expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[0].isHardTab).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy() - - describe "when a line is wrapped", -> - it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", -> - tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) - - expect(tokenizedLine.tokens[0].value).toBe(" ") - expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy() - - expect(tokenizedLine.tokens[1].value).toBe(" ") - expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy() - - expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeFalsy() - - describe "when editor.softWrapHangingIndent is set", -> - beforeEach -> - atom.config.set('editor.softWrapHangingIndent', 3) - - it "further indents wrapped lines", -> - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe " return " - expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe " sort(left).concat(pivot).concat(sort(right)" - expect(displayBuffer.tokenizedLineForScreenRow(12).text).toBe " );" - - it "includes hanging indent when breaking soft-wrap indentation into tokens", -> - tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) - - expect(tokenizedLine.tokens[0].value).toBe(" ") - expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy() - - expect(tokenizedLine.tokens[1].value).toBe(" ") - expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy() - - expect(tokenizedLine.tokens[2].value).toBe(" ") # hanging indent - expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeTruthy() - - expect(tokenizedLine.tokens[3].value).toBe(" ") # odd space - expect(tokenizedLine.tokens[3].isSoftWrapIndentation).toBeTruthy() - - expect(tokenizedLine.tokens[4].isSoftWrapIndentation).toBeFalsy() - - describe "when the buffer changes", -> - describe "when buffer lines are updated", -> - describe "when whitespace is added after the max line length", -> - it "adds whitespace to the end of the current line and wraps an empty line", -> - fiftyCharacters = _.multiplyString("x", 50) - buffer.setText(fiftyCharacters) - buffer.insert([0, 51], " ") - - describe "when the update makes a soft-wrapped line shorter than the max line length", -> - it "rewraps the line and emits a change event", -> - buffer.delete([[6, 24], [6, 42]]) - expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? : right.push(current);' - expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' }' - - expect(changeHandler).toHaveBeenCalled() - [[event]]= changeHandler.argsForCall - - expect(event).toEqual(start: 7, end: 8, screenDelta: -1, bufferDelta: 0) - - describe "when the update causes a line to soft wrap an additional time", -> - it "rewraps the line and emits a change event", -> - buffer.insert([6, 28], '1234567890') - expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? ' - expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' left1234567890.push(current) : ' - expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe ' right.push(current);' - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' }' - - expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, screenDelta: 1, bufferDelta: 0) - - describe "when buffer lines are inserted", -> - it "inserts / updates wrapped lines and emits a change event", -> - buffer.insert([6, 21], '1234567890 abcdefghij 1234567890\nabcdefghij') - expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot1234567890 abcdefghij ' - expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' 1234567890' - expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe 'abcdefghij ? left.push(current) : ' - expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe 'right.push(current);' - - expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, screenDelta: 2, bufferDelta: 1) - - describe "when buffer lines are removed", -> - it "removes lines and emits a change event", -> - buffer.setTextInRange([[3, 21], [7, 5]], ';') - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items;' - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' return ' - expect(displayBuffer.tokenizedLineForScreenRow(5).text).toBe ' sort(left).concat(pivot).concat(sort(right));' - expect(displayBuffer.tokenizedLineForScreenRow(6).text).toBe ' };' - - expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 9, screenDelta: -6, bufferDelta: -4) - - describe "when a newline is inserted, deleted, and re-inserted at the end of a wrapped line (regression)", -> - it "correctly renders the original wrapped line", -> - buffer = atom.project.buildBufferSync(null, '') - displayBuffer = new DisplayBuffer({ - buffer, tabLength, editorWidthInChars: 30, config: atom.config, - grammarRegistry: atom.grammars, packageManager: atom.packages, assert: -> - }) - displayBuffer.setDefaultCharWidth(1) - displayBuffer.setSoftWrapped(true) - - buffer.insert([0, 0], "the quick brown fox jumps over the lazy dog.") - buffer.insert([0, Infinity], '\n') - buffer.delete([[0, Infinity], [1, 0]]) - buffer.insert([0, Infinity], '\n') - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "the quick brown fox jumps over " - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "the lazy dog." - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "" - - describe "position translation", -> - it "translates positions accounting for wrapped lines", -> - # before any wrapped lines - expect(displayBuffer.screenPositionForBufferPosition([0, 5])).toEqual([0, 5]) - expect(displayBuffer.bufferPositionForScreenPosition([0, 5])).toEqual([0, 5]) - expect(displayBuffer.screenPositionForBufferPosition([0, 29])).toEqual([0, 29]) - expect(displayBuffer.bufferPositionForScreenPosition([0, 29])).toEqual([0, 29]) - - # on a wrapped line - expect(displayBuffer.screenPositionForBufferPosition([3, 5])).toEqual([3, 5]) - expect(displayBuffer.bufferPositionForScreenPosition([3, 5])).toEqual([3, 5]) - expect(displayBuffer.screenPositionForBufferPosition([3, 50])).toEqual([3, 50]) - expect(displayBuffer.screenPositionForBufferPosition([3, 51])).toEqual([3, 50]) - expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual([3, 50]) - expect(displayBuffer.bufferPositionForScreenPosition([3, 50])).toEqual([3, 50]) - expect(displayBuffer.screenPositionForBufferPosition([3, 62])).toEqual([4, 15]) - expect(displayBuffer.bufferPositionForScreenPosition([4, 11])).toEqual([3, 58]) - - # following a wrapped line - expect(displayBuffer.screenPositionForBufferPosition([4, 5])).toEqual([5, 5]) - expect(displayBuffer.bufferPositionForScreenPosition([5, 5])).toEqual([4, 5]) - - # clip screen position inputs before translating - expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0]) - expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([12, 2]) - expect(displayBuffer.bufferPositionForScreenPosition([3, -5])).toEqual([3, 0]) - expect(displayBuffer.bufferPositionForScreenPosition([3, Infinity])).toEqual([3, 50]) - - describe ".setEditorWidthInChars(length)", -> - it "changes the length at which lines are wrapped and emits a change event for all screen lines", -> - tokensText = (tokens) -> - _.pluck(tokens, 'value').join('') - - displayBuffer.setEditorWidthInChars(40) - expect(tokensText displayBuffer.tokenizedLineForScreenRow(4).tokens).toBe ' left = [], right = [];' - expect(tokensText displayBuffer.tokenizedLineForScreenRow(5).tokens).toBe ' while(items.length > 0) {' - expect(tokensText displayBuffer.tokenizedLineForScreenRow(12).tokens).toBe ' sort(left).concat(pivot).concat(sort' - expect(changeHandler).toHaveBeenCalledWith(start: 0, end: 15, screenDelta: 3, bufferDelta: 0) - - it "only allows positive widths to be assigned", -> - displayBuffer.setEditorWidthInChars(0) - expect(displayBuffer.editorWidthInChars).not.toBe 0 - displayBuffer.setEditorWidthInChars(-1) - expect(displayBuffer.editorWidthInChars).not.toBe -1 - - describe "primitive folding", -> - beforeEach -> - displayBuffer.destroy() - buffer.release() - buffer = atom.project.bufferForPathSync('two-hundred.txt') - displayBuffer = new DisplayBuffer({ - buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, - packageManager: atom.packages, assert: -> - }) - displayBuffer.onDidChange changeHandler - - describe "when folds are created and destroyed", -> - describe "when a fold spans multiple lines", -> - it "replaces the lines spanned by the fold with a placeholder that references the fold object", -> - fold = displayBuffer.foldBufferRowRange(4, 7) - expect(fold).toBeDefined() - - [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) - expect(line4.fold).toBe fold - expect(line4.text).toMatch /^4-+/ - expect(line5.text).toBe '8' - - expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 7, screenDelta: -3, bufferDelta: 0) - changeHandler.reset() - - fold.destroy() - [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) - expect(line4.fold).toBeUndefined() - expect(line4.text).toMatch /^4-+/ - expect(line5.text).toBe '5' - - expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 3, bufferDelta: 0) - - describe "when a fold spans a single line", -> - it "renders a fold placeholder for the folded line but does not skip any lines", -> - fold = displayBuffer.foldBufferRowRange(4, 4) - - [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) - expect(line4.fold).toBe fold - expect(line4.text).toMatch /^4-+/ - expect(line5.text).toBe '5' - - expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 0, bufferDelta: 0) - - # Line numbers don't actually change, but it's not worth the complexity to have this - # be false for single line folds since they are so rare - changeHandler.reset() - - fold.destroy() - - [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) - expect(line4.fold).toBeUndefined() - expect(line4.text).toMatch /^4-+/ - expect(line5.text).toBe '5' - - expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 0, bufferDelta: 0) - - describe "when a fold is nested within another fold", -> - it "does not render the placeholder for the inner fold until the outer fold is destroyed", -> - innerFold = displayBuffer.foldBufferRowRange(6, 7) - outerFold = displayBuffer.foldBufferRowRange(4, 8) - - [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) - expect(line4.fold).toBe outerFold - expect(line4.text).toMatch /4-+/ - expect(line5.text).toMatch /9-+/ - - outerFold.destroy() - [line4, line5, line6, line7] = displayBuffer.tokenizedLinesForScreenRows(4, 7) - expect(line4.fold).toBeUndefined() - expect(line4.text).toMatch /^4-+/ - expect(line5.text).toBe '5' - expect(line6.fold).toBe innerFold - expect(line6.text).toBe '6' - expect(line7.text).toBe '8' - - it "allows the outer fold to start at the same location as the inner fold", -> - innerFold = displayBuffer.foldBufferRowRange(4, 6) - outerFold = displayBuffer.foldBufferRowRange(4, 8) - - [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5) - expect(line4.fold).toBe outerFold - expect(line4.text).toMatch /4-+/ - expect(line5.text).toMatch /9-+/ - - describe "when creating a fold where one already exists", -> - it "returns existing fold and does't create new fold", -> - fold = displayBuffer.foldBufferRowRange(0, 10) - expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1 - - newFold = displayBuffer.foldBufferRowRange(0, 10) - expect(newFold).toBe fold - 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", -> - outerFold = displayBuffer.foldBufferRowRange(0, 10) - changeHandler.reset() - - innerFold = displayBuffer.foldBufferRowRange(2, 5) - expect(changeHandler).not.toHaveBeenCalled() - [line0, line1] = displayBuffer.tokenizedLinesForScreenRows(0, 1) - expect(line0.fold).toBe outerFold - expect(line1.fold).toBeUndefined() - - changeHandler.reset() - innerFold.destroy() - expect(changeHandler).not.toHaveBeenCalled() - [line0, line1] = displayBuffer.tokenizedLinesForScreenRows(0, 1) - expect(line0.fold).toBe outerFold - expect(line1.fold).toBeUndefined() - - describe "when a fold ends where another fold begins", -> - it "continues to hide the lines inside the second fold", -> - fold2 = displayBuffer.foldBufferRowRange(4, 9) - fold1 = displayBuffer.foldBufferRowRange(0, 4) - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toMatch /^0/ - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toMatch /^10/ - - describe "when there is another display buffer pointing to the same buffer", -> - it "does not consider folds to be nested inside of folds from the other display buffer", -> - otherDisplayBuffer = new DisplayBuffer({ - buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, - packageManager: atom.packages, assert: -> - }) - otherDisplayBuffer.foldBufferRowRange(1, 5) - - displayBuffer.foldBufferRowRange(2, 4) - expect(otherDisplayBuffer.foldsStartingAtBufferRow(2).length).toBe 0 - - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2' - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe '5' - - describe "when the buffer changes", -> - [fold1, fold2] = [] - beforeEach -> - fold1 = displayBuffer.foldBufferRowRange(2, 4) - fold2 = displayBuffer.foldBufferRowRange(6, 8) - changeHandler.reset() - - describe "when the old range surrounds a fold", -> - beforeEach -> - buffer.setTextInRange([[1, 0], [5, 1]], 'party!') - - it "removes the fold and replaces the selection with the new text", -> - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "0" - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "party!" - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold2 - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch /^9-+/ - - expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 3, screenDelta: -2, bufferDelta: -4) - - describe "when the changes is subsequently undone", -> - xit "restores destroyed folds", -> - buffer.undo() - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2' - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1 - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe '5' - - describe "when the old range surrounds two nested folds", -> - it "removes both folds and replaces the selection with the new text", -> - displayBuffer.foldBufferRowRange(2, 9) - changeHandler.reset() - - buffer.setTextInRange([[1, 0], [10, 0]], 'goodbye') - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "0" - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "goodbye10" - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "11" - - expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 3, screenDelta: -2, bufferDelta: -9) - - describe "when multiple changes happen above the fold", -> - it "repositions folds correctly", -> - buffer.delete([[1, 1], [2, 0]]) - buffer.insert([0, 1], "\nnew") - - expect(fold1.getStartRow()).toBe 2 - expect(fold1.getEndRow()).toBe 4 - - describe "when the old range precedes lines with a fold", -> - describe "when the new range precedes lines with a fold", -> - it "updates the buffer and re-positions subsequent folds", -> - buffer.setTextInRange([[0, 0], [1, 1]], 'abc') - - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "abc" - expect(displayBuffer.tokenizedLineForScreenRow(1).fold).toBe fold1 - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "5" - expect(displayBuffer.tokenizedLineForScreenRow(3).fold).toBe fold2 - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toMatch /^9-+/ - - expect(changeHandler).toHaveBeenCalledWith(start: 0, end: 1, screenDelta: -1, bufferDelta: -1) - changeHandler.reset() - - fold1.destroy() - expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "abc" - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "2" - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch /^4-+/ - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe "5" - expect(displayBuffer.tokenizedLineForScreenRow(5).fold).toBe fold2 - expect(displayBuffer.tokenizedLineForScreenRow(6).text).toMatch /^9-+/ - - expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 1, screenDelta: 2, bufferDelta: 0) - - describe "when the old range straddles the beginning of a fold", -> - it "destroys the fold", -> - buffer.setTextInRange([[1, 1], [3, 0]], "a\nb\nc\nd\n") - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1a' - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe 'b' - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBeUndefined() - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe 'c' - - describe "when the old range follows a fold", -> - it "re-positions the screen ranges for the change event based on the preceding fold", -> - buffer.setTextInRange([[10, 0], [11, 0]], 'abc') - - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1" - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1 - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe "5" - expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2 - expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/ - - expect(changeHandler).toHaveBeenCalledWith(start: 6, end: 7, screenDelta: -1, bufferDelta: -1) - - describe "when the old range is inside a fold", -> - describe "when the end of the new range precedes the end of the fold", -> - it "updates the fold and ensures the change is present when the fold is destroyed", -> - buffer.insert([3, 0], '\n') - expect(fold1.getStartRow()).toBe 2 - expect(fold1.getEndRow()).toBe 5 - - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1" - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "2" - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1 - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "5" - expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2 - expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/ - - expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 2, screenDelta: 0, bufferDelta: 1) - - describe "when the end of the new range exceeds the end of the fold", -> - it "expands the fold to contain all the inserted lines", -> - buffer.setTextInRange([[3, 0], [4, 0]], 'a\nb\nc\nd\n') - expect(fold1.getStartRow()).toBe 2 - expect(fold1.getEndRow()).toBe 7 - - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1" - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "2" - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1 - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "5" - expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2 - expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/ - - expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 2, screenDelta: 0, bufferDelta: 3) - - describe "when the old range straddles the end of the fold", -> - describe "when the end of the new range precedes the end of the fold", -> - it "destroys the fold", -> - fold2.destroy() - buffer.setTextInRange([[3, 0], [6, 0]], 'a\n') - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2' - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBeUndefined() - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe 'a' - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe '6' - - describe "when the old range is contained to a single line in-between two folds", -> - it "re-renders the line with the placeholder and re-positions the second fold", -> - buffer.insert([5, 0], 'abc\n') - - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1" - expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1 - expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "abc" - expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe "5" - expect(displayBuffer.tokenizedLineForScreenRow(5).fold).toBe fold2 - expect(displayBuffer.tokenizedLineForScreenRow(6).text).toMatch /^9-+/ - - expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 3, screenDelta: 1, bufferDelta: 1) - - describe "when the change starts at the beginning of a fold but does not extend to the end (regression)", -> - it "preserves a proper mapping between buffer and screen coordinates", -> - expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [4, 0] - buffer.setTextInRange([[2, 0], [3, 0]], "\n") - expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [4, 0] - - describe "position translation", -> - it "translates positions to account for folded lines and characters and the placeholder", -> - fold = displayBuffer.foldBufferRowRange(4, 7) - - # preceding fold: identity - expect(displayBuffer.screenPositionForBufferPosition([3, 0])).toEqual [3, 0] - expect(displayBuffer.screenPositionForBufferPosition([4, 0])).toEqual [4, 0] - - expect(displayBuffer.bufferPositionForScreenPosition([3, 0])).toEqual [3, 0] - expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual [4, 0] - - # inside of fold: translate to the start of the fold - expect(displayBuffer.screenPositionForBufferPosition([4, 35])).toEqual [4, 0] - expect(displayBuffer.screenPositionForBufferPosition([5, 5])).toEqual [4, 0] - - # following fold - expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [5, 0] - expect(displayBuffer.screenPositionForBufferPosition([11, 2])).toEqual [8, 2] - - expect(displayBuffer.bufferPositionForScreenPosition([5, 0])).toEqual [8, 0] - expect(displayBuffer.bufferPositionForScreenPosition([9, 2])).toEqual [12, 2] - - # clip screen positions before translating - expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0]) - expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([200, 0]) - - # after fold is destroyed - fold.destroy() - - expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [8, 0] - expect(displayBuffer.screenPositionForBufferPosition([11, 2])).toEqual [11, 2] - - expect(displayBuffer.bufferPositionForScreenPosition([5, 0])).toEqual [5, 0] - expect(displayBuffer.bufferPositionForScreenPosition([9, 2])).toEqual [9, 2] - - describe ".unfoldBufferRow(row)", -> - it "destroys all folds containing the given row", -> - displayBuffer.foldBufferRowRange(2, 4) - displayBuffer.foldBufferRowRange(2, 6) - displayBuffer.foldBufferRowRange(7, 8) - displayBuffer.foldBufferRowRange(1, 9) - displayBuffer.foldBufferRowRange(11, 12) - - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1' - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '10' - - displayBuffer.unfoldBufferRow(2) - expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1' - expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2' - expect(displayBuffer.tokenizedLineForScreenRow(7).fold).toBeDefined() - expect(displayBuffer.tokenizedLineForScreenRow(8).text).toMatch /^9-+/ - expect(displayBuffer.tokenizedLineForScreenRow(10).fold).toBeDefined() - - describe ".outermostFoldsInBufferRowRange(startRow, endRow)", -> - it "returns the outermost folds entirely contained in the given row range, exclusive of end row", -> - fold1 = displayBuffer.foldBufferRowRange(4, 7) - fold2 = displayBuffer.foldBufferRowRange(5, 6) - fold3 = displayBuffer.foldBufferRowRange(11, 15) - fold4 = displayBuffer.foldBufferRowRange(12, 13) - fold5 = displayBuffer.foldBufferRowRange(16, 17) - - expect(displayBuffer.outermostFoldsInBufferRowRange(3, 18)).toEqual [fold1, fold3, fold5] - expect(displayBuffer.outermostFoldsInBufferRowRange(5, 16)).toEqual [fold3] - - describe "::clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, clip: 'closest')", -> - beforeEach -> - tabLength = 4 - - displayBuffer.setDefaultCharWidth(1) - displayBuffer.setTabLength(tabLength) - displayBuffer.setSoftWrapped(true) - displayBuffer.setEditorWidthInChars(50) - - it "allows valid positions", -> - expect(displayBuffer.clipScreenPosition([4, 5])).toEqual [4, 5] - expect(displayBuffer.clipScreenPosition([4, 11])).toEqual [4, 11] - - it "disallows negative positions", -> - expect(displayBuffer.clipScreenPosition([-1, -1])).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([-1, 10])).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, -1])).toEqual [0, 0] - - it "disallows positions beyond the last row", -> - expect(displayBuffer.clipScreenPosition([1000, 0])).toEqual [15, 2] - expect(displayBuffer.clipScreenPosition([1000, 1000])).toEqual [15, 2] - - describe "when wrapBeyondNewlines is false (the default)", -> - it "wraps positions beyond the end of hard newlines to the end of the line", -> - expect(displayBuffer.clipScreenPosition([1, 10000])).toEqual [1, 30] - expect(displayBuffer.clipScreenPosition([4, 30])).toEqual [4, 15] - expect(displayBuffer.clipScreenPosition([4, 1000])).toEqual [4, 15] - - describe "when wrapBeyondNewlines is true", -> - it "wraps positions past the end of hard newlines to the next line", -> - expect(displayBuffer.clipScreenPosition([0, 29], wrapBeyondNewlines: true)).toEqual [0, 29] - expect(displayBuffer.clipScreenPosition([0, 30], wrapBeyondNewlines: true)).toEqual [1, 0] - expect(displayBuffer.clipScreenPosition([0, 1000], wrapBeyondNewlines: true)).toEqual [1, 0] - - it "wraps positions in the middle of fold lines to the next screen line", -> - displayBuffer.foldBufferRowRange(3, 5) - expect(displayBuffer.clipScreenPosition([3, 5], wrapBeyondNewlines: true)).toEqual [4, 0] - - describe "when skipSoftWrapIndentation is false (the default)", -> - it "wraps positions at the end of previous soft-wrapped line", -> - expect(displayBuffer.clipScreenPosition([4, 0])).toEqual [3, 50] - expect(displayBuffer.clipScreenPosition([4, 1])).toEqual [3, 50] - expect(displayBuffer.clipScreenPosition([4, 3])).toEqual [3, 50] - - describe "when skipSoftWrapIndentation is true", -> - it "clips positions to the beginning of the line", -> - expect(displayBuffer.clipScreenPosition([4, 0], skipSoftWrapIndentation: true)).toEqual [4, 4] - expect(displayBuffer.clipScreenPosition([4, 1], skipSoftWrapIndentation: true)).toEqual [4, 4] - expect(displayBuffer.clipScreenPosition([4, 3], skipSoftWrapIndentation: true)).toEqual [4, 4] - - describe "when wrapAtSoftNewlines is false (the default)", -> - it "clips positions at the end of soft-wrapped lines to the character preceding the end of the line", -> - expect(displayBuffer.clipScreenPosition([3, 50])).toEqual [3, 50] - expect(displayBuffer.clipScreenPosition([3, 51])).toEqual [3, 50] - expect(displayBuffer.clipScreenPosition([3, 58])).toEqual [3, 50] - expect(displayBuffer.clipScreenPosition([3, 1000])).toEqual [3, 50] - - describe "when wrapAtSoftNewlines is true", -> - it "wraps positions at the end of soft-wrapped lines to the next screen line", -> - expect(displayBuffer.clipScreenPosition([3, 50], wrapAtSoftNewlines: true)).toEqual [3, 50] - expect(displayBuffer.clipScreenPosition([3, 51], wrapAtSoftNewlines: true)).toEqual [4, 4] - expect(displayBuffer.clipScreenPosition([3, 58], wrapAtSoftNewlines: true)).toEqual [4, 4] - expect(displayBuffer.clipScreenPosition([3, 1000], wrapAtSoftNewlines: true)).toEqual [4, 4] - - describe "when clip is 'closest' (the default)", -> - it "clips screen positions in the middle of atomic tab characters to the closest edge of the character", -> - buffer.insert([0, 0], '\t') - expect(displayBuffer.clipScreenPosition([0, 0])).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, 1])).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, 2])).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, tabLength-1])).toEqual [0, tabLength] - expect(displayBuffer.clipScreenPosition([0, tabLength])).toEqual [0, tabLength] - - describe "when clip is 'backward'", -> - it "clips screen positions in the middle of atomic tab characters to the beginning of the character", -> - buffer.insert([0, 0], '\t') - expect(displayBuffer.clipScreenPosition([0, 0], clip: 'backward')).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, tabLength-1], clip: 'backward')).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'backward')).toEqual [0, tabLength] - - describe "when clip is 'forward'", -> - it "clips screen positions in the middle of atomic tab characters to the end of the character", -> - buffer.insert([0, 0], '\t') - expect(displayBuffer.clipScreenPosition([0, 0], clip: 'forward')).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, 1], clip: 'forward')).toEqual [0, tabLength] - expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'forward')).toEqual [0, tabLength] - - describe "::screenPositionForBufferPosition(bufferPosition, options)", -> - it "clips the specified buffer position", -> - expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 2] - expect(displayBuffer.screenPositionForBufferPosition([0, 100000])).toEqual [0, 29] - expect(displayBuffer.screenPositionForBufferPosition([100000, 0])).toEqual [12, 2] - expect(displayBuffer.screenPositionForBufferPosition([100000, 100000])).toEqual [12, 2] - - it "clips to the (left or right) edge of an atomic token without simply rounding up", -> - tabLength = 4 - displayBuffer.setTabLength(tabLength) - - buffer.insert([0, 0], '\t') - expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0] - expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, tabLength] - - it "clips to the edge closest to the given position when it's inside a soft tab", -> - tabLength = 4 - displayBuffer.setTabLength(tabLength) - - buffer.insert([0, 0], ' ') - expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0] - expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, 0] - expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 0] - expect(displayBuffer.screenPositionForBufferPosition([0, 3])).toEqual [0, 4] - expect(displayBuffer.screenPositionForBufferPosition([0, 4])).toEqual [0, 4] - - describe "position translation in the presence of hard tabs", -> - it "correctly translates positions on either side of a tab", -> - buffer.setText('\t') - expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, 2] - expect(displayBuffer.bufferPositionForScreenPosition([0, 2])).toEqual [0, 1] - - it "correctly translates positions on soft wrapped lines containing tabs", -> - buffer.setText('\t\taa bb cc dd ee ff gg') - displayBuffer.setSoftWrapped(true) - displayBuffer.setDefaultCharWidth(1) - displayBuffer.setEditorWidthInChars(10) - expect(displayBuffer.screenPositionForBufferPosition([0, 10], wrapAtSoftNewlines: true)).toEqual [1, 4] - expect(displayBuffer.bufferPositionForScreenPosition([1, 0])).toEqual [0, 9] - - describe "::getMaxLineLength()", -> - it "returns the length of the longest screen line", -> - expect(displayBuffer.getMaxLineLength()).toBe 65 - buffer.delete([[6, 0], [6, 65]]) - expect(displayBuffer.getMaxLineLength()).toBe 62 - - it "correctly updates the location of the longest screen line when changes occur", -> - expect(displayBuffer.getLongestScreenRow()).toBe 6 - buffer.delete([[3, 0], [5, 0]]) - expect(displayBuffer.getLongestScreenRow()).toBe 4 - - buffer.delete([[4, 0], [5, 0]]) - expect(displayBuffer.getLongestScreenRow()).toBe 5 - expect(displayBuffer.getMaxLineLength()).toBe 56 - - buffer.delete([[6, 0], [8, 0]]) - expect(displayBuffer.getLongestScreenRow()).toBe 5 - expect(displayBuffer.getMaxLineLength()).toBe 56 - - describe "::destroy()", -> - it "unsubscribes all display buffer markers from their underlying buffer marker (regression)", -> - marker = displayBuffer.markBufferPosition([12, 2]) - displayBuffer.destroy() - expect( -> buffer.insert([12, 2], '\n')).not.toThrow() - - describe "markers", -> - beforeEach -> - displayBuffer.foldBufferRowRange(4, 7) - - describe "marker creation and manipulation", -> - it "allows markers to be created in terms of both screen and buffer coordinates", -> - marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker2 = displayBuffer.markBufferRange([[8, 4], [8, 10]]) - expect(marker1.getBufferRange()).toEqual [[8, 4], [8, 10]] - expect(marker2.getScreenRange()).toEqual [[5, 4], [5, 10]] - - it "emits a 'marker-created' event on the DisplayBuffer whenever a marker is created", -> - displayBuffer.onDidCreateMarker markerCreatedHandler = jasmine.createSpy("markerCreatedHandler") - - marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - expect(markerCreatedHandler).toHaveBeenCalledWith(marker1) - markerCreatedHandler.reset() - - marker2 = buffer.markRange([[5, 4], [5, 10]]) - expect(markerCreatedHandler).toHaveBeenCalledWith(displayBuffer.getMarker(marker2.id)) - - it "allows marker head and tail positions to be manipulated in both screen and buffer coordinates", -> - marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker.setHeadScreenPosition([5, 4]) - marker.setTailBufferPosition([5, 4]) - expect(marker.isReversed()).toBeFalsy() - expect(marker.getBufferRange()).toEqual [[5, 4], [8, 4]] - marker.setHeadBufferPosition([5, 4]) - marker.setTailScreenPosition([5, 4]) - expect(marker.isReversed()).toBeTruthy() - expect(marker.getBufferRange()).toEqual [[5, 4], [8, 4]] - - it "returns whether a position changed when it is assigned", -> - marker = displayBuffer.markScreenRange([[0, 0], [0, 0]]) - expect(marker.setHeadScreenPosition([5, 4])).toBeTruthy() - expect(marker.setHeadScreenPosition([5, 4])).toBeFalsy() - expect(marker.setHeadBufferPosition([1, 0])).toBeTruthy() - expect(marker.setHeadBufferPosition([1, 0])).toBeFalsy() - expect(marker.setTailScreenPosition([5, 4])).toBeTruthy() - expect(marker.setTailScreenPosition([5, 4])).toBeFalsy() - expect(marker.setTailBufferPosition([1, 0])).toBeTruthy() - expect(marker.setTailBufferPosition([1, 0])).toBeFalsy() - - describe "marker change events", -> - [markerChangedHandler, marker] = [] - - beforeEach -> - 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", -> - marker.setHeadScreenPosition([8, 20]) - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [5, 10] - oldHeadBufferPosition: [8, 10] - newHeadScreenPosition: [8, 20] - newHeadBufferPosition: [11, 20] - oldTailScreenPosition: [5, 4] - oldTailBufferPosition: [8, 4] - newTailScreenPosition: [5, 4] - newTailBufferPosition: [8, 4] - textChanged: false - isValid: true - } - markerChangedHandler.reset() - - buffer.insert([11, 0], '...') - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [8, 20] - oldHeadBufferPosition: [11, 20] - newHeadScreenPosition: [8, 23] - newHeadBufferPosition: [11, 23] - oldTailScreenPosition: [5, 4] - oldTailBufferPosition: [8, 4] - newTailScreenPosition: [5, 4] - newTailBufferPosition: [8, 4] - textChanged: true - isValid: true - } - markerChangedHandler.reset() - - displayBuffer.unfoldBufferRow(4) - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [8, 23] - oldHeadBufferPosition: [11, 23] - newHeadScreenPosition: [11, 23] - newHeadBufferPosition: [11, 23] - oldTailScreenPosition: [5, 4] - oldTailBufferPosition: [8, 4] - newTailScreenPosition: [8, 4] - newTailBufferPosition: [8, 4] - textChanged: false - isValid: true - } - markerChangedHandler.reset() - - displayBuffer.foldBufferRowRange(4, 7) - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [11, 23] - oldHeadBufferPosition: [11, 23] - newHeadScreenPosition: [8, 23] - newHeadBufferPosition: [11, 23] - oldTailScreenPosition: [8, 4] - oldTailBufferPosition: [8, 4] - newTailScreenPosition: [5, 4] - newTailBufferPosition: [8, 4] - textChanged: false - isValid: true - } - - it "triggers the 'changed' event whenever the marker tail's position changes in the buffer or on screen", -> - marker.setTailScreenPosition([8, 20]) - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [5, 10] - oldHeadBufferPosition: [8, 10] - newHeadScreenPosition: [5, 10] - newHeadBufferPosition: [8, 10] - oldTailScreenPosition: [5, 4] - oldTailBufferPosition: [8, 4] - newTailScreenPosition: [8, 20] - newTailBufferPosition: [11, 20] - textChanged: false - isValid: true - } - markerChangedHandler.reset() - - buffer.insert([11, 0], '...') - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [5, 10] - oldHeadBufferPosition: [8, 10] - newHeadScreenPosition: [5, 10] - newHeadBufferPosition: [8, 10] - oldTailScreenPosition: [8, 20] - oldTailBufferPosition: [11, 20] - newTailScreenPosition: [8, 23] - newTailBufferPosition: [11, 23] - textChanged: true - isValid: true - } - - it "triggers the 'changed' event whenever the marker is invalidated or revalidated", -> - buffer.deleteRow(8) - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [5, 10] - oldHeadBufferPosition: [8, 10] - newHeadScreenPosition: [5, 0] - newHeadBufferPosition: [8, 0] - oldTailScreenPosition: [5, 4] - oldTailBufferPosition: [8, 4] - newTailScreenPosition: [5, 0] - newTailBufferPosition: [8, 0] - textChanged: true - isValid: false - } - - markerChangedHandler.reset() - buffer.undo() - - expect(markerChangedHandler).toHaveBeenCalled() - expect(markerChangedHandler.argsForCall[0][0]).toEqual { - oldHeadScreenPosition: [5, 0] - oldHeadBufferPosition: [8, 0] - newHeadScreenPosition: [5, 10] - newHeadBufferPosition: [8, 10] - oldTailScreenPosition: [5, 0] - oldTailBufferPosition: [8, 0] - newTailScreenPosition: [5, 4] - newTailBufferPosition: [8, 4] - textChanged: true - isValid: true - } - - it "does not call the callback for screen changes that don't change the position of the marker", -> - displayBuffer.foldBufferRowRange(10, 11) - expect(markerChangedHandler).not.toHaveBeenCalled() - - it "updates markers before emitting buffer change events, but does not notify their observers until the change event", -> - marker2 = displayBuffer.addMarkerLayer(maintainHistory: true).markBufferRange([[8, 1], [8, 1]]) - marker2.onDidChange marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler") - displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange() - - # New change ---- - - onDisplayBufferChange = -> - # calls change handler first - expect(markerChangedHandler).not.toHaveBeenCalled() - expect(marker2ChangedHandler).not.toHaveBeenCalled() - # but still updates the markers - expect(marker.getScreenRange()).toEqual [[5, 7], [5, 13]] - expect(marker.getHeadScreenPosition()).toEqual [5, 13] - expect(marker.getTailScreenPosition()).toEqual [5, 7] - expect(marker2.isValid()).toBeFalsy() - - buffer.setTextInRange([[8, 0], [8, 2]], ".....") - expect(changeHandler).toHaveBeenCalled() - expect(markerChangedHandler).toHaveBeenCalled() - expect(marker2ChangedHandler).toHaveBeenCalled() - - # Undo change ---- - - changeHandler.reset() - markerChangedHandler.reset() - marker2ChangedHandler.reset() - - marker3 = displayBuffer.markBufferRange([[8, 1], [8, 2]]) - marker3.onDidChange marker3ChangedHandler = jasmine.createSpy("marker3ChangedHandler") - - onDisplayBufferChange = -> - # calls change handler first - expect(markerChangedHandler).not.toHaveBeenCalled() - expect(marker2ChangedHandler).not.toHaveBeenCalled() - expect(marker3ChangedHandler).not.toHaveBeenCalled() - - # markers positions are updated based on the text change - expect(marker.getScreenRange()).toEqual [[5, 4], [5, 10]] - expect(marker.getHeadScreenPosition()).toEqual [5, 10] - expect(marker.getTailScreenPosition()).toEqual [5, 4] - - buffer.undo() - expect(changeHandler).toHaveBeenCalled() - expect(markerChangedHandler).toHaveBeenCalled() - expect(marker2ChangedHandler).toHaveBeenCalled() - expect(marker3ChangedHandler).toHaveBeenCalled() - expect(marker2.isValid()).toBeTruthy() - expect(marker3.isValid()).toBeFalsy() - - # Redo change ---- - - changeHandler.reset() - markerChangedHandler.reset() - marker2ChangedHandler.reset() - marker3ChangedHandler.reset() - - onDisplayBufferChange = -> - # calls change handler first - expect(markerChangedHandler).not.toHaveBeenCalled() - expect(marker2ChangedHandler).not.toHaveBeenCalled() - expect(marker3ChangedHandler).not.toHaveBeenCalled() - - # markers positions are updated based on the text change - expect(marker.getScreenRange()).toEqual [[5, 7], [5, 13]] - expect(marker.getHeadScreenPosition()).toEqual [5, 13] - expect(marker.getTailScreenPosition()).toEqual [5, 7] - - # but marker snapshots are not restored until the end of the undo. - expect(marker2.isValid()).toBeFalsy() - expect(marker3.isValid()).toBeFalsy() - - buffer.redo() - expect(changeHandler).toHaveBeenCalled() - expect(markerChangedHandler).toHaveBeenCalled() - expect(marker2ChangedHandler).toHaveBeenCalled() - expect(marker3ChangedHandler).toHaveBeenCalled() - - it "updates the position of markers before emitting change events that aren't caused by a buffer change", -> - displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> - # calls change handler first - expect(markerChangedHandler).not.toHaveBeenCalled() - # but still updates the markers - expect(marker.getScreenRange()).toEqual [[8, 4], [8, 10]] - expect(marker.getHeadScreenPosition()).toEqual [8, 10] - expect(marker.getTailScreenPosition()).toEqual [8, 4] - - displayBuffer.unfoldBufferRow(4) - - expect(changeHandler).toHaveBeenCalled() - expect(markerChangedHandler).toHaveBeenCalled() - - it "emits the correct events when markers are mutated inside event listeners", -> - marker.onDidChange -> - if marker.getHeadScreenPosition().isEqual([5, 9]) - marker.setHeadScreenPosition([5, 8]) - - marker.setHeadScreenPosition([5, 9]) - - headChanges = for [event] in markerChangedHandler.argsForCall - {old: event.oldHeadScreenPosition, new: event.newHeadScreenPosition} - - expect(headChanges).toEqual [ - {old: [5, 10], new: [5, 9]} - {old: [5, 9], new: [5, 8]} - ] - - describe "::findMarkers(attributes)", -> - it "allows the startBufferRow and endBufferRow to be specified", -> - marker1 = displayBuffer.markBufferRange([[0, 0], [3, 0]], class: 'a') - marker2 = displayBuffer.markBufferRange([[0, 0], [5, 0]], class: 'a') - marker3 = displayBuffer.markBufferRange([[9, 0], [10, 0]], class: 'b') - - expect(displayBuffer.findMarkers(class: 'a', startBufferRow: 0)).toEqual [marker2, marker1] - expect(displayBuffer.findMarkers(class: 'a', startBufferRow: 0, endBufferRow: 3)).toEqual [marker1] - expect(displayBuffer.findMarkers(endBufferRow: 10)).toEqual [marker3] - - it "allows the startScreenRow and endScreenRow to be specified", -> - marker1 = displayBuffer.markBufferRange([[6, 0], [7, 0]], class: 'a') - marker2 = displayBuffer.markBufferRange([[9, 0], [10, 0]], class: 'a') - displayBuffer.foldBufferRowRange(4, 7) - expect(displayBuffer.findMarkers(class: 'a', startScreenRow: 6, endScreenRow: 7)).toEqual [marker2] - - it "allows intersectsBufferRowRange to be specified", -> - marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.foldBufferRowRange(4, 7) - expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRowRange: [5, 6])).toEqual [marker1] - - it "allows intersectsScreenRowRange to be specified", -> - marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.foldBufferRowRange(4, 7) - expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 10])).toEqual [marker2] - - it "allows containedInScreenRange to be specified", -> - marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.foldBufferRowRange(4, 7) - expect(displayBuffer.findMarkers(class: 'a', containedInScreenRange: [[5, 0], [7, 0]])).toEqual [marker2] - - it "allows intersectsBufferRange to be specified", -> - marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.foldBufferRowRange(4, 7) - expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRange: [[5, 0], [6, 0]])).toEqual [marker1] - - it "allows intersectsScreenRange to be specified", -> - marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayBuffer.foldBufferRowRange(4, 7) - expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRange: [[5, 0], [10, 0]])).toEqual [marker2] - - describe "marker destruction", -> - it "allows markers to be destroyed", -> - marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker.destroy() - expect(marker.isValid()).toBeFalsy() - expect(displayBuffer.getMarker(marker.id)).toBeUndefined() - - it "notifies ::onDidDestroy observers when markers are destroyed", -> - destroyedHandler = jasmine.createSpy("destroyedHandler") - marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker.onDidDestroy destroyedHandler - marker.destroy() - expect(destroyedHandler).toHaveBeenCalled() - destroyedHandler.reset() - - marker2 = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker2.onDidDestroy destroyedHandler - buffer.getMarker(marker2.id).destroy() - expect(destroyedHandler).toHaveBeenCalled() - - describe "Marker::copy(attributes)", -> - it "creates a copy of the marker with the given attributes merged in", -> - initialMarkerCount = displayBuffer.getMarkerCount() - marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]], a: 1, b: 2) - expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 1 - - marker2 = marker1.copy(b: 3) - expect(marker2.getBufferRange()).toEqual marker1.getBufferRange() - expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 2 - expect(marker1.getProperties()).toEqual a: 1, b: 2 - expect(marker2.getProperties()).toEqual a: 1, b: 3 - - describe 'when there are multiple DisplayBuffers for a buffer', -> - describe 'when a marker is created', -> - it 'the second display buffer will not emit a marker-created event when the marker has been deleted in the first marker-created event', -> - displayBuffer2 = new DisplayBuffer({ - buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, - packageManager: atom.packages, assert: -> - }) - displayBuffer.onDidCreateMarker markerCreated1 = jasmine.createSpy().andCallFake (marker) -> marker.destroy() - displayBuffer2.onDidCreateMarker markerCreated2 = jasmine.createSpy() - - displayBuffer.markBufferRange([[0, 0], [1, 5]], {}) - - expect(markerCreated1).toHaveBeenCalled() - expect(markerCreated2).not.toHaveBeenCalled() - - describe "decorations", -> - [marker, decoration, decorationProperties] = [] - beforeEach -> - marker = displayBuffer.markBufferRange([[2, 13], [3, 15]]) - decorationProperties = {type: 'line-number', class: 'one'} - decoration = displayBuffer.decorateMarker(marker, decorationProperties) - - it "can add decorations associated with markers and remove them", -> - expect(decoration).toBeDefined() - expect(decoration.getProperties()).toBe decorationProperties - expect(displayBuffer.decorationForId(decoration.id)).toBe decoration - expect(displayBuffer.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration - - decoration.destroy() - expect(displayBuffer.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined() - expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined() - - it "will not fail if the decoration is removed twice", -> - decoration.destroy() - decoration.destroy() - expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined() - - it "does not allow destroyed markers to be decorated", -> - marker.destroy() - expect(-> - displayBuffer.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')}) - ).toThrow("Cannot decorate a destroyed marker") - expect(displayBuffer.getOverlayDecorations()).toEqual [] - - 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() - decoration.setProperties type: 'line-number', class: 'two' - - {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0] - expect(oldProperties).toEqual decorationProperties - expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'} - - describe "::getDecorations(properties)", -> - it "returns decorations matching the given optional properties", -> - expect(displayBuffer.getDecorations()).toEqual [decoration] - expect(displayBuffer.getDecorations(class: 'two').length).toEqual 0 - expect(displayBuffer.getDecorations(class: 'one').length).toEqual 1 - - describe "::scrollToScreenPosition(position, [options])", -> - it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", -> - scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll") - displayBuffer.onDidRequestAutoscroll(scrollSpy) - - displayBuffer.scrollToScreenPosition([8, 20]) - displayBuffer.scrollToScreenPosition([8, 20], center: true) - displayBuffer.scrollToScreenPosition([8, 20], center: false, reversed: true) - - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}) - - describe "::decorateMarker", -> - describe "when decorating gutters", -> - [marker] = [] - - beforeEach -> - marker = displayBuffer.markBufferRange([[1, 0], [1, 0]]) - - it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", -> - decorationProperties = {type: 'line-number', class: 'one'} - decoration = displayBuffer.decorateMarker(marker, decorationProperties) - expect(decoration.isType('line-number')).toBe true - expect(decoration.isType('gutter')).toBe true - expect(decoration.getProperties().gutterName).toBe 'line-number' - expect(decoration.getProperties().class).toBe 'one' - - it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", -> - decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'} - decoration = displayBuffer.decorateMarker(marker, decorationProperties) - expect(decoration.isType('gutter')).toBe true - expect(decoration.isType('line-number')).toBe false - expect(decoration.getProperties().gutterName).toBe 'test-gutter' - expect(decoration.getProperties().class).toBe 'one' diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee index 3924a8412..06c50e80f 100644 --- a/spec/random-editor-spec.coffee +++ b/spec/random-editor-spec.coffee @@ -17,7 +17,7 @@ describe "TextEditor", -> buffer = new TextBuffer editor = atom.workspace.buildTextEditor({buffer}) editor.setEditorWidthInChars(80) - tokenizedBuffer = editor.displayBuffer.tokenizedBuffer + tokenizedBuffer = editor.tokenizedBuffer steps = [] times 30, -> diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index c7c9f9277..43f473e9d 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1477,7 +1477,7 @@ describe('TextEditorComponent', function () { component.measureDimensions() await nextViewUpdatePromise() - let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) + let marker2 = editor.markBufferRange([[9, 0], [9, 0]]) editor.decorateMarker(marker2, { type: ['line-number', 'line'], 'class': 'b' @@ -1889,7 +1889,7 @@ describe('TextEditorComponent', function () { component.measureDimensions() await nextViewUpdatePromise() - marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], { + marker = editor.markBufferRange([[9, 2], [9, 4]], { invalidate: 'inside' }) editor.decorateMarker(marker, { @@ -2084,7 +2084,7 @@ describe('TextEditorComponent', function () { describe('when the marker is empty', function () { it('renders an overlay decoration when added and removes the overlay when the decoration is destroyed', async function () { - let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], { + let marker = editor.markBufferRange([[2, 13], [2, 13]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { @@ -2106,7 +2106,7 @@ describe('TextEditorComponent', function () { }) it('renders the overlay element with the CSS class specified by the decoration', async function () { - let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], { + let marker = editor.markBufferRange([[2, 13], [2, 13]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { @@ -2127,7 +2127,7 @@ describe('TextEditorComponent', function () { describe('when the marker is not empty', function () { it('renders at the head of the marker by default', async function () { - let marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], { + let marker = editor.markBufferRange([[2, 5], [2, 10]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { @@ -2173,7 +2173,7 @@ describe('TextEditorComponent', function () { }) it('slides horizontally left when near the right edge on #win32 and #darwin', async function () { - let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], { + let marker = editor.markBufferRange([[0, 26], [0, 26]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { @@ -4934,7 +4934,7 @@ describe('TextEditorComponent', function () { function lineNumberForBufferRowHasClass (bufferRow, klass) { let screenRow - screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow) + screenRow = editor.screenRowForBufferRow(bufferRow) return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) } diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 95003a498..39124bb17 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5710,6 +5710,19 @@ describe "TextEditor", -> expect(editor.getFirstVisibleScreenRow()).toEqual 89 expect(editor.getVisibleRowRange()).toEqual [89, 99] + describe "::scrollToScreenPosition(position, [options])", -> + it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", -> + scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll") + editor.onDidRequestAutoscroll(scrollSpy) + + editor.scrollToScreenPosition([8, 20]) + editor.scrollToScreenPosition([8, 20], center: true) + editor.scrollToScreenPosition([8, 20], center: false, reversed: true) + + expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {}) + expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true}) + expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}) + describe '.get/setPlaceholderText()', -> it 'can be created with placeholderText', -> newEditor = atom.workspace.buildTextEditor( diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index f8746a511..cb9378c4e 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -419,7 +419,7 @@ describe "TokenizedBuffer", -> atom.workspace.open('sample.js').then (o) -> editor = o runs -> - tokenizedBuffer = editor.displayBuffer.tokenizedBuffer + tokenizedBuffer = editor.tokenizedBuffer tokenizedBuffer.onDidTokenize tokenizedHandler fullyTokenize(tokenizedBuffer) expect(tokenizedHandler.callCount).toBe(1) @@ -432,7 +432,7 @@ describe "TokenizedBuffer", -> atom.workspace.open('sample.js').then (o) -> editor = o runs -> - tokenizedBuffer = editor.displayBuffer.tokenizedBuffer + tokenizedBuffer = editor.tokenizedBuffer fullyTokenize(tokenizedBuffer) tokenizedBuffer.onDidTokenize tokenizedHandler @@ -450,7 +450,7 @@ describe "TokenizedBuffer", -> atom.workspace.open('coffee.coffee').then (o) -> editor = o runs -> - tokenizedBuffer = editor.displayBuffer.tokenizedBuffer + tokenizedBuffer = editor.tokenizedBuffer tokenizedBuffer.onDidTokenize tokenizedHandler fullyTokenize(tokenizedBuffer) tokenizedHandler.reset() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 97139f6bb..4aeeb4183 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -428,7 +428,7 @@ describe "Workspace", -> workspace.open('sample.js').then (e) -> editor = e runs -> - expect(editor.displayBuffer.largeFileMode).toBe true + expect(editor.largeFileMode).toBe true describe "when the file is over 20MB", -> it "prompts the user to make sure they want to open a file this big", -> @@ -453,7 +453,7 @@ describe "Workspace", -> runs -> expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - expect(editor.displayBuffer.largeFileMode).toBe true + expect(editor.largeFileMode).toBe true describe "when passed a path that matches a custom opener", -> it "returns the resource returned by the custom opener", -> diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee new file mode 100644 index 000000000..e96727fe3 --- /dev/null +++ b/src/decoration-manager.coffee @@ -0,0 +1,180 @@ +{Emitter} = require 'event-kit' +Model = require './model' +Decoration = require './decoration' +LayerDecoration = require './layer-decoration' + +module.exports = +class DecorationManager extends Model + didUpdateDecorationsEventScheduled: false + updatedSynchronously: false + + constructor: (@displayLayer, @defaultMarkerLayer) -> + super + + @emitter = new Emitter + @decorationsById = {} + @decorationsByMarkerId = {} + @overlayDecorationsById = {} + @layerDecorationsByMarkerLayerId = {} + @decorationCountsByLayerId = {} + @layerUpdateDisposablesByLayerId = {} + + observeDecorations: (callback) -> + callback(decoration) for decoration in @getDecorations() + @onDidAddDecoration(callback) + + onDidAddDecoration: (callback) -> + @emitter.on 'did-add-decoration', callback + + onDidRemoveDecoration: (callback) -> + @emitter.on 'did-remove-decoration', callback + + onDidUpdateDecorations: (callback) -> + @emitter.on 'did-update-decorations', callback + + setUpdatedSynchronously: (@updatedSynchronously) -> + + decorationForId: (id) -> + @decorationsById[id] + + getDecorations: (propertyFilter) -> + allDecorations = [] + for markerId, decorations of @decorationsByMarkerId + allDecorations.push(decorations...) if decorations? + if propertyFilter? + allDecorations = allDecorations.filter (decoration) -> + for key, value of propertyFilter + return false unless decoration.properties[key] is value + true + allDecorations + + getLineDecorations: (propertyFilter) -> + @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line') + + getLineNumberDecorations: (propertyFilter) -> + @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number') + + getHighlightDecorations: (propertyFilter) -> + @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight') + + getOverlayDecorations: (propertyFilter) -> + result = [] + for id, decoration of @overlayDecorationsById + result.push(decoration) + if propertyFilter? + result.filter (decoration) -> + for key, value of propertyFilter + return false unless decoration.properties[key] is value + true + else + result + + decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> + decorationsByMarkerId = {} + for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) + if decorations = @decorationsByMarkerId[marker.id] + decorationsByMarkerId[marker.id] = decorations + decorationsByMarkerId + + decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> + decorationsState = {} + + for layerId of @decorationCountsByLayerId + layer = @displayLayer.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) -> + throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed() + marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id) + decoration = new Decoration(marker, this, decorationParams) + @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 + + 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] + + scheduleUpdateDecorationsEvent: -> + if @updatedSynchronously + @emitter.emit 'did-update-decorations' + return + + unless @didUpdateDecorationsEventScheduled + @didUpdateDecorationsEventScheduled = true + process.nextTick => + @didUpdateDecorationsEventScheduled = false + @emitter.emit 'did-update-decorations' + + decorationDidChangeType: (decoration) -> + if decoration.isType('overlay') + @overlayDecorationsById[decoration.id] = decoration + 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] diff --git a/src/decoration.coffee b/src/decoration.coffee index b5273d57b..63be29d86 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -62,7 +62,7 @@ class Decoration Section: Construction and Destruction ### - constructor: (@marker, @displayBuffer, properties) -> + constructor: (@marker, @decorationManager, properties) -> @emitter = new Emitter @id = nextId() @setProperties properties @@ -78,7 +78,7 @@ class Decoration @markerDestroyDisposable.dispose() @markerDestroyDisposable = null @destroyed = true - @displayBuffer.didDestroyDecoration(this) + @decorationManager.didDestroyDecoration(this) @emitter.emit 'did-destroy' @emitter.dispose() @@ -149,8 +149,8 @@ class Decoration oldProperties = @properties @properties = translateDecorationParamsOldToNew(newProperties) if newProperties.type? - @displayBuffer.decorationDidChangeType(this) - @displayBuffer.scheduleUpdateDecorationsEvent() + @decorationManager.decorationDidChangeType(this) + @decorationManager.scheduleUpdateDecorationsEvent() @emitter.emit 'did-change-properties', {oldProperties, newProperties} ### @@ -175,5 +175,5 @@ class Decoration @properties.flashCount++ @properties.flashClass = klass @properties.flashDuration = duration - @displayBuffer.scheduleUpdateDecorationsEvent() + @decorationManager.scheduleUpdateDecorationsEvent() @emitter.emit 'did-flash' diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee deleted file mode 100644 index 63508e4cb..000000000 --- a/src/display-buffer.coffee +++ /dev/null @@ -1,750 +0,0 @@ -_ = require 'underscore-plus' -{CompositeDisposable, Emitter} = require 'event-kit' -{Point, Range} = require 'text-buffer' -TokenizedBuffer = require './tokenized-buffer' -RowMap = require './row-map' -Model = require './model' -Token = require './token' -Decoration = require './decoration' -LayerDecoration = require './layer-decoration' -{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' - -ZERO_WIDTH_NBSP = '\ufeff' - -class BufferToScreenConversionError extends Error - constructor: (@message, @metadata) -> - super - Error.captureStackTrace(this, BufferToScreenConversionError) - -module.exports = -class DisplayBuffer extends Model - verticalScrollMargin: 2 - horizontalScrollMargin: 6 - changeCount: 0 - softWrapped: null - editorWidthInChars: null - lineHeightInPixels: null - defaultCharWidth: null - height: null - width: null - didUpdateDecorationsEventScheduled: false - updatedSynchronously: false - - @deserialize: (state, atomEnvironment) -> - state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) - state.displayLayer = state.tokenizedBuffer.buffer.getDisplayLayer(state.displayLayerId) - state.config = atomEnvironment.config - state.assert = atomEnvironment.assert - state.grammarRegistry = atomEnvironment.grammars - state.packageManager = atomEnvironment.packages - new this(state) - - constructor: (params={}) -> - super - - { - tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @ignoreInvisibles, - @largeFileMode, @config, @assert, @grammarRegistry, @packageManager, @displayLayer - } = params - - @emitter = new Emitter - @disposables = new CompositeDisposable - - @tokenizedBuffer ?= new TokenizedBuffer({ - tabLength, buffer, @largeFileMode, @config, - @grammarRegistry, @packageManager, @assert - }) - @buffer = @tokenizedBuffer.buffer - @displayLayer ?= @buffer.addDisplayLayer() - @displayLayer.setTextDecorationLayer(@tokenizedBuffer) - @charWidthsByScope = {} - @defaultMarkerLayer = @displayLayer.addMarkerLayer() - @decorationsById = {} - @decorationsByMarkerId = {} - @overlayDecorationsById = {} - @layerDecorationsByMarkerLayerId = {} - @decorationCountsByLayerId = {} - @layerUpdateDisposablesByLayerId = {} - - @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings - @disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker - - subscribeToScopedConfigSettings: => - @scopedConfigSubscriptions?.dispose() - @scopedConfigSubscriptions = subscriptions = new CompositeDisposable - - scopeDescriptor = @getRootScopeDescriptor() - subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - - @resetDisplayLayer() - - serialize: -> - deserializer: 'DisplayBuffer' - id: @id - softWrapped: @isSoftWrapped() - editorWidthInChars: @editorWidthInChars - tokenizedBuffer: @tokenizedBuffer.serialize() - largeFileMode: @largeFileMode - displayLayerId: @displayLayer.id - - copy: -> - new DisplayBuffer({ - @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, - @grammarRegistry, @packageManager, displayLayer: @buffer.copyDisplayLayer(@displayLayer.id) - }) - - resetDisplayLayer: -> - scopeDescriptor = @getRootScopeDescriptor() - invisibles = - if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles - @config.get('editor.invisibles', scope: scopeDescriptor) - else - {} - - softWrapColumn = - if @isSoftWrapped() - if @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) - @config.get('editor.preferredLineLength', scope: scopeDescriptor) - else - @getEditorWidthInChars() - else - Infinity - - @displayLayer.reset({ - invisibles: invisibles - softWrapColumn: softWrapColumn - showIndentGuides: @config.get('editor.showIndentGuide', scope: scopeDescriptor) - tabLength: @getTabLength(), - ratioForCharacter: @ratioForCharacter.bind(this), - isWrapBoundary: isWrapBoundary, - foldCharacter: ZERO_WIDTH_NBSP - }) - - onDidChangeSoftWrapped: (callback) -> - @emitter.on 'did-change-soft-wrapped', callback - - onDidChangeGrammar: (callback) -> - @tokenizedBuffer.onDidChangeGrammar(callback) - - onDidTokenize: (callback) -> - @tokenizedBuffer.onDidTokenize(callback) - - onDidChange: (callback) -> - @emitter.on 'did-change', callback - - onDidChangeCharacterWidths: (callback) -> - @emitter.on 'did-change-character-widths', callback - - onDidRequestAutoscroll: (callback) -> - @emitter.on 'did-request-autoscroll', callback - - observeDecorations: (callback) -> - callback(decoration) for decoration in @getDecorations() - @onDidAddDecoration(callback) - - onDidAddDecoration: (callback) -> - @emitter.on 'did-add-decoration', callback - - onDidRemoveDecoration: (callback) -> - @emitter.on 'did-remove-decoration', callback - - onDidCreateMarker: (callback) -> - @emitter.on 'did-create-marker', callback - - onDidUpdateMarkers: (callback) -> - @emitter.on 'did-update-markers', callback - - onDidUpdateDecorations: (callback) -> - @emitter.on 'did-update-decorations', callback - - # Sets the visibility of the tokenized buffer. - # - # 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) - - setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - - getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2)) - setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin - - getHeight: -> - @height - - setHeight: (@height) -> - @height - - getWidth: -> - @width - - setWidth: (newWidth) -> - oldWidth = @width - @width = newWidth - @resetDisplayLayer() if newWidth isnt oldWidth and @isSoftWrapped() - @width - - getLineHeightInPixels: -> @lineHeightInPixels - setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels - - ratioForCharacter: (character) -> - if isKoreanCharacter(character) - @getKoreanCharWidth() / @getDefaultCharWidth() - else if isHalfWidthCharacter(character) - @getHalfWidthCharWidth() / @getDefaultCharWidth() - else if isDoubleWidthCharacter(character) - @getDoubleWidthCharWidth() / @getDefaultCharWidth() - else - 1 - - getKoreanCharWidth: -> @koreanCharWidth - - getHalfWidthCharWidth: -> @halfWidthCharWidth - - getDoubleWidthCharWidth: -> @doubleWidthCharWidth - - getDefaultCharWidth: -> @defaultCharWidth - - setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> - doubleWidthCharWidth ?= defaultCharWidth - halfWidthCharWidth ?= defaultCharWidth - koreanCharWidth ?= defaultCharWidth - if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth - @defaultCharWidth = defaultCharWidth - @doubleWidthCharWidth = doubleWidthCharWidth - @halfWidthCharWidth = halfWidthCharWidth - @koreanCharWidth = koreanCharWidth - @resetDisplayLayer() if @isSoftWrapped() and @getEditorWidthInChars()? - defaultCharWidth - - getCursorWidth: -> 1 - - scrollToScreenRange: (screenRange, options = {}) -> - scrollEvent = {screenRange, options} - @emitter.emit "did-request-autoscroll", scrollEvent - - scrollToScreenPosition: (screenPosition, options) -> - @scrollToScreenRange(new Range(screenPosition, screenPosition), options) - - scrollToBufferPosition: (bufferPosition, options) -> - @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options) - - # Retrieves the current tab length. - # - # Returns a {Number}. - getTabLength: -> - if @tabLength? - @tabLength - else - @config.get('editor.tabLength', scope: @getRootScopeDescriptor()) - - # Specifies the tab length. - # - # tabLength - A {Number} that defines the new tab length. - setTabLength: (tabLength) -> - return if tabLength is @tabLength - - @tabLength = tabLength - @tokenizedBuffer.setTabLength(@tabLength) - @resetDisplayLayer() - - setIgnoreInvisibles: (ignoreInvisibles) -> - return if ignoreInvisibles is @ignoreInvisibles - - @ignoreInvisibles = ignoreInvisibles - @resetDisplayLayer() - - setSoftWrapped: (softWrapped) -> - if softWrapped isnt @softWrapped - @softWrapped = softWrapped - @resetDisplayLayer() - softWrapped = @isSoftWrapped() - @emitter.emit 'did-change-soft-wrapped', softWrapped - softWrapped - else - @isSoftWrapped() - - isSoftWrapped: -> - if @largeFileMode - false - else - scopeDescriptor = @getRootScopeDescriptor() - @softWrapped ? @config.get('editor.softWrap', scope: scopeDescriptor) ? false - - # Set the number of characters that fit horizontally in the editor. - # - # editorWidthInChars - A {Number} of characters. - setEditorWidthInChars: (editorWidthInChars) -> - if editorWidthInChars > 0 - previousWidthInChars = @editorWidthInChars - @editorWidthInChars = editorWidthInChars - if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped() - @resetDisplayLayer() - - # Returns the editor width in characters for soft wrap. - getEditorWidthInChars: -> - width = @getWidth() - if width? and @defaultCharWidth > 0 - Math.max(0, Math.floor(width / @defaultCharWidth)) - else - @editorWidthInChars - - indentLevelForLine: (line) -> - @tokenizedBuffer.indentLevelForLine(line) - - # Given starting and ending screen rows, this returns an array of the - # buffer rows corresponding to every screen row in the range - # - # startScreenRow - The screen row {Number} to start at - # endScreenRow - The screen row {Number} to end at (default: the last screen row) - # - # Returns an {Array} of buffer rows as {Numbers}s. - bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> - for screenRow in [startScreenRow..endScreenRow] - @bufferRowForScreenRow(screenRow) - - # Creates a new fold between two row numbers. - # - # startRow - The row {Number} to start folding at - # endRow - The row {Number} to end the fold - # - # Returns the new {Fold}. - foldBufferRowRange: (startRow, endRow) -> - @displayLayer.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) - - isFoldedAtBufferRow: (bufferRow) -> - @displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0 - - isFoldedAtScreenRow: (screenRow) -> - @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow)) - - isFoldableAtBufferRow: (row) -> - @tokenizedBuffer.isFoldableAtRow(row) - - # Removes any folds found that contain the given buffer row. - # - # bufferRow - The buffer row {Number} to check against - unfoldBufferRow: (bufferRow) -> - @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))) - - # Given a buffer row, this converts it into a screen row. - # - # bufferRow - A {Number} representing a buffer row - # - # Returns a {Number}. - screenRowForBufferRow: (bufferRow) -> - if @largeFileMode - bufferRow - else - @displayLayer.translateScreenPosition(Point(screenRow, 0)).row - - lastScreenRowForBufferRow: (bufferRow) -> - if @largeFileMode - bufferRow - else - @displayLayer.translateBufferPosition(Point(bufferRow, 0), clip: 'forward').row - - # Given a screen row, this converts it into a buffer row. - # - # screenRow - A {Number} representing a screen row - # - # Returns a {Number}. - bufferRowForScreenRow: (screenRow) -> - @displayLayer.translateScreenPosition(Point(screenRow, 0)).row - - # Given a buffer range, this converts it into a screen position. - # - # bufferRange - The {Range} to convert - # - # Returns a {Range}. - screenRangeForBufferRange: (bufferRange, options) -> - bufferRange = Range.fromObject(bufferRange) - start = @screenPositionForBufferPosition(bufferRange.start, options) - end = @screenPositionForBufferPosition(bufferRange.end, options) - new Range(start, end) - - # Given a screen range, this converts it into a buffer position. - # - # screenRange - The {Range} to convert - # - # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> - screenRange = Range.fromObject(screenRange) - start = @bufferPositionForScreenPosition(screenRange.start) - end = @bufferPositionForScreenPosition(screenRange.end) - new Range(start, end) - - # Gets the number of screen lines. - # - # Returns a {Number}. - getLineCount: -> - @displayLayer.getScreenLineCount() - - # Gets the number of the last screen line. - # - # Returns a {Number}. - getLastRow: -> - @getLineCount() - 1 - - # Given a buffer position, this converts it into a screen position. - # - # bufferPosition - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # options - A hash of options with the following keys: - # wrapBeyondNewlines: - # wrapAtSoftNewlines: - # - # Returns a {Point}. - screenPositionForBufferPosition: (bufferPosition, options) -> - throw new Error("This TextEditor has been destroyed") if @isDestroyed() - - @displayLayer.translateBufferPosition(bufferPosition, options) - - # Given a buffer position, this converts it into a screen position. - # - # screenPosition - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # options - A hash of options with the following keys: - # wrapBeyondNewlines: - # wrapAtSoftNewlines: - # - # Returns a {Point}. - bufferPositionForScreenPosition: (screenPosition, options) -> - return @displayLayer.translateScreenPosition(screenPosition, options) - - # Retrieves the grammar's token scopeDescriptor for a buffer position. - # - # bufferPosition - A {Point} in the {TextBuffer} - # - # Returns a {ScopeDescriptor}. - scopeDescriptorForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) - - bufferRangeForScopeAtPosition: (selector, position) -> - @tokenizedBuffer.bufferRangeForScopeAtPosition(selector, position) - - # Retrieves the grammar's token for a buffer position. - # - # bufferPosition - A {Point} in the {TextBuffer}. - # - # Returns a {Token}. - tokenForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.tokenForPosition(bufferPosition) - - # Get the grammar for this buffer. - # - # Returns the current {Grammar} or the {NullGrammar}. - getGrammar: -> - @tokenizedBuffer.grammar - - # Sets the grammar for the buffer. - # - # grammar - Sets the new grammar rules - setGrammar: (grammar) -> - @tokenizedBuffer.setGrammar(grammar) - - # Reloads the current grammar. - reloadGrammar: -> - @tokenizedBuffer.reloadGrammar() - - # Given a position, this clips it to a real position. - # - # For example, if `position`'s row exceeds the row count of the buffer, - # or if its column goes beyond a line's length, this "sanitizes" the value - # to a real position. - # - # position - The {Point} to clip - # options - A hash with the following values: - # wrapBeyondNewlines: if `true`, continues wrapping past newlines - # wrapAtSoftNewlines: if `true`, continues wrapping past soft newlines - # skipSoftWrapIndentation: if `true`, skips soft wrap indentation without wrapping to the previous line - # screenLine: if `true`, indicates that you're using a line number, not a row number - # - # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. - clipScreenPosition: (screenPosition, options={}) -> - @displayLayer.clipScreenPosition(screenPosition, options) - - # Clip the start and end of the given range to valid positions on screen. - # See {::clipScreenPosition} for more information. - # - # * `range` The {Range} to clip. - # * `options` (optional) See {::clipScreenPosition} `options`. - # Returns a {Range}. - clipScreenRange: (range, options) -> - start = @clipScreenPosition(range.start, options) - end = @clipScreenPosition(range.end, options) - - new Range(start, end) - - # Calculates a {Range} representing the start of the {TextBuffer} until the end. - # - # Returns a {Range}. - rangeForAllLines: -> - new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) - - decorationForId: (id) -> - @decorationsById[id] - - getDecorations: (propertyFilter) -> - allDecorations = [] - for markerId, decorations of @decorationsByMarkerId - allDecorations.push(decorations...) if decorations? - if propertyFilter? - allDecorations = allDecorations.filter (decoration) -> - for key, value of propertyFilter - return false unless decoration.properties[key] is value - true - allDecorations - - getLineDecorations: (propertyFilter) -> - @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line') - - getLineNumberDecorations: (propertyFilter) -> - @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number') - - getHighlightDecorations: (propertyFilter) -> - @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight') - - getOverlayDecorations: (propertyFilter) -> - result = [] - for id, decoration of @overlayDecorationsById - result.push(decoration) - if propertyFilter? - result.filter (decoration) -> - for key, value of propertyFilter - return false unless decoration.properties[key] is value - true - else - result - - decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> - decorationsByMarkerId = {} - for marker in @findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) - if decorations = @decorationsByMarkerId[marker.id] - 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) -> - throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed() - marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id) - decoration = new Decoration(marker, this, decorationParams) - @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 - - 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 {DisplayMarker} based on its id. - # - # id - A {Number} representing a marker id - # - # Returns the {DisplayMarker} (if it exists). - getMarker: (id) -> - @defaultMarkerLayer.getMarker(id) - - # Retrieves the active markers in the buffer. - # - # Returns an {Array} of existing {DisplayMarker}s. - getMarkers: -> - @defaultMarkerLayer.getMarkers() - - getMarkerCount: -> - @buffer.getMarkerCount() - - # 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 {DisplayMarker} constructor - # - # Returns a {Number} representing the new marker's ID. - 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 {DisplayMarker} constructor - # - # Returns a {Number} representing the new marker's 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 {DisplayMarker} constructor - # - # Returns a {Number} representing the new marker's ID. - markScreenPosition: (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 {DisplayMarker} constructor - # - # Returns a {Number} representing the new marker's ID. - markBufferPosition: (bufferPosition, options) -> - @defaultMarkerLayer.markBufferPosition(bufferPosition, options) - - # Finds the first marker satisfying the given attributes - # - # Refer to {DisplayBuffer::findMarkers} for details. - # - # Returns a {DisplayMarker} or null - findMarker: (params) -> - @defaultMarkerLayer.findMarkers(params)[0] - - # Public: Find all markers satisfying a set of parameters. - # - # params - An {Object} containing parameters that all returned markers must - # satisfy. Unreserved keys will be compared against the markers' custom - # properties. There are also the following reserved keys with special - # meaning for the query: - # :startBufferRow - A {Number}. Only returns markers starting at this row in - # buffer coordinates. - # :endBufferRow - A {Number}. Only returns markers ending at this row in - # buffer coordinates. - # :containsBufferRange - A {Range} or range-compatible {Array}. Only returns - # markers containing this range in buffer coordinates. - # :containsBufferPosition - A {Point} or point-compatible {Array}. Only - # returns markers containing this position in buffer coordinates. - # :containedInBufferRange - A {Range} or range-compatible {Array}. Only - # returns markers contained within this range. - # - # Returns an {Array} of {DisplayMarker}s - findMarkers: (params) -> - @defaultMarkerLayer.findMarkers(params) - - addMarkerLayer: (options) -> - @displayLayer.addMarkerLayer(options) - - getMarkerLayer: (id) -> - @displayLayer.getMarkerLayer(id) - - getDefaultMarkerLayer: -> @defaultMarkerLayer - - refreshMarkerScreenPositions: -> - @defaultMarkerLayer.refreshMarkerScreenPositions() - layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById - return - - destroyed: -> - @displayLayer.destroy() - @defaultMarkerLayer.destroy() - @scopedConfigSubscriptions.dispose() - @disposables.dispose() - @tokenizedBuffer.destroy() - - getRootScopeDescriptor: -> - @tokenizedBuffer.rootScopeDescriptor - - 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' - - decorationDidChangeType: (decoration) -> - if decoration.isType('overlay') - @overlayDecorationsById[decoration.id] = decoration - 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] diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 605f8454b..a0392acf6 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -131,7 +131,7 @@ class LanguageMode for currentRow in [bufferRow..0] by -1 [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? and startRow <= bufferRow <= endRow - unless @editor.displayBuffer.isFoldedAtBufferRow(startRow) + unless @editor.isFoldedAtBufferRow(startRow) return @editor.foldBufferRowRange(startRow, endRow) # Find the row range for a fold at a given bufferRow. Will handle comments @@ -146,19 +146,19 @@ class LanguageMode rowRange rowRangeForCommentAtBufferRow: (bufferRow) -> - return unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() + return unless @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() startRow = bufferRow endRow = bufferRow if bufferRow > 0 for currentRow in [bufferRow-1..0] by -1 - break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() + break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() startRow = currentRow if bufferRow < @buffer.getLastRow() for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1 - break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() + break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() endRow = currentRow return [startRow, endRow] if startRow isnt endRow @@ -181,13 +181,13 @@ class LanguageMode [bufferRow, foldEndRow] isFoldableAtBufferRow: (bufferRow) -> - @editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow) + @editor.tokenizedBuffer.isFoldableAtRow(bufferRow) # Returns a {Boolean} indicating whether the line at the given buffer # row is a comment. isLineCommentedAtBufferRow: (bufferRow) -> return false unless 0 <= bufferRow <= @editor.getLastBufferRow() - @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() + @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() # Find a row range for a 'paragraph' around specified bufferRow. A paragraph # is a block of text bounded by and empty line or a block of text that is not @@ -240,11 +240,11 @@ class LanguageMode # Returns a {Number}. suggestedIndentForBufferRow: (bufferRow, options) -> line = @buffer.lineForRow(bufferRow) - tokenizedLine = @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow) + tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow) @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) suggestedIndentForLineAtBufferRow: (bufferRow, line, options) -> - tokenizedLine = @editor.displayBuffer.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line) + tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line) @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) -> diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee index 7d2396e0d..e00e024cb 100644 --- a/src/layer-decoration.coffee +++ b/src/layer-decoration.coffee @@ -7,7 +7,7 @@ nextId = -> idCounter++ # layer. Created via {TextEditor::decorateMarkerLayer}. module.exports = class LayerDecoration - constructor: (@markerLayer, @displayBuffer, @properties) -> + constructor: (@markerLayer, @decorationManager, @properties) -> @id = nextId() @destroyed = false @markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy() @@ -19,7 +19,7 @@ class LayerDecoration @markerLayerDestroyedDisposable.dispose() @markerLayerDestroyedDisposable = null @destroyed = true - @displayBuffer.didDestroyLayerDecoration(this) + @decorationManager.didDestroyLayerDecoration(this) # Essential: Determine whether this decoration is destroyed. # @@ -44,7 +44,7 @@ class LayerDecoration setProperties: (newProperties) -> return if @destroyed @properties = newProperties - @displayBuffer.scheduleUpdateDecorationsEvent() + @decorationManager.scheduleUpdateDecorationsEvent() # Essential: Override the decoration properties for a specific marker. # @@ -58,4 +58,4 @@ class LayerDecoration @overridePropertiesByMarkerId[marker.id] = properties else delete @overridePropertiesByMarkerId[marker.id] - @displayBuffer.scheduleUpdateDecorationsEvent() + @decorationManager.scheduleUpdateDecorationsEvent() diff --git a/src/marker-observation-window.coffee b/src/marker-observation-window.coffee index aa7b71f69..ffb92c0ab 100644 --- a/src/marker-observation-window.coffee +++ b/src/marker-observation-window.coffee @@ -1,9 +1,9 @@ module.exports = class MarkerObservationWindow - constructor: (@displayBuffer, @bufferWindow) -> + constructor: (@decorationManager, @bufferWindow) -> setScreenRange: (range) -> - @bufferWindow.setRange(@displayBuffer.bufferRangeForScreenRange(range)) + @bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range)) setBufferRange: (range) -> @bufferWindow.setRange(range) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index ffb5a4706..40dd451d7 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -4,7 +4,8 @@ Grim = require 'grim' {CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = TextBuffer = require 'text-buffer' LanguageMode = require './language-mode' -DisplayBuffer = require './display-buffer' +DecorationManager = require './decoration-manager' +TokenizedBuffer = require './tokenized-buffer' Cursor = require './cursor' Model = require './model' Selection = require './selection' @@ -12,6 +13,9 @@ TextMateScopeSelector = require('first-mate').ScopeSelector {Directory} = require "pathwatcher" GutterContainer = require './gutter-container' TextEditorElement = require './text-editor-element' +{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' + +ZERO_WIDTH_NBSP = '\ufeff' # Essential: This class represents all essential editing state for a single # {TextBuffer}, including cursor and selection positions, folds, and soft wraps. @@ -63,21 +67,30 @@ class TextEditor extends Model selectionFlashDuration: 500 gutterContainer: null editorElement: null + verticalScrollMargin: 2 + horizontalScrollMargin: 6 + softWrapped: null + editorWidthInChars: null + lineHeightInPixels: null + defaultCharWidth: null + height: null + width: null Object.defineProperty @prototype, "element", get: -> @getElement() @deserialize: (state, atomEnvironment) -> try - displayBuffer = DisplayBuffer.deserialize(state.displayBuffer, atomEnvironment) + state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) catch error if error.syscall is 'read' return # Error reading the file, don't deserialize an editor for it else throw error - state.displayBuffer = displayBuffer - state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) + state.buffer = state.tokenizedBuffer.buffer + state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId) + state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config state.notificationManager = atomEnvironment.notifications state.packageManager = atomEnvironment.packages @@ -97,13 +110,19 @@ class TextEditor extends Model super { - @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, - softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, - @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, + @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, @tabLength, + @softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, + @mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, - @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd + @project, @assert, @applicationDelegate, grammar, @showInvisibles, @autoHeight, @scrollPastEnd } = params + { + tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @ignoreInvisibles, + @largeFileMode, @config, @assert, @grammarRegistry, @packageManager, @displayLayer + } = params + + throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager? throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager? @@ -124,17 +143,23 @@ class TextEditor extends Model @scrollPastEnd ?= true @hasTerminatedPendingState = false - showInvisibles ?= true + @showInvisibles ?= true - buffer ?= new TextBuffer - @displayBuffer ?= new DisplayBuffer({ - buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode, - @config, @assert, @grammarRegistry, @packageManager + @buffer ?= new TextBuffer + @tokenizedBuffer ?= new TokenizedBuffer({ + tabLength, @buffer, @largeFileMode, @config, + @grammarRegistry, @packageManager, @assert }) - {@buffer, @displayLayer} = @displayBuffer + @displayLayer ?= @buffer.addDisplayLayer() + @displayLayer.setTextDecorationLayer(@tokenizedBuffer) + @defaultMarkerLayer = @displayLayer.addMarkerLayer() + @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) + + @decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer) + @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) - @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) + @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings for marker in @selectionsMarkerLayer.getMarkers() marker.setProperties(preserveFolds: true) @@ -142,7 +167,7 @@ class TextEditor extends Model @subscribeToTabTypeConfig() @subscribeToBuffer() - @subscribeToDisplayBuffer() + @subscribeToDisplayLayer() if @cursors.length is 0 and not suppressCursorCreation initialLine = Math.max(parseInt(initialLine) or 0, 0) @@ -168,8 +193,12 @@ class TextEditor extends Model softTabs: @softTabs firstVisibleScreenRow: @getFirstVisibleScreenRow() firstVisibleScreenColumn: @getFirstVisibleScreenColumn() - displayBuffer: @displayBuffer.serialize() selectionsMarkerLayerId: @selectionsMarkerLayer.id + softWrapped: @isSoftWrapped() + editorWidthInChars: @editorWidthInChars + tokenizedBuffer: @tokenizedBuffer.serialize() + largeFileMode: @largeFileMode + displayLayerId: @displayLayer.id registered: atom.textEditors.editors.has this subscribeToBuffer: -> @@ -194,10 +223,26 @@ class TextEditor extends Model onDidTerminatePendingState: (callback) -> @emitter.on 'did-terminate-pending-state', callback - subscribeToDisplayBuffer: -> + subscribeToScopedConfigSettings: => + @scopedConfigSubscriptions?.dispose() + @scopedConfigSubscriptions = subscriptions = new CompositeDisposable + + scopeDescriptor = @getRootScopeDescriptor() + subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) + + @resetDisplayLayer() + + subscribeToDisplayLayer: -> @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) - @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) - @disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this) + @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) + @disposables.add @tokenizedBuffer.onDidTokenize @handleTokenization.bind(this) @disposables.add @displayLayer.onDidChangeSync (e) => @mergeIntersectingSelections() @emitter.emit 'did-change', e @@ -207,13 +252,27 @@ class TextEditor extends Model @tabTypeSubscription = @config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), => @softTabs = @shouldUseSoftTabs(defaultValue: @softTabs) + resetDisplayLayer: -> + @displayLayer.reset({ + invisibles: @getInvisibles(), + softWrapColumn: @getSoftWrapColumn(), + showIndentGuides: @config.get('editor.showIndentGuide', scope: @getRootScopeDescriptor()), + tabLength: @getTabLength(), + ratioForCharacter: @ratioForCharacter.bind(this), + isWrapBoundary: isWrapBoundary, + foldCharacter: ZERO_WIDTH_NBSP + }) + destroyed: -> @disposables.dispose() + @displayLayer.destroy() + @scopedConfigSubscriptions.dispose() + @disposables.dispose() + @tokenizedBuffer.destroy() @tabTypeSubscription.dispose() selection.destroy() for selection in @selections.slice() @selectionsMarkerLayer.destroy() @buffer.release() - @displayBuffer.destroy() @languageMode.destroy() @gutterContainer.destroy() @emitter.emit 'did-destroy' @@ -297,7 +356,7 @@ class TextEditor extends Model # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeSoftWrapped: (callback) -> - @displayBuffer.onDidChangeSoftWrapped(callback) + @emitter.on 'did-change-soft-wrapped', callback # Extended: Calls your `callback` when the buffer's encoding has changed. # @@ -451,7 +510,7 @@ class TextEditor extends Model # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeDecorations: (callback) -> - @displayBuffer.observeDecorations(callback) + @decorationManager.observeDecorations(callback) # Extended: Calls your `callback` when a {Decoration} is added to the editor. # @@ -460,7 +519,7 @@ class TextEditor extends Model # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddDecoration: (callback) -> - @displayBuffer.onDidAddDecoration(callback) + @decorationManager.onDidAddDecoration(callback) # Extended: Calls your `callback` when a {Decoration} is removed from the editor. # @@ -469,7 +528,7 @@ class TextEditor extends Model # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveDecoration: (callback) -> - @displayBuffer.onDidRemoveDecoration(callback) + @decorationManager.onDidRemoveDecoration(callback) # Extended: Calls your `callback` when the placeholder text is changed. # @@ -480,9 +539,6 @@ class TextEditor extends Model onDidChangePlaceholderText: (callback) -> @emitter.on 'did-change-placeholder-text', callback - onDidChangeCharacterWidths: (callback) -> - @displayBuffer.onDidChangeCharacterWidths(callback) - onDidChangeFirstVisibleScreenRow: (callback, fromView) -> @emitter.on 'did-change-first-visible-screen-row', callback @@ -497,17 +553,14 @@ class TextEditor extends Model @viewRegistry.getView(this).onDidChangeScrollLeft(callback) onDidRequestAutoscroll: (callback) -> - @displayBuffer.onDidRequestAutoscroll(callback) + @emitter.on 'did-request-autoscroll', callback # TODO Remove once the tabs package no longer uses .on subscriptions onDidChangeIcon: (callback) -> @emitter.on 'did-change-icon', callback - onDidUpdateMarkers: (callback) -> - @displayBuffer.onDidUpdateMarkers(callback) - onDidUpdateDecorations: (callback) -> - @displayBuffer.onDidUpdateDecorations(callback) + @decorationManager.onDidUpdateDecorations(callback) # Essential: Retrieves the current {TextBuffer}. getBuffer: -> @buffer @@ -517,31 +570,31 @@ 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) + selectionsMarkerLayer = @getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) softTabs = @getSoftTabs() newEditor = new TextEditor({ - @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, + @buffer, selectionsMarkerLayer, @tabLength, softTabs, suppressCursorCreation: true, @config, @notificationManager, @packageManager, @firstVisibleScreenRow, @firstVisibleScreenColumn, - @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate + @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate, + displayLayer: @buffer.copyDisplayLayer(@displayLayer.id) }) newEditor # Controls visibility based on the given {Boolean}. - setVisible: (visible) -> @displayBuffer.setVisible(visible) + setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) setMini: (mini) -> if mini isnt @mini @mini = mini - @displayBuffer.setIgnoreInvisibles(@mini) + @setIgnoreInvisibles(@mini) @emitter.emit 'did-change-mini', @mini @mini isMini: -> @mini setUpdatedSynchronously: (updatedSynchronously) -> - @displayBuffer.setUpdatedSynchronously(updatedSynchronously) + @decorationManager.setUpdatedSynchronously(updatedSynchronously) onDidChangeMini: (callback) -> @emitter.on 'did-change-mini', callback @@ -594,12 +647,19 @@ class TextEditor extends Model # * `editorWidthInChars` A {Number} representing the width of the # {TextEditorElement} in characters. setEditorWidthInChars: (editorWidthInChars) -> - @displayBuffer.setEditorWidthInChars(editorWidthInChars) + if editorWidthInChars > 0 + previousWidthInChars = @editorWidthInChars + @editorWidthInChars = editorWidthInChars + if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped() + @resetDisplayLayer() # Returns the editor width in characters. getEditorWidthInChars: -> - @displayBuffer.getEditorWidthInChars() - + width = @getWidth() + if width? and @defaultCharWidth > 0 + Math.max(0, Math.floor(width / @defaultCharWidth)) + else + @editorWidthInChars ### Section: File Details @@ -788,16 +848,21 @@ class TextEditor extends Model return if screenRow < 0 or screenRow > @getLastScreenRow() @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] - bufferRowForScreenRow: (row) -> @displayLayer.translateScreenPosition(Point(row, 0)).row + bufferRowForScreenRow: (screenRow) -> + @displayLayer.translateScreenPosition(Point(screenRow, 0)).row - # {Delegates to: DisplayBuffer.bufferRowsForScreenRows} - bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) + bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> + for screenRow in [startScreenRow..endScreenRow] + @bufferRowForScreenRow(screenRow) - screenRowForBufferRow: (row) -> @displayLayer.translateBufferPosition(Point(row, 0)).row + screenRowForBufferRow: (row) -> + if @largeFileMode + bufferRow + else + @displayLayer.translateScreenPosition(Point(screenRow, 0)).row getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() - # {Delegates to: DisplayBuffer.getMaxLineLength} getMaxScreenLineLength: -> @getRightmostScreenPosition().column getLongestScreenRow: -> @getRightmostScreenPosition().row @@ -1342,7 +1407,10 @@ class TextEditor extends Model # * `options` (optional) An options hash for {::clipScreenPosition}. # # Returns a {Point}. - screenPositionForBufferPosition: (bufferPosition, options) -> @displayLayer.translateBufferPosition(bufferPosition, options) + screenPositionForBufferPosition: (bufferPosition, options) -> + throw new Error("This TextEditor has been destroyed") if @isDestroyed() + + @displayLayer.translateBufferPosition(bufferPosition, options) # Essential: Convert a position in screen-coordinates to buffer-coordinates. # @@ -1352,21 +1420,30 @@ class TextEditor extends Model # * `options` (optional) An options hash for {::clipScreenPosition}. # # Returns a {Point}. - bufferPositionForScreenPosition: (screenPosition, options) -> @displayLayer.translateScreenPosition(screenPosition, options) + bufferPositionForScreenPosition: (screenPosition, options) -> + @displayLayer.translateScreenPosition(screenPosition, options) # Essential: Convert a range in buffer-coordinates to screen-coordinates. # # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. # # Returns a {Range}. - screenRangeForBufferRange: (bufferRange) -> @displayLayer.translateBufferRange(bufferRange) + screenRangeForBufferRange: (bufferRange, options) -> + bufferRange = Range.fromObject(bufferRange) + start = @screenPositionForBufferPosition(bufferRange.start, options) + end = @screenPositionForBufferPosition(bufferRange.end, options) + new Range(start, end) # Essential: Convert a range in screen-coordinates to buffer-coordinates. # # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. # # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> @displayLayer.translateScreenRange(screenRange) + bufferRangeForScreenRange: (screenRange) -> + screenRange = Range.fromObject(screenRange) + start = @bufferPositionForScreenPosition(screenRange.start) + end = @bufferPositionForScreenPosition(screenRange.end) + new Range(start, end) # Extended: Clip the given {Point} to a valid position in the buffer. # @@ -1510,7 +1587,7 @@ class TextEditor extends Model # # Returns a {Decoration} object decorateMarker: (marker, decorationParams) -> - @displayBuffer.decorateMarker(marker, decorationParams) + @decorationManager.decorateMarker(marker, decorationParams) # Essential: *Experimental:* Add a decoration to every marker in the given # marker layer. Can be used to decorate a large number of markers without @@ -1524,7 +1601,7 @@ class TextEditor extends Model # # Returns a {LayerDecoration}. decorateMarkerLayer: (markerLayer, decorationParams) -> - @displayBuffer.decorateMarkerLayer(markerLayer, decorationParams) + @decorationManager.decorateMarkerLayer(markerLayer, decorationParams) # Deprecated: Get all the decorations within a screen row range on the default # layer. @@ -1538,10 +1615,10 @@ class TextEditor extends Model # params objects attached to the marker. # Returns an empty object when no decorations are found decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> - @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow) + @decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow) decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> - @displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) + @decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) # Extended: Get all decorations. # @@ -1550,7 +1627,7 @@ class TextEditor extends Model # # Returns an {Array} of {Decoration}s. getDecorations: (propertyFilter) -> - @displayBuffer.getDecorations(propertyFilter) + @decorationManager.getDecorations(propertyFilter) # Extended: Get all decorations of type 'line'. # @@ -1559,7 +1636,7 @@ class TextEditor extends Model # # Returns an {Array} of {Decoration}s. getLineDecorations: (propertyFilter) -> - @displayBuffer.getLineDecorations(propertyFilter) + @decorationManager.getLineDecorations(propertyFilter) # Extended: Get all decorations of type 'line-number'. # @@ -1568,7 +1645,7 @@ class TextEditor extends Model # # Returns an {Array} of {Decoration}s. getLineNumberDecorations: (propertyFilter) -> - @displayBuffer.getLineNumberDecorations(propertyFilter) + @decorationManager.getLineNumberDecorations(propertyFilter) # Extended: Get all decorations of type 'highlight'. # @@ -1577,7 +1654,7 @@ class TextEditor extends Model # # Returns an {Array} of {Decoration}s. getHighlightDecorations: (propertyFilter) -> - @displayBuffer.getHighlightDecorations(propertyFilter) + @decorationManager.getHighlightDecorations(propertyFilter) # Extended: Get all decorations of type 'overlay'. # @@ -1586,13 +1663,13 @@ class TextEditor extends Model # # Returns an {Array} of {Decoration}s. getOverlayDecorations: (propertyFilter) -> - @displayBuffer.getOverlayDecorations(propertyFilter) + @decorationManager.getOverlayDecorations(propertyFilter) decorationForId: (id) -> - @displayBuffer.decorationForId(id) + @decorationManager.decorationForId(id) decorationsForMarkerId: (id) -> - @displayBuffer.decorationsForMarkerId(id) + @decorationManager.decorationsForMarkerId(id) ### Section: Markers @@ -1630,8 +1707,8 @@ class TextEditor extends Model # start or start at the marker's end. This is the most fragile strategy. # # Returns a {DisplayMarker}. - markBufferRange: (args...) -> - @displayBuffer.markBufferRange(args...) + markBufferRange: (bufferRange, options) -> + @defaultMarkerLayer.markBufferRange(bufferRange, options) # 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 @@ -1665,8 +1742,8 @@ class TextEditor extends Model # start or start at the marker's end. This is the most fragile strategy. # # Returns a {DisplayMarker}. - markScreenRange: (args...) -> - @displayBuffer.markScreenRange(args...) + markScreenRange: (screenRange, options) -> + @defaultMarkerLayer.markScreenRange(screenRange, options) # Essential: Mark the given position in buffer coordinates on the default # marker layer. @@ -1675,8 +1752,8 @@ class TextEditor extends Model # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayMarker}. - markBufferPosition: (args...) -> - @displayBuffer.markBufferPosition(args...) + markBufferPosition: (bufferPosition, options) -> + @defaultMarkerLayer.markBufferPosition(bufferPosition, options) # Essential: Mark the given position in screen coordinates on the default # marker layer. @@ -1685,8 +1762,8 @@ class TextEditor extends Model # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayMarker}. - markScreenPosition: (args...) -> - @displayBuffer.markScreenPosition(args...) + markScreenPosition: (screenPosition, options) -> + @defaultMarkerLayer.markScreenPosition(screenPosition, options) # Essential: Find all {DisplayMarker}s on the default marker layer that # match the given properties. @@ -1708,20 +1785,22 @@ class TextEditor extends Model # in range-compatible {Array} in buffer coordinates. # * `containsBufferPosition` Only include markers containing this {Point} # or {Array} of `[row, column]` in buffer coordinates. - findMarkers: (properties) -> - @displayBuffer.findMarkers(properties) + # + # Returns an {Array} of {DisplayMarker}s + findMarkers: (params) -> + @defaultMarkerLayer.findMarkers(params) # Extended: Get the {DisplayMarker} on the default layer for the given # marker id. # # * `id` {Number} id of the marker getMarker: (id) -> - @displayBuffer.getMarker(id) + @defaultMarkerLayer.getMarker(id) # Extended: Get all {DisplayMarker}s on the default marker layer. Consider # using {::findMarkers} getMarkers: -> - @displayBuffer.getMarkers() + @defaultMarkerLayer.getMarkers() # Extended: Get the number of markers in the default marker layer. # @@ -1742,7 +1821,7 @@ class TextEditor extends Model # # Returns a {DisplayMarkerLayer}. addMarkerLayer: (options) -> - @displayBuffer.addMarkerLayer(options) + @displayLayer.addMarkerLayer(options) # Public: *Experimental:* Get a {DisplayMarkerLayer} by id. # @@ -1753,7 +1832,7 @@ class TextEditor extends Model # Returns a {MarkerLayer} or `undefined` if no layer exists with the given # id. getMarkerLayer: (id) -> - @displayBuffer.getMarkerLayer(id) + @displayLayer.getMarkerLayer(id) # Public: *Experimental:* Get the default {DisplayMarkerLayer}. # @@ -1764,7 +1843,7 @@ class TextEditor extends Model # # Returns a {DisplayMarkerLayer}. getDefaultMarkerLayer: -> - @displayBuffer.getDefaultMarkerLayer() + @defaultMarkerLayer ### Section: Cursors @@ -2576,14 +2655,36 @@ class TextEditor extends Model # Essential: Get the on-screen length of tab characters. # # Returns a {Number}. - getTabLength: -> @displayBuffer.getTabLength() + getTabLength: -> + if @tabLength? + @tabLength + else + @config.get('editor.tabLength', scope: @getRootScopeDescriptor()) # Essential: Set the on-screen length of tab characters. Setting this to a # {Number} This will override the `editor.tabLength` setting. # # * `tabLength` {Number} length of a single tab. Setting to `null` will # fallback to using the `editor.tabLength` config setting - setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) + setTabLength: (tabLength) -> + return if tabLength is @tabLength + + @tabLength = tabLength + @tokenizedBuffer.setTabLength(@tabLength) + @resetDisplayLayer() + + setIgnoreInvisibles: (ignoreInvisibles) -> + return if ignoreInvisibles is @ignoreInvisibles + + @ignoreInvisibles = ignoreInvisibles + @resetDisplayLayer() + + getInvisibles: -> + scopeDescriptor = @getRootScopeDescriptor() + if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles and @showInvisibles + @config.get('editor.invisibles', scope: scopeDescriptor) + else + {} # Extended: Determine if the buffer uses hard or soft tabs. # @@ -2594,7 +2695,7 @@ class TextEditor extends Model # whitespace. usesSoftTabs: -> for bufferRow in [0..@buffer.getLastRow()] - continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() + continue if @tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() line = @buffer.lineForRow(bufferRow) return true if line[0] is ' ' @@ -2637,14 +2738,27 @@ class TextEditor extends Model # Essential: Determine whether lines in this editor are soft-wrapped. # # Returns a {Boolean}. - isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped() + isSoftWrapped: -> + if @largeFileMode + false + else + scopeDescriptor = @getRootScopeDescriptor() + @softWrapped ? @config.get('editor.softWrap', scope: scopeDescriptor) ? false # Essential: Enable or disable soft wrapping for this editor. # # * `softWrapped` A {Boolean} # # Returns a {Boolean}. - setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped) + setSoftWrapped: (softWrapped) -> + if softWrapped isnt @softWrapped + @softWrapped = softWrapped + @resetDisplayLayer() + softWrapped = @isSoftWrapped() + @emitter.emit 'did-change-soft-wrapped', softWrapped + softWrapped + else + @isSoftWrapped() # Essential: Toggle soft wrapping for this editor # @@ -2652,7 +2766,15 @@ class TextEditor extends Model toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) # Essential: Gets the column at which column will soft wrap - getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() + getSoftWrapColumn: -> + scopeDescriptor = @getRootScopeDescriptor() + if @isSoftWrapped() + if @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) + @config.get('editor.preferredLineLength', scope: scopeDescriptor) + else + @getEditorWidthInChars() + else + Infinity ### Section: Indentation @@ -2710,7 +2832,7 @@ class TextEditor extends Model # # Returns a {Number}. indentLevelForLine: (line) -> - @displayBuffer.indentLevelForLine(line) + @tokenizedBuffer.indentLevelForLine(line) # Extended: Indent rows intersecting selections based on the grammar's suggested # indent level. @@ -2738,7 +2860,7 @@ class TextEditor extends Model # Essential: Get the current {Grammar} of this editor. getGrammar: -> - @displayBuffer.getGrammar() + @tokenizedBuffer.grammar # Essential: Set the current {Grammar} of this editor. # @@ -2747,11 +2869,11 @@ class TextEditor extends Model # # * `grammar` {Grammar} setGrammar: (grammar) -> - @displayBuffer.setGrammar(grammar) + @tokenizedBuffer.setGrammar(grammar) # Reload the grammar based on the file name. reloadGrammar: -> - @displayBuffer.reloadGrammar() + @tokenizedBuffer.reloadGrammar() ### Section: Managing Syntax Scopes @@ -2761,7 +2883,7 @@ class TextEditor extends Model # e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with # {Config::get} to get language specific config values. getRootScopeDescriptor: -> - @displayBuffer.getRootScopeDescriptor() + @tokenizedBuffer.rootScopeDescriptor # Essential: Get the syntactic scopeDescriptor for the given position in buffer # coordinates. Useful with {Config::get}. @@ -2774,7 +2896,7 @@ class TextEditor extends Model # # Returns a {ScopeDescriptor}. scopeDescriptorForBufferPosition: (bufferPosition) -> - @displayBuffer.scopeDescriptorForBufferPosition(bufferPosition) + @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) # Extended: Get the range in buffer coordinates of all tokens surrounding the # cursor that match the given scope selector. @@ -2786,7 +2908,7 @@ class TextEditor extends Model # # Returns a {Range}. bufferRangeForScopeAtCursor: (scopeSelector) -> - @displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) + @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) # Extended: Determine if the given row is entirely a comment isBufferRowCommented: (bufferRow) -> @@ -2802,8 +2924,8 @@ class TextEditor extends Model @notificationManager.addInfo(content, dismissable: true) - # {Delegates to: DisplayBuffer.tokenForBufferPosition} - tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) + tokenForBufferPosition: (bufferPosition) -> + @tokenizedBuffer.tokenForPosition(bufferPosition) ### Section: Clipboard Operations @@ -2935,7 +3057,7 @@ class TextEditor extends Model # # * `bufferRow` A {Number} unfoldBufferRow: (bufferRow) -> - @displayBuffer.unfoldBufferRow(bufferRow) + @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))) # Extended: For each selection, fold the rows it intersects. foldSelectedLines: -> @@ -2965,7 +3087,7 @@ class TextEditor extends Model # # Returns a {Boolean}. isFoldableAtBufferRow: (bufferRow) -> - @displayBuffer.isFoldableAtBufferRow(bufferRow) + @tokenizedBuffer.isFoldableAtRow(bufferRow) # Extended: Determine whether the given row in screen coordinates is foldable. # @@ -2997,7 +3119,7 @@ class TextEditor extends Model # # Returns a {Boolean}. isFoldedAtBufferRow: (bufferRow) -> - @displayBuffer.isFoldedAtBufferRow(bufferRow) + @displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0 # Extended: Determine whether the given row in screen coordinates is folded. # @@ -3005,10 +3127,16 @@ class TextEditor extends Model # # Returns a {Boolean}. isFoldedAtScreenRow: (screenRow) -> - @displayBuffer.isFoldedAtScreenRow(screenRow) + @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow)) + # Creates a new fold between two row numbers. + # + # startRow - The row {Number} to start folding at + # endRow - The row {Number} to end the fold + # + # Returns the new {Fold}. foldBufferRowRange: (startRow, endRow) -> - @displayBuffer.foldBufferRowRange(startRow, endRow) + @foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) foldBufferRange: (range) -> @displayLayer.foldBufferRange(range) @@ -3066,7 +3194,7 @@ class TextEditor extends Model # * `options` (optional) {Object} # * `center` Center the editor around the position if possible. (default: false) scrollToBufferPosition: (bufferPosition, options) -> - @displayBuffer.scrollToBufferPosition(bufferPosition, options) + @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options) # Essential: Scrolls the editor to the given screen position. # @@ -3075,7 +3203,7 @@ class TextEditor extends Model # * `options` (optional) {Object} # * `center` Center the editor around the position if possible. (default: false) scrollToScreenPosition: (screenPosition, options) -> - @displayBuffer.scrollToScreenPosition(screenPosition, options) + @scrollToScreenRange(new Range(screenPosition, screenPosition), options) scrollToTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") @@ -3087,7 +3215,9 @@ class TextEditor extends Model @viewRegistry.getView(this).scrollToBottom() - scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) + scrollToScreenRange: (screenRange, options = {}) -> + scrollEvent = {screenRange, options} + @emitter.emit "did-request-autoscroll", scrollEvent getHorizontalScrollbarHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") @@ -3173,48 +3303,69 @@ class TextEditor extends Model getSelectionMarkerAttributes: -> {type: 'selection', invalidate: 'never'} - getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin() - setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin) + getVerticalScrollMargin: -> + maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2) + Math.min(@verticalScrollMargin, maxScrollMargin) - getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin() - setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin) + setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels() - setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels) + getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2)) + setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin - getKoreanCharWidth: -> @displayBuffer.getKoreanCharWidth() + getLineHeightInPixels: -> @lineHeightInPixels + setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels - getHalfWidthCharWidth: -> @displayBuffer.getHalfWidthCharWidth() + getKoreanCharWidth: -> @koreanCharWidth + getHalfWidthCharWidth: -> @halfWidthCharWidth + getDoubleWidthCharWidth: -> @doubleWidthCharWidth + getDefaultCharWidth: -> @defaultCharWidth - getDoubleWidthCharWidth: -> @displayBuffer.getDoubleWidthCharWidth() + ratioForCharacter: (character) -> + if isKoreanCharacter(character) + @getKoreanCharWidth() / @getDefaultCharWidth() + else if isHalfWidthCharacter(character) + @getHalfWidthCharWidth() / @getDefaultCharWidth() + else if isDoubleWidthCharacter(character) + @getDoubleWidthCharWidth() / @getDefaultCharWidth() + else + 1 - getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth() setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> - @displayBuffer.setDefaultCharWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) + doubleWidthCharWidth ?= defaultCharWidth + halfWidthCharWidth ?= defaultCharWidth + koreanCharWidth ?= defaultCharWidth + if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth + @defaultCharWidth = defaultCharWidth + @doubleWidthCharWidth = doubleWidthCharWidth + @halfWidthCharWidth = halfWidthCharWidth + @koreanCharWidth = koreanCharWidth + @resetDisplayLayer() if @isSoftWrapped() and @getEditorWidthInChars()? + defaultCharWidth setHeight: (height, reentrant=false) -> if reentrant - @displayBuffer.setHeight(height) + @height = height else Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") @viewRegistry.getView(this).setHeight(height) getHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") - @displayBuffer.getHeight() - - getClientHeight: -> @displayBuffer.getClientHeight() + @height setWidth: (width, reentrant=false) -> if reentrant - @displayBuffer.setWidth(width) + oldWidth = @width + @width = width + @resetDisplayLayer() if width isnt oldWidth and @isSoftWrapped() + @width else Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") @viewRegistry.getView(this).setWidth(width) getWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") - @displayBuffer.getWidth() + @width # Experimental: Scroll the editor such that the given screen row is at the # top of the visible area. @@ -3222,10 +3373,8 @@ class TextEditor extends Model unless fromView maxScreenRow = @getScreenLineCount() - 1 unless @config.get('editor.scrollPastEnd') and @scrollPastEnd - height = @displayBuffer.getHeight() - lineHeightInPixels = @displayBuffer.getLineHeightInPixels() - if height? and lineHeightInPixels? - maxScreenRow -= Math.floor(height / lineHeightInPixels) + if @height? and @lineHeightInPixels? + maxScreenRow -= Math.floor(@height / @lineHeightInPixels) screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0) unless screenRow is @firstVisibleScreenRow @@ -3235,10 +3384,8 @@ class TextEditor extends Model getFirstVisibleScreenRow: -> @firstVisibleScreenRow getLastVisibleScreenRow: -> - height = @displayBuffer.getHeight() - lineHeightInPixels = @displayBuffer.getLineHeightInPixels() - if height? and lineHeightInPixels? - Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getScreenLineCount() - 1) + if @height? and @lineHeightInPixels? + Math.min(@firstVisibleScreenRow + Math.floor(@height / @lineHeightInPixels), @getScreenLineCount() - 1) else null @@ -3333,8 +3480,6 @@ class TextEditor extends Model inspect: -> "" - logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) - emitWillInsertTextEvent: (text) -> result = true cancel = -> result = false From ebd9e71b0bb501c2c404a552473f2bed185030df Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Apr 2016 18:48:04 +0200 Subject: [PATCH 083/262] Make serialization format backwards/forwards compatible Also, introduce a serialization version so we can drop the compatibility fallbacks in the future. --- src/text-editor.coffee | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 40dd451d7..6f39c0699 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -59,6 +59,8 @@ ZERO_WIDTH_NBSP = '\ufeff' # soft wraps and folds to ensure your code interacts with them correctly. module.exports = class TextEditor extends Model + serializationVersion: 1 + buffer: null languageMode: null cursors: null @@ -80,6 +82,10 @@ class TextEditor extends Model get: -> @getElement() @deserialize: (state, atomEnvironment) -> + # TODO: Return null on version mismatch when 1.8.0 has been out for a while + if state.version isnt @prototype.serializationVersion and state.displayBuffer? + state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer + try state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) catch error @@ -89,7 +95,7 @@ class TextEditor extends Model throw error state.buffer = state.tokenizedBuffer.buffer - state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId) + state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId) ? state.buffer.addDisplayLayer() state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config state.notificationManager = atomEnvironment.notifications @@ -188,7 +194,10 @@ class TextEditor extends Model @setGrammar(grammar) serialize: -> + tokenizedBufferState = @tokenizedBuffer.serialize() + deserializer: 'TextEditor' + version: @serializationVersion id: @id softTabs: @softTabs firstVisibleScreenRow: @getFirstVisibleScreenRow() @@ -196,7 +205,9 @@ class TextEditor extends Model selectionsMarkerLayerId: @selectionsMarkerLayer.id softWrapped: @isSoftWrapped() editorWidthInChars: @editorWidthInChars - tokenizedBuffer: @tokenizedBuffer.serialize() + # TODO: Remove this forward-compatible fallback once 1.8 reaches stable. + displayBuffer: {tokenizedBuffer: tokenizedBufferState} + tokenizedBuffer: tokenizedBufferState largeFileMode: @largeFileMode displayLayerId: @displayLayer.id registered: atom.textEditors.editors.has this From 117a3fb1b57465e3164c378219eefcca4b5c22a5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Apr 2016 12:11:11 -0600 Subject: [PATCH 084/262] Provide deprecated TextEditor.prototype.displayBuffer shim Even though it was a private field, we used it in some of our packages and it is widespread enough to warrant some effort to smooth the transition. Also, we added a couple methods that were formerly implemented on DisplayBuffer to TextEditor itself because they were used in packages. --- src/text-editor.coffee | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6f39c0699..641e9f9da 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -81,6 +81,16 @@ class TextEditor extends Model Object.defineProperty @prototype, "element", get: -> @getElement() + Object.defineProperty(@prototype, 'displayBuffer', get: -> + Grim.deprecate(""" + `TextEditor.prototype.displayBuffer` has always been private, but now + it is gone. Reading the `displayBuffer` property now returns a reference + to the containing `TextEditor`, which now provides *some* of the API of + the defunct `DisplayBuffer` class. + """) + this + ) + @deserialize: (state, atomEnvironment) -> # TODO: Return null on version mismatch when 1.8.0 has been out for a while if state.version isnt @prototype.serializationVersion and state.displayBuffer? @@ -2886,6 +2896,10 @@ class TextEditor extends Model reloadGrammar: -> @tokenizedBuffer.reloadGrammar() + # Experimental: Get a notification when async tokenization is completed. + onDidTokenize: (callback) -> + @tokenizedBuffer.onDidTokenize(callback) + ### Section: Managing Syntax Scopes ### @@ -2919,7 +2933,10 @@ class TextEditor extends Model # # Returns a {Range}. bufferRangeForScopeAtCursor: (scopeSelector) -> - @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) + @bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) + + bufferRangeForScopeAtPosition: (scopeSelector, position) -> + @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position) # Extended: Determine if the given row is entirely a comment isBufferRowCommented: (bufferRow) -> From a790ae21586826ff4adc95b6a89281300ead9fd0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Apr 2016 12:12:14 -0600 Subject: [PATCH 085/262] Avoid deprecation warnings for methods that are now on TextEditor --- src/text-editor.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 641e9f9da..20d09b590 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -676,9 +676,8 @@ class TextEditor extends Model # Returns the editor width in characters. getEditorWidthInChars: -> - width = @getWidth() - if width? and @defaultCharWidth > 0 - Math.max(0, Math.floor(width / @defaultCharWidth)) + if @width? and @defaultCharWidth > 0 + Math.max(0, Math.floor(@width / @defaultCharWidth)) else @editorWidthInChars @@ -3337,7 +3336,7 @@ class TextEditor extends Model setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2)) + getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@width / @getDefaultCharWidth()) - 1) / 2)) setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin getLineHeightInPixels: -> @lineHeightInPixels From b9222628718a806f1f3f24c4d2bd8565bb1cd096 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Apr 2016 17:18:24 -0600 Subject: [PATCH 086/262] Avoid internal deprecation warning on getHeight --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 20d09b590..a7638afe9 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3331,7 +3331,7 @@ class TextEditor extends Model {type: 'selection', invalidate: 'never'} getVerticalScrollMargin: -> - maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2) + maxScrollMargin = Math.floor(((@height / @getLineHeightInPixels()) - 1) / 2) Math.min(@verticalScrollMargin, maxScrollMargin) setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin From af28fa075238cc58d721df5a5004ea807698eede Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Apr 2016 18:47:11 -0600 Subject: [PATCH 087/262] When copying, create selections marker layer on the *new* display layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we were copying the selections marker layer into the current editor’s display layer. This would work fine until the spatial mapping drifted in the copied editor, and would then have counterintuitive results. --- src/text-editor.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a7638afe9..7d4ad14ef 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -591,14 +591,15 @@ class TextEditor extends Model # Create an {TextEditor} with its initial state based on this object copy: -> - selectionsMarkerLayer = @getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) + displayLayer = @displayLayer.copy() + selectionsMarkerLayer = displayLayer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) softTabs = @getSoftTabs() newEditor = new TextEditor({ @buffer, selectionsMarkerLayer, @tabLength, softTabs, suppressCursorCreation: true, @config, @notificationManager, @packageManager, @firstVisibleScreenRow, @firstVisibleScreenColumn, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate, - displayLayer: @buffer.copyDisplayLayer(@displayLayer.id) + displayLayer }) newEditor From 66b9383c4e088327c4ee82c87bb1556fccee75f9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 14:20:27 +0200 Subject: [PATCH 088/262] Use clipDirection instead of the clip option --- src/selection.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/selection.coffee b/src/selection.coffee index 7f77ad6c0..69aee901a 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -392,7 +392,7 @@ class Selection extends Model if options.select @setBufferRange(newBufferRange, reversed: wasReversed) else - @cursor.setBufferPosition(newBufferRange.end, clip: 'forward') if wasReversed + @cursor.setBufferPosition(newBufferRange.end, clipDirection: 'forward') if wasReversed if autoIndentFirstLine @editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel) From 00f03fd614c32ee88c4ebbb946506fbfd22c8e27 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 14:21:06 +0200 Subject: [PATCH 089/262] :memo: Document {clipDirection: 'closest'} --- src/text-editor.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 7d4ad14ef..f828b0e84 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1515,8 +1515,9 @@ class TextEditor extends Model # * `options` (optional) {Object} # * `clipDirection` {String} If `'backward'`, returns the first valid # position preceding an invalid position. If `'forward'`, returns the - # first valid position following a valid position. Defaults to - # `'backward'`. + # first valid position following an invalid position. If `'closest'`, + # returns the first valid position closest to an invalid position. + # Defaults to `'closest'`. # # Returns a {Point}. clipScreenPosition: (screenPosition, options) -> @displayLayer.clipScreenPosition(screenPosition, options) From 3ec1027d5ca2b878008ef0b4da827f9209b24bc6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 14:38:53 +0200 Subject: [PATCH 090/262] Remove clipDirection parameter in a call to cursor.setBufferPosition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We aren’t clipping in that code path, so we might as well avoid to pass the parameter. --- src/selection.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/selection.coffee b/src/selection.coffee index 69aee901a..bd0963974 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -392,7 +392,7 @@ class Selection extends Model if options.select @setBufferRange(newBufferRange, reversed: wasReversed) else - @cursor.setBufferPosition(newBufferRange.end, clipDirection: 'forward') if wasReversed + @cursor.setBufferPosition(newBufferRange.end) if wasReversed if autoIndentFirstLine @editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel) From 25b71b804df0d7f304067f7988e0a0232c21225c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 14:44:26 +0200 Subject: [PATCH 091/262] Un-document passing clip options to setCursorBufferPosition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …the marker’s screen position will be clipped automatically when translating coordinates. --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index f828b0e84..a4f0ce096 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1889,7 +1889,7 @@ class TextEditor extends Model # If there are multiple cursors, they will be consolidated to a single cursor. # # * `position` A {Point} or {Array} of `[row, column]` - # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: + # * `options` (optional) An {Object} containing the following keys: # * `autoscroll` Determines whether the editor scrolls to the new cursor's # position. Defaults to true. setCursorBufferPosition: (position, options) -> From ffd3b1829d741c187eaeb921219405e8b74709bf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 15:42:53 +0200 Subject: [PATCH 092/262] Provide deprecation warnings for clipScreenPosition parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on a cursory search, there’s only one package using that API. Even though we could submit a PR, we’re introducing deprecations to ensure we're not missing any other package. --- src/text-editor.coffee | 43 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a4f0ce096..79f2107ac 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1431,6 +1431,16 @@ class TextEditor extends Model screenPositionForBufferPosition: (bufferPosition, options) -> throw new Error("This TextEditor has been destroyed") if @isDestroyed() + if options?.clip? + Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") + options.clipDirection ?= options.clip + if options?.wrapAtSoftNewlines? + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' + if options?.wrapBeyondNewlines? + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' + @displayLayer.translateBufferPosition(bufferPosition, options) # Essential: Convert a position in screen-coordinates to buffer-coordinates. @@ -1442,6 +1452,16 @@ class TextEditor extends Model # # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> + if options?.clip? + Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") + options.clipDirection ?= options.clip + if options?.wrapAtSoftNewlines? + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' + if options?.wrapBeyondNewlines? + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' + @displayLayer.translateScreenPosition(screenPosition, options) # Essential: Convert a range in buffer-coordinates to screen-coordinates. @@ -1520,7 +1540,18 @@ class TextEditor extends Model # Defaults to `'closest'`. # # Returns a {Point}. - clipScreenPosition: (screenPosition, options) -> @displayLayer.clipScreenPosition(screenPosition, options) + clipScreenPosition: (screenPosition, options) -> + if options?.clip? + Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") + options.clipDirection ?= options.clip + if options?.wrapAtSoftNewlines? + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' + if options?.wrapBeyondNewlines? + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' + + @displayLayer.clipScreenPosition(screenPosition, options) # Extended: Clip the start and end of the given range to valid positions on screen. # See {::clipScreenPosition} for more information. @@ -1927,6 +1958,16 @@ class TextEditor extends Model # * `autoscroll` Determines whether the editor scrolls to the new cursor's # position. Defaults to true. setCursorScreenPosition: (position, options) -> + if options?.clip? + Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") + options.clipDirection ?= options.clip + if options?.wrapAtSoftNewlines? + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' + if options?.wrapBeyondNewlines? + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' + @moveCursors (cursor) -> cursor.setScreenPosition(position, options) # Essential: Add a cursor at the given position in buffer coordinates. From 4f28f9f3f84c08fe352a24d1a7b0584c699306c1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 15:48:44 +0200 Subject: [PATCH 093/262] :fire: Remove defensive assertion We're not throwing this exception anywhere else, so we might as well delete it from here. /cc: @nathansobo --- src/text-editor.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 79f2107ac..1ebbf97bf 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1429,8 +1429,6 @@ class TextEditor extends Model # # Returns a {Point}. screenPositionForBufferPosition: (bufferPosition, options) -> - throw new Error("This TextEditor has been destroyed") if @isDestroyed() - if options?.clip? Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") options.clipDirection ?= options.clip From c2396396a5fd46bd0e7ee4fb81b2fa26289457c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 19:20:05 +0200 Subject: [PATCH 094/262] :memo: Update documentation for mark*Position --- src/text-editor.coffee | 51 +++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 1ebbf97bf..85f16152a 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1796,21 +1796,56 @@ class TextEditor extends Model markScreenRange: (screenRange, options) -> @defaultMarkerLayer.markScreenRange(screenRange, options) - # Essential: Mark the given position in buffer coordinates on the default - # marker layer. + # Essential: Create a marker on the default marker layer with the given buffer + # position and no tail. To group multiple markers together in their own + # private layer, see {::addMarkerLayer}. # - # * `position` A {Point} or {Array} of `[row, column]`. - # * `options` (optional) See {TextBuffer::markRange}. + # * `bufferPosition` A {Point} or point-compatible {Array} + # * `options` (optional) An {Object} with the following keys: + # * `invalidate` (optional) {String} Determines the rules by which changes + # to the buffer *invalidate* the marker. (default: 'overlap') It can be + # any of the following strategies, in order of fragility: + # * __never__: The marker is never marked as invalid. This is a good choice for + # markers representing selections in an editor. + # * __surround__: The marker is invalidated by changes that completely surround it. + # * __overlap__: The marker is invalidated by changes that surround the + # start or end of the marker. This is the default. + # * __inside__: The marker is invalidated by changes that extend into the + # inside of the marker. Changes that end at the marker's start or + # start at the marker's end do not invalidate the marker. + # * __touch__: The marker is invalidated by a change that touches the marked + # 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 {DisplayMarker}. markBufferPosition: (bufferPosition, options) -> @defaultMarkerLayer.markBufferPosition(bufferPosition, options) - # Essential: Mark the given position in screen coordinates on the default - # marker layer. + # Essential: Create a marker on the default marker layer with the given screen + # position and no tail. To group multiple markers together in their own + # private layer, see {::addMarkerLayer}. # - # * `position` A {Point} or {Array} of `[row, column]`. - # * `options` (optional) See {TextBuffer::markRange}. + # * `screenPosition` A {Point} or point-compatible {Array} + # * `options` (optional) An {Object} with the following keys: + # * `invalidate` (optional) {String} Determines the rules by which changes + # to the buffer *invalidate* the marker. (default: 'overlap') It can be + # any of the following strategies, in order of fragility: + # * __never__: The marker is never marked as invalid. This is a good choice for + # markers representing selections in an editor. + # * __surround__: The marker is invalidated by changes that completely surround it. + # * __overlap__: The marker is invalidated by changes that surround the + # start or end of the marker. This is the default. + # * __inside__: The marker is invalidated by changes that extend into the + # inside of the marker. Changes that end at the marker's start or + # start at the marker's end do not invalidate the marker. + # * __touch__: The marker is invalidated by a change that touches the marked + # 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. + # * `clipDirection` {String} If `'backward'`, returns the first valid + # position preceding an invalid position. If `'forward'`, returns the + # first valid position following an invalid position. If `'closest'`, + # returns the first valid position closest to an invalid position. + # Defaults to `'closest'`. # # Returns a {DisplayMarker}. markScreenPosition: (screenPosition, options) -> From e236b8c3be6eca2088fed58c4d54e921f11c6c97 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Apr 2016 20:00:08 +0200 Subject: [PATCH 095/262] Avoid storing properties in selection markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously we were storing a {type: ‘selection’} property, probably leftover from the “pre-marker-layer” world. We were also storing `Selection`’s `goalScreenRange` on the marker, which is now just an instance variable. --- src/selection.coffee | 22 +++++++++++++--------- src/text-editor.coffee | 11 ++++------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/selection.coffee b/src/selection.coffee index bd0963974..bd15d1543 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -174,7 +174,7 @@ class Selection extends Model # range. Defaults to `true` if this is the most recently added selection, # `false` otherwise. clear: (options) -> - @marker.setProperties(goalScreenRange: null) + @goalScreenRange = null @marker.clearTail() unless @retainSelection @autoscroll() if options?.autoscroll ? @isLastSelection() @finalize() @@ -682,7 +682,7 @@ class Selection extends Model # Public: Moves the selection down one row. addSelectionBelow: -> - range = (@getGoalScreenRange() ? @getScreenRange()).copy() + range = @getGoalScreenRange().copy() nextRow = range.end.row + 1 for row in [nextRow..@editor.getLastScreenRow()] @@ -695,14 +695,15 @@ class Selection extends Model else continue if clippedRange.isEmpty() - @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range) + selection = @editor.addSelectionForScreenRange(clippedRange) + selection.setGoalScreenRange(range) break return # Public: Moves the selection up one row. addSelectionAbove: -> - range = (@getGoalScreenRange() ? @getScreenRange()).copy() + range = @getGoalScreenRange().copy() previousRow = range.end.row - 1 for row in [previousRow..0] @@ -715,7 +716,8 @@ class Selection extends Model else continue if clippedRange.isEmpty() - @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range) + selection = @editor.addSelectionForScreenRange(clippedRange) + selection.setGoalScreenRange(range) break return @@ -754,6 +756,12 @@ class Selection extends Model Section: Private Utilities ### + setGoalScreenRange: (range) -> + @goalScreenRange = Range.fromObject(range) + + getGoalScreenRange: -> + @goalScreenRange ? @getScreenRange() + markerDidChange: (e) -> {oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e @@ -824,7 +832,3 @@ class Selection extends Model # Returns a {Point} representing the new tail position. plantTail: -> @marker.plantTail() - - getGoalScreenRange: -> - if goalScreenRange = @marker.getProperties().goalScreenRange - Range.fromObject(goalScreenRange) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 85f16152a..cd960f2e8 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2009,7 +2009,7 @@ class TextEditor extends Model # # Returns a {Cursor}. addCursorAtBufferPosition: (bufferPosition, options) -> - @selectionsMarkerLayer.markBufferPosition(bufferPosition, @getSelectionMarkerAttributes()) + @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor @@ -2019,7 +2019,7 @@ class TextEditor extends Model # # Returns a {Cursor}. addCursorAtScreenPosition: (screenPosition, options) -> - @selectionsMarkerLayer.markScreenPosition(screenPosition, @getSelectionMarkerAttributes()) + @selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'}) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor @@ -2305,7 +2305,7 @@ class TextEditor extends Model # # Returns the added {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> - @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) + @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults({invalidate: 'never'}, options)) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -2318,7 +2318,7 @@ class TextEditor extends Model # # Returns the added {Selection}. addSelectionForScreenRange: (screenRange, options={}) -> - @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options)) + @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults({invalidate: 'never'}, options)) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -3403,9 +3403,6 @@ class TextEditor extends Model Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition) - getSelectionMarkerAttributes: -> - {type: 'selection', invalidate: 'never'} - getVerticalScrollMargin: -> maxScrollMargin = Math.floor(((@height / @getLineHeightInPixels()) - 1) / 2) Math.min(@verticalScrollMargin, maxScrollMargin) From b0c485c4ea4a3355e0ba866f0f1f447a5beb48c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Apr 2016 11:48:18 +0200 Subject: [PATCH 096/262] :fire: Remove TokenizedLine specs --- spec/tokenized-line-spec.coffee | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 spec/tokenized-line-spec.coffee diff --git a/spec/tokenized-line-spec.coffee b/spec/tokenized-line-spec.coffee deleted file mode 100644 index f1dce7b9e..000000000 --- a/spec/tokenized-line-spec.coffee +++ /dev/null @@ -1,19 +0,0 @@ -describe "TokenizedLine", -> - editor = null - - beforeEach -> - waitsForPromise -> atom.packages.activatePackage('language-coffee-script') - - describe "::isOnlyWhitespace()", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('coffee.coffee').then (o) -> editor = o - - it "returns true when the line is only whitespace", -> - expect(editor.tokenizedLineForScreenRow(3).isOnlyWhitespace()).toBe true - expect(editor.tokenizedLineForScreenRow(7).isOnlyWhitespace()).toBe true - expect(editor.tokenizedLineForScreenRow(23).isOnlyWhitespace()).toBe true - - it "returns false when the line is not only whitespace", -> - expect(editor.tokenizedLineForScreenRow(0).isOnlyWhitespace()).toBe false - expect(editor.tokenizedLineForScreenRow(2).isOnlyWhitespace()).toBe false From a532000af45cef34f9bed752f25a29fc4e6ace70 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Apr 2016 12:28:41 +0200 Subject: [PATCH 097/262] Handle only buffer coordinates in TokenIterator --- spec/tokenized-buffer-spec.coffee | 24 ++++++++++ src/lines-tile-component.coffee | 1 - src/lines-yardstick.coffee | 1 - src/token-iterator.coffee | 77 +++++++++++++------------------ src/token.coffee | 2 +- 5 files changed, 56 insertions(+), 49 deletions(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index cb9378c4e..ee418a386 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -849,3 +849,27 @@ describe "TokenizedBuffer", -> iterator.seek(Point(0, 8)) expect(iterator.getPosition().column).toBe(7) + + it "correctly terminates scopes at the beginning of the line (regression)", -> + grammar = atom.grammars.createGrammar('test', { + 'scopeName': 'text.broken' + 'name': 'Broken grammar' + 'patterns': [ + {'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'}, + {'match': '.', 'name': 'yellow.broken'} + ] + }) + + buffer = new TextBuffer(text: 'start x\nend x\nx') + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) + tokenizedBuffer.setGrammar(grammar) + fullyTokenize(tokenizedBuffer) + + iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(1, 0)) + + expect(iterator.getPosition()).toEqual([1, 0]) + expect(iterator.getCloseTags()).toEqual ['blue.broken'] + expect(iterator.getOpenTags()).toEqual ['yellow.broken'] diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 6e9a15e6b..d006daa86 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -1,7 +1,6 @@ _ = require 'underscore-plus' HighlightsComponent = require './highlights-component' -TokenIterator = require './token-iterator' AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} TokenTextEscapeRegex = /[&"'<>]/g MaxTokenLength = 20000 diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index f53b6348e..17af35f74 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -1,4 +1,3 @@ -TokenIterator = require './token-iterator' {Point} = require 'text-buffer' {isPairedCharacter} = require './text-utils' diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee index 259bfd346..f9af1e4ca 100644 --- a/src/token-iterator.coffee +++ b/src/token-iterator.coffee @@ -1,72 +1,57 @@ module.exports = class TokenIterator - constructor: ({@grammarRegistry}, line, enableScopes) -> - @reset(line, enableScopes) if line? + constructor: ({@grammarRegistry}, line) -> + @reset(line) if line? - reset: (@line, @enableScopes=true) -> + reset: (@line) -> @index = null - @bufferStart = @line.startBufferColumn - @bufferEnd = @bufferStart - @screenStart = 0 - @screenEnd = 0 - @resetScopes() if @enableScopes + @startColumn = 0 + @endColumn = 0 + @scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id) + @scopeStarts = @scopes.slice() + @scopeEnds = [] this next: -> {tags} = @line if @index? + @startColumn = @endColumn + @scopeEnds.length = 0 + @scopeStarts.length = 0 @index++ - @bufferStart = @bufferEnd - @screenStart = @screenEnd - @clearScopeStartsAndEnds() if @enableScopes else @index = 0 while @index < tags.length tag = tags[@index] if tag < 0 - @handleScopeForTag(tag) if @enableScopes + scope = @grammarRegistry.scopeForId(tag) + if tag % 2 is 0 + if @scopeStarts[@scopeStarts.length - 1] is scope + @scopeStarts.pop() + else + @scopeEnds.push(scope) + @scopes.pop() + else + @scopeStarts.push(scope) + @scopes.push(scope) @index++ else - @screenEnd = @screenStart + tag - @bufferEnd = @bufferStart + tag - - @text = @line.text.substring(@screenStart, @screenEnd) + @endColumn += tag + @text = @line.text.substring(@startColumn, @endColumn) return true false - resetScopes: -> - @scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id) - @scopeStarts = @scopes.slice() - @scopeEnds = [] - - clearScopeStartsAndEnds: -> - @scopeEnds.length = 0 - @scopeStarts.length = 0 - - handleScopeForTag: (tag) -> - scope = @grammarRegistry.scopeForId(tag) - if tag % 2 is 0 - if @scopeStarts[@scopeStarts.length - 1] is scope - @scopeStarts.pop() - else - @scopeEnds.push(scope) - @scopes.pop() - else - @scopeStarts.push(scope) - @scopes.push(scope) - - getBufferStart: -> @bufferStart - getBufferEnd: -> @bufferEnd - - getScreenStart: -> @screenStart - getScreenEnd: -> @screenEnd - - getScopeStarts: -> @scopeStarts - getScopeEnds: -> @scopeEnds - getScopes: -> @scopes + getScopeStarts: -> @scopeStarts + + getScopeEnds: -> @scopeEnds + getText: -> @text + + getBufferStart: -> @startColumn + + getBufferEnd: -> @endColumn diff --git a/src/token.coffee b/src/token.coffee index 5e1f1e811..d531ba04a 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -14,7 +14,7 @@ class Token isEqual: (other) -> # TODO: scopes is deprecated. This is here for the sake of lang package tests - @value is other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic is !!other.isAtomic + @value is other.value and _.isEqual(@scopes, other.scopes) isBracket: -> /^meta\.brace\b/.test(_.last(@scopes)) From c23ef9a168c7f05655f8c3c944c117013c83d898 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Apr 2016 13:54:13 +0200 Subject: [PATCH 098/262] Stop using tokenizedLineForScreenRow in random-editor-spec.coffee --- spec/random-editor-spec.coffee | 7 ++++--- src/text-editor.coffee | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee index 06c50e80f..cada2fe22 100644 --- a/spec/random-editor-spec.coffee +++ b/spec/random-editor-spec.coffee @@ -33,8 +33,8 @@ describe "TextEditor", -> logLines() throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", ) - actualScreenLine = editor.tokenizedLineForScreenRow(screenRow) - unless actualScreenLine.text is referenceScreenLine.text + actualScreenLine = editor.lineTextForScreenRow(screenRow) + unless actualScreenLine is referenceScreenLine logLines() throw new Error("Invalid line text at screen row #{screenRow}") @@ -84,7 +84,8 @@ describe "TextEditor", -> referenceEditor.setEditorWidthInChars(80) referenceEditor.setText(editor.getText()) referenceEditor.setSoftWrapped(editor.isSoftWrapped()) - screenLines = referenceEditor.tokenizedLinesForScreenRows(0, referenceEditor.getLastScreenRow()) + + screenLines = [0..referenceEditor.getLastScreenRow()].map (row) => referenceEditor.lineTextForScreenRow(row) bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow()) {screenLines, bufferRows} diff --git a/src/text-editor.coffee b/src/text-editor.coffee index cd960f2e8..142405487 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -861,6 +861,12 @@ class TextEditor extends Model lineTextForScreenRow: (screenRow) -> @screenLineForScreenRow(screenRow)?.lineText + logScreenLines: (start=0, end=@getLastScreenRow()) -> + for row in [start..end] + line = @lineTextForScreenRow(row) + console.log row, @bufferRowForScreenRow(row), line, line.length + return + tokensForScreenRow: (screenRow) -> for tagCode in @screenLineForScreenRow(screenRow).tagCodes when @displayLayer.isOpenTagCode(tagCode) @displayLayer.tagForCode(tagCode) From afdd8d2b6dce2de309ceafb02dfd5dc20c4f89fb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Apr 2016 16:09:33 +0200 Subject: [PATCH 099/262] Avoid creating line nodes twice in lines-yardstick-spec.coffee --- spec/lines-yardstick-spec.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 15ec2a533..2f7233e2f 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -42,11 +42,12 @@ describe "LinesYardstick", -> lineNode mockLineNodesProvider = + lineNodesById: {} lineIdForScreenRow: (screenRow) -> editor.screenLineForScreenRow(screenRow).id lineNodeForScreenRow: (screenRow) -> - buildLineNode(screenRow) + @lineNodesById[@lineIdForScreenRow(screenRow)] ?= buildLineNode(screenRow) textNodesForScreenRow: (screenRow) -> lineNode = @lineNodeForScreenRow(screenRow) From e587a67f3bd4b3bf479820e9c11ba6ebdd50d756 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Apr 2016 10:22:50 -0600 Subject: [PATCH 100/262] Document persistent option in TextEditor::addMarkerLayer --- src/text-editor.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 142405487..e99ebafb3 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1908,6 +1908,11 @@ class TextEditor extends Model # * `options` An {Object} containing the following keys: # * `maintainHistory` A {Boolean} indicating whether marker state should be # restored on undo/redo. Defaults to `false`. + # * `persistent` A {Boolean} indicating whether or not this marker layer + # should be serialized and deserialized along with the rest of the + # buffer. Defaults to `false`. If `true`, the marker layer's id will be + # maintained across the serialization boundary, allowing you to retrieve + # it via {::getMarkerLayer}. # # This API is experimental and subject to change on any release. # From 37bfbe8538063c2afbe6666a1b88abbc77dacb8f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Apr 2016 10:23:01 -0600 Subject: [PATCH 101/262] Document return value correctly --- src/text-editor.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index e99ebafb3..5e0141d02 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1926,8 +1926,8 @@ class TextEditor extends Model # # This API is experimental and subject to change on any release. # - # Returns a {MarkerLayer} or `undefined` if no layer exists with the given - # id. + # Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the + # given id. getMarkerLayer: (id) -> @displayLayer.getMarkerLayer(id) From 5e5cea96003121a5aa18017eb1f4ed4c07f5d626 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Apr 2016 10:25:25 -0600 Subject: [PATCH 102/262] =?UTF-8?q?Drop=20=E2=80=9Cexperimental=E2=80=9D?= =?UTF-8?q?=20warnings=20from=20marker=20layer=20APIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/text-editor.coffee | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 5e0141d02..403191030 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1646,16 +1646,14 @@ class TextEditor extends Model decorateMarker: (marker, decorationParams) -> @decorationManager.decorateMarker(marker, decorationParams) - # 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. + # Essential: 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 {DisplayMarkerLayer} 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) -> @decorationManager.decorateMarkerLayer(markerLayer, decorationParams) @@ -1903,7 +1901,7 @@ class TextEditor extends Model destroyMarker: (id) -> @getMarker(id)?.destroy() - # Extended: *Experimental:* Create a marker layer to group related markers. + # Essential: Create a marker layer to group related markers. # # * `options` An {Object} containing the following keys: # * `maintainHistory` A {Boolean} indicating whether marker state should be @@ -1914,30 +1912,24 @@ class TextEditor extends Model # maintained across the serialization boundary, allowing you to retrieve # it via {::getMarkerLayer}. # - # This API is experimental and subject to change on any release. - # # Returns a {DisplayMarkerLayer}. addMarkerLayer: (options) -> @displayLayer.addMarkerLayer(options) - # Public: *Experimental:* Get a {DisplayMarkerLayer} by id. + # Essential: Get a {DisplayMarkerLayer} by id. # # * `id` The id of the marker layer to retrieve. # - # This API is experimental and subject to change on any release. - # # Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the # given id. getMarkerLayer: (id) -> @displayLayer.getMarkerLayer(id) - # Public: *Experimental:* Get the default {DisplayMarkerLayer}. + # Essential: Get the default {DisplayMarkerLayer}. # # 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 {DisplayMarkerLayer}. getDefaultMarkerLayer: -> @defaultMarkerLayer From effe882a8d602ad4ab4a5762a772a0a4083c5ecc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Apr 2016 15:30:33 -0600 Subject: [PATCH 103/262] Make atomic leading whitespace optional (defaults to enabled) Closes #3174 --- src/config-schema.coffee | 4 ++++ src/text-editor.coffee | 1 + 2 files changed, 5 insertions(+) diff --git a/src/config-schema.coffee b/src/config-schema.coffee index 346551ff5..ed6691380 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -155,6 +155,10 @@ module.exports = type: 'boolean' default: true description: 'Show line numbers in the editor\'s gutter.' + atomicSoftTabs: + type: 'boolean' + default: true + description: 'Skip over tab-length runs of leading whitespace when moving the cursor.' autoIndent: type: 'boolean' default: true diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 403191030..cb5524e65 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -278,6 +278,7 @@ class TextEditor extends Model invisibles: @getInvisibles(), softWrapColumn: @getSoftWrapColumn(), showIndentGuides: @config.get('editor.showIndentGuide', scope: @getRootScopeDescriptor()), + atomicSoftTabs: @config.get('editor.atomicSoftTabs', scope: @getRootScopeDescriptor()), tabLength: @getTabLength(), ratioForCharacter: @ratioForCharacter.bind(this), isWrapBoundary: isWrapBoundary, From d691eb0e3422970209f63649145886350b1c36cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Apr 2016 16:10:13 -0600 Subject: [PATCH 104/262] :arrow_up: text-buffer@9.0.0-beta2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 689b6f9df..81bfdcbb3 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "8.4.6", + "text-buffer": "9.0.0-beta2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 99ede51e75b71eb085e5cebf0af615529171c677 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 8 Apr 2016 18:16:28 -0600 Subject: [PATCH 105/262] Delete duplicated method --- src/lines-tile-component.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index d006daa86..3cc3accee 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -418,8 +418,5 @@ class LinesTileComponent lineIdForScreenRow: (screenRow) -> @lineIdsByScreenRow[screenRow] - lineNodeForScreenRow: (screenRow) -> - @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] - textNodesForScreenRow: (screenRow) -> @textNodesByLineId[@lineIdsByScreenRow[screenRow]]?.slice() From f3c6a779542ffe67c81fb86768d169a7e78a8d14 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 9 Apr 2016 09:30:47 +0200 Subject: [PATCH 106/262] :green_heart: Use persistent instead of maintainHistory in specs --- spec/project-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 499efd017..7a6168ad5 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -87,7 +87,7 @@ describe "Project", -> runs -> bufferA = atom.project.getBuffers()[0] - layerA = bufferA.addMarkerLayer(maintainHistory: true) + layerA = bufferA.addMarkerLayer(persistent: true) markerA = layerA.markPosition([0, 3]) notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) From ab0d69ce90594c8db38d41e385fa11d73484a3f3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 9 Apr 2016 09:50:12 +0200 Subject: [PATCH 107/262] Make selectionsMarkerLayer also persistent --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index cb5524e65..a580aa7db 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -169,7 +169,7 @@ class TextEditor extends Model @displayLayer ?= @buffer.addDisplayLayer() @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @defaultMarkerLayer = @displayLayer.addMarkerLayer() - @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) + @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) @decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer) From 8cd48004dd68d5ea3e4d4f0fada256aa1b83186d Mon Sep 17 00:00:00 2001 From: Willem Van Lint Date: Sat, 9 Apr 2016 18:42:18 -0700 Subject: [PATCH 108/262] Fixed positioning for overlay --- src/text-editor-presenter.coffee | 12 +++++++----- static/text-editor-light.less | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ef1b403c3..e5a9cd589 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -459,19 +459,21 @@ class TextEditorPresenter pixelPosition = @pixelPositionForScreenPosition(screenPosition) - top = pixelPosition.top + @lineHeight - left = pixelPosition.left + @gutterWidth + # Fixed positioning. + top = @boundingClientRect.top + pixelPosition.top + @lineHeight + left = @boundingClientRect.left + pixelPosition.left + @gutterWidth if overlayDimensions = @overlayDimensions[decoration.id] {itemWidth, itemHeight, contentMargin} = overlayDimensions - rightDiff = left + @boundingClientRect.left + itemWidth + contentMargin - @windowWidth + rightDiff = left + itemWidth + contentMargin - @windowWidth left -= rightDiff if rightDiff > 0 - leftDiff = left + @boundingClientRect.left + contentMargin + leftDiff = left + contentMargin left -= leftDiff if leftDiff < 0 - if top + @boundingClientRect.top + itemHeight > @windowHeight and top - (itemHeight + @lineHeight) >= 0 + if top + itemHeight > @windowHeight and + top - (itemHeight + @lineHeight) >= 0 top -= itemHeight + @lineHeight pixelPosition.top = top diff --git a/static/text-editor-light.less b/static/text-editor-light.less index 7fafade1e..f5429fd7f 100644 --- a/static/text-editor-light.less +++ b/static/text-editor-light.less @@ -15,7 +15,7 @@ atom-text-editor[mini] { } atom-overlay { - position: absolute; + position: fixed; display: block; z-index: 4; } From 2a9bc0fa1281bdb7027fdcfbff967ef47dcfd309 Mon Sep 17 00:00:00 2001 From: Wliu Date: Tue, 12 Apr 2016 17:08:01 -0400 Subject: [PATCH 109/262] Actually fix require paths --- src/module-cache.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/module-cache.coffee b/src/module-cache.coffee index c8e3b9105..a84251a93 100644 --- a/src/module-cache.coffee +++ b/src/module-cache.coffee @@ -202,12 +202,12 @@ registerBuiltins = (devMode) -> atomShellRoot = path.join(process.resourcesPath, 'atom.asar') - commonRoot = path.join(atomShellRoot, 'lib', 'common', 'api') + commonRoot = path.join(atomShellRoot, 'common', 'api') commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell'] for builtin in commonBuiltins cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js") - rendererRoot = path.join(atomShellRoot, 'lib', 'renderer', 'api') + rendererRoot = path.join(atomShellRoot, 'renderer', 'api') rendererBuiltins = ['ipc-renderer', 'remote'] for builtin in rendererBuiltins cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js") From 373b931c5123917e3d2c368298ce143b0ed48ec9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 Apr 2016 13:52:28 +0200 Subject: [PATCH 110/262] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0933e8be4..6256f5927 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta2", + "text-buffer": "9.0.0-beta3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 3270abe1f6ca5653ba962b7f85d4a88e8a6d026e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 Apr 2016 18:42:52 +0200 Subject: [PATCH 111/262] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 382e7ed3e..fadd361e9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta3", + "text-buffer": "9.0.0-beta4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From bbef0b45518ad20edf6ba426f72e1efc77adcf19 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 Apr 2016 11:19:45 +0200 Subject: [PATCH 112/262] :green_heart: --- src/text-editor-component.coffee | 2 +- src/text-editor.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 3faeab139..52a43db54 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -607,7 +607,7 @@ class TextEditorComponent clickedScreenRow = @screenPositionForMouseEvent(event).row clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow) initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]]) - @editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false) + @editor.addSelectionForScreenRange(initialScreenRange, autoscroll: false) @handleGutterDrag(initialScreenRange) onGutterShiftClick: (event) => diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 08694ec32..3311c0f7e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2309,7 +2309,7 @@ class TextEditor extends Model # # Returns the added {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> - @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults({invalidate: 'never'}, options)) + @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -2322,7 +2322,7 @@ class TextEditor extends Model # # Returns the added {Selection}. addSelectionForScreenRange: (screenRange, options={}) -> - @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults({invalidate: 'never'}, options)) + @selectionsMarkerLayer.markScreenRange(screenRange, {invalidate: 'never', reversed: options.reversed ? false}) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() From 09b88cd1d5bf6da90e572741788e344f8cbfd7e1 Mon Sep 17 00:00:00 2001 From: Wliu Date: Thu, 14 Apr 2016 14:11:25 +0000 Subject: [PATCH 113/262] Fix defective spec and associate 'screen' as a renderer module --- spec/module-cache-spec.coffee | 2 +- src/module-cache.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/module-cache-spec.coffee b/spec/module-cache-spec.coffee index 3a995aec7..e70257103 100644 --- a/spec/module-cache-spec.coffee +++ b/spec/module-cache-spec.coffee @@ -14,7 +14,7 @@ describe 'ModuleCache', -> for builtinName, builtinPath of builtins expect(require.resolve(builtinName)).toBe builtinPath - expect(fs.isFileSync(require.resolve(builtinName))) + expect(fs.isFileSync(require.resolve(builtinName))).toBeTruthy() expect(Module._findPath.callCount).toBe 0 diff --git a/src/module-cache.coffee b/src/module-cache.coffee index a84251a93..9a2961bf6 100644 --- a/src/module-cache.coffee +++ b/src/module-cache.coffee @@ -203,12 +203,12 @@ registerBuiltins = (devMode) -> atomShellRoot = path.join(process.resourcesPath, 'atom.asar') commonRoot = path.join(atomShellRoot, 'common', 'api') - commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell'] + commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'shell'] for builtin in commonBuiltins cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js") rendererRoot = path.join(atomShellRoot, 'renderer', 'api') - rendererBuiltins = ['ipc-renderer', 'remote'] + rendererBuiltins = ['ipc-renderer', 'remote', 'screen'] for builtin in rendererBuiltins cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js") From 58f029e92d9434f52bf2353b598c831962bb60b0 Mon Sep 17 00:00:00 2001 From: Wliu Date: Thu, 14 Apr 2016 14:19:09 +0000 Subject: [PATCH 114/262] :memo: --- spec/module-cache-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/module-cache-spec.coffee b/spec/module-cache-spec.coffee index e70257103..4c0a549aa 100644 --- a/spec/module-cache-spec.coffee +++ b/spec/module-cache-spec.coffee @@ -8,7 +8,7 @@ describe 'ModuleCache', -> beforeEach -> spyOn(Module, '_findPath').andCallThrough() - it 'resolves atom shell module paths without hitting the filesystem', -> + it 'resolves Electron module paths without hitting the filesystem', -> builtins = ModuleCache.cache.builtins expect(Object.keys(builtins).length).toBeGreaterThan 0 From 698854679072603f7d564fbab36ad6df8c7653be Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 14 Apr 2016 22:02:35 -0700 Subject: [PATCH 115/262] Update to use current APIs --- spec/pane-spec.coffee | 140 ++++++++++++++++++------------------------ src/pane.coffee | 32 +--------- 2 files changed, 62 insertions(+), 110 deletions(-) diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 5b4830dd6..8abbb0ece 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -917,103 +917,81 @@ describe "Pane", -> expect(item1.save).not.toHaveBeenCalled() expect(pane.isDestroyed()).toBe false - it "does not destroy the pane if save fails and user clicks cancel", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) - [item1, item2] = pane.getItems() + describe "when item fails to save", -> + [pane, item1, item2] = [] - item1.shouldPromptToSave = -> true - item1.getURI = -> "/test/path" + beforeEach -> + pane = new Pane({items: [new Item("A"), new Item("B")], applicationDelegate: atom.applicationDelegate, config: atom.config}) + [item1, item2] = pane.getItems() - item1.save = jasmine.createSpy("save").andCallFake -> - error = new Error("EACCES, permission denied '/test/path'") - error.path = '/test/path' - error.code = 'EACCES' - throw error + item1.shouldPromptToSave = -> true + item1.getURI = -> "/test/path" - confirmations = 0 - spyOn(atom, 'confirm').andCallFake -> - confirmations++ - if confirmations is 1 - return 0 - else - return 1 + item1.save = jasmine.createSpy("save").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error - pane.close() + it "does not destroy the pane if save fails and user clicks cancel", -> + confirmations = 0 + confirm.andCallFake -> + confirmations++ + if confirmations is 1 + return 0 # click save + else + return 1 # click cancel - expect(atom.confirm).toHaveBeenCalled() - expect(confirmations).toBe(2) - expect(item1.save).toHaveBeenCalled() - expect(pane.isDestroyed()).toBe false + pane.close() - it "does destroy the pane if the user saves the file under a new name", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) - [item1, item2] = pane.getItems() + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(item1.save).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe false - item1.shouldPromptToSave = -> true - item1.getURI = -> "/test/path" + it "does destroy the pane if the user saves the file under a new name", -> + item1.saveAs = jasmine.createSpy("saveAs").andReturn(true) - item1.save = jasmine.createSpy("save").andCallFake -> - error = new Error("EACCES, permission denied '/test/path'") - error.path = '/test/path' - error.code = 'EACCES' - throw error + confirmations = 0 + confirm.andCallFake -> + confirmations++ + return 0 # save and then save as - item1.saveAs = jasmine.createSpy("saveAs").andReturn(true) + showSaveDialog.andReturn("new/path") - confirmations = 0 - spyOn(atom, 'confirm').andCallFake -> - confirmations++ - return 0 + pane.close() - spyOn(atom, 'showSaveDialogSync').andReturn("new/path") + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true - pane.close() + it "asks again if the saveAs also fails", -> + item1.saveAs = jasmine.createSpy("saveAs").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error - expect(atom.confirm).toHaveBeenCalled() - expect(confirmations).toBe(2) - expect(atom.showSaveDialogSync).toHaveBeenCalled() - expect(item1.save).toHaveBeenCalled() - expect(item1.saveAs).toHaveBeenCalled() - expect(pane.isDestroyed()).toBe true + confirmations = 0 + confirm.andCallFake -> + confirmations++ + if confirmations < 3 + return 0 # save, save as, save as + return 2 # don't save - it "asks again if the saveAs also fails", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) - [item1, item2] = pane.getItems() + showSaveDialog.andReturn("new/path") - item1.shouldPromptToSave = -> true - item1.getURI = -> "/test/path" - - item1.save = jasmine.createSpy("save").andCallFake -> - error = new Error("EACCES, permission denied '/test/path'") - error.path = '/test/path' - error.code = 'EACCES' - throw error - - item1.saveAs = jasmine.createSpy("saveAs").andCallFake -> - error = new Error("EACCES, permission denied '/test/path'") - error.path = '/test/path' - error.code = 'EACCES' - throw error - - - confirmations = 0 - spyOn(atom, 'confirm').andCallFake -> - confirmations++ - if confirmations < 3 - return 0 - return 2 - - spyOn(atom, 'showSaveDialogSync').andReturn("new/path") - - pane.close() - - expect(atom.confirm).toHaveBeenCalled() - expect(confirmations).toBe(3) - expect(atom.showSaveDialogSync).toHaveBeenCalled() - expect(item1.save).toHaveBeenCalled() - expect(item1.saveAs).toHaveBeenCalled() - expect(pane.isDestroyed()).toBe true + pane.close() + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(3) + expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true describe "::destroy()", -> [container, pane1, pane2] = [] diff --git a/src/pane.coffee b/src/pane.coffee index 0f33ef218..e1dbc8596 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -579,8 +579,8 @@ class Pane extends Model saveError = (error) => if error - chosen = atom.confirm - message: @getSaveErrorMessage(error) + chosen = @applicationDelegate.confirm + message: @getMessageForErrorCode(error) detailedMessage: "Your changes will be lost if you close this item without saving." buttons: ["Save as", "Cancel", "Don't save"] switch chosen @@ -657,7 +657,7 @@ class Pane extends Model nextAction?() catch error if nextAction - nextAction() + nextAction(error) else @handleSaveError(error, item) @@ -851,7 +851,6 @@ class Pane extends Model return false unless @promptToSaveItem(item) true -# <<<<<<< HEAD handleSaveError: (error, item) -> itemPath = error.path ? item?.getPath?() addWarningWithPath = (message, options) => @@ -868,31 +867,6 @@ class Pane extends Model else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) fileName = errorMatch[1] @notificationManager.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") -# ======= -# # Translate an error object to a human readable string -# getSaveErrorMessage: (error) -> -# if error.code is 'EISDIR' or error.message.endsWith('is a directory') -# "Unable to save file: #{error.message}." -# else if error.code is 'EACCES' and error.path? -# "Unable to save file: Permission denied '#{error.path}'." -# else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] and error.path? -# "Unable to save file '#{error.path}': #{error.message}." -# else if error.code is 'EROFS' and error.path? -# "Unable to save file: Read-only file system '#{error.path}'." -# else if error.code is 'ENOSPC' and error.path? -# "Unable to save file: No space left on device '#{error.path}'." -# else if error.code is 'ENXIO' and error.path? -# "Unable to save file: No such device or address '#{error.path}'." -# else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) -# fileName = errorMatch[1] -# "Unable to save file: A directory in the path '#{fileName}' could not be written to." -# -# # Display a popup warning to the user -# handleSaveError: (error) -> -# errorMessage = Pane::getSaveErrorMessage(error) -# if errorMessage? -# atom.notifications.addWarning(errorMessage) -# >>>>>>> b5c9a90ae00ec44e91782b0d6e30be7ca2fea11c else throw error From 4e9048d22de5fce1fb4bce20515fcee96aae786d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 14 Apr 2016 23:25:27 -0700 Subject: [PATCH 116/262] Create saveDialog helper function and name file in message --- spec/fixtures/sample.txt | 2 +- src/pane.coffee | 33 ++++++++++++++------------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/spec/fixtures/sample.txt b/spec/fixtures/sample.txt index 9701a96c5..34dad6adf 100644 --- a/spec/fixtures/sample.txt +++ b/spec/fixtures/sample.txt @@ -1 +1 @@ -Some textSome textSome text. +Some textSome textSome textSome textSome textSome textSome textSome textSome textSome textSome text. diff --git a/src/pane.coffee b/src/pane.coffee index e1dbc8596..add6a365b 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -577,28 +577,23 @@ class Pane extends Model else return true + saveDialog = (saveButtonText, saveFn, message) => + chosen = @applicationDelegate.confirm + message: message + detailedMessage: "Your changes will be lost if you close this item without saving." + buttons: [saveButtonText, "Cancel", "Don't save"] + switch chosen + when 0 then saveFn(item, saveError) + when 1 then false + when 2 then true + saveError = (error) => if error - chosen = @applicationDelegate.confirm - message: @getMessageForErrorCode(error) - detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: ["Save as", "Cancel", "Don't save"] - switch chosen - when 0 then @saveItemAs item, saveError - when 1 then false - when 2 then true + saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}") else true - chosen = @applicationDelegate.confirm - message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?" - detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: ["Save", "Cancel", "Don't Save"] - - switch chosen - when 0 then @saveItem(item, saveError) - when 1 then false - when 2 then true + saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?") # Public: Save the active item. saveActiveItem: (nextAction) -> @@ -619,7 +614,7 @@ class Pane extends Model # after the item is successfully saved, or with the error if it failed. # The return value will be that of `nextAction` or `undefined` if it was not # provided - saveItem: (item, nextAction) -> + saveItem: (item, nextAction) => if typeof item?.getURI is 'function' itemURI = item.getURI() else if typeof item?.getUri is 'function' @@ -645,7 +640,7 @@ class Pane extends Model # after the item is successfully saved, or with the error if it failed. # The return value will be that of `nextAction` or `undefined` if it was not # provided - saveItemAs: (item, nextAction) -> + saveItemAs: (item, nextAction) => return unless item?.saveAs? saveOptions = item.getSaveDialogOptions?() ? {} From b790716caae25587dc8f5bdc811c90c8304c0f40 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 14 Apr 2016 23:28:13 -0700 Subject: [PATCH 117/262] Revert sample.txt file --- spec/fixtures/sample.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/fixtures/sample.txt b/spec/fixtures/sample.txt index 34dad6adf..9701a96c5 100644 --- a/spec/fixtures/sample.txt +++ b/spec/fixtures/sample.txt @@ -1 +1 @@ -Some textSome textSome textSome textSome textSome textSome textSome textSome textSome textSome text. +Some textSome textSome text. From 7f77c677614d17ff56f9cf0a45c120b94a5500ca Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 15 Apr 2016 15:40:22 +0200 Subject: [PATCH 118/262] :fire: preserveFolds from marker's properties --- src/text-editor.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 3311c0f7e..19fddb2e1 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -178,7 +178,6 @@ class TextEditor extends Model @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings for marker in @selectionsMarkerLayer.getMarkers() - marker.setProperties(preserveFolds: true) @addSelection(marker) @subscribeToTabTypeConfig() @@ -2306,9 +2305,13 @@ class TextEditor extends Model # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. + # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + # selection is set. # # Returns the added {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> + unless options.preserveFolds + @destroyFoldsIntersectingBufferRange(bufferRange) @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -2319,12 +2322,11 @@ class TextEditor extends Model # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. - # + # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + # selection is set. # Returns the added {Selection}. addSelectionForScreenRange: (screenRange, options={}) -> - @selectionsMarkerLayer.markScreenRange(screenRange, {invalidate: 'never', reversed: options.reversed ? false}) - @getLastSelection().autoscroll() unless options.autoscroll is false - @getLastSelection() + @addSelectionForBufferRange(@bufferRangeForScreenRange(screenRange), options) # Essential: Select from the current cursor position to the given position in # buffer coordinates. @@ -2638,13 +2640,11 @@ class TextEditor extends Model # # Returns the new {Selection}. addSelection: (marker, options={}) -> - unless marker.getProperties().preserveFolds - @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) selection = new Selection(_.extend({editor: this, marker, cursor, @clipboard}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds) + @mergeIntersectingSelections(preserveFolds: options.preserveFolds) if selection.destroyed for selection in @getSelections() From fd483cb918c3e1d1fa89a6f9ca78596cf386e4f5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 15 Apr 2016 17:45:04 +0200 Subject: [PATCH 119/262] Fix deprecation --- src/selection.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/selection.coffee b/src/selection.coffee index bd15d1543..cee88caf2 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -508,7 +508,7 @@ class Selection extends Model if selectedRange.isEmpty() return if selectedRange.start.row is @editor.buffer.getLastRow() else - joinMarker = @editor.markBufferRange(selectedRange, invalidationStrategy: 'never') + joinMarker = @editor.markBufferRange(selectedRange, invalidate: 'never') rowCount = Math.max(1, selectedRange.getRowCount() - 1) for row in [0...rowCount] From 99e716f9ed2142e83c9d73b45aba42735d40ad94 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 15 Apr 2016 11:25:05 -0700 Subject: [PATCH 120/262] Fix specs failures on Windows including paths --- spec/atom-environment-spec.coffee | 6 ++++-- spec/git-repository-async-spec.js | 2 +- spec/git-spec.coffee | 2 +- spec/project-spec.coffee | 2 +- spec/spec-helper.coffee | 26 ++++++++++++++++---------- spec/text-editor-component-spec.js | 4 ++-- spec/text-editor-spec.coffee | 14 ++++++++------ spec/theme-manager-spec.coffee | 8 ++++---- spec/workspace-spec.coffee | 21 +++++++++++++++------ 9 files changed, 52 insertions(+), 33 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 5fd4b11f1..846083b0e 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -28,8 +28,10 @@ describe "AtomEnvironment", -> atom.setSize(originalSize.width, originalSize.height) it 'sets the size of the window, and can retrieve the size just set', -> - atom.setSize(100, 400) - expect(atom.getSize()).toEqual width: 100, height: 400 + newWidth = originalSize.width + 12 + newHeight = originalSize.height + 23 + atom.setSize(newWidth, newHeight) + expect(atom.getSize()).toEqual width: newWidth, height: newHeight describe ".isReleasedVersion()", -> it "returns false if the version is a SHA and true otherwise", -> diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0442248e1..251734448 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -103,7 +103,7 @@ describe('GitRepositoryAsync', () => { it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') const repoPath = await repo.getPath() - expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + expect(repoPath).toBePath(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index 3afd4da75..87aeab12a 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -33,7 +33,7 @@ describe "GitRepository", -> waitsForPromise -> repo.async.getPath().then(onSuccess) runs -> - expect(onSuccess.mostRecentCall.args[0]).toBe(repoPath) + expect(onSuccess.mostRecentCall.args[0]).toBePath(repoPath) describe "new GitRepository(path)", -> it "throws an exception when no repository is found", -> diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 499efd017..953cf103e 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -526,7 +526,7 @@ describe "Project", -> expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true expect(atom.project.relativizePath(inputPath)).toEqual [ atom.project.getPaths()[1], - 'somewhere/something.txt' + path.join('somewhere', 'something.txt') ] describe ".contains(path)", -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 1194f2f76..78a47ab0b 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -172,8 +172,8 @@ jasmine.useRealClock = -> addCustomMatchers = (spec) -> spec.addMatchers toBeInstanceOf: (expected) -> - notText = if @isNot then " not" else "" - this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class" + beOrNotBe = if @isNot then "not be" else "be" + this.message = => "Expected #{jasmine.pp(@actual)} to #{beOrNotBe} instance of #{expected.name} class" @actual instanceof expected toHaveLength: (expected) -> @@ -181,32 +181,38 @@ addCustomMatchers = (spec) -> this.message = => "Expected object #{@actual} has no length method" false else - notText = if @isNot then " not" else "" - this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}" + haveOrNotHave = if @isNot then "not have" else "have" + this.message = => "Expected object with length #{@actual.length} to #{haveOrNotHave} length #{expected}" @actual.length is expected toExistOnDisk: (expected) -> - notText = this.isNot and " not" or "" - @message = -> return "Expected path '" + @actual + "'" + notText + " to exist." + toOrNotTo = this.isNot and "not to" or "to" + @message = -> return "Expected path '#{@actual}' #{toOrNotTo} exist." fs.existsSync(@actual) toHaveFocus: -> - notText = this.isNot and " not" or "" + toOrNotTo = this.isNot and "not to" or "to" if not document.hasFocus() console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner." - @message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus." + @message = -> return "Expected element '#{@actual}' or its descendants #{toOrNotTo} have focus." element = @actual element = element.get(0) if element.jquery element is document.activeElement or element.contains(document.activeElement) toShow: -> - notText = if @isNot then " not" else "" + toOrNotTo = this.isNot and "not to" or "to" element = @actual element = element.get(0) if element.jquery - @message = -> return "Expected element '#{element}' or its descendants#{notText} to show." + @message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show." element.style.display in ['block', 'inline-block', 'static', 'fixed'] + toBePath: (expected) -> + actualPath = path.normalize(@actual) + expectedPath = path.normalize(expected) + @message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'." + actualPath is expectedPath + window.waitsForPromise = (args...) -> label = null if args.length > 1 diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index b031cba33..c1b2f8f46 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4028,9 +4028,9 @@ describe('TextEditorComponent', function () { component.setFontSize(10) await nextViewUpdatePromise() expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) - expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) + expect(editor.getKoreanCharWidth()).toBeCloseTo(10, 0) expect(editor.getDoubleWidthCharWidth()).toBe(10) - expect(editor.getHalfWidthCharWidth()).toBe(5) + expect(editor.getHalfWidthCharWidth()).toBeCloseTo(6, 5) }) }) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 4de7168b7..7d85f1cad 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -182,17 +182,19 @@ describe "TextEditor", -> expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1" expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2" - it "returns '' when opened files have identical file and dir names", -> + it "returns '' when opened files have identical file names in subdirectories", -> editor1 = null editor2 = null + path1 = path.join('sample-theme-1', 'src', 'js') + path2 = path.join('sample-theme-2', 'src', 'js') waitsForPromise -> - atom.workspace.open(path.join('sample-theme-1', 'src', 'js', 'main.js')).then (o) -> + atom.workspace.open(path.join(path1, 'main.js')).then (o) -> editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> + atom.workspace.open(path.join(path2, 'main.js')).then (o) -> editor2 = o runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 sample-theme-1/src/js" - expect(editor2.getLongTitle()).toBe "main.js \u2014 sample-theme-2/src/js" + expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}" + expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}" it "returns '' when opened files have identical file and same parent dir name", -> editor1 = null @@ -204,7 +206,7 @@ describe "TextEditor", -> editor2 = o runs -> expect(editor1.getLongTitle()).toBe "main.js \u2014 js" - expect(editor2.getLongTitle()).toBe "main.js \u2014 js/plugin" + expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin') it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> observed = [] diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index ea0ca19e6..0ab8a58b3 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -175,7 +175,7 @@ describe "atom.themes", -> expect(styleElementAddedHandler).toHaveBeenCalled() element = document.querySelector('head style[source-path*="css.css"]') - expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(cssPath) + expect(element.getAttribute('source-path')).toBePath atom.themes.stringToId(cssPath) expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8') # doesn't append twice @@ -194,7 +194,7 @@ describe "atom.themes", -> expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 element = document.querySelector('head style[source-path*="sample.less"]') - expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(lessPath) + expect(element.getAttribute('source-path')).toBePath atom.themes.stringToId(lessPath) expect(element.textContent).toBe """ #header { color: #4d926f; @@ -213,9 +213,9 @@ describe "atom.themes", -> it "supports requiring css and less stylesheets without an explicit extension", -> atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css') - expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css')) + expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toBePath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css')) atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample') - expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less')) + expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toBePath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less')) document.querySelector('head style[source-path*="css.css"]').remove() document.querySelector('head style[source-path*="sample.less"]').remove() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 97139f6bb..fc22f07c3 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -80,7 +80,8 @@ describe "Workspace", -> expect(untitledEditor.getText()).toBe("An untitled editor.") expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath() - expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{pathEscaped}/// describe "where there are no open panes or editors", -> it "constructs the view with no open editors", -> @@ -833,25 +834,29 @@ describe "Workspace", -> describe "when there is an active pane item", -> it "sets the title to the pane item's title plus the project path", -> item = atom.workspace.getActivePaneItem() - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the title of the active pane item changes", -> it "updates the window title based on the item's new title", -> editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(temp.dir, 'hi')) - expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the active pane's item changes", -> it "updates the title to the new item's title plus the project path", -> atom.workspace.getActivePane().activateNextItem() item = atom.workspace.getActivePaneItem() - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the last pane item is removed", -> it "updates the title to contain the project's path", -> atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(document.title).toMatch ///^#{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{pathEscaped}/// describe "when an inactive pane's item changes", -> it "does not update the title", -> @@ -875,7 +880,8 @@ describe "Workspace", -> }) workspace2.deserialize(atom.workspace.serialize(), atom.deserializers) item = workspace2.getActivePaneItem() - expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}/// workspace2.destroy() describe "document edited status", -> @@ -1610,3 +1616,6 @@ describe "Workspace", -> runs -> expect(pane.getPendingItem()).toBeFalsy() + + escapeStringRegex = (str) -> + str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') From 89bc3c888800711e0dbd3ca82d8b5c272d86d54d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 18 Apr 2016 17:31:35 -0400 Subject: [PATCH 121/262] :arrow_up: language-git@0.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 128c40e43..4bad84f77 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "language-csharp": "0.12.1", "language-css": "0.36.1", "language-gfm": "0.86.0", - "language-git": "0.12.1", + "language-git": "0.13.0", "language-go": "0.42.0", "language-html": "0.44.1", "language-hyperlink": "0.16.0", From 8516f1914e676a3daf4d47ac7537a2c779aa49da Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Apr 2016 15:20:18 -0700 Subject: [PATCH 122/262] :arrow_up: fuzzy-finder@1.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4bad84f77..1dc192378 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "dev-live-reload": "0.47.0", "encoding-selector": "0.21.0", "exception-reporting": "0.38.1", - "fuzzy-finder": "1.0.4", + "fuzzy-finder": "1.0.5", "git-diff": "1.0.1", "find-and-replace": "0.198.0", "go-to-line": "0.30.0", From 53b516a6ec21b7f7e26b4577e1e3fd1759520eb5 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 18 Apr 2016 20:48:34 -0700 Subject: [PATCH 123/262] Quote spaces in paths on Win cmd line --- resources/win/atom.cmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index a1af5cd53..8a4fed05c 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -2,6 +2,7 @@ SET EXPECT_OUTPUT= SET WAIT= +SET PSARGS=%* FOR %%a IN (%*) DO ( IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES @@ -25,7 +26,7 @@ FOR %%a IN (%*) DO ( IF "%EXPECT_OUTPUT%"=="YES" ( SET ELECTRON_ENABLE_LOGGING=YES IF "%WAIT%"=="YES" ( - powershell -noexit "%~dp0\..\..\atom.exe" --pid=$pid %* ; wait-event + powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event" ) ELSE ( "%~dp0\..\..\atom.exe" %* ) From 1500381ac9216bd533199f5b59460b5ac596527c Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 19 Apr 2016 14:25:44 -0700 Subject: [PATCH 124/262] Tweaks to the specs improvements from feedback --- spec/git-repository-async-spec.js | 2 +- spec/git-spec.coffee | 2 +- spec/spec-helper.coffee | 2 +- spec/text-editor-component-spec.js | 32 +++++++++++++++++++++++------- spec/theme-manager-spec.coffee | 8 ++++---- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 251734448..224280b39 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -103,7 +103,7 @@ describe('GitRepositoryAsync', () => { it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') const repoPath = await repo.getPath() - expect(repoPath).toBePath(path.join(__dirname, 'fixtures', 'git', 'master.git')) + expect(repoPath).toEqualPath(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index 87aeab12a..7d9e9bbd4 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -33,7 +33,7 @@ describe "GitRepository", -> waitsForPromise -> repo.async.getPath().then(onSuccess) runs -> - expect(onSuccess.mostRecentCall.args[0]).toBePath(repoPath) + expect(onSuccess.mostRecentCall.args[0]).toEqualPath(repoPath) describe "new GitRepository(path)", -> it "throws an exception when no repository is found", -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 78a47ab0b..9c4e09da0 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -207,7 +207,7 @@ addCustomMatchers = (spec) -> @message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show." element.style.display in ['block', 'inline-block', 'static', 'fixed'] - toBePath: (expected) -> + toEqualPath: (expected) -> actualPath = path.normalize(@actual) expectedPath = path.normalize(expected) @message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'." diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index c1b2f8f46..1b3a7b5c8 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4022,15 +4022,33 @@ describe('TextEditorComponent', function () { }) }) - describe('when changing the font', async function () { - it('measures the default char, the korean char, the double width char and the half width char widths', async function () { - expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) + describe('when decreasing the fontSize', async function () { + it('decreases the widths of the korean char, the double width char and the half width char', async function () { + originalDefaultCharWidth = editor.getDefaultCharWidth() + koreanDefaultCharWidth = editor.getKoreanCharWidth() + doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth() + halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth() component.setFontSize(10) await nextViewUpdatePromise() - expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) - expect(editor.getKoreanCharWidth()).toBeCloseTo(10, 0) - expect(editor.getDoubleWidthCharWidth()).toBe(10) - expect(editor.getHalfWidthCharWidth()).toBeCloseTo(6, 5) + expect(editor.getDefaultCharWidth()).toBeLessThan(originalDefaultCharWidth) + expect(editor.getKoreanCharWidth()).toBeLessThan(koreanDefaultCharWidth) + expect(editor.getDoubleWidthCharWidth()).toBeLessThan(doubleWidthDefaultCharWidth) + expect(editor.getHalfWidthCharWidth()).toBeLessThan(halfWidthDefaultCharWidth) + }) + }) + + describe('when increasing the fontSize', function() { + it('increases the widths of the korean char, the double width char and the half width char', async function () { + originalDefaultCharWidth = editor.getDefaultCharWidth() + koreanDefaultCharWidth = editor.getKoreanCharWidth() + doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth() + halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth() + component.setFontSize(25) + await nextViewUpdatePromise() + expect(editor.getDefaultCharWidth()).toBeGreaterThan(originalDefaultCharWidth) + expect(editor.getKoreanCharWidth()).toBeGreaterThan(koreanDefaultCharWidth) + expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(doubleWidthDefaultCharWidth) + expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(halfWidthDefaultCharWidth) }) }) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 0ab8a58b3..47b848809 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -175,7 +175,7 @@ describe "atom.themes", -> expect(styleElementAddedHandler).toHaveBeenCalled() element = document.querySelector('head style[source-path*="css.css"]') - expect(element.getAttribute('source-path')).toBePath atom.themes.stringToId(cssPath) + expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(cssPath) expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8') # doesn't append twice @@ -194,7 +194,7 @@ describe "atom.themes", -> expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 element = document.querySelector('head style[source-path*="sample.less"]') - expect(element.getAttribute('source-path')).toBePath atom.themes.stringToId(lessPath) + expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(lessPath) expect(element.textContent).toBe """ #header { color: #4d926f; @@ -213,9 +213,9 @@ describe "atom.themes", -> it "supports requiring css and less stylesheets without an explicit extension", -> atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css') - expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toBePath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css')) + expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css')) atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample') - expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toBePath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less')) + expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less')) document.querySelector('head style[source-path*="css.css"]').remove() document.querySelector('head style[source-path*="sample.less"]').remove() From 405a16ecdc2e08ed1ce044a5c5992655ade2301e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Apr 2016 20:38:25 -0400 Subject: [PATCH 125/262] :arrow_up: language-java@0.18.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1dc192378..84ec5a36e 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "language-go": "0.42.0", "language-html": "0.44.1", "language-hyperlink": "0.16.0", - "language-java": "0.17.0", + "language-java": "0.18.0", "language-javascript": "0.110.0", "language-json": "0.18.0", "language-less": "0.29.3", From 9feb27e10775dca6f6722c5b1e7ddc771bd8086c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Apr 2016 20:39:24 -0400 Subject: [PATCH 126/262] :arrow_up: language-sass@0.47.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84ec5a36e..7c9e37363 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-python": "0.43.1", "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", - "language-sass": "0.46.0", + "language-sass": "0.47.0", "language-shellscript": "0.21.1", "language-source": "0.9.0", "language-sql": "0.21.0", From 89a5d01968a6615558d15706ded86dec29652cdc Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Apr 2016 20:40:12 -0400 Subject: [PATCH 127/262] :arrow_up: language-coffee-script@0.47.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7c9e37363..436534d72 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "wrap-guide": "0.38.1", "language-c": "0.51.3", "language-clojure": "0.20.0", - "language-coffee-script": "0.46.1", + "language-coffee-script": "0.47.0", "language-csharp": "0.12.1", "language-css": "0.36.1", "language-gfm": "0.86.0", From 25895e1364c83325407213f545e513ac3b5d3a5a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Apr 2016 20:40:49 -0400 Subject: [PATCH 128/262] :arrow_up: language-yaml@0.26.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 436534d72..19cc758e7 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "language-todo": "0.27.0", "language-toml": "0.18.0", "language-xml": "0.34.5", - "language-yaml": "0.25.2" + "language-yaml": "0.26.0" }, "private": true, "scripts": { From ea5ad4ae59833789ebbfb7d60e8d9c6a895284cc Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Apr 2016 21:31:53 -0400 Subject: [PATCH 129/262] Coffeescript comment tokenization no longer matches the newline character --- spec/tokenized-buffer-spec.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 88c095f68..7a1f4d221 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -445,13 +445,12 @@ describe "TokenizedBuffer", -> expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}" {tokens} = screenLine0 - expect(tokens.length).toBe 4 + expect(tokens.length).toBe 3 expect(tokens[0].value).toBe "#" expect(tokens[1].value).toBe " Econ 101" expect(tokens[2].value).toBe tabAsSpaces expect(tokens[2].scopes).toEqual tokens[1].scopes expect(tokens[2].isAtomic).toBeTruthy() - expect(tokens[3].value).toBe "" expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand" From 1e147fbaf161c27d4382f63b32303a6b2250321b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 20 Apr 2016 09:33:04 +0200 Subject: [PATCH 130/262] :arrow_up: about --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19cc758e7..8fbc557d4 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "one-light-syntax": "1.2.0", "solarized-dark-syntax": "1.0.2", "solarized-light-syntax": "1.0.2", - "about": "1.5.0", + "about": "1.5.1", "archive-view": "0.61.1", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.1", From 9ff5fb21fb0cccea293e43a36d3c04edd989df6c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 20 Apr 2016 07:30:34 -0400 Subject: [PATCH 131/262] :arrow_up: language-make@0.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8fbc557d4..0cd63e918 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "language-javascript": "0.110.0", "language-json": "0.18.0", "language-less": "0.29.3", - "language-make": "0.21.1", + "language-make": "0.22.0", "language-mustache": "0.13.0", "language-objective-c": "0.15.1", "language-perl": "0.34.0", From 196bc53e6bef729c4925e0ae3789c3360def8dfd Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 20 Apr 2016 09:57:41 -0700 Subject: [PATCH 132/262] Ensure Windows Bash script works on all versions --- resources/win/atom.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/win/atom.sh b/resources/win/atom.sh index 7380bf122..cd90ff8fb 100644 --- a/resources/win/atom.sh +++ b/resources/win/atom.sh @@ -1,2 +1,5 @@ #!/bin/sh -$(dirname "$0")/atom.cmd "$@" +pushd $(dirname "$0") > /dev/null +ATOMCMD=""$(pwd -W)"/atom.cmd" +popd > /dev/null +cmd.exe //c "$ATOMCMD" "$@" From eb739cd025b7914b97a1476e1140f6866563c0b6 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 20 Apr 2016 12:40:45 -0700 Subject: [PATCH 133/262] Add `build-and-sign` grunt task for codesigning Atom --- build/Gruntfile.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index f8ee607e5..73279779c 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -298,6 +298,7 @@ module.exports = (grunt) -> unless process.platform is 'linux' or grunt.option('no-install') defaultTasks.push 'install' grunt.registerTask('default', defaultTasks) + grunt.registerTask('build-and-sign', ['download-electron', 'download-electron-chromedriver', 'build', 'set-version', 'generate-asar', 'codesign:app', 'install']) getDefaultChannelAndReleaseBranch = (version) -> if version.match(/dev/) or isBuildingPR() From aa96d471609e6c804222252ea13e3fd1fca3bfe3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 21 Apr 2016 12:36:45 -0400 Subject: [PATCH 134/262] First pass --- package.json | 2 +- src/git-repository-async.js | 689 +++--------------------------------- 2 files changed, 46 insertions(+), 645 deletions(-) diff --git a/package.json b/package.json index 0cd63e918..3fa4a72d3 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,9 @@ "less-cache": "0.23", "line-top-index": "0.2.0", "marked": "^0.3.4", - "nodegit": "0.12.2", "normalize-package-data": "^2.0.0", "nslog": "^3", + "ohnogit": "0.0.8", "oniguruma": "^5", "pathwatcher": "~6.2", "property-accessors": "^1.1.3", diff --git a/src/git-repository-async.js b/src/git-repository-async.js index aacd482f7..6b91572d2 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,20 +1,7 @@ 'use babel' -import fs from 'fs-plus' -import path from 'path' -import Git from 'nodegit' -import ResourcePool from './resource-pool' -import {Emitter, CompositeDisposable, Disposable} from 'event-kit' - -const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE -const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW -const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED -const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -const ignoredStatusFlags = 1 << 14 // TODO: compose this from libgit2 constants -const submoduleMode = 57344 // TODO: compose this from libgit2 constants - -// Just using this for _.isEqual and _.object, we should impl our own here -import _ from 'underscore-plus' +import {Repository} from 'ohnogit' +import {CompositeDisposable, Disposable} from 'event-kit' // For the most part, this class behaves the same as `GitRepository`, with a few // notable differences: @@ -29,7 +16,7 @@ export default class GitRepositoryAsync { } static get Git () { - return Git + return Repository.Git } // The name of the error thrown when an action is attempted on a destroyed @@ -39,29 +26,9 @@ export default class GitRepositoryAsync { } constructor (_path, options = {}) { - // We'll serialize our access manually. - Git.setThreadSafetyStatus(Git.THREAD_SAFETY.DISABLED) + this.repo = Repository.open(_path, options) - this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() - this.pathStatusCache = {} - this.path = null - - // NB: These needs to happen before the following .openRepository call. - this.openedPath = _path - this._openExactPath = options.openExactPath || false - - this.repoPromise = this.openRepository() - // NB: We don't currently _use_ the pooled object. But by giving it one - // thing, we're really just serializing all the work. Down the road, we - // could open multiple connections to the repository. - this.repoPool = new ResourcePool([this.repoPromise]) - - this.isCaseInsensitive = fs.isCaseInsensitive() - this.upstream = {} - this.submodules = {} - - this._refreshingPromise = Promise.resolve() let {refreshOnWindowFocus = true} = options if (refreshOnWindowFocus) { @@ -83,18 +50,12 @@ export default class GitRepositoryAsync { // This destroys any tasks and subscriptions and releases the underlying // libgit2 repository handle. This method is idempotent. destroy () { - if (this.emitter) { - this.emitter.emit('did-destroy') - this.emitter.dispose() - this.emitter = null - } + this.repo.destroy() if (this.subscriptions) { this.subscriptions.dispose() this.subscriptions = null } - - this.repoPromise = null } // Event subscription @@ -107,7 +68,7 @@ export default class GitRepositoryAsync { // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy (callback) { - return this.emitter.on('did-destroy', callback) + return this.repo.onDidDestroy(callback) } // Public: Invoke the given callback when a specific file's status has @@ -122,7 +83,7 @@ export default class GitRepositoryAsync { // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatus (callback) { - return this.emitter.on('did-change-status', callback) + return this.repo.onDidChangeStatus(callback) } // Public: Invoke the given callback when a multiple files' statuses have @@ -134,7 +95,7 @@ export default class GitRepositoryAsync { // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses (callback) { - return this.emitter.on('did-change-statuses', callback) + return this.repo.onDidChangeStatuses(callback) } // Repository details @@ -151,25 +112,13 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to the {String} path of the // repository. getPath () { - return this.getRepo().then(repo => { - if (!this.path) { - this.path = repo.path().replace(/\/$/, '') - } - - return this.path - }) + return this.repo.getPath() } // Public: Returns a {Promise} which resolves to the {String} working // directory path of the repository. getWorkingDirectory (_path) { - return this.getRepo(_path).then(repo => { - if (!repo.cachedWorkdir) { - repo.cachedWorkdir = repo.workdir() - } - - return repo.cachedWorkdir - }) + return this.repo.getWorkingDirectory() } // Public: Returns a {Promise} that resolves to true if at the root, false if @@ -191,8 +140,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the relative {String} path. relativizeToWorkingDirectory (_path) { - return this.getWorkingDirectory() - .then(wd => this.relativize(_path, wd)) + return this.repo.relativizeToWorkingDirectory(_path) } // Public: Makes a path relative to the repository's working directory. @@ -202,78 +150,13 @@ export default class GitRepositoryAsync { // // Returns the relative {String} path. relativize (_path, workingDirectory) { - // The original implementation also handled null workingDirectory as it - // pulled it from a sync function that could return null. We require it - // to be passed here. - let openedWorkingDirectory - if (!_path || !workingDirectory) { - return _path - } - - // If the opened directory and the workdir differ, this is a symlinked repo - // root, so we have to do all the checks below twice--once against the realpath - // and one against the opened path - const opened = this.openedPath.replace(/\/\.git$/, '') - if (path.relative(opened, workingDirectory) !== '') { - openedWorkingDirectory = opened - } - - if (process.platform === 'win32') { - _path = _path.replace(/\\/g, '/') - } else { - if (_path[0] !== '/') { - return _path - } - } - - workingDirectory = workingDirectory.replace(/\/$/, '') - - // Depending on where the paths come from, they may have a '/private/' - // prefix. Standardize by stripping that out. - _path = _path.replace(/^\/private\//i, '/') - workingDirectory = workingDirectory.replace(/^\/private\//i, '/') - - const originalPath = _path - const originalWorkingDirectory = workingDirectory - if (this.isCaseInsensitive) { - _path = _path.toLowerCase() - workingDirectory = workingDirectory.toLowerCase() - } - - if (_path.indexOf(workingDirectory) === 0) { - return originalPath.substring(originalWorkingDirectory.length + 1) - } else if (_path === workingDirectory) { - return '' - } - - if (openedWorkingDirectory) { - openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '') - openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//i, '/') - - const originalOpenedWorkingDirectory = openedWorkingDirectory - if (this.isCaseInsensitive) { - openedWorkingDirectory = openedWorkingDirectory.toLowerCase() - } - - if (_path.indexOf(openedWorkingDirectory) === 0) { - return originalPath.substring(originalOpenedWorkingDirectory.length + 1) - } else if (_path === openedWorkingDirectory) { - return '' - } - } - - return _path + return this.repo.relativize(_path, workingDirectory) } // Public: Returns a {Promise} which resolves to whether the given branch // exists. hasBranch (branch) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.getBranch(branch)) - .then(branch => branch != null) - .catch(_ => false) - }) + return this.repo.hasBranch(branch) } // Public: Retrieves a shortened version of the HEAD reference value. @@ -287,11 +170,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to a {String}. getShortHead (_path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.getCurrentBranch()) - .then(branch => branch.shorthand()) - }) + return this.repo.getShortHead(_path) } // Public: Is the given path a submodule in the repository? @@ -301,19 +180,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves true if the given path is a submodule in // the repository. isSubmodule (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.index()) - .then(index => { - const entry = index.getByPath(relativePath) - if (!entry) return false - - return entry.mode === submoduleMode - }) - }) - }) + return this.repo.isSubmodule(_path) } // Public: Returns the number of commits behind the current branch is from the @@ -327,18 +194,7 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getAheadBehindCount (reference, _path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => Promise.all([repo, repo.getBranch(reference)])) - .then(([repo, local]) => { - const upstream = Git.Branch.upstream(local) - return Promise.all([repo, local, upstream]) - }) - .then(([repo, local, upstream]) => { - return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) - }) - .catch(_ => ({ahead: 0, behind: 0})) - }) + return this.repo.getAheadBehindCount(reference, _path) } // Public: Get the cached ahead/behind commit counts for the current branch's @@ -351,15 +207,7 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getCachedUpstreamAheadBehindCount (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this._submoduleForPath(_path)) - .then(submodule => { - if (submodule) { - return submodule.getCachedUpstreamAheadBehindCount(_path) - } else { - return this.upstream - } - }) + return this.repo.getCachedUpstreamAheadBehindCount(_path) } // Public: Returns the git configuration value specified by the key. @@ -370,12 +218,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} git configuration value // specified by the key. getConfigValue (key, _path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.configSnapshot()) - .then(config => config.getStringBuf(key)) - .catch(_ => null) - }) + return this.repo.getConfigValue(key, _path) } // Public: Get the URL for the 'origin' remote. @@ -386,7 +229,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} origin url of the // repository. getOriginURL (_path) { - return this.getConfigValue('remote.origin.url', _path) + return this.repo.getOriginalURL(_path) } // Public: Returns the upstream branch for the current HEAD, or null if there @@ -398,11 +241,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {String} branch name such as // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.getCurrentBranch()) - .then(branch => Git.Branch.upstream(branch)) - }) + return this.getUpstreamBranch(_path) } // Public: Gets all the local and remote references. @@ -415,25 +254,7 @@ export default class GitRepositoryAsync { // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. getReferences (_path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) - .then(refs => { - const heads = [] - const remotes = [] - const tags = [] - for (const ref of refs) { - if (ref.isTag()) { - tags.push(ref.name()) - } else if (ref.isRemote()) { - remotes.push(ref.name()) - } else if (ref.isBranch()) { - heads.push(ref.name()) - } - } - return {heads, remotes, tags} - }) - }) + return this.repo.getReferences(_path) } // Public: Get the SHA for the given reference. @@ -445,11 +266,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the current {String} SHA for the // given reference. getReferenceTarget (reference, _path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => Git.Reference.nameToId(repo, reference)) - .then(oid => oid.tostrS()) - }) + return this.repo.getReferenceTarget(reference, _path) } // Reading Status @@ -462,9 +279,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this._getStatus([relativePath])) - .then(statuses => statuses.some(status => status.isModified())) + return this.repo.isPathModified(_path) } // Public: Resolves true if the given path is new. @@ -474,9 +289,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this._getStatus([relativePath])) - .then(statuses => statuses.some(status => status.isNew())) + return this.repo.isPathNew(_path) } // Public: Is the given path ignored? @@ -486,17 +299,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is ignored. isPathIgnored (_path) { - return this.getWorkingDirectory() - .then(wd => { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => { - const relativePath = this.relativize(_path, wd) - return Git.Ignore.pathIsIgnored(repo, relativePath) - }) - .then(ignored => Boolean(ignored)) - }) - }) + return this.repo.isPathIgnored(_path) } // Get the status of a directory in the repository's working directory. @@ -507,18 +310,7 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - return this.relativizeToWorkingDirectory(directoryPath) - .then(relativePath => { - const pathspec = relativePath + '/**' - return this._getStatus([pathspec]) - }) - .then(statuses => { - return Promise.all(statuses.map(s => s.statusBit())).then(bits => { - return bits - .filter(b => b > 0) - .reduce((status, bit) => status | bit, 0) - }) - }) + return this.repo.getDirectoryStatus(directoryPath) } // Refresh the status bit for the given path. @@ -531,27 +323,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Number} which is the refreshed // status bit for the path. refreshStatusForPath (_path) { - let relativePath - return this.getWorkingDirectory() - .then(wd => { - relativePath = this.relativize(_path, wd) - return this._getStatus([relativePath]) - }) - .then(statuses => { - const cachedStatus = this.pathStatusCache[relativePath] || 0 - const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus) { - if (status === Git.Status.STATUS.CURRENT) { - delete this.pathStatusCache[relativePath] - } else { - this.pathStatusCache[relativePath] = status - } - - this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) - } - - return status - }) + return this.repo.refreshStatusForPath(_path) } // Returns a Promise that resolves to the status bit of a given path if it has @@ -567,8 +339,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a status {Number} or null if the // path is not in the cache. getCachedPathStatus (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this.pathStatusCache[relativePath]) + return this.repo.getCachedPathStatus(_path) } // Public: Get the cached statuses for the repository. @@ -576,7 +347,7 @@ export default class GitRepositoryAsync { // Returns an {Object} of {Number} statuses, keyed by {String} working // directory-relative file names. getCachedPathStatuses () { - return this.pathStatusCache + return this.repo.pathStatusCache } // Public: Returns true if the given status indicates modification. @@ -585,7 +356,7 @@ export default class GitRepositoryAsync { // // Returns a {Boolean} that's true if the `statusBit` indicates modification. isStatusModified (statusBit) { - return (statusBit & modifiedStatusFlags) > 0 + return this.repo.isStatusModified(statusBit) } // Public: Returns true if the given status indicates a new path. @@ -594,7 +365,7 @@ export default class GitRepositoryAsync { // // Returns a {Boolean} that's true if the `statusBit` indicates a new path. isStatusNew (statusBit) { - return (statusBit & newStatusFlags) > 0 + return this.repo.isStatusNew(statusBit) } // Public: Returns true if the given status indicates the path is staged. @@ -604,7 +375,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // staged. isStatusStaged (statusBit) { - return (statusBit & indexStatusFlags) > 0 + return this.repo.isStatusStaged(statusBit) } // Public: Returns true if the given status indicates the path is ignored. @@ -614,7 +385,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // ignored. isStatusIgnored (statusBit) { - return (statusBit & ignoredStatusFlags) > 0 + return this.repo.isStatusIgnored(statusBit) } // Public: Returns true if the given status indicates the path is deleted. @@ -624,7 +395,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // deleted. isStatusDeleted (statusBit) { - return (statusBit & deletedStatusFlags) > 0 + return this.repo.isStatusDeleted(statusBit) } // Retrieving Diffs @@ -640,40 +411,7 @@ export default class GitRepositoryAsync { // * `added` The {Number} of added lines. // * `deleted` The {Number} of deleted lines. getDiffStats (_path) { - return this.getWorkingDirectory(_path) - .then(wd => { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) - .then(([repo, tree]) => { - const options = new Git.DiffOptions() - options.contextLines = 0 - options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH - options.pathspec = this.relativize(_path, wd) - if (process.platform === 'win32') { - // Ignore eol of line differences on windows so that files checked in - // as LF don't report every line modified when the text contains CRLF - // endings. - options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL - } - return Git.Diff.treeToWorkdir(repo, tree, options) - }) - .then(diff => this._getDiffLines(diff)) - .then(lines => { - const stats = {added: 0, deleted: 0} - for (const line of lines) { - const origin = line.origin() - if (origin === Git.Diff.LINE.ADDITION) { - stats.added++ - } else if (origin === Git.Diff.LINE.DELETION) { - stats.deleted++ - } - } - return stats - }) - }) - }) + return this.repo.getDiffStats(_path) } // Public: Retrieves the line diffs comparing the `HEAD` version of the given @@ -688,30 +426,7 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { - return this.getWorkingDirectory(_path) - .then(wd => { - let relativePath = null - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => { - relativePath = this.relativize(_path, wd) - return repo.getHeadCommit() - }) - .then(commit => commit.getEntry(relativePath)) - .then(entry => entry.getBlob()) - .then(blob => { - const options = new Git.DiffOptions() - options.contextLines = 0 - if (process.platform === 'win32') { - // Ignore eol of line differences on windows so that files checked in - // as LF don't report every line modified when the text contains CRLF - // endings. - options.flags = Git.Diff.OPTION.IGNORE_WHITESPACE_EOL - } - return this._diffBlobToBuffer(blob, text, options) - }) - }) - }) + return this.repo.getLineDiffs(_path, text) } // Checking Out @@ -732,19 +447,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves or rejects depending on whether the // method was successful. checkoutHead (_path) { - return this.getWorkingDirectory(_path) - .then(wd => { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => { - const checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, wd)] - checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - return Git.Checkout.head(repo, checkoutOptions) - }) - }) - }) - .then(() => this.refreshStatusForPath(_path)) + return this.repo.checkoutHead(_path) } // Public: Checks out a branch in your repository. @@ -755,19 +458,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} that resolves if the method was successful. checkoutReference (reference, create) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.checkoutBranch(reference)) - }) - .catch(error => { - if (create) { - return this._createBranch(reference) - .then(_ => this.checkoutReference(reference, false)) - } else { - throw error - } - }) - .then(_ => null) + return this.repo.checkoutReference(reference, create) } // Private @@ -786,107 +477,10 @@ export default class GitRepositoryAsync { return this.checkoutHead(filePath) } - // Create a new branch with the given name. + // Refreshes the git status. // - // * `name` The {String} name of the new branch. - // - // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the - // created branch. - _createBranch (name) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, commit]) => repo.createBranch(name, commit)) - }) - } - - // Get all the hunks in the diff. - // - // * `diff` The {NodeGit.Diff} whose hunks should be retrieved. - // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. - _getDiffHunks (diff) { - return diff.patches() - .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array - .then(hunks => _.flatten(hunks)) // hunks :: Array> - } - - // Get all the lines contained in the diff. - // - // * `diff` The {NodeGit.Diff} use lines should be retrieved. - // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.Line}. - _getDiffLines (diff) { - return this._getDiffHunks(diff) - .then(hunks => Promise.all(hunks.map(h => h.lines()))) - .then(lines => _.flatten(lines)) // lines :: Array> - } - - // Diff the given blob and buffer with the provided options. - // - // * `blob` The {NodeGit.Blob} - // * `buffer` The {String} buffer. - // * `options` The {NodeGit.DiffOptions} - // - // Returns a {Promise} which resolves to an {Array} of {Object}s which have - // the following keys: - // * `oldStart` The {Number} of the old starting line. - // * `newStart` The {Number} of the new starting line. - // * `oldLines` The {Number} of old lines. - // * `newLines` The {Number} of new lines. - _diffBlobToBuffer (blob, buffer, options) { - const hunks = [] - const hunkCallback = (delta, hunk, payload) => { - hunks.push({ - oldStart: hunk.oldStart(), - newStart: hunk.newStart(), - oldLines: hunk.oldLines(), - newLines: hunk.newLines() - }) - } - - return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null) - .then(_ => hunks) - } - - // Get the current branch and update this.branch. - // - // Returns a {Promise} which resolves to a {boolean} indicating whether the - // branch name changed. - _refreshBranch () { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.getCurrentBranch()) - .then(ref => ref.name()) - .then(branchName => { - const changed = branchName !== this.branch - this.branch = branchName - return changed - }) - }) - } - - // Refresh the cached ahead/behind count with the given branch. - // - // * `branchName` The {String} name of the branch whose ahead/behind should be - // used for the refresh. - // - // Returns a {Promise} which will resolve to a {boolean} indicating whether - // the ahead/behind count changed. - _refreshAheadBehindCount (branchName) { - return this.getAheadBehindCount(branchName) - .then(counts => { - const changed = !_.isEqual(counts, this.upstream) - this.upstream = counts - return changed - }) - } - - // Get the status for this repository. - // - // Returns a {Promise} that will resolve to an object of {String} paths to the - // {Number} status. - _getRepositoryStatus () { + // Returns a {Promise} which will resolve to {null} when refresh is complete. + refreshStatus () { let projectPathsPromises = [Promise.resolve('')] if (this.project) { projectPathsPromises = this.project.getPaths() @@ -895,163 +489,7 @@ export default class GitRepositoryAsync { return Promise.all(projectPathsPromises) .then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*')) - .then(projectPaths => { - return this._getStatus(projectPaths.length > 0 ? projectPaths : null) - }) - .then(statuses => { - const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) - return _.object(statusPairs) - }) - } - - // Get the status for the given submodule. - // - // * `submodule` The {GitRepositoryAsync} for the submodule. - // - // Returns a {Promise} which resolves to an {Object}, keyed by {String} - // repo-relative {Number} statuses. - async _getSubmoduleStatus (submodule) { - // At this point, we've called submodule._refreshSubmodules(), which would - // have refreshed the status on *its* submodules, etc. So we know that its - // cached path statuses are up-to-date. - // - // Now we just need to hoist those statuses into our repository by changing - // their paths to be relative to us. - - const statuses = submodule.getCachedPathStatuses() - const repoRelativeStatuses = {} - const submoduleRepo = await submodule.getRepo() - const submoduleWorkDir = submoduleRepo.workdir() - for (const relativePath in statuses) { - const statusBit = statuses[relativePath] - const absolutePath = path.join(submoduleWorkDir, relativePath) - const repoRelativePath = await this.relativizeToWorkingDirectory(absolutePath) - repoRelativeStatuses[repoRelativePath] = statusBit - } - - return repoRelativeStatuses - } - - // Refresh the list of submodules in the repository. - // - // Returns a {Promise} which resolves to an {Object} keyed by {String} - // submodule names with {GitRepositoryAsync} values. - async _refreshSubmodules () { - const repo = await this.getRepo() - const wd = await this.getWorkingDirectory() - const submoduleNames = await repo.getSubmoduleNames() - for (const name of submoduleNames) { - const alreadyExists = Boolean(this.submodules[name]) - if (alreadyExists) continue - - const submodule = await Git.Submodule.lookup(repo, name) - const absolutePath = path.join(wd, submodule.path()) - const submoduleRepo = GitRepositoryAsync.open(absolutePath, {openExactPath: true, refreshOnWindowFocus: false}) - this.submodules[name] = submoduleRepo - } - - for (const name in this.submodules) { - const repo = this.submodules[name] - const gone = submoduleNames.indexOf(name) < 0 - if (gone) { - repo.destroy() - delete this.submodules[name] - } else { - try { - await repo.refreshStatus() - } catch (e) { - // libgit2 will sometimes report submodules that aren't actually valid - // (https://github.com/libgit2/libgit2/issues/3580). So check the - // validity of the submodules by removing any that fail. - repo.destroy() - delete this.submodules[name] - } - } - } - - return _.values(this.submodules) - } - - // Get the status for the submodules in the repository. - // - // Returns a {Promise} that will resolve to an object of {String} paths to the - // {Number} status. - _getSubmoduleStatuses () { - return this._refreshSubmodules() - .then(repos => { - return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo))) - }) - .then(statuses => _.extend({}, ...statuses)) - } - - // Refresh the cached status. - // - // Returns a {Promise} which will resolve to a {boolean} indicating whether - // any statuses changed. - _refreshStatus () { - return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()]) - .then(([repositoryStatus, submoduleStatus]) => { - const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus) - const changed = !_.isEqual(this.pathStatusCache, statusesByPath) - this.pathStatusCache = statusesByPath - return changed - }) - } - - // Refreshes the git status. - // - // Returns a {Promise} which will resolve to {null} when refresh is complete. - refreshStatus () { - const status = this._refreshStatus() - const branch = this._refreshBranch() - const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch)) - - this._refreshingPromise = this._refreshingPromise.then(_ => { - return Promise.all([status, branch, aheadBehind]) - .then(([statusChanged, branchChanged, aheadBehindChanged]) => { - if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) { - this.emitter.emit('did-change-statuses') - } - - return null - }) - // Because all these refresh steps happen asynchronously, it's entirely - // possible the repository was destroyed while we were working. In which - // case we should just swallow the error. - .catch(e => { - if (this._isDestroyed()) { - return null - } else { - return Promise.reject(e) - } - }) - .catch(e => { - console.error('Error refreshing repository status:') - console.error(e) - return Promise.reject(e) - }) - }) - return this._refreshingPromise - } - - // Get the submodule for the given path. - // - // Returns a {Promise} which resolves to the {GitRepositoryAsync} submodule or - // null if it isn't a submodule path. - async _submoduleForPath (_path) { - let relativePath = await this.relativizeToWorkingDirectory(_path) - for (const submodulePath in this.submodules) { - const submoduleRepo = this.submodules[submodulePath] - if (relativePath === submodulePath) { - return submoduleRepo - } else if (relativePath.indexOf(`${submodulePath}/`) === 0) { - relativePath = relativePath.substring(submodulePath.length + 1) - const innerSubmodule = await submoduleRepo._submoduleForPath(relativePath) - return innerSubmodule || submoduleRepo - } - } - - return null + .then(pathspecs => this.repo.refreshStatus(pathspecs)) } // Get the NodeGit repository for the given path. @@ -1062,16 +500,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the {NodeGit.Repository}. getRepo (_path) { - if (this._isDestroyed()) { - const error = new Error('Repository has been destroyed') - error.name = GitRepositoryAsync.DestroyedErrorName - return Promise.reject(error) - } - - if (!_path) return this.repoPromise - - return this._submoduleForPath(_path) - .then(submodule => submodule ? submodule.getRepo() : this.repoPromise) + return this.repo.getRepo(_path) } // Open a new instance of the underlying {NodeGit.Repository}. @@ -1081,11 +510,7 @@ export default class GitRepositoryAsync { // // Returns the new {NodeGit.Repository}. openRepository () { - if (this._openExactPath) { - return Git.Repository.open(this.openedPath) - } else { - return Git.Repository.openExt(this.openedPath, 0, '') - } + return this.repo.openRepository() } // Section: Private @@ -1095,7 +520,7 @@ export default class GitRepositoryAsync { // // Returns a {Boolean}. _isDestroyed () { - return this.repoPromise == null + return this.repo._isDestroyed() } // Subscribe to events on the given buffer. @@ -1121,28 +546,4 @@ export default class GitRepositoryAsync { this.subscriptions.add(bufferSubscriptions) } - - // Get the status for the given paths. - // - // * `paths` The {String} paths whose status is wanted. If undefined, get the - // status for the whole repository. - // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} - // statuses for the paths. - _getStatus (paths) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => { - const opts = { - flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS - } - - if (paths) { - opts.pathspec = paths - } - - return repo.getStatusExt(opts) - }) - }) - } } From 5ed30f910c788077621bc193eea334c3c6a5af90 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Apr 2016 10:43:21 -0600 Subject: [PATCH 135/262] Create synthetic iterator boundaries between open and close tags If no non-negative integer exists between an open and close tag code, we still need to report a boundary there, since close tags always have to come first at any given iterator position. :pear:ed with @as-cii --- spec/tokenized-buffer-iterator-spec.js | 40 ++++++++++++++++++++++++++ src/tokenized-buffer-iterator.coffee | 11 +++++-- 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 spec/tokenized-buffer-iterator-spec.js diff --git a/spec/tokenized-buffer-iterator-spec.js b/spec/tokenized-buffer-iterator-spec.js new file mode 100644 index 000000000..639714136 --- /dev/null +++ b/spec/tokenized-buffer-iterator-spec.js @@ -0,0 +1,40 @@ +/** @babel */ + +import TokenizedBufferIterator from '../src/tokenized-buffer-iterator' +import {Point} from 'text-buffer' + +describe('TokenizedBufferIterator', () => { + it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => { + const tokenizedBuffer = { + tokenizedLineForRow () { + return { + tags: [-1, -2, -1, -2], + text: '', + openScopes: [] + } + } + } + + const grammarRegistry = { + scopeForId () { + return 'foo' + } + } + + const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry) + + iterator.seek(Point(0, 0)) + expect(iterator.getPosition()).toEqual(Point(0, 0)) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['foo']) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(0, 0)) + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual(['foo']) + + iterator.moveToSuccessor() + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual([]) + }) +}) diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee index e6e129d7a..725041def 100644 --- a/src/tokenized-buffer-iterator.coffee +++ b/src/tokenized-buffer-iterator.coffee @@ -28,7 +28,11 @@ class TokenizedBufferIterator else scopeName = @grammarRegistry.scopeForId(tag) if tag % 2 is 0 - @closeTags.push(scopeName) + if @openTags.length > 0 + @tagIndex = index + break + else + @closeTags.push(scopeName) else @openTags.push(scopeName) @@ -56,7 +60,10 @@ class TokenizedBufferIterator else scopeName = @grammarRegistry.scopeForId(tag) if tag % 2 is 0 - @closeTags.push(scopeName) + if @openTags.length > 0 + break + else + @closeTags.push(scopeName) else @openTags.push(scopeName) @tagIndex++ From ea57a25a3fd865be35c053710ba0b75d4bf85bc2 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 21 Apr 2016 13:08:10 -0400 Subject: [PATCH 136/262] :arrow_up: ohnogit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fa4a72d3..d50a888ed 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "marked": "^0.3.4", "normalize-package-data": "^2.0.0", "nslog": "^3", - "ohnogit": "0.0.8", + "ohnogit": "0.0.9", "oniguruma": "^5", "pathwatcher": "~6.2", "property-accessors": "^1.1.3", From 809d194f316e0a966a8d13c38ff57d8ec94d6f9a Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 21 Apr 2016 13:17:35 -0400 Subject: [PATCH 137/262] Update the tests --- spec/git-repository-async-spec.js | 11 +++++------ src/git-repository-async.js | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 224280b39..d36b9fd58 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -3,7 +3,6 @@ import fs from 'fs-plus' import path from 'path' import temp from 'temp' -import Git from 'nodegit' import {it, beforeEach, afterEach} from './async-spec-helpers' @@ -47,7 +46,7 @@ describe('GitRepositoryAsync', () => { let threw = false try { - await repo.repoPromise + await repo.getRepo() } catch (e) { threw = true } @@ -64,19 +63,19 @@ describe('GitRepositoryAsync', () => { }) it('returns the repository when not given a path', async () => { - const nodeGitRepo1 = await repo.repoPromise + const nodeGitRepo1 = await repo.getRepo() const nodeGitRepo2 = await repo.getRepo() expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir()) }) it('returns the repository when given a non-submodule path', async () => { - const nodeGitRepo1 = await repo.repoPromise + const nodeGitRepo1 = await repo.getRepo() const nodeGitRepo2 = await repo.getRepo('README') expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir()) }) it('returns the submodule repository when given a submodule path', async () => { - const nodeGitRepo1 = await repo.repoPromise + const nodeGitRepo1 = await repo.getRepo() const nodeGitRepo2 = await repo.getRepo('jstips') expect(nodeGitRepo1.workdir()).not.toBe(nodeGitRepo2.workdir()) @@ -303,7 +302,7 @@ describe('GitRepositoryAsync', () => { await repo.getPathStatus(filePath) expect(statusHandler.callCount).toBe(1) - const status = Git.Status.STATUS.WT_MODIFIED + const status = GitRepositoryAsync.Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) fs.writeFileSync(filePath, 'abc') diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 6b91572d2..8201b6f6f 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -22,7 +22,7 @@ export default class GitRepositoryAsync { // The name of the error thrown when an action is attempted on a destroyed // repository. static get DestroyedErrorName () { - return 'GitRepositoryAsync.destroyed' + return Repository.DestroyedErrorName } constructor (_path, options = {}) { From 866a26a754ad87b2977a5ac27ed4a01be82b2c7c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 21 Apr 2016 13:25:14 -0400 Subject: [PATCH 138/262] Update the ignore paths for ohnogit. --- build/tasks/build-task.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index 9164f8dab..544b09753 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -54,9 +54,9 @@ module.exports = (grunt) -> # so that it doesn't becomes larger than it needs to be. ignoredPaths = [ path.join('git-utils', 'deps') - path.join('nodegit', 'vendor') - path.join('nodegit', 'node_modules', 'node-pre-gyp') - path.join('nodegit', 'node_modules', '.bin') + path.join('ohnogit', 'node_modules', 'nodegit', 'vendor') + path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', 'node-pre-gyp') + path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', '.bin') path.join('oniguruma', 'deps') path.join('less', 'dist') path.join('bootstrap', 'docs') @@ -122,9 +122,9 @@ module.exports = (grunt) -> # Ignore *.cc and *.h files from native modules ignoredPaths.push "#{_.escapeRegExp(path.join('ctags', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('git-utils', 'src') + path.sep)}.*\\.(cc|h)*" - ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'src') + path.sep)}.*\\.(cc|h)?" - ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'generate') + path.sep)}.*\\.(cc|h)?" - ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'include') + path.sep)}.*\\.(cc|h)?" + ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'src') + path.sep)}.*\\.(cc|h)?" + ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'generate') + path.sep)}.*\\.(cc|h)?" + ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'include') + path.sep)}.*\\.(cc|h)?" ignoredPaths.push "#{_.escapeRegExp(path.join('keytar', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('nslog', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('oniguruma', 'src') + path.sep)}.*\\.(cc|h)*" From c0eb8baae0c16a0cd015f5bfa4a2a2b0c501f486 Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 21 Apr 2016 13:55:43 -0400 Subject: [PATCH 139/262] Provide _refreshingPromise. --- src/git-repository-async.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 8201b6f6f..37131dd24 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -45,6 +45,11 @@ export default class GitRepositoryAsync { } } + // This exists to provide backwards compatibility. + get _refreshingPromise () { + return this.repo._refreshingPromise + } + // Public: Destroy this {GitRepositoryAsync} object. // // This destroys any tasks and subscriptions and releases the underlying From 3574613fa32242ef72016785f2333d9dc5d1ce4c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 21 Apr 2016 14:19:24 -0400 Subject: [PATCH 140/262] Don't need resource pool anymore. --- spec/resource-pool-spec.js | 66 -------------------------------------- src/resource-pool.js | 57 -------------------------------- 2 files changed, 123 deletions(-) delete mode 100644 spec/resource-pool-spec.js delete mode 100644 src/resource-pool.js diff --git a/spec/resource-pool-spec.js b/spec/resource-pool-spec.js deleted file mode 100644 index 27893360a..000000000 --- a/spec/resource-pool-spec.js +++ /dev/null @@ -1,66 +0,0 @@ -/** @babel */ - -import ResourcePool from '../src/resource-pool' - -import {it} from './async-spec-helpers' - -describe('ResourcePool', () => { - let queue - - beforeEach(() => { - queue = new ResourcePool([{}]) - }) - - describe('.enqueue', () => { - it('calls the enqueued function', async () => { - let called = false - await queue.enqueue(() => { - called = true - return Promise.resolve() - }) - expect(called).toBe(true) - }) - - it('forwards values from the inner promise', async () => { - const result = await queue.enqueue(() => Promise.resolve(42)) - expect(result).toBe(42) - }) - - it('forwards errors from the inner promise', async () => { - let threw = false - try { - await queue.enqueue(() => Promise.reject(new Error('down with the sickness'))) - } catch (e) { - threw = true - } - expect(threw).toBe(true) - }) - - it('continues to dequeue work after a promise has been rejected', async () => { - try { - await queue.enqueue(() => Promise.reject(new Error('down with the sickness'))) - } catch (e) {} - - const result = await queue.enqueue(() => Promise.resolve(42)) - expect(result).toBe(42) - }) - - it('queues up work', async () => { - let resolve = null - queue.enqueue(() => { - return new Promise((resolve_, reject) => { - resolve = resolve_ - }) - }) - - expect(queue.getQueueDepth()).toBe(0) - - queue.enqueue(() => new Promise((resolve, reject) => {})) - - expect(queue.getQueueDepth()).toBe(1) - resolve() - - waitsFor(() => queue.getQueueDepth() === 0) - }) - }) -}) diff --git a/src/resource-pool.js b/src/resource-pool.js deleted file mode 100644 index ae7cb71d0..000000000 --- a/src/resource-pool.js +++ /dev/null @@ -1,57 +0,0 @@ -/** @babel */ - -// Manages a pool of some resource. -export default class ResourcePool { - constructor (pool) { - this.pool = pool - - this.queue = [] - } - - // Enqueue the given function. The function will be given an object from the - // pool. The function must return a {Promise}. - enqueue (fn) { - let resolve = null - let reject = null - const wrapperPromise = new Promise((resolve_, reject_) => { - resolve = resolve_ - reject = reject_ - }) - - this.queue.push(this.wrapFunction(fn, resolve, reject)) - - this.dequeueIfAble() - - return wrapperPromise - } - - wrapFunction (fn, resolve, reject) { - return (resource) => { - const promise = fn(resource) - promise - .then(result => { - resolve(result) - this.taskDidComplete(resource) - }, error => { - reject(error) - this.taskDidComplete(resource) - }) - } - } - - taskDidComplete (resource) { - this.pool.push(resource) - - this.dequeueIfAble() - } - - dequeueIfAble () { - if (!this.pool.length || !this.queue.length) return - - const fn = this.queue.shift() - const resource = this.pool.shift() - fn(resource) - } - - getQueueDepth () { return this.queue.length } -} From 4a0c9467854016c6ee20e8906e9b9c9dceb3179c Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 21 Apr 2016 15:29:50 -0400 Subject: [PATCH 141/262] :arrow_up: status-bar@1.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d50a888ed..a9a5314fd 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "settings-view": "0.235.1", "snippets": "1.0.2", "spell-check": "0.67.1", - "status-bar": "1.2.3", + "status-bar": "1.2.4", "styleguide": "0.45.2", "symbols-view": "0.112.0", "tabs": "0.92.2", From 53c52342e5eac36a3dbe6020c3798b6ffb535289 Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Thu, 21 Apr 2016 13:36:50 -0700 Subject: [PATCH 142/262] Add Ctrl+F4 keybinding for Linux --- keymaps/linux.cson | 1 + 1 file changed, 1 insertion(+) diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 7d67e2ce5..1f78739a9 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -27,6 +27,7 @@ 'ctrl-n': 'application:new-file' 'ctrl-s': 'core:save' 'ctrl-S': 'core:save-as' + 'ctrl-f4': 'core:close' 'ctrl-w': 'core:close' 'ctrl-z': 'core:undo' 'ctrl-y': 'core:redo' From d276f93a233423375496da79682afa838387bc75 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Apr 2016 11:12:15 +0200 Subject: [PATCH 143/262] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0cd63e918..9b79835b0 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "8.4.6", + "text-buffer": "8.5.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 630cbb7bb72a9c59b2a87300545df4737bda2a57 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Apr 2016 11:14:24 +0200 Subject: [PATCH 144/262] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f51f22895..d20e7636a 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta4", + "text-buffer": "9.0.0-beta5", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 6fc3a364e1c43921fe0987e4e91ca254b364d33e Mon Sep 17 00:00:00 2001 From: Riley Dallas Date: Fri, 22 Apr 2016 09:30:45 -0500 Subject: [PATCH 145/262] :memo: Update CSON documentation link in snippets.cson --- dot-atom/snippets.cson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dot-atom/snippets.cson b/dot-atom/snippets.cson index eb8f1b22a..cd66bba04 100644 --- a/dot-atom/snippets.cson +++ b/dot-atom/snippets.cson @@ -18,4 +18,4 @@ # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it in the # Atom Flight Manual: -# https://atom.io/docs/latest/using-atom-basic-customization#cson +# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson From 58e1953e43e206788706087a813396915f470316 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 22 Apr 2016 12:07:19 -0700 Subject: [PATCH 146/262] Win32 build now installs to localappdata. Admin elevation now smart. --- build/Gruntfile.coffee | 2 +- build/tasks/install-task.coffee | 20 ++++++++++++++++---- docs/build-instructions/windows.md | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 73279779c..16e0ed5d6 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -57,7 +57,7 @@ module.exports = (grunt) -> homeDir = process.env.USERPROFILE contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') - installDir ?= path.join(process.env.ProgramFiles, appName) + installDir ?= path.join(process.env.LOCALAPPDATA, appName, 'app-dev') killCommand = 'taskkill /F /IM atom.exe' else if process.platform is 'darwin' homeDir = process.env.HOME diff --git a/build/tasks/install-task.coffee b/build/tasks/install-task.coffee index 2d9054385..19fd3d383 100644 --- a/build/tasks/install-task.coffee +++ b/build/tasks/install-task.coffee @@ -16,10 +16,22 @@ module.exports = (grunt) -> {description} = grunt.config.get('atom.metadata') if process.platform is 'win32' - runas ?= require 'runas' - copyFolder = path.resolve 'script', 'copy-folder.cmd' - if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0 - grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}") + done = @async() + fs.access(installDir, fs.W_OK, (err) -> + adminRequired = true if err + if adminRequired + grunt.log.ok("User does not have write access to #{installDir}, elevating to admin") + runas ?= require 'runas' + copyFolder = path.resolve 'script', 'copy-folder.cmd' + + if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: adminRequired) isnt 0 + grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}") + else + grunt.log.ok("Installed into #{installDir}") + + done() + ) + else if process.platform is 'darwin' rm installDir mkdir path.dirname(installDir) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 3ec28f139..d0e101ba0 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -34,7 +34,7 @@ git clone https://github.com/atom/atom/ cd atom script/build ``` -This will create the Atom application in the `out\Atom` folder as well as copy it to a folder named `Atom` within `Program Files`. +This will create the Atom application in the `out\Atom` folder as well as copy it to a subfolder of your user profile (e.g. `c:\Users\Bob`) called `AppData\Local\atom\app-dev`. ### `script/build` Options * `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional): From 11170696d54f28ac8c7bbc67c89030610ee3e4a9 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 22 Apr 2016 18:10:53 -0400 Subject: [PATCH 147/262] :arrow_up: tree-view@0.206.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b07b4190f..1b6994fed 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "symbols-view": "0.112.0", "tabs": "0.92.2", "timecop": "0.33.1", - "tree-view": "0.206.0", + "tree-view": "0.206.1", "update-package-dependencies": "0.10.0", "welcome": "0.34.0", "whitespace": "0.32.2", From 8e78441e3287a781cda6e95a785557c22f3f183f Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Fri, 22 Apr 2016 15:16:08 -0700 Subject: [PATCH 148/262] :arrow_up: tabs@0.92.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b6994fed..9317866ef 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "status-bar": "1.2.4", "styleguide": "0.45.2", "symbols-view": "0.112.0", - "tabs": "0.92.2", + "tabs": "0.92.3", "timecop": "0.33.1", "tree-view": "0.206.1", "update-package-dependencies": "0.10.0", From e5e1226ac062ca67cb291e2a3cfe72a1ac6122be Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Apr 2016 11:49:20 -0400 Subject: [PATCH 149/262] :arrow_up: language-c@0.51.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9317866ef..98dbd7385 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "welcome": "0.34.0", "whitespace": "0.32.2", "wrap-guide": "0.38.1", - "language-c": "0.51.3", + "language-c": "0.51.4", "language-clojure": "0.20.0", "language-coffee-script": "0.47.0", "language-csharp": "0.12.1", From cf79619292fcad749fccf56c8179c656c04727dc Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Sat, 23 Apr 2016 18:31:07 -0700 Subject: [PATCH 150/262] :arrow_up: tree-view@0.206.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98dbd7385..94478dbf0 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "symbols-view": "0.112.0", "tabs": "0.92.3", "timecop": "0.33.1", - "tree-view": "0.206.1", + "tree-view": "0.206.2", "update-package-dependencies": "0.10.0", "welcome": "0.34.0", "whitespace": "0.32.2", From f6f5c93b01a5a867b048f3da2baa8fac931b653a Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Sat, 23 Apr 2016 18:31:27 -0700 Subject: [PATCH 151/262] :arrow_up: tabs@0.93.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 94478dbf0..9dd47b12b 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "status-bar": "1.2.4", "styleguide": "0.45.2", "symbols-view": "0.112.0", - "tabs": "0.92.3", + "tabs": "0.93.0", "timecop": "0.33.1", "tree-view": "0.206.2", "update-package-dependencies": "0.10.0", From 4825d7ee4b6fcf53b8a9df674a91b07fd4c7edcd Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Sat, 23 Apr 2016 20:15:31 -0700 Subject: [PATCH 152/262] :arrow_up: tabs@0.93.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9dd47b12b..957d262f0 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "status-bar": "1.2.4", "styleguide": "0.45.2", "symbols-view": "0.112.0", - "tabs": "0.93.0", + "tabs": "0.93.1", "timecop": "0.33.1", "tree-view": "0.206.2", "update-package-dependencies": "0.10.0", From 8bf34d45d7f4857788b63ea094d12dc7f95eb8ee Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:12:07 -0400 Subject: [PATCH 153/262] Don't require notification manager in TextEditor Instead expose getCursorScope and let users outside the text editor show the notification. --- src/text-editor.coffee | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 0d1b3795e..9d81f7dd0 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -79,7 +79,6 @@ class TextEditor extends Model state.displayBuffer = displayBuffer state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config - state.notificationManager = atomEnvironment.notifications state.packageManager = atomEnvironment.packages state.clipboard = atomEnvironment.clipboard state.viewRegistry = atomEnvironment.views @@ -100,12 +99,11 @@ class TextEditor extends Model @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, - @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, + @packageManager, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? - throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager? throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager? throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry? @@ -520,7 +518,7 @@ class TextEditor extends Model softTabs = @getSoftTabs() newEditor = new TextEditor({ @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, - suppressCursorCreation: true, @config, @notificationManager, @packageManager, + suppressCursorCreation: true, @config, @packageManager, @firstVisibleScreenRow, @firstVisibleScreenColumn, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate }) @@ -2827,13 +2825,9 @@ class TextEditor extends Model @commentScopeSelector ?= new TextMateScopeSelector('comment.*') @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) - logCursorScope: -> - scopeDescriptor = @getLastCursor().getScopeDescriptor() - list = scopeDescriptor.scopes.toString().split(',') - list = list.map (item) -> "* #{item}" - content = "Scopes at Cursor\n#{list.join('\n')}" - - @notificationManager.addInfo(content, dismissable: true) + # Get the scope descriptor at the cursor. + getCursorScope: -> + @getLastCursor().getScopeDescriptor() # {Delegates to: DisplayBuffer.tokenForBufferPosition} tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) From 48e49061e6bdece8d2f128987a71649aecbea7c9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:12:20 -0400 Subject: [PATCH 154/262] Don't need to pass in the notification manager anymore. --- src/workspace.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index b8ed79fd6..838a1dcec 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -564,7 +564,7 @@ class Workspace extends Model # Returns a {TextEditor}. buildTextEditor: (params) -> params = _.extend({ - @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, + @config, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate }, params) new TextEditor(params) From 694af93009b5339171cb8f5f98b572a99bc11024 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:12:38 -0400 Subject: [PATCH 155/262] Implement the show cursor scope functionality in the default commands. --- src/atom-environment.coffee | 2 +- src/register-default-commands.coffee | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index ffff564ba..cf54ee0bc 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -255,7 +255,7 @@ class AtomEnvironment extends Model @deserializers.add(TextBuffer) registerDefaultCommands: -> - registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller}) + registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications}) registerDefaultViewProviders: -> @views.addViewProvider Workspace, (model, env) -> diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index bb3630117..4f329e943 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -1,6 +1,6 @@ {ipcRenderer} = require 'electron' -module.exports = ({commandRegistry, commandInstaller, config}) -> +module.exports = ({commandRegistry, commandInstaller, config, notificationManager}) -> commandRegistry.add 'atom-workspace', 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() @@ -187,7 +187,7 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) - 'editor:log-cursor-scope': -> @logCursorScope() + 'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager) 'editor:copy-path': -> @copyPathToClipboard(false) 'editor:copy-project-path': -> @copyPathToClipboard(true) 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) @@ -232,3 +232,10 @@ stopEventPropagationAndGroupUndo = (config, commandListeners) -> model.transact config.get('editor.undoGroupingInterval'), -> commandListener.call(model, event) newCommandListeners + +showCursorScope = (descriptor, notificationManager) -> + list = descriptor.scopes.toString().split(',') + list = list.map (item) -> "* #{item}" + content = "Scopes at Cursor\n#{list.join('\n')}" + + notificationManager.addInfo(content, dismissable: true) From d494f065df90065c0b784ded343b1baac1bede4d Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:12:42 -0400 Subject: [PATCH 156/262] Update the spec. --- spec/text-editor-spec.coffee | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 7d85f1cad..9dcb61285 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1189,14 +1189,10 @@ describe "TextEditor", -> cursor2 = editor.addCursorAtBufferPosition([1, 4]) expect(cursor2.marker).toBe cursor1.marker - describe '.logCursorScope()', -> - beforeEach -> - spyOn(atom.notifications, 'addInfo') - - it 'opens a notification', -> - editor.logCursorScope() - - expect(atom.notifications.addInfo).toHaveBeenCalled() + describe '.getCursorScope()', -> + it 'returns the current scope', -> + descriptor = editor.getCursorScope() + expect(descriptor.scopes).toContain ('source.js') describe "selection", -> selection = null From 956e037681d023877147a15da1916f5d1737963f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:20:54 -0400 Subject: [PATCH 157/262] But without the space. --- spec/text-editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 9dcb61285..bc08b3f0e 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1192,7 +1192,7 @@ describe "TextEditor", -> describe '.getCursorScope()', -> it 'returns the current scope', -> descriptor = editor.getCursorScope() - expect(descriptor.scopes).toContain ('source.js') + expect(descriptor.scopes).toContain('source.js') describe "selection", -> selection = null From 6b309be6dac0ee8e2ac3028a36f7d92526f8ec11 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:49:43 -0400 Subject: [PATCH 158/262] Propagate a did-use-grammar event out of the tokenized buffer. --- src/display-buffer.coffee | 3 +++ src/text-editor.coffee | 3 +++ src/tokenized-buffer.coffee | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index d01ad03c9..717ab5e4f 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -137,6 +137,9 @@ class DisplayBuffer extends Model onDidChangeGrammar: (callback) -> @tokenizedBuffer.onDidChangeGrammar(callback) + onDidUseGrammar: (callback) -> + @tokenizedBuffer.onDidUseGrammar(callback) + onDidTokenize: (callback) -> @tokenizedBuffer.onDidTokenize(callback) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 9d81f7dd0..aaa63b2e5 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -325,6 +325,9 @@ class TextEditor extends Model onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback + onDidUseGrammar: (callback) -> + @displayBuffer.onDidUseGrammar(callback) + # Extended: Calls your `callback` when the result of {::isModified} changes. # # * `callback` {Function} diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 5c62f9ecd..1188aeaf0 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -126,9 +126,12 @@ class TokenizedBuffer extends Model @disposables.add(@configSubscriptions) @retokenizeLines() - @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") + @emitter.emit 'did-use-grammar', grammar @emitter.emit 'did-change-grammar', grammar + onDidUseGrammar: (callback) -> + @emitter.on 'did-use-grammar', callback + getGrammarSelectionContent: -> @buffer.getTextInRange([[0, 0], [10, 0]]) From e8a4f38c6945f0f50691a46f3c73b3582fb781c5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:50:14 -0400 Subject: [PATCH 159/262] Don't need to pass the package manager through anymore. --- src/display-buffer.coffee | 7 +++---- src/text-editor.coffee | 8 +++----- src/tokenized-buffer.coffee | 3 +-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 717ab5e4f..ccc68535f 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -35,7 +35,6 @@ class DisplayBuffer extends Model state.config = atomEnvironment.config state.assert = atomEnvironment.assert state.grammarRegistry = atomEnvironment.grammars - state.packageManager = atomEnvironment.packages new this(state) constructor: (params={}) -> @@ -43,7 +42,7 @@ class DisplayBuffer extends Model { tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer, - ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager + ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry } = params @emitter = new Emitter @@ -51,7 +50,7 @@ class DisplayBuffer extends Model @tokenizedBuffer ?= new TokenizedBuffer({ tabLength, buffer, ignoreInvisibles, @largeFileMode, @config, - @grammarRegistry, @packageManager, @assert + @grammarRegistry, @assert }) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @@ -122,7 +121,7 @@ class DisplayBuffer extends Model foldsMarkerLayer = @foldsMarkerLayer.copy() new DisplayBuffer({ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, - @grammarRegistry, @packageManager, foldsMarkerLayer + @grammarRegistry, foldsMarkerLayer }) updateAllScreenLines: -> diff --git a/src/text-editor.coffee b/src/text-editor.coffee index aaa63b2e5..865026e20 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -79,7 +79,6 @@ class TextEditor extends Model state.displayBuffer = displayBuffer state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config - state.packageManager = atomEnvironment.packages state.clipboard = atomEnvironment.clipboard state.viewRegistry = atomEnvironment.views state.grammarRegistry = atomEnvironment.grammars @@ -99,12 +98,11 @@ class TextEditor extends Model @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, - @packageManager, @clipboard, @viewRegistry, @grammarRegistry, + @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? - throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager? throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry? throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? @@ -127,7 +125,7 @@ class TextEditor extends Model buffer ?= new TextBuffer @displayBuffer ?= new DisplayBuffer({ buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode, - @config, @assert, @grammarRegistry, @packageManager + @config, @assert, @grammarRegistry }) @buffer = @displayBuffer.buffer @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) @@ -521,7 +519,7 @@ class TextEditor extends Model softTabs = @getSoftTabs() newEditor = new TextEditor({ @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, - suppressCursorCreation: true, @config, @packageManager, + suppressCursorCreation: true, @config, @firstVisibleScreenRow, @firstVisibleScreenColumn, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate }) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 1188aeaf0..f01498b68 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -29,14 +29,13 @@ class TokenizedBuffer extends Model state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) state.config = atomEnvironment.config state.grammarRegistry = atomEnvironment.grammars - state.packageManager = atomEnvironment.packages state.assert = atomEnvironment.assert new this(state) constructor: (params) -> { @buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config, - @grammarRegistry, @packageManager, @assert, grammarScopeName + @grammarRegistry, @assert, grammarScopeName } = params @emitter = new Emitter From b1301a5f7416236a99cbe6203c91284f7d486355 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 14:50:34 -0400 Subject: [PATCH 160/262] Trigger the activation hook outside the text editor. --- src/workspace.coffee | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index 838a1dcec..a54ace2a2 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -550,9 +550,15 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options)) disposable = atom.textEditors.add(editor) - editor.onDidDestroy -> disposable.dispose() + grammarSubscription = editor.onDidUseGrammar(@handleDidUseGrammar.bind(this)) + editor.onDidDestroy -> + grammarSubscription.dispose() + disposable.dispose() editor + handleDidUseGrammar: (grammar) -> + @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") + # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. # # * `object` An {Object} you want to perform the check against. From 02bbb140529357db559610011a4ba60a5b19771c Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 15:02:16 -0400 Subject: [PATCH 161/262] Better method name --- src/workspace.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index a54ace2a2..c8567489d 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -550,13 +550,13 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options)) disposable = atom.textEditors.add(editor) - grammarSubscription = editor.onDidUseGrammar(@handleDidUseGrammar.bind(this)) + grammarSubscription = editor.onDidUseGrammar(@activateGrammar.bind(this)) editor.onDidDestroy -> grammarSubscription.dispose() disposable.dispose() editor - handleDidUseGrammar: (grammar) -> + activateGrammar: (grammar) -> @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. From 9014634b3a7d3591c68b66c3259cb410fbaabfd3 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 15:09:03 -0400 Subject: [PATCH 162/262] Just use the already existing change grammar event. --- src/display-buffer.coffee | 3 --- src/text-editor.coffee | 3 --- src/tokenized-buffer.coffee | 4 ---- src/workspace.coffee | 2 +- 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index ccc68535f..109b791a1 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -136,9 +136,6 @@ class DisplayBuffer extends Model onDidChangeGrammar: (callback) -> @tokenizedBuffer.onDidChangeGrammar(callback) - onDidUseGrammar: (callback) -> - @tokenizedBuffer.onDidUseGrammar(callback) - onDidTokenize: (callback) -> @tokenizedBuffer.onDidTokenize(callback) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 865026e20..aeb5ebe5c 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -323,9 +323,6 @@ class TextEditor extends Model onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback - onDidUseGrammar: (callback) -> - @displayBuffer.onDidUseGrammar(callback) - # Extended: Calls your `callback` when the result of {::isModified} changes. # # * `callback` {Function} diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index f01498b68..38e92f247 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -125,12 +125,8 @@ class TokenizedBuffer extends Model @disposables.add(@configSubscriptions) @retokenizeLines() - @emitter.emit 'did-use-grammar', grammar @emitter.emit 'did-change-grammar', grammar - onDidUseGrammar: (callback) -> - @emitter.on 'did-use-grammar', callback - getGrammarSelectionContent: -> @buffer.getTextInRange([[0, 0], [10, 0]]) diff --git a/src/workspace.coffee b/src/workspace.coffee index c8567489d..96ba00259 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -550,7 +550,7 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options)) disposable = atom.textEditors.add(editor) - grammarSubscription = editor.onDidUseGrammar(@activateGrammar.bind(this)) + grammarSubscription = editor.onDidChangeGrammar(@activateGrammar.bind(this)) editor.onDidDestroy -> grammarSubscription.dispose() disposable.dispose() From 2266f1e3b67a054f2a816dfe9a21876f4f1ebd82 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 15:15:31 -0400 Subject: [PATCH 163/262] Revert "Just use the already existing change grammar event." This reverts commit 9014634b3a7d3591c68b66c3259cb410fbaabfd3. --- src/display-buffer.coffee | 3 +++ src/text-editor.coffee | 3 +++ src/tokenized-buffer.coffee | 4 ++++ src/workspace.coffee | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 109b791a1..ccc68535f 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -136,6 +136,9 @@ class DisplayBuffer extends Model onDidChangeGrammar: (callback) -> @tokenizedBuffer.onDidChangeGrammar(callback) + onDidUseGrammar: (callback) -> + @tokenizedBuffer.onDidUseGrammar(callback) + onDidTokenize: (callback) -> @tokenizedBuffer.onDidTokenize(callback) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index aeb5ebe5c..865026e20 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -323,6 +323,9 @@ class TextEditor extends Model onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback + onDidUseGrammar: (callback) -> + @displayBuffer.onDidUseGrammar(callback) + # Extended: Calls your `callback` when the result of {::isModified} changes. # # * `callback` {Function} diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 38e92f247..f01498b68 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -125,8 +125,12 @@ class TokenizedBuffer extends Model @disposables.add(@configSubscriptions) @retokenizeLines() + @emitter.emit 'did-use-grammar', grammar @emitter.emit 'did-change-grammar', grammar + onDidUseGrammar: (callback) -> + @emitter.on 'did-use-grammar', callback + getGrammarSelectionContent: -> @buffer.getTextInRange([[0, 0], [10, 0]]) diff --git a/src/workspace.coffee b/src/workspace.coffee index 96ba00259..c8567489d 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -550,7 +550,7 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options)) disposable = atom.textEditors.add(editor) - grammarSubscription = editor.onDidChangeGrammar(@activateGrammar.bind(this)) + grammarSubscription = editor.onDidUseGrammar(@activateGrammar.bind(this)) editor.onDidDestroy -> grammarSubscription.dispose() disposable.dispose() From eaf6036a2c76bc94ed01e93a24f400fce65c018e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 15:34:37 -0400 Subject: [PATCH 164/262] Wait a tick before sending the event. --- src/tokenized-buffer.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index f01498b68..b1b0749b4 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -125,9 +125,14 @@ class TokenizedBuffer extends Model @disposables.add(@configSubscriptions) @retokenizeLines() - @emitter.emit 'did-use-grammar', grammar + @emitter.emit 'did-change-grammar', grammar + # Delay this to the next tick to ensure whoever created the buffer has the + # change to listen for this event before we send it. + process.nextTick => + @emitter.emit 'did-use-grammar', grammar + onDidUseGrammar: (callback) -> @emitter.on 'did-use-grammar', callback From cf1b4e22172c9bd512fba0d0947b064eaa0be10c Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 15:34:44 -0400 Subject: [PATCH 165/262] Another new name. --- src/workspace.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index c8567489d..975300ae4 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -550,13 +550,13 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options)) disposable = atom.textEditors.add(editor) - grammarSubscription = editor.onDidUseGrammar(@activateGrammar.bind(this)) + grammarSubscription = editor.onDidUseGrammar(@handleGrammarUsed.bind(this)) editor.onDidDestroy -> grammarSubscription.dispose() disposable.dispose() editor - activateGrammar: (grammar) -> + handleGrammarUsed: (grammar) -> @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. From 6852d6e91c290e104b3f10ad66116e9660210781 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 15:34:46 -0400 Subject: [PATCH 166/262] Test it. --- spec/workspace-spec.coffee | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index fc22f07c3..c61a57bf4 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -1619,3 +1619,15 @@ describe "Workspace", -> escapeStringRegex = (str) -> str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + + describe "grammar activation", -> + it "activates grammars", -> + editor = null + + atom.workspace.handleGrammarUsed = jasmine.createSpy() + + waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o + runs -> + atom.grammars.setGrammarOverrideForPath(editor.getPath(), 'source.coffee') + editor.reloadGrammar() + waitsFor -> atom.workspace.handleGrammarUsed.callCount is 1 From 759d64501d1b1b80611b29a435ef14ef45f9919e Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 16:07:49 -0400 Subject: [PATCH 167/262] Better test. --- spec/workspace-spec.coffee | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index c61a57bf4..b84e873da 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -1617,17 +1617,21 @@ describe "Workspace", -> runs -> expect(pane.getPendingItem()).toBeFalsy() - escapeStringRegex = (str) -> - str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') - describe "grammar activation", -> - it "activates grammars", -> + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + it "notifies the workspace of which grammar is used", -> editor = null - atom.workspace.handleGrammarUsed = jasmine.createSpy() + grammarUsed = jasmine.createSpy() + atom.workspace.handleGrammarUsed = grammarUsed waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o + waitsFor -> grammarUsed.callCount is 1 runs -> - atom.grammars.setGrammarOverrideForPath(editor.getPath(), 'source.coffee') - editor.reloadGrammar() - waitsFor -> atom.workspace.handleGrammarUsed.callCount is 1 + expect(grammarUsed.argsForCall[0][0].name).toBe 'JavaScript' + + escapeStringRegex = (str) -> + str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') From 58d8e6bca8d5f0c6e2bd6e26b48c3d98d5612b20 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 16:11:38 -0400 Subject: [PATCH 168/262] Typo --- src/tokenized-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index b1b0749b4..7e3c4fe49 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -129,7 +129,7 @@ class TokenizedBuffer extends Model @emitter.emit 'did-change-grammar', grammar # Delay this to the next tick to ensure whoever created the buffer has the - # change to listen for this event before we send it. + # chance to listen for this event before we send it. process.nextTick => @emitter.emit 'did-use-grammar', grammar From 3a63f90ab2469a61975ecc084c36d41270b26938 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 16:24:27 -0400 Subject: [PATCH 169/262] We just say getElement now. --- src/text-editor.coffee | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 865026e20..bd0c9f9f8 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -486,12 +486,12 @@ class TextEditor extends Model onDidChangeScrollTop: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") - @viewRegistry.getView(this).onDidChangeScrollTop(callback) + @getElement().onDidChangeScrollTop(callback) onDidChangeScrollLeft: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") - @viewRegistry.getView(this).onDidChangeScrollLeft(callback) + @getElement().onDidChangeScrollLeft(callback) onDidRequestAutoscroll: (callback) -> @displayBuffer.onDidRequestAutoscroll(callback) @@ -3133,24 +3133,24 @@ class TextEditor extends Model scrollToTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - @viewRegistry.getView(this).scrollToTop() + @getElement().scrollToTop() scrollToBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - @viewRegistry.getView(this).scrollToBottom() + @getElement().scrollToBottom() scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) getHorizontalScrollbarHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") - @viewRegistry.getView(this).getHorizontalScrollbarHeight() + @getElement().getHorizontalScrollbarHeight() getVerticalScrollbarWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") - @viewRegistry.getView(this).getVerticalScrollbarWidth() + @getElement().getVerticalScrollbarWidth() pageUp: -> @moveUp(@getRowsPerPage()) @@ -3217,11 +3217,11 @@ class TextEditor extends Model pixelPositionForBufferPosition: (bufferPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") - @viewRegistry.getView(this).pixelPositionForBufferPosition(bufferPosition) + @getElement().pixelPositionForBufferPosition(bufferPosition) pixelPositionForScreenPosition: (screenPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") - @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition) + @getElement().pixelPositionForScreenPosition(screenPosition) getSelectionMarkerAttributes: -> {type: 'selection', invalidate: 'never'} @@ -3250,7 +3250,7 @@ class TextEditor extends Model @displayBuffer.setHeight(height) else Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") - @viewRegistry.getView(this).setHeight(height) + @getElement().setHeight(height) getHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") @@ -3263,7 +3263,7 @@ class TextEditor extends Model @displayBuffer.setWidth(width) else Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") - @viewRegistry.getView(this).setWidth(width) + @getElement().setWidth(width) getWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") @@ -3307,77 +3307,77 @@ class TextEditor extends Model getScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") - @viewRegistry.getView(this).getScrollTop() + @getElement().getScrollTop() setScrollTop: (scrollTop) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") - @viewRegistry.getView(this).setScrollTop(scrollTop) + @getElement().setScrollTop(scrollTop) getScrollBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") - @viewRegistry.getView(this).getScrollBottom() + @getElement().getScrollBottom() setScrollBottom: (scrollBottom) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") - @viewRegistry.getView(this).setScrollBottom(scrollBottom) + @getElement().setScrollBottom(scrollBottom) getScrollLeft: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") - @viewRegistry.getView(this).getScrollLeft() + @getElement().getScrollLeft() setScrollLeft: (scrollLeft) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") - @viewRegistry.getView(this).setScrollLeft(scrollLeft) + @getElement().setScrollLeft(scrollLeft) getScrollRight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") - @viewRegistry.getView(this).getScrollRight() + @getElement().getScrollRight() setScrollRight: (scrollRight) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") - @viewRegistry.getView(this).setScrollRight(scrollRight) + @getElement().setScrollRight(scrollRight) getScrollHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") - @viewRegistry.getView(this).getScrollHeight() + @getElement().getScrollHeight() getScrollWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") - @viewRegistry.getView(this).getScrollWidth() + @getElement().getScrollWidth() getMaxScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.") - @viewRegistry.getView(this).getMaxScrollTop() + @getElement().getMaxScrollTop() intersectsVisibleRowRange: (startRow, endRow) -> Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") - @viewRegistry.getView(this).intersectsVisibleRowRange(startRow, endRow) + @getElement().intersectsVisibleRowRange(startRow, endRow) selectionIntersectsVisibleRowRange: (selection) -> Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") - @viewRegistry.getView(this).selectionIntersectsVisibleRowRange(selection) + @getElement().selectionIntersectsVisibleRowRange(selection) screenPositionForPixelPosition: (pixelPosition) -> Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") - @viewRegistry.getView(this).screenPositionForPixelPosition(pixelPosition) + @getElement().screenPositionForPixelPosition(pixelPosition) pixelRectForScreenRange: (screenRange) -> Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") - @viewRegistry.getView(this).pixelRectForScreenRange(screenRange) + @getElement().pixelRectForScreenRange(screenRange) ### Section: Utility From 1c6d9728c49c754a459c50af20eab4bc5f93b576 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 16:25:36 -0400 Subject: [PATCH 170/262] Don't need to pass packageManager in anymore. --- src/workspace.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index 975300ae4..9a52dc937 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -570,8 +570,8 @@ class Workspace extends Model # Returns a {TextEditor}. buildTextEditor: (params) -> params = _.extend({ - @config, @packageManager, @clipboard, @viewRegistry, - @grammarRegistry, @project, @assert, @applicationDelegate + @config, @clipboard, @viewRegistry, @grammarRegistry, + @project, @assert, @applicationDelegate }, params) new TextEditor(params) From 8d7f1b8fba62694677aeeb16d3c6870a6ddeb323 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 16:26:28 -0400 Subject: [PATCH 171/262] Don't need to pass view registry through anymore. --- src/text-editor.coffee | 7 ++----- src/workspace.coffee | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index bd0c9f9f8..e30696479 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -80,7 +80,6 @@ class TextEditor extends Model state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config state.clipboard = atomEnvironment.clipboard - state.viewRegistry = atomEnvironment.views state.grammarRegistry = atomEnvironment.grammars state.project = atomEnvironment.project state.assert = atomEnvironment.assert.bind(atomEnvironment) @@ -97,14 +96,12 @@ class TextEditor extends Model { @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, - @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, - @clipboard, @viewRegistry, @grammarRegistry, + @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @clipboard, @grammarRegistry, @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? - throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry? throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? throw new Error("Must pass a project parameter when constructing TextEditors") unless @project? throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert? @@ -521,7 +518,7 @@ class TextEditor extends Model @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, suppressCursorCreation: true, @config, @firstVisibleScreenRow, @firstVisibleScreenColumn, - @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate + @clipboard, @grammarRegistry, @project, @assert, @applicationDelegate }) newEditor diff --git a/src/workspace.coffee b/src/workspace.coffee index 9a52dc937..c6ab13faf 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -570,7 +570,7 @@ class Workspace extends Model # Returns a {TextEditor}. buildTextEditor: (params) -> params = _.extend({ - @config, @clipboard, @viewRegistry, @grammarRegistry, + @config, @packageManager, @clipboard, @grammarRegistry, @project, @assert, @applicationDelegate }, params) new TextEditor(params) From 856697e55f7815cb78b7138c3b2fb59d93c8a403 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 16:31:28 -0400 Subject: [PATCH 172/262] Uh we seriously don't need package manager. Not sure how that slipped back in there. --- src/workspace.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index c6ab13faf..8f973d2fa 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -570,7 +570,7 @@ class Workspace extends Model # Returns a {TextEditor}. buildTextEditor: (params) -> params = _.extend({ - @config, @packageManager, @clipboard, @grammarRegistry, + @config, @clipboard, @grammarRegistry, @project, @assert, @applicationDelegate }, params) new TextEditor(params) From 5cf532ebe889b809a465c85333cb5e89643d756b Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 17:02:36 -0400 Subject: [PATCH 173/262] Move checkoutHeadRevision to Workspace. --- src/register-default-commands.coffee | 2 +- src/text-editor.coffee | 19 ------------------- src/workspace.coffee | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 4f329e943..035750363 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -204,7 +204,7 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'editor:newline-below': -> @insertNewlineBelow() 'editor:newline-above': -> @insertNewlineAbove() 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() - 'editor:checkout-head-revision': -> @checkoutHeadRevision() + 'editor:checkout-head-revision': -> atom.workspace.checkoutHeadRevision(this) 'editor:move-line-up': -> @moveLineUp() 'editor:move-line-down': -> @moveLineDown() 'editor:move-selection-left': -> @moveSelectionLeft() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index e30696479..85e285f8a 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -712,25 +712,6 @@ class TextEditor extends Model # via {Pane::saveItemAs}. getSaveDialogOptions: -> {} - checkoutHeadRevision: -> - if @getPath() - checkoutHead = => - @project.repositoryForDirectory(new Directory(@getDirectoryPath())) - .then (repository) => - repository?.async.checkoutHeadForEditor(this) - - if @config.get('editor.confirmCheckoutHeadRevision') - @applicationDelegate.confirm - message: 'Confirm Checkout HEAD Revision' - detailedMessage: "Are you sure you want to discard all changes to \"#{@getFileName()}\" since the last Git commit?" - buttons: - OK: checkoutHead - Cancel: null - else - checkoutHead() - else - Promise.resolve(false) - ### Section: Reading Text ### diff --git a/src/workspace.coffee b/src/workspace.coffee index 8f973d2fa..9e662c984 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -1085,3 +1085,22 @@ class Workspace extends Model inProcessFinished = true checkFinished() + + checkoutHeadRevision: (editor) -> + if editor.getPath() + checkoutHead = => + @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) + .then (repository) => + repository?.async.checkoutHeadForEditor(editor) + + if @config.get('editor.confirmCheckoutHeadRevision') + @applicationDelegate.confirm + message: 'Confirm Checkout HEAD Revision' + detailedMessage: "Are you sure you want to discard all changes to \"#{editor.getFileName()}\" since the last Git commit?" + buttons: + OK: checkoutHead + Cancel: null + else + checkoutHead() + else + Promise.resolve(false) From 9fa669b2937a7d985a80fa16e96853ba98578889 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 17:02:51 -0400 Subject: [PATCH 174/262] Set the initial path after saving in Project. --- src/project.coffee | 4 ++++ src/text-editor.coffee | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index 93a3ed496..70d5b93a2 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -391,6 +391,10 @@ class Project extends Model subscribeToBuffer: (buffer) -> buffer.onDidDestroy => @removeBuffer(buffer) + buffer.onDidChangePath => + console.log('did change path! ' + buffer.getPath()) + unless @getPaths().length > 0 + @setPaths([path.dirname(buffer.getPath())]) buffer.onWillThrowWatchError ({error, handle}) => handle() @notificationManager.addWarning """ diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 85e285f8a..4ed926848 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -166,8 +166,6 @@ class TextEditor extends Model subscribeToBuffer: -> @buffer.retain() @disposables.add @buffer.onDidChangePath => - unless @project.getPaths().length > 0 - @project.setPaths([path.dirname(@getPath())]) @emitter.emit 'did-change-title', @getTitle() @emitter.emit 'did-change-path', @getPath() @disposables.add @buffer.onDidChangeEncoding => From 7ed4d60967277a4dc890b2066522f52df7a09e0d Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 17:03:26 -0400 Subject: [PATCH 175/262] Don't need to pass Project around anymore. --- src/text-editor.coffee | 6 ++---- src/workspace.coffee | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 4ed926848..b9996bd8d 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -81,7 +81,6 @@ class TextEditor extends Model state.config = atomEnvironment.config state.clipboard = atomEnvironment.clipboard state.grammarRegistry = atomEnvironment.grammars - state.project = atomEnvironment.project state.assert = atomEnvironment.assert.bind(atomEnvironment) state.applicationDelegate = atomEnvironment.applicationDelegate editor = new this(state) @@ -97,13 +96,12 @@ class TextEditor extends Model @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @clipboard, @grammarRegistry, - @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd + @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? - throw new Error("Must pass a project parameter when constructing TextEditors") unless @project? throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert? @firstVisibleScreenRow ?= 0 @@ -516,7 +514,7 @@ class TextEditor extends Model @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, suppressCursorCreation: true, @config, @firstVisibleScreenRow, @firstVisibleScreenColumn, - @clipboard, @grammarRegistry, @project, @assert, @applicationDelegate + @clipboard, @grammarRegistry, @assert, @applicationDelegate }) newEditor diff --git a/src/workspace.coffee b/src/workspace.coffee index 9e662c984..29c497b99 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -571,7 +571,7 @@ class Workspace extends Model buildTextEditor: (params) -> params = _.extend({ @config, @clipboard, @grammarRegistry, - @project, @assert, @applicationDelegate + @assert, @applicationDelegate }, params) new TextEditor(params) From fdb439be9c6c99da9c1edeb8d456487a4ccb9a0f Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 17:09:07 -0400 Subject: [PATCH 176/262] Call through to the underlying repo. --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 37131dd24..26840b822 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -246,7 +246,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {String} branch name such as // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - return this.getUpstreamBranch(_path) + return this.repo.getUpstreamBranch(_path) } // Public: Gets all the local and remote references. From 8d26fe133aeaf9682027c36ed0736b75476020f7 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 25 Apr 2016 17:10:39 -0400 Subject: [PATCH 177/262] s/original/origin --- src/git-repository-async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 26840b822..b691994bc 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -234,7 +234,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} origin url of the // repository. getOriginURL (_path) { - return this.repo.getOriginalURL(_path) + return this.repo.getOriginURL(_path) } // Public: Returns the upstream branch for the current HEAD, or null if there From 891071196f35de2edfd3fd712937a90f0ceb427b Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 25 Apr 2016 15:28:22 -0700 Subject: [PATCH 178/262] Ensure atom.cmd with --wait returns exit code of 0 for git commit usage #11605 --- resources/win/atom.cmd | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index 8a4fed05c..73c4ddb01 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -27,6 +27,7 @@ IF "%EXPECT_OUTPUT%"=="YES" ( SET ELECTRON_ENABLE_LOGGING=YES IF "%WAIT%"=="YES" ( powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event" + exit 0 ) ELSE ( "%~dp0\..\..\atom.exe" %* ) From e3773f24fc81667d0041e1a80da87cad79e18640 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Apr 2016 10:05:35 +0200 Subject: [PATCH 179/262] :fire: Delete obsolete code --- src/lines-tile-component.coffee | 147 ++------------------------------ 1 file changed, 6 insertions(+), 141 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 3cc3accee..c9ecd0982 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -193,7 +193,7 @@ class LinesTileComponent screenRowForNode: (node) -> parseInt(node.dataset.screenRow) buildLineNode: (id) -> - {screenRow, decorationClasses} = @newTileState.lines[id] + {lineText, tagCodes, screenRow, decorationClasses} = @newTileState.lines[id] lineNode = @domElementPool.buildElement("div", "line") lineNode.dataset.screenRow = screenRow @@ -202,57 +202,7 @@ class LinesTileComponent for decorationClass in decorationClasses lineNode.classList.add(decorationClass) - @currentLineTextNodes = [] - # if words.length is 0 - # @setEmptyLineInnerNodes(id, lineNode) - - @setLineInnerNodes(id, lineNode) - @textNodesByLineId[id] = @currentLineTextNodes - - # lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold - lineNode - - setEmptyLineInnerNodes: (id, lineNode) -> - {indentGuidesVisible} = @newState - {indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id] - - if indentGuidesVisible and indentLevel > 0 - invisibleIndex = 0 - for i in [0...indentLevel] - indentGuide = @domElementPool.buildElement("span", "indent-guide") - for j in [0...tabLength] - if invisible = endOfLineInvisibles?[invisibleIndex++] - invisibleSpan = @domElementPool.buildElement("span", "invisible-character") - textNode = @domElementPool.buildText(invisible) - invisibleSpan.appendChild(textNode) - indentGuide.appendChild(invisibleSpan) - - @currentLineTextNodes.push(textNode) - else - textNode = @domElementPool.buildText(" ") - indentGuide.appendChild(textNode) - - @currentLineTextNodes.push(textNode) - lineNode.appendChild(indentGuide) - - while invisibleIndex < endOfLineInvisibles?.length - invisible = endOfLineInvisibles[invisibleIndex++] - invisibleSpan = @domElementPool.buildElement("span", "invisible-character") - textNode = @domElementPool.buildText(invisible) - invisibleSpan.appendChild(textNode) - lineNode.appendChild(invisibleSpan) - - @currentLineTextNodes.push(textNode) - else - unless @appendEndOfLineNodes(id, lineNode) - textNode = @domElementPool.buildText("\u00a0") - lineNode.appendChild(textNode) - - @currentLineTextNodes.push(textNode) - - setLineInnerNodes: (id, lineNode) -> - {lineText, tagCodes} = @newTileState.lines[id] - + textNodes = [] lineLength = 0 startIndex = 0 openScopeNode = lineNode @@ -268,100 +218,15 @@ class LinesTileComponent textNode = @domElementPool.buildText(lineText.substr(startIndex, tagCode)) startIndex += tagCode openScopeNode.appendChild(textNode) - @currentLineTextNodes.push(textNode) + textNodes.push(textNode) if startIndex is 0 textNode = @domElementPool.buildText(' ') lineNode.appendChild(textNode) - @currentLineTextNodes.push(textNode) + textNodes.push(textNode) - appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) -> - if isHardTab - textNode = @domElementPool.buildText(tokenText) - hardTabNode = @domElementPool.buildElement("span", "hard-tab") - hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex? - hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex? - hardTabNode.classList.add("indent-guide") if hasIndentGuide - hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters - hardTabNode.appendChild(textNode) - - scopeNode.appendChild(hardTabNode) - @currentLineTextNodes.push(textNode) - else - startIndex = 0 - endIndex = tokenText.length - - leadingWhitespaceNode = null - leadingWhitespaceTextNode = null - trailingWhitespaceNode = null - trailingWhitespaceTextNode = null - - if firstNonWhitespaceIndex? - leadingWhitespaceTextNode = - @domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex)) - leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace") - leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide - leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters - leadingWhitespaceNode.appendChild(leadingWhitespaceTextNode) - - startIndex = firstNonWhitespaceIndex - - if firstTrailingWhitespaceIndex? - tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0 - - trailingWhitespaceTextNode = - @domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex)) - trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace") - trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace - trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters - trailingWhitespaceNode.appendChild(trailingWhitespaceTextNode) - - endIndex = firstTrailingWhitespaceIndex - - if leadingWhitespaceNode? - scopeNode.appendChild(leadingWhitespaceNode) - @currentLineTextNodes.push(leadingWhitespaceTextNode) - - if tokenText.length > MaxTokenLength - while startIndex < endIndex - textNode = @domElementPool.buildText( - @sliceText(tokenText, startIndex, startIndex + MaxTokenLength) - ) - textSpan = @domElementPool.buildElement("span") - - textSpan.appendChild(textNode) - scopeNode.appendChild(textSpan) - startIndex += MaxTokenLength - @currentLineTextNodes.push(textNode) - else - textNode = @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex)) - scopeNode.appendChild(textNode) - @currentLineTextNodes.push(textNode) - - if trailingWhitespaceNode? - scopeNode.appendChild(trailingWhitespaceNode) - @currentLineTextNodes.push(trailingWhitespaceTextNode) - - sliceText: (tokenText, startIndex, endIndex) -> - if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length - tokenText = tokenText.slice(startIndex, endIndex) - tokenText - - appendEndOfLineNodes: (id, lineNode) -> - {endOfLineInvisibles} = @newTileState.lines[id] - - hasInvisibles = false - if endOfLineInvisibles? - for invisible in endOfLineInvisibles - hasInvisibles = true - invisibleSpan = @domElementPool.buildElement("span", "invisible-character") - textNode = @domElementPool.buildText(invisible) - invisibleSpan.appendChild(textNode) - lineNode.appendChild(invisibleSpan) - - @currentLineTextNodes.push(textNode) - - hasInvisibles + @textNodesByLineId[id] = textNodes + lineNode updateLineNode: (id) -> oldLineState = @oldTileState.lines[id] From ebfd821237ab06a7060fe47371ab29724a962c2f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Apr 2016 10:47:42 +0200 Subject: [PATCH 180/262] Destroy folds corresponding to fold-markers and not the whole buffer row --- spec/text-editor-component-spec.js | 62 ++++++++++++++++++++++++------ src/text-editor-component.coffee | 4 +- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 0fe1da5b2..6b4e86540 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2755,20 +2755,60 @@ describe('TextEditorComponent', function () { }) }) - describe('when a line is folded', function () { - beforeEach(async function () { - editor.foldBufferRow(4) + describe('when a fold marker is clicked', function () { + function clickElementAtPosition (marker, position) { + linesNode.dispatchEvent( + buildMouseEvent('mousedown', clientCoordinatesForScreenPosition(position), {target: marker}) + ) + } + + it('unfolds only the selected fold when other folds are on the same line', async function () { + editor.foldBufferRange([[4, 6], [4, 10]]) + editor.foldBufferRange([[4, 15], [4, 20]]) await nextViewUpdatePromise() + let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') + expect(foldMarkers.length).toBe(2) + expect(editor.isFoldedAtBufferRow(4)).toBe(true) + + clickElementAtPosition(foldMarkers[0], [4, 6]) + await nextViewUpdatePromise() + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') + expect(foldMarkers.length).toBe(1) + expect(editor.isFoldedAtBufferRow(4)).toBe(true) + + clickElementAtPosition(foldMarkers[0], [4, 15]) + await nextViewUpdatePromise() + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') + expect(foldMarkers.length).toBe(0) + expect(editor.isFoldedAtBufferRow(4)).toBe(false) }) - describe('when the folded line\'s fold-marker is clicked', function () { - it('unfolds the buffer row', function () { - let target = component.lineNodeForScreenRow(4).querySelector('.fold-marker') - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), { - target: target - })) - expect(editor.isFoldedAtBufferRow(4)).toBe(false) - }) + it('unfolds only the selected fold when other folds are inside it', async function () { + editor.foldBufferRange([[4, 10], [4, 15]]) + editor.foldBufferRange([[4, 4], [4, 5]]) + editor.foldBufferRange([[4, 4], [4, 20]]) + await nextViewUpdatePromise() + let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') + expect(foldMarkers.length).toBe(1) + expect(editor.isFoldedAtBufferRow(4)).toBe(true) + + clickElementAtPosition(foldMarkers[0], [4, 4]) + await nextViewUpdatePromise() + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') + expect(foldMarkers.length).toBe(1) + expect(editor.isFoldedAtBufferRow(4)).toBe(true) + + clickElementAtPosition(foldMarkers[0], [4, 4]) + await nextViewUpdatePromise() + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') + expect(foldMarkers.length).toBe(1) + expect(editor.isFoldedAtBufferRow(4)).toBe(true) + + clickElementAtPosition(foldMarkers[0], [4, 10]) + await nextViewUpdatePromise() + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') + expect(foldMarkers.length).toBe(0) + expect(editor.isFoldedAtBufferRow(4)).toBe(false) }) }) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 52a43db54..3f9bb2029 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -560,8 +560,8 @@ class TextEditorComponent screenPosition = @screenPositionForMouseEvent(event) if event.target?.classList.contains('fold-marker') - bufferRow = @editor.bufferRowForScreenRow(screenPosition.row) - @editor.unfoldBufferRow(bufferRow) + bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition) + @editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition]) return switch detail From f85c72978e445951b0986b7deebc18e4a791ef0f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Apr 2016 12:06:41 +0200 Subject: [PATCH 181/262] Fix wrong implementation in TextEditor.prototype.screenRowForBufferRow --- src/text-editor.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 710a9e342..ce53cc3ed 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -877,9 +877,9 @@ class TextEditor extends Model screenRowForBufferRow: (row) -> if @largeFileMode - bufferRow + row else - @displayLayer.translateScreenPosition(Point(screenRow, 0)).row + @displayLayer.translateBufferPosition(Point(row, 0)).row getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() From 4f6687324e394e7f3f067206ad9ce338c842c551 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Apr 2016 12:09:38 +0200 Subject: [PATCH 182/262] Include bufferRange in `decorationsStateForScreenRowRange` --- spec/text-editor-spec.coffee | 11 +++++++++++ src/decoration-manager.coffee | 5 +++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 466d1b501..565f92430 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5888,6 +5888,7 @@ describe "TextEditor", -> expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual { properties: {type: 'highlight', class: 'foo'} screenRange: marker.getScreenRange(), + bufferRange: marker.getBufferRange(), rangeIsReversed: false } @@ -5908,26 +5909,31 @@ describe "TextEditor", -> expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'foo'}, screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), rangeIsReversed: false } expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual { properties: {type: 'highlight', class: 'foo'}, screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), rangeIsReversed: false } expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'bar'}, screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), rangeIsReversed: false } expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { properties: {type: 'highlight', class: 'bar'}, screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), rangeIsReversed: false } expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { properties: {type: 'highlight', class: 'baz'}, screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), rangeIsReversed: false } @@ -5939,16 +5945,19 @@ describe "TextEditor", -> expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'bar'}, screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), rangeIsReversed: false } expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { properties: {type: 'highlight', class: 'bar'}, screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), rangeIsReversed: false } expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { properties: {type: 'highlight', class: 'baz'}, screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), rangeIsReversed: false } @@ -5957,6 +5966,7 @@ describe "TextEditor", -> expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'quux'}, screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), rangeIsReversed: false } @@ -5965,6 +5975,7 @@ describe "TextEditor", -> expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'bar'}, screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), rangeIsReversed: false } diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee index e96727fe3..edb9dfb33 100644 --- a/src/decoration-manager.coffee +++ b/src/decoration-manager.coffee @@ -84,20 +84,21 @@ class DecorationManager extends Model for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid() screenRange = marker.getScreenRange() + bufferRange = marker.getBufferRange() rangeIsReversed = marker.isReversed() if decorations = @decorationsByMarkerId[marker.id] for decoration in decorations decorationsState[decoration.id] = { properties: decoration.properties - screenRange, rangeIsReversed + screenRange, bufferRange, rangeIsReversed } if layerDecorations = @layerDecorationsByMarkerLayerId[layerId] for layerDecoration in layerDecorations decorationsState["#{layerDecoration.id}-#{marker.id}"] = { properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties - screenRange, rangeIsReversed + screenRange, bufferRange, rangeIsReversed } decorationsState From f81f54e08a1543762101fce5a60ffb38eba16956 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Apr 2016 12:11:05 +0200 Subject: [PATCH 183/262] Apply 'folded' decoration only to 1st screen row of a wrapped buffer row --- spec/text-editor-component-spec.js | 10 +++++++- spec/text-editor-presenter-spec.coffee | 10 ++++++++ src/text-editor-presenter.coffee | 35 +++++++++++++++----------- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 6b4e86540..4d2a68cac 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1053,7 +1053,7 @@ describe('TextEditorComponent', function () { beforeEach(async function () { editor.setSoftWrapped(true) await nextViewUpdatePromise() - componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() await nextViewUpdatePromise() }) @@ -1062,6 +1062,14 @@ describe('TextEditorComponent', function () { expect(lineNumberHasClass(0, 'foldable')).toBe(true) expect(lineNumberHasClass(1, 'foldable')).toBe(false) }) + + it('does not add the folded class for soft-wrapped lines that contain a fold', async function () { + editor.foldBufferRange([[3, 19], [3, 21]]) + await nextViewUpdatePromise() + + expect(lineNumberHasClass(11, 'folded')).toBe(true) + expect(lineNumberHasClass(12, 'folded')).toBe(false) + }) }) }) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index a7beb3c57..9318809d7 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -3116,6 +3116,16 @@ describe "TextEditorPresenter", -> expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' + it "applies the 'folded' decoration only to the initial screen row of a soft-wrapped buffer row", -> + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(15) + editor.foldBufferRange([[0, 20], [0, 22]]) + presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2) + + expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'folded' + expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() + describe ".foldable", -> it "marks line numbers at the start of a foldable region as foldable", -> presenter = buildPresenter() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 9b66c6312..85c85c655 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1127,9 +1127,9 @@ class TextEditorPresenter @customGutterDecorationsByGutterName = {} for decorationId, decorationState of @decorations - {properties, screenRange, rangeIsReversed} = decorationState + {properties, bufferRange, screenRange, rangeIsReversed} = decorationState if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number') - @addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed) + @addToLineDecorationCaches(decorationId, properties, bufferRange, screenRange, rangeIsReversed) else if Decoration.isType(properties, 'gutter') and properties.gutterName? @customGutterDecorationsByGutterName[properties.gutterName] ?= {} @@ -1150,7 +1150,7 @@ class TextEditorPresenter return - addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) -> + addToLineDecorationCaches: (decorationId, properties, bufferRange, screenRange, rangeIsReversed) -> if screenRange.isEmpty() return if properties.onlyNonEmpty else @@ -1158,21 +1158,28 @@ class TextEditorPresenter omitLastRow = screenRange.end.column is 0 if rangeIsReversed - headPosition = screenRange.start + headScreenPosition = screenRange.start + headBufferPosition = bufferRange.start else - headPosition = screenRange.end + headScreenPosition = screenRange.end + headBufferPosition = bufferRange.end - 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 properties.class is 'folded' and Decoration.isType(properties, 'line-number') + screenRow = @model.screenRowForBufferRow(headBufferPosition.row) + @lineNumberDecorationsByScreenRow[screenRow] ?= {} + @lineNumberDecorationsByScreenRow[screenRow][decorationId] = properties + else + for row in [screenRange.start.row..screenRange.end.row] by 1 + continue if properties.onlyHead and row isnt headScreenPosition.row + continue if omitLastRow and row is screenRange.end.row - if Decoration.isType(properties, 'line') - @lineDecorationsByScreenRow[row] ?= {} - @lineDecorationsByScreenRow[row][decorationId] = properties + if Decoration.isType(properties, 'line') + @lineDecorationsByScreenRow[row] ?= {} + @lineDecorationsByScreenRow[row][decorationId] = properties - if Decoration.isType(properties, 'line-number') - @lineNumberDecorationsByScreenRow[row] ?= {} - @lineNumberDecorationsByScreenRow[row][decorationId] = properties + if Decoration.isType(properties, 'line-number') + @lineNumberDecorationsByScreenRow[row] ?= {} + @lineNumberDecorationsByScreenRow[row][decorationId] = properties return From 38bd120fb1595b253290cf7bb195a2c0bf08a334 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Apr 2016 12:11:13 +0200 Subject: [PATCH 184/262] :art: --- spec/text-editor-component-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 4d2a68cac..9b1922494 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1092,7 +1092,7 @@ describe('TextEditorComponent', function () { component.destroy() lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') - return target.dispatchEvent(buildClickEvent(target)) + target.dispatchEvent(buildClickEvent(target)) }) }) From 7ba9cc6329e649a267e7153a8ca6260d83e8af6f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 26 Apr 2016 13:00:23 +0200 Subject: [PATCH 185/262] Unfold all the folds intersecting the row when clicking fold indicators --- spec/text-editor-component-spec.js | 31 +++++++++++++++++++++++++ src/line-number-gutter-component.coffee | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 9b1922494..1b29cbd7b 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1116,6 +1116,37 @@ describe('TextEditorComponent', function () { expect(lineNumberHasClass(1, 'folded')).toBe(false) }) + it('unfolds all the free-form folds intersecting the buffer row when clicked', async function () { + expect(lineNumberHasClass(3, 'foldable')).toBe(false) + + editor.foldBufferRange([[3, 4], [5, 4]]) + editor.foldBufferRange([[5, 5], [8, 10]]) + await nextViewUpdatePromise() + expect(lineNumberHasClass(3, 'folded')).toBe(true) + expect(lineNumberHasClass(5, 'folded')).toBe(false) + + let lineNumber = component.lineNumberNodeForScreenRow(3) + let target = lineNumber.querySelector('.icon-right') + target.dispatchEvent(buildClickEvent(target)) + await nextViewUpdatePromise() + expect(lineNumberHasClass(3, 'folded')).toBe(false) + expect(lineNumberHasClass(5, 'folded')).toBe(true) + + editor.setSoftWrapped(true) + componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + editor.foldBufferRange([[3, 19], [3, 21]]) // fold starting on a soft-wrapped portion of the line + await nextViewUpdatePromise() + expect(lineNumberHasClass(11, 'folded')).toBe(true) + + lineNumber = component.lineNumberNodeForScreenRow(11) + target = lineNumber.querySelector('.icon-right') + target.dispatchEvent(buildClickEvent(target)) + await nextViewUpdatePromise() + expect(lineNumberHasClass(11, 'folded')).toBe(false) + }) + it('does not fold when the line number componentNode is clicked', function () { let lineNumber = component.lineNumberNodeForScreenRow(1) lineNumber.dispatchEvent(buildClickEvent(lineNumber)) diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee index bb66ff144..3a3c199c2 100644 --- a/src/line-number-gutter-component.coffee +++ b/src/line-number-gutter-component.coffee @@ -93,9 +93,9 @@ class LineNumberGutterComponent extends TiledComponent {target} = event lineNumber = target.parentNode - if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') + if target.classList.contains('icon-right') bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row')) if lineNumber.classList.contains('folded') @editor.unfoldBufferRow(bufferRow) - else + else if lineNumber.classList.contains('foldable') @editor.foldBufferRow(bufferRow) From f589bdd8d9cd5d7b0d05c5ab5c4ccc55482a44d4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 09:22:38 -0400 Subject: [PATCH 186/262] Add a remote to the fixture. --- spec/fixtures/git/repo-with-submodules/git.git/config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/fixtures/git/repo-with-submodules/git.git/config b/spec/fixtures/git/repo-with-submodules/git.git/config index ab57cc5f1..209e37653 100644 --- a/spec/fixtures/git/repo-with-submodules/git.git/config +++ b/spec/fixtures/git/repo-with-submodules/git.git/config @@ -5,6 +5,9 @@ logallrefupdates = true ignorecase = true precomposeunicode = true +[remote "origin"] + url = git@github.com:atom/some-repo-i-guess.git + fetch = +refs/heads/*:refs/remotes/origin/* [submodule "jstips"] url = https://github.com/loverajoel/jstips [submodule "You-Dont-Need-jQuery"] From f86a15e5fca34cee1351dbeeb5108719b6915282 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 09:22:56 -0400 Subject: [PATCH 187/262] Spec for getOriginURL --- spec/git-repository-async-spec.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index d36b9fd58..77ff1a4b5 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -877,4 +877,16 @@ describe('GitRepositoryAsync', () => { }) }) }) + + describe('.getOriginURL()', () => { + beforeEach(() => { + const workingDirectory = copyRepository('repo-with-submodules') + repo = GitRepositoryAsync.open(workingDirectory) + }) + + it('returns the origin URL', async () => { + const URL = await repo.getOriginURL() + expect(URL).toBe('git@github.com:atom/some-repo-i-guess.git') + }) + }) }) From ff43d917be636f486de059e07a02bd636180fb5b Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 09:23:54 -0400 Subject: [PATCH 188/262] Lower case --- spec/git-repository-async-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 77ff1a4b5..75f6848aa 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -885,8 +885,8 @@ describe('GitRepositoryAsync', () => { }) it('returns the origin URL', async () => { - const URL = await repo.getOriginURL() - expect(URL).toBe('git@github.com:atom/some-repo-i-guess.git') + const url = await repo.getOriginURL() + expect(url).toBe('git@github.com:atom/some-repo-i-guess.git') }) }) }) From 33a9240fe18ef01b5e223d9029da87e2f563c57f Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 09:45:56 -0400 Subject: [PATCH 189/262] :arrow_up: ohnogit@0.0.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 957d262f0..89a042915 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "marked": "^0.3.4", "normalize-package-data": "^2.0.0", "nslog": "^3", - "ohnogit": "0.0.9", + "ohnogit": "0.0.11", "oniguruma": "^5", "pathwatcher": "~6.2", "property-accessors": "^1.1.3", From e16e987e08ba30f5853c8ab9428a061663304862 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 09:46:04 -0400 Subject: [PATCH 190/262] Give the fixture a remote --- spec/fixtures/git/repo-with-submodules/git.git/config | 3 +++ .../repo-with-submodules/git.git/refs/remotes/origin/master | 1 + 2 files changed, 4 insertions(+) create mode 100644 spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master diff --git a/spec/fixtures/git/repo-with-submodules/git.git/config b/spec/fixtures/git/repo-with-submodules/git.git/config index 209e37653..ff94b83d6 100644 --- a/spec/fixtures/git/repo-with-submodules/git.git/config +++ b/spec/fixtures/git/repo-with-submodules/git.git/config @@ -5,6 +5,9 @@ logallrefupdates = true ignorecase = true precomposeunicode = true +[branch "master"] + remote = origin + merge = refs/heads/master [remote "origin"] url = git@github.com:atom/some-repo-i-guess.git fetch = +refs/heads/*:refs/remotes/origin/* diff --git a/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master b/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master new file mode 100644 index 000000000..3507a23dc --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master @@ -0,0 +1 @@ +d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 From 67c7c60dcc874c22e86282a87bc302f1de06e7f0 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 09:49:03 -0400 Subject: [PATCH 191/262] Test getUpstreamBranch --- spec/git-repository-async-spec.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 75f6848aa..1fbe537d5 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -889,4 +889,22 @@ describe('GitRepositoryAsync', () => { expect(url).toBe('git@github.com:atom/some-repo-i-guess.git') }) }) + + describe('.getUpstreamBranch()', () => { + it('returns null when there is no upstream branch', async () => { + const workingDirectory = copyRepository() + repo = GitRepositoryAsync.open(workingDirectory) + + const upstream = await repo.getUpstreamBranch() + expect(upstream).toBe(null) + }) + + it('returns the upstream branch', async () => { + const workingDirectory = copyRepository('repo-with-submodules') + repo = GitRepositoryAsync.open(workingDirectory) + + const upstream = await repo.getUpstreamBranch() + expect(upstream).toBe('refs/remotes/origin/master') + }) + }) }) From 0541755ac8cf5f16792e081213234b5e868fbb84 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 09:49:38 -0400 Subject: [PATCH 192/262] Consistent indentation. --- spec/fixtures/git/repo-with-submodules/git.git/config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/fixtures/git/repo-with-submodules/git.git/config b/spec/fixtures/git/repo-with-submodules/git.git/config index ff94b83d6..323ba7d9b 100644 --- a/spec/fixtures/git/repo-with-submodules/git.git/config +++ b/spec/fixtures/git/repo-with-submodules/git.git/config @@ -9,8 +9,8 @@ remote = origin merge = refs/heads/master [remote "origin"] - url = git@github.com:atom/some-repo-i-guess.git - fetch = +refs/heads/*:refs/remotes/origin/* + url = git@github.com:atom/some-repo-i-guess.git + fetch = +refs/heads/*:refs/remotes/origin/* [submodule "jstips"] url = https://github.com/loverajoel/jstips [submodule "You-Dont-Need-jQuery"] From 5d7c2fc8bae0b48cf35581ec613f42ff81a72481 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 10:32:41 -0400 Subject: [PATCH 193/262] Just observe the grammar. --- src/display-buffer.coffee | 3 --- src/text-editor.coffee | 3 --- src/tokenized-buffer.coffee | 8 -------- src/workspace.coffee | 2 +- 4 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index ccc68535f..109b791a1 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -136,9 +136,6 @@ class DisplayBuffer extends Model onDidChangeGrammar: (callback) -> @tokenizedBuffer.onDidChangeGrammar(callback) - onDidUseGrammar: (callback) -> - @tokenizedBuffer.onDidUseGrammar(callback) - onDidTokenize: (callback) -> @tokenizedBuffer.onDidTokenize(callback) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 865026e20..aeb5ebe5c 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -323,9 +323,6 @@ class TextEditor extends Model onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback - onDidUseGrammar: (callback) -> - @displayBuffer.onDidUseGrammar(callback) - # Extended: Calls your `callback` when the result of {::isModified} changes. # # * `callback` {Function} diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 7e3c4fe49..065715806 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -128,14 +128,6 @@ class TokenizedBuffer extends Model @emitter.emit 'did-change-grammar', grammar - # Delay this to the next tick to ensure whoever created the buffer has the - # chance to listen for this event before we send it. - process.nextTick => - @emitter.emit 'did-use-grammar', grammar - - onDidUseGrammar: (callback) -> - @emitter.on 'did-use-grammar', callback - getGrammarSelectionContent: -> @buffer.getTextInRange([[0, 0], [10, 0]]) diff --git a/src/workspace.coffee b/src/workspace.coffee index 9a52dc937..7e67de97d 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -550,7 +550,7 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options)) disposable = atom.textEditors.add(editor) - grammarSubscription = editor.onDidUseGrammar(@handleGrammarUsed.bind(this)) + grammarSubscription = editor.observeGrammar(@handleGrammarUsed.bind(this)) editor.onDidDestroy -> grammarSubscription.dispose() disposable.dispose() From 97328749995349305a6f25725584f2afd6d167d8 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 11:02:01 -0400 Subject: [PATCH 194/262] Don't log anymore. --- src/project.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/project.coffee b/src/project.coffee index 70d5b93a2..bf64753cf 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -392,7 +392,6 @@ class Project extends Model subscribeToBuffer: (buffer) -> buffer.onDidDestroy => @removeBuffer(buffer) buffer.onDidChangePath => - console.log('did change path! ' + buffer.getPath()) unless @getPaths().length > 0 @setPaths([path.dirname(buffer.getPath())]) buffer.onWillThrowWatchError ({error, handle}) => From e1c17ed8563b6ce0c20a65634c5a3f14d7539f3a Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 11:17:20 -0400 Subject: [PATCH 195/262] :arrow_up: status-bar@1.2.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89a042915..627a411e1 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "settings-view": "0.235.1", "snippets": "1.0.2", "spell-check": "0.67.1", - "status-bar": "1.2.4", + "status-bar": "1.2.5", "styleguide": "0.45.2", "symbols-view": "0.112.0", "tabs": "0.93.1", From 53b7a20ad7e45960365b01d31f309be5f178b8cd Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 11:40:56 -0400 Subject: [PATCH 196/262] :arrow_up: status-bar@1.2.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 627a411e1..2874eaeb0 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "settings-view": "0.235.1", "snippets": "1.0.2", "spell-check": "0.67.1", - "status-bar": "1.2.5", + "status-bar": "1.2.6", "styleguide": "0.45.2", "symbols-view": "0.112.0", "tabs": "0.93.1", From 40d77613502f01b11c2ba36115acdab6aeb29080 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 11:48:52 -0400 Subject: [PATCH 197/262] Bail if we don't have a grammar yet. --- src/workspace.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/workspace.coffee b/src/workspace.coffee index 7e67de97d..c8be01fef 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -557,6 +557,8 @@ class Workspace extends Model editor handleGrammarUsed: (grammar) -> + return unless grammar? + @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. From 5fd556ac58f8e2406f6943bec25628b5d0f67169 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 14:55:37 -0400 Subject: [PATCH 198/262] We need Directory now. --- src/workspace.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/workspace.coffee b/src/workspace.coffee index 1458ac9cd..1cc79f1ce 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -4,6 +4,7 @@ path = require 'path' {join} = path {Emitter, Disposable, CompositeDisposable} = require 'event-kit' fs = require 'fs-plus' +{Directory} = require 'pathwatcher' DefaultDirectorySearcher = require './default-directory-searcher' Model = require './model' TextEditor = require './text-editor' From a7606710c012e23d342210d66a25c4741eef1bee Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 14:55:43 -0400 Subject: [PATCH 199/262] Don't need Directory anymore. --- src/text-editor.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a2ecb3663..a61afea17 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -9,7 +9,6 @@ Cursor = require './cursor' Model = require './model' Selection = require './selection' TextMateScopeSelector = require('first-mate').ScopeSelector -{Directory} = require "pathwatcher" GutterContainer = require './gutter-container' TextEditorElement = require './text-editor-element' From aa9e8ab620d55de235b1b7bb097b70a64903bca5 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 14:56:00 -0400 Subject: [PATCH 200/262] Update the spec. --- spec/text-editor-spec.coffee | 22 ---------------------- spec/workspace-spec.coffee | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index bc08b3f0e..e5e58a5cc 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5727,28 +5727,6 @@ describe "TextEditor", -> expect(handler).toHaveBeenCalledWith 'OK' expect(editor.getPlaceholderText()).toBe 'OK' - describe ".checkoutHeadRevision()", -> - it "reverts to the version of its file checked into the project repository", -> - atom.config.set("editor.confirmCheckoutHeadRevision", false) - - editor.setCursorBufferPosition([0, 0]) - editor.insertText("---\n") - expect(editor.lineTextForBufferRow(0)).toBe "---" - - waitsForPromise -> - editor.checkoutHeadRevision() - - runs -> - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () {" - - describe "when there's no repository for the editor's file", -> - it "doesn't do anything", -> - editor = atom.workspace.buildTextEditor() - editor.setText("stuff") - editor.checkoutHeadRevision() - - waitsForPromise -> editor.checkoutHeadRevision() - describe 'gutters', -> describe 'the TextEditor constructor', -> it 'creates a line-number gutter', -> diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index b84e873da..6fa8001aa 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -1633,5 +1633,31 @@ describe "Workspace", -> runs -> expect(grammarUsed.argsForCall[0][0].name).toBe 'JavaScript' + describe ".checkoutHeadRevision()", -> + editor = null + beforeEach -> + atom.config.set("editor.confirmCheckoutHeadRevision", false) + + waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o + + it "reverts to the version of its file checked into the project repository", -> + editor.setCursorBufferPosition([0, 0]) + editor.insertText("---\n") + expect(editor.lineTextForBufferRow(0)).toBe "---" + + waitsForPromise -> + atom.workspace.checkoutHeadRevision(editor) + + runs -> + expect(editor.lineTextForBufferRow(0)).toBe "" + + describe "when there's no repository for the editor's file", -> + it "doesn't do anything", -> + editor = atom.workspace.buildTextEditor() + editor.setText("stuff") + atom.workspace.checkoutHeadRevision(editor) + + waitsForPromise -> atom.workspace.checkoutHeadRevision(editor) + escapeStringRegex = (str) -> str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') From e038ff326085317cc55e07782cd2be3f2da313eb Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 16:04:41 -0400 Subject: [PATCH 201/262] Remove applicationDelegate from TextEditor --- src/text-editor.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a61afea17..4c5b8a956 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -81,7 +81,6 @@ class TextEditor extends Model state.clipboard = atomEnvironment.clipboard state.grammarRegistry = atomEnvironment.grammars state.assert = atomEnvironment.assert.bind(atomEnvironment) - state.applicationDelegate = atomEnvironment.applicationDelegate editor = new this(state) if state.registered disposable = atomEnvironment.textEditors.add(editor) @@ -95,7 +94,7 @@ class TextEditor extends Model @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @clipboard, @grammarRegistry, - @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd + @assert, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? @@ -510,7 +509,7 @@ class TextEditor extends Model @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, suppressCursorCreation: true, @config, @firstVisibleScreenRow, @firstVisibleScreenColumn, - @clipboard, @grammarRegistry, @assert, @applicationDelegate + @clipboard, @grammarRegistry, @assert }) newEditor From 46c97ee2b2fbfeb1210118087dcd754cc8f793b4 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 16:14:25 -0400 Subject: [PATCH 202/262] Provide a default assert implementation if one isn't passed in. --- src/text-editor.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a61afea17..6ceedf943 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -101,8 +101,8 @@ class TextEditor extends Model throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? - throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert? + @assert ?= @defaultAssert.bind(this) @firstVisibleScreenRow ?= 0 @firstVisibleScreenColumn ?= 0 @emitter = new Emitter @@ -3365,6 +3365,9 @@ class TextEditor extends Model @emitter.emit 'will-insert-text', willInsertEvent result + defaultAssert: (condition, message, callback) -> + condition + ### Section: Language Mode Delegated Methods ### From c3bfba0e53f06d2e6b68d8df07d03b9a614babe9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Tue, 26 Apr 2016 16:15:30 -0400 Subject: [PATCH 203/262] We don't need to pass in an app delegate here anymore. --- src/workspace.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index 1cc79f1ce..f75f00bc6 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -573,8 +573,7 @@ class Workspace extends Model # Returns a {TextEditor}. buildTextEditor: (params) -> params = _.extend({ - @config, @clipboard, @grammarRegistry, - @assert, @applicationDelegate + @config, @clipboard, @grammarRegistry, @assert }, params) new TextEditor(params) From 6bb5e09af92f18ad101bcbee42b50f68d9da741f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 26 Apr 2016 16:46:00 -0400 Subject: [PATCH 204/262] :arrow_up: language-shellscript@0.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2874eaeb0..51d1f06e8 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", "language-sass": "0.47.0", - "language-shellscript": "0.21.1", + "language-shellscript": "0.22.0", "language-source": "0.9.0", "language-sql": "0.21.0", "language-text": "0.7.1", From f97d197a60e59ed5e0e42eef5a913a1620e49286 Mon Sep 17 00:00:00 2001 From: Rahat Ahmed Date: Tue, 26 Apr 2016 14:22:47 -0500 Subject: [PATCH 205/262] =?UTF-8?q?=F0=9F=8F=87=20Improve=20perf=20for=20c?= =?UTF-8?q?licking=20on=20long=20buffer=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reimplements the `screenPositionforPixelPosition` method in `lines-yardstick.coffee` to use two binary searches to find the position in order to minimize the number of `getBoundingClientRect` calls. The first stage performs a binary search on the text nodes of a row to find the node containing the pixelPosition. The second stage performs a binary search within the text node to find the character that contains the pixelPosition. This improves responsiveness when clicking near the end of a very long line of text. Resolves #10769 --- package.json | 1 + src/lines-yardstick.coffee | 59 ++++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index ffe6d30e0..31b8bb722 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "async": "0.2.6", "atom-keymap": "6.3.2", "babel-core": "^5.8.21", + "binary-search-with-index": "^1.3.0", "bootstrap": "^3.3.4", "cached-run-in-this-context": "0.4.1", "clear-cut": "^2.0.1", diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 17af35f74..f865a039c 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -1,5 +1,6 @@ {Point} = require 'text-buffer' {isPairedCharacter} = require './text-utils' +binarySearch = require 'binary-search-with-index' module.exports = class LinesYardstick @@ -19,8 +20,8 @@ class LinesYardstick targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() + return Point(0, 0) if targetTop < 0 row = @lineTopIndex.rowForPixelPosition(targetTop) - targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) @@ -32,32 +33,27 @@ class LinesYardstick lineOffset = lineNode.getBoundingClientRect().left targetLeft += lineOffset - textNodeStartColumn = 0 - for textNode in textNodes - {length: textNodeLength, textContent: textNodeContent} = textNode - textNodeRight = @clientRectForRange(textNode, 0, textNodeLength).right + textNodeComparator = (textNode, position) => + {length: textNodeLength} = textNode + rangeRect = @clientRectForRange(textNode, 0, textNodeLength) + return -1 if rangeRect.right < position + return 1 if rangeRect.left > position + return 0 - if textNodeRight > targetLeft - characterIndex = 0 - while characterIndex < textNodeLength - if isPairedCharacter(textNodeContent, characterIndex) - nextCharacterIndex = characterIndex + 2 - else - nextCharacterIndex = characterIndex + 1 + textNodeIndex = binarySearch(textNodes, targetLeft, textNodeComparator) - rangeRect = @clientRectForRange(textNode, characterIndex, nextCharacterIndex) + if textNodeIndex >= 0 + textNodeStartColumn = textNodes + .slice(0, textNodeIndex) + .reduce(((totalLength, node) -> totalLength + node.length), 0) + charIndex = @charIndexForScreenPosition(textNodes[textNodeIndex], targetLeft) - if rangeRect.right > targetLeft - if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) - return Point(row, textNodeStartColumn + characterIndex) - else - return Point(row, textNodeStartColumn + nextCharacterIndex) - else - characterIndex = nextCharacterIndex + return Point(row, textNodeStartColumn + charIndex) - textNodeStartColumn += textNodeLength + textNodeStartColumn = textNodes + .reduce(((totalLength, node) -> totalLength + node.length), 0) - Point(row, textNodeStartColumn) + return Point(row, textNodeStartColumn) pixelPositionForScreenPosition: (screenPosition) -> targetRow = screenPosition.row @@ -107,3 +103,22 @@ class LinesYardstick @rangeForMeasurement.setStart(textNode, startIndex) @rangeForMeasurement.setEnd(textNode, endIndex) @rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect() + + charIndexForScreenPosition: (textNode, targetLeft) -> + {textContent: textNodeContent} = textNode + rangeRect = null + nextCharIndex = -1 + characterComparator = (char, position, charIndex) => + if isPairedCharacter(textNodeContent, charIndex) + nextCharIndex = charIndex + 2 + else + nextCharIndex = charIndex + 1 + rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex) + return -1 if rangeRect.right < position + return 1 if rangeRect.left > position + return 0 + + characterIndex = binarySearch(textNodeContent, targetLeft, characterComparator) + if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) + return characterIndex + return nextCharIndex From 3b204d404f4cb6216bedc0afa9c4035f8245e4a8 Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Tue, 26 Apr 2016 15:46:47 -0700 Subject: [PATCH 206/262] :arrow_up: apm@1.9.3 --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index 6623876f9..4ddb4dabe 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.9.2" + "atom-package-manager": "1.9.3" } } From e0688bef1ab96e75baa417c52dcaee01590c49c0 Mon Sep 17 00:00:00 2001 From: David Elliott Date: Tue, 26 Apr 2016 16:15:41 -0700 Subject: [PATCH 207/262] Ctrl-o opens last directory where you opened a file --- src/browser/atom-application.coffee | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 69eff1845..38c2a354a 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -58,6 +58,7 @@ class AtomApplication resourcePath: null version: null quitting: false + lastFilePathOpened: null exit: (status) -> app.exit(status) @@ -655,6 +656,7 @@ class AtomApplication # :window - An {AtomWindow} to use for opening a selected file path. promptForPathToOpen: (type, {devMode, safeMode, window}) -> @promptForPath type, (pathsToOpen) => + @lastFilePathOpened = pathsToOpen[0] if pathsToOpen.length > 0 @openPaths({pathsToOpen, devMode, safeMode, window}) promptForPath: (type, callback) -> @@ -680,8 +682,8 @@ class AtomApplication when 'folder' then 'Open Folder' else 'Open' - if process.platform is 'linux' - if projectPath = @lastFocusedWindow?.projectPath - openOptions.defaultPath = projectPath + # If a file was previously opened, set default path to open there again. + if @lastFilePathOpened? and type is 'file' + openOptions.defaultPath = @lastFilePathOpened dialog.showOpenDialog(parentWindow, openOptions, callback) From 1dc518050a8e38818b3f689f6ac431b50d617c2d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Apr 2016 10:56:50 +0200 Subject: [PATCH 208/262] Missing `break` statement when encountering an unmatched scope end tag --- src/tokenized-buffer.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 00d4cdf63..4636ceddc 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -381,6 +381,7 @@ class TokenizedBuffer extends Model filePath: @buffer.getPath() fileContents: @buffer.getText() } + break scopes indentLevelForRow: (bufferRow) -> From 5db3280bf04dce58684ecaec26ee044f05de9780 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 11:59:05 -0400 Subject: [PATCH 209/262] Provide openedPath For backward compatibility with GitRepoAsync before moving to ohnogit. --- src/git-repository-async.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/git-repository-async.js b/src/git-repository-async.js index b691994bc..66b73ba77 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -50,6 +50,10 @@ export default class GitRepositoryAsync { return this.repo._refreshingPromise } + get openedPath () { + return this.repo.openedPath + } + // Public: Destroy this {GitRepositoryAsync} object. // // This destroys any tasks and subscriptions and releases the underlying From 30d6353fd8e46af780bd6b47da585b62b10eddfa Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 12:03:29 -0400 Subject: [PATCH 210/262] Spec for openedPath. --- spec/git-repository-async-spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 1fbe537d5..fcb528819 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -55,6 +55,14 @@ describe('GitRepositoryAsync', () => { }) }) + describe('openedPath', () => { + it('is the path passed to .open', () => { + const workingDirPath = copyRepository() + repo = GitRepositoryAsync.open(workingDirPath) + expect(repo.openedPath).toBe(workingDirPath) + }) + }) + describe('.getRepo()', () => { beforeEach(() => { const workingDirectory = copySubmoduleRepository() From 5cfe97160d766170293770975127f4d9e11682d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 Apr 2016 17:47:49 +0200 Subject: [PATCH 211/262] Report boundary when next line's `openScopes` don't match containingTags Sometimes, when performing an edit, a change on some row can cause another row's tokenization to be affected: the classic example is opening a multi-line comment on a line, thereby causing subsequent lines to become commented out without changing the buffer's contents at those locations. We call this technique "spill detection". Since the amount of affected lines can grow quite large, Atom tokenizes synchronously only those lines where the edit occurred, triggering background (i.e. `setInterval`) tokenization for all the other lines that need to be refreshed because of a "spill". As predictable, this approach causes a temporary inconsistency in the stored tokenized lines. In particular, suppose we had two tokenized lines, and that there's an open tag in the middle of the first one which closes on the second one. If we perform an edit that causes that tag to be deleted, when reading the second tokenized line we now have a dangling close tag. This didn't matter much in the `DisplayBuffer` version, because for each line we reopened all the tags found in the stored `openScopes` property, and closed all the tags starting on such line right at the end of it. In the `DisplayLayer` world, however, we don't read tags from each tokenized line, but we let `TokenizedBufferIterator` report tag boundaries and their respective location: since this is an iterator-based approach, we were not reading `openScopes` for each `TokenizedLine`, making the dangling close tag example showed above evident (e.g. close and open tags didn't match anymore, and exceptions were being thrown all over the place). To solve this issue I have considered several approaches: 1. Recompute all the lines where a spill occurs synchronously when the buffer changes. For large files, this can be pretty onerous, and we don't want to regress in terms of performance. 2. Let `TokenizedBuffer.tokenizedLineForRow(bufferRow)` recompute potential invalid lines lazily (starting from the first invalid line, down to the requested buffer row). When editing the first lines of a long file and causing a spill to occur, Atom (or any other package, for that matter) could request a line down in the file, causing this method to recompute lots and lots of lines. 3. Let `DisplayLayer` deal with closing an un-opened tag. This is nice because we already keep track of containing tags there. However, it also feels like the wrong spot where to put this logic, as display layers shouldn't deal with grammar-related stuff. 4. Keep track of containing tags in `TokenizedBufferIterator`, and report a boundary at the end of the line when the subsequent one's `openScopes` property doesn't match the `containingTags` that the iterator has been keeping track of. Of all these solutions I've chosen 4), because it's the most performant and clean in terms of code. --- spec/tokenized-buffer-iterator-spec.js | 63 ++++++++++++++++++++++++++ src/tokenized-buffer-iterator.coffee | 56 +++++++++++++++++------ 2 files changed, 104 insertions(+), 15 deletions(-) diff --git a/spec/tokenized-buffer-iterator-spec.js b/spec/tokenized-buffer-iterator-spec.js index 639714136..8d0e458f4 100644 --- a/spec/tokenized-buffer-iterator-spec.js +++ b/spec/tokenized-buffer-iterator-spec.js @@ -37,4 +37,67 @@ describe('TokenizedBufferIterator', () => { expect(iterator.getCloseTags()).toEqual(['foo']) expect(iterator.getOpenTags()).toEqual([]) }) + + it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => { + const tokenizedBuffer = { + tokenizedLineForRow (row) { + if (row === 0) { + return { + tags: [-1, 3, -2, -3], + text: 'bar', + openScopes: [] + } + } else if (row === 1) { + return { + tags: [3], + text: 'baz', + openScopes: [-1] + } + } else if (row === 2) { + return { + tags: [-2], + text: '', + openScopes: [-1] + } + } + } + } + + const grammarRegistry = { + scopeForId (id) { + if (id === -2 || id === -1) { + return 'foo' + } else if (id === -3) { + return 'qux' + } + } + } + + const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry) + + iterator.seek(Point(0, 0)) + expect(iterator.getPosition()).toEqual(Point(0, 0)) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['foo']) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(0, 3)) + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual(['qux']) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(0, 3)) + expect(iterator.getCloseTags()).toEqual(['qux']) + expect(iterator.getOpenTags()).toEqual([]) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(1, 0)) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['foo']) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(2, 0)) + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual([]) + }) }) diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee index 725041def..ad1834f09 100644 --- a/src/tokenized-buffer-iterator.coffee +++ b/src/tokenized-buffer-iterator.coffee @@ -1,10 +1,12 @@ {Point} = require 'text-buffer' +{isEqual} = require 'underscore-plus' module.exports = class TokenizedBufferIterator constructor: (@tokenizedBuffer, @grammarRegistry) -> @openTags = null @closeTags = null + @containingTags = null seek: (position) -> @openTags = [] @@ -12,9 +14,10 @@ class TokenizedBufferIterator @tagIndex = null currentLine = @tokenizedBuffer.tokenizedLineForRow(position.row) - containingTags = currentLine.openScopes.map (id) => @grammarRegistry.scopeForId(id) @currentTags = currentLine.tags + @currentLineOpenTags = currentLine.openScopes @currentLineLength = currentLine.text.length + @containingTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id) currentColumn = 0 for tag, index in @currentTags if tag >= 0 @@ -23,8 +26,8 @@ class TokenizedBufferIterator break else currentColumn += tag - containingTags.pop() while @closeTags.shift() - containingTags.push(tag) while tag = @openTags.shift() + @containingTags.pop() while @closeTags.shift() + @containingTags.push(tag) while tag = @openTags.shift() else scopeName = @grammarRegistry.scopeForId(tag) if tag % 2 is 0 @@ -38,9 +41,11 @@ class TokenizedBufferIterator @tagIndex ?= @currentTags.length @position = Point(position.row, Math.min(@currentLineLength, currentColumn)) - containingTags + @containingTags.slice() moveToSuccessor: -> + @containingTags.pop() for tag in @closeTags + @containingTags.push(tag) for tag in @openTags @openTags = [] @closeTags = [] @@ -49,7 +54,16 @@ class TokenizedBufferIterator if @isAtTagBoundary() break else - return false unless @moveToNextLine() + if @shouldMoveToNextLine + @moveToNextLine() + @openTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id) + @shouldMoveToNextLine = false + else if @hasNextLine() and not isEqual(@containingTags, @nextLineOpeningScopes()) + @closeTags = @containingTags.slice().reverse() + @containingTags = [] + @shouldMoveToNextLine = true + else + return false unless @moveToNextLine() else tag = @currentTags[@tagIndex] if tag >= 0 @@ -70,16 +84,6 @@ class TokenizedBufferIterator true - # Private - moveToNextLine: -> - @position = Point(@position.row + 1, 0) - tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(@position.row) - return false unless tokenizedLine? - @currentTags = tokenizedLine.tags - @currentLineLength = tokenizedLine.text.length - @tagIndex = 0 - true - getPosition: -> @position @@ -89,5 +93,27 @@ class TokenizedBufferIterator getOpenTags: -> @openTags.slice() + ### + Section: Private Methods + ### + + hasNextLine: -> + @tokenizedBuffer.tokenizedLineForRow(@position.row + 1)? + + nextLineOpeningScopes: -> + line = @tokenizedBuffer.tokenizedLineForRow(@position.row + 1) + line.openScopes.map (id) => @grammarRegistry.scopeForId(id) + + moveToNextLine: -> + @position = Point(@position.row + 1, 0) + if tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(@position.row) + @currentTags = tokenizedLine.tags + @currentLineLength = tokenizedLine.text.length + @currentLineOpenTags = tokenizedLine.openScopes + @tagIndex = 0 + true + else + false + isAtTagBoundary: -> @closeTags.length > 0 or @openTags.length > 0 From a279db5568707e5c17009735eea4d72295a0eed8 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 12:50:13 -0400 Subject: [PATCH 212/262] Failing test. --- spec/git-spec.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index 7d9e9bbd4..82e371146 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -289,6 +289,16 @@ describe "GitRepository", -> expect(repo.isStatusModified(status)).toBe true expect(repo.isStatusNew(status)).toBe false + it 'caches statuses that were looked up synchronously', -> + originalContent = 'undefined' + fs.writeFileSync(modifiedPath, 'making this path modified') + repo.getPathStatus('file.txt') + + fs.writeFileSync(modifiedPath, originalContent) + waitsForPromise -> repo.refreshStatus() + runs -> + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() + describe "buffer events", -> [editor] = [] From 69e97204d51ec412f0b81acf1eb89369aed08a15 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 12:50:27 -0400 Subject: [PATCH 213/262] Test for undefinedness instead of 0. --- src/git-repository.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index a04124b78..cea000efc 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -369,7 +369,10 @@ class GitRepository @getCachedRelativePathStatus(relativePath) getCachedRelativePathStatus: (relativePath) -> - @statusesByPath[relativePath] ? @async.getCachedPathStatuses()[relativePath] + cachedStatus = @statusesByPath[relativePath] + return cachedStatus if cachedStatus? + + @async.getCachedPathStatuses()[relativePath] # Public: Returns true if the given status indicates modification. # From b4733732dcf45546e976160508e4100339f7f7f8 Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Wed, 27 Apr 2016 10:35:39 -0700 Subject: [PATCH 214/262] :arrow_up: autocomplete-plus@2.30.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51d1f06e8..1802e8bd1 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.1", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.29.2", + "autocomplete-plus": "2.30.0", "autocomplete-snippets": "1.10.0", "autoflow": "0.27.0", "autosave": "0.23.1", From 6ec5cf497b0357657cffd30b5ad89bc6b909c7a2 Mon Sep 17 00:00:00 2001 From: David Elliott Date: Wed, 27 Apr 2016 10:52:57 -0700 Subject: [PATCH 215/262] :bug: Ctrl-O opens file dialog in directory of currently active editor --- src/browser/atom-application.coffee | 32 ++++++++++++++++------------ src/register-default-commands.coffee | 12 ++++++----- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 38c2a354a..b40dbfaad 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -58,7 +58,6 @@ class AtomApplication resourcePath: null version: null quitting: false - lastFilePathOpened: null exit: (status) -> app.exit(status) @@ -171,11 +170,6 @@ class AtomApplication @on 'application:quit', -> app.quit() @on 'application:new-window', -> @openPath(getLoadSettings()) @on 'application:new-file', -> (@focusedWindow() ? this).openPath() - @on 'application:open', -> @promptForPathToOpen('all', getLoadSettings()) - @on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings()) - @on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings()) - @on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true) - @on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true) @on 'application:inspect', ({x, y, atomWindow}) -> atomWindow ?= @focusedWindow() atomWindow?.browserWindow.inspectElement(x, y) @@ -256,6 +250,15 @@ class AtomApplication ipcMain.on 'command', (event, command) => @emit(command) + ipcMain.on 'open-command', (event, command, args...) => + switch command + when 'application:open' then @promptForPathToOpen('all', getLoadSettings()) + when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), args[0]) + when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings()) + when 'application:open-dev' then @promptForPathToOpen('all', devMode: true) + when 'application:open-safe' then @promptForPathToOpen('all', safeMode: true) + else console.log "Invalid open-command received: " + command + ipcMain.on 'window-command', (event, command, args...) -> win = BrowserWindow.fromWebContents(event.sender) win.emit(command, args...) @@ -654,12 +657,13 @@ class AtomApplication # :safeMode - A Boolean which controls whether any newly opened windows # should be in safe mode or not. # :window - An {AtomWindow} to use for opening a selected file path. - promptForPathToOpen: (type, {devMode, safeMode, window}) -> - @promptForPath type, (pathsToOpen) => - @lastFilePathOpened = pathsToOpen[0] if pathsToOpen.length > 0 - @openPaths({pathsToOpen, devMode, safeMode, window}) + # :path - An optional String which controls the default path to which the + # file dialog opens. + promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) -> + @promptForPath type, ((pathsToOpen) => + @openPaths({pathsToOpen, devMode, safeMode, window})), path - promptForPath: (type, callback) -> + promptForPath: (type, callback, path) -> properties = switch type when 'file' then ['openFile'] @@ -682,8 +686,8 @@ class AtomApplication when 'folder' then 'Open Folder' else 'Open' - # If a file was previously opened, set default path to open there again. - if @lastFilePathOpened? and type is 'file' - openOptions.defaultPath = @lastFilePathOpened + # File dialog defaults to project directory of currently active editor + if path? and type is 'file' + openOptions.defaultPath = path dialog.showOpenDialog(parentWindow, openOptions, callback) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 035750363..e0b08abdc 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -31,11 +31,13 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications') 'application:new-window': -> ipcRenderer.send('command', 'application:new-window') 'application:new-file': -> ipcRenderer.send('command', 'application:new-file') - 'application:open': -> ipcRenderer.send('command', 'application:open') - 'application:open-file': -> ipcRenderer.send('command', 'application:open-file') - 'application:open-folder': -> ipcRenderer.send('command', 'application:open-folder') - 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') - 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe') + 'application:open': -> ipcRenderer.send('open-command', 'application:open') + 'application:open-file': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open-file', defaultPath) + 'application:open-folder': -> ipcRenderer.send('open-command', 'application:open-folder') + 'application:open-dev': -> ipcRenderer.send('open-command', 'application:open-dev') + 'application:open-safe': -> ipcRenderer.send('open-command', 'application:open-safe') 'application:add-project-folder': -> atom.addProjectFolder() 'application:minimize': -> ipcRenderer.send('command', 'application:minimize') 'application:zoom': -> ipcRenderer.send('command', 'application:zoom') From c0adf125ecaf2100a4e3bd584565d5c6c5435fad Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 16:05:00 -0400 Subject: [PATCH 216/262] Reset the cache before calling the callback. --- src/git-repository.coffee | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index cea000efc..28455c983 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -166,12 +166,10 @@ class GitRepository # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses: (callback) -> - @async.onDidChangeStatuses -> - # Defer the callback to the next tick so that we've reset - # `@statusesByPath` by the time it's called. Otherwise reads from within - # the callback could be inconsistent. - # See https://github.com/atom/atom/issues/11396 - process.nextTick callback + @async.onDidChangeStatuses => + @branch = @async?.branch + @statusesByPath = {} + callback() ### Section: Repository Details @@ -369,10 +367,7 @@ class GitRepository @getCachedRelativePathStatus(relativePath) getCachedRelativePathStatus: (relativePath) -> - cachedStatus = @statusesByPath[relativePath] - return cachedStatus if cachedStatus? - - @async.getCachedPathStatuses()[relativePath] + @statusesByPath[relativePath] ? @async.getCachedPathStatuses()[relativePath] # Public: Returns true if the given status indicates modification. # @@ -499,10 +494,7 @@ class GitRepository # # Returns a promise that resolves when the repository has been refreshed. refreshStatus: -> - asyncRefresh = @async.refreshStatus().then => - @statusesByPath = {} - @branch = @async?.branch - + asyncRefresh = @async.refreshStatus() syncRefresh = new Promise (resolve, reject) => @handlerPath ?= require.resolve('./repository-status-handler') From 630b8c69a603e69351864bb6fd851e1e9b4ef063 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 16:24:29 -0400 Subject: [PATCH 217/262] Fix the test. --- spec/git-spec.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index 82e371146..88c5c9a86 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -295,7 +295,12 @@ describe "GitRepository", -> repo.getPathStatus('file.txt') fs.writeFileSync(modifiedPath, originalContent) - waitsForPromise -> repo.refreshStatus() + + statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses statusHandler + repo.refreshStatus() + + waitsFor -> statusHandler.callCount > 0 runs -> expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() From 4727d6611e53cb8454a77ad607973a2c8efa8885 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 17:06:37 -0400 Subject: [PATCH 218/262] Revert "Fix the test." This reverts commit 630b8c69a603e69351864bb6fd851e1e9b4ef063. --- spec/git-spec.coffee | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index 88c5c9a86..82e371146 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -295,12 +295,7 @@ describe "GitRepository", -> repo.getPathStatus('file.txt') fs.writeFileSync(modifiedPath, originalContent) - - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> statusHandler.callCount > 0 + waitsForPromise -> repo.refreshStatus() runs -> expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() From d11e30579b3203bc179018dacd91441ce37a4fff Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 17:17:09 -0400 Subject: [PATCH 219/262] Manually emit the change event. --- src/git-repository.coffee | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 28455c983..a92a03a89 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -166,10 +166,7 @@ class GitRepository # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses: (callback) -> - @async.onDidChangeStatuses => - @branch = @async?.branch - @statusesByPath = {} - callback() + @emitter.on 'did-change-statuses', callback ### Section: Repository Details @@ -494,7 +491,23 @@ class GitRepository # # Returns a promise that resolves when the repository has been refreshed. refreshStatus: -> - asyncRefresh = @async.refreshStatus() + statusesChanged = false + subscription = @async.onDidChangeStatuses -> + subscription?.dispose() + subscription = null + + statusesChanged = true + + asyncRefresh = @async.refreshStatus().then => + subscription?.dispose() + subscription = null + + @branch = @async?.branch + @statusesByPath = {} + + if statusesChanged + @emitter.emit 'did-change-statuses' + syncRefresh = new Promise (resolve, reject) => @handlerPath ?= require.resolve('./repository-status-handler') From cd8b28da4da588b4bf63d410da6a14f96adbc8d9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Wed, 27 Apr 2016 22:40:34 -0400 Subject: [PATCH 220/262] Document why --- src/git-repository.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index a92a03a89..fcbc40830 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -492,6 +492,11 @@ class GitRepository # Returns a promise that resolves when the repository has been refreshed. refreshStatus: -> statusesChanged = false + + # Listen for `did-change-statuses` so we know if something changed. But we + # need to wait to propagate it until after we've set the branch and cleared + # the `statusesByPath` cache. So just set a flag, and we'll emit the event + # after refresh is done. subscription = @async.onDidChangeStatuses -> subscription?.dispose() subscription = null From 3b5bcf1c0dc71eaccb0ee29b648d8285706ecf37 Mon Sep 17 00:00:00 2001 From: livelazily Date: Mon, 25 Apr 2016 22:23:40 +0800 Subject: [PATCH 221/262] :bug: Wait for connection end to get completed data; --- src/browser/atom-application.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 69eff1845..3255b0a19 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -138,7 +138,11 @@ class AtomApplication return unless @socketPath? @deleteSocketFile() server = net.createServer (connection) => - connection.on 'data', (data) => + data = '' + connection.on 'data', (chunk) -> + data = data + chunk + + connection.on 'end', => options = JSON.parse(data) @openWithOptions(options) From 57442781ec6979e66bb5819a0782b75dfa15cd02 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 28 Apr 2016 12:05:58 +0200 Subject: [PATCH 222/262] Fix bug when positioning cursors after the fold-marker --- package.json | 2 +- spec/text-editor-component-spec.js | 11 +++++++++++ src/lines-tile-component.coffee | 10 ++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bff11c2f2..abbcd7b70 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta5", + "text-buffer": "9.0.0-beta6", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 1b29cbd7b..83f5d1b80 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1249,6 +1249,17 @@ describe('TextEditorComponent', function () { expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) }) + it('positions cursors after the fold-marker when a fold ends the line', async function () { + editor.foldBufferRow(0) + await nextViewUpdatePromise() + editor.setCursorScreenPosition([0, 30]) + await nextViewUpdatePromise() + + let cursorRect = componentNode.querySelector('.cursor').getBoundingClientRect() + let foldMarkerRect = componentNode.querySelector('.fold-marker').getBoundingClientRect() + expect(cursorRect.left).toBeCloseTo(foldMarkerRect.right, 0) + }) + it('positions cursors correctly after character widths are changed via a stylesheet change', async function () { atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index c9ecd0982..3d82d9d9f 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -4,6 +4,7 @@ HighlightsComponent = require './highlights-component' AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} TokenTextEscapeRegex = /[&"'<>]/g MaxTokenLength = 20000 +ZERO_WIDTH_NBSP = '\ufeff' cloneObject = (object) -> clone = {} @@ -225,6 +226,15 @@ class LinesTileComponent lineNode.appendChild(textNode) textNodes.push(textNode) + if lineText.endsWith(@presenter.displayLayer.foldCharacter) + # Insert a zero-width non-breaking whitespace, so that + # LinesYardstick can take the fold-marker::after pseudo-element + # into account during measurements when such marker is the last + # character on the line. + textNode = @domElementPool.buildText(ZERO_WIDTH_NBSP) + lineNode.appendChild(textNode) + textNodes.push(textNode) + @textNodesByLineId[id] = textNodes lineNode From f2a497d591654bcce4d9727efa6796be012935e8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 28 Apr 2016 13:43:00 +0200 Subject: [PATCH 223/262] Don't create folds for empty ranges --- spec/selection-spec.coffee | 19 +++++++++++++++++++ src/selection.coffee | 5 +++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index 18095d6f8..7511c4b39 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -101,3 +101,22 @@ describe "Selection", -> selection.setBufferRange [[2, 0], [3, 0]] selection.insertText("\r\n", autoIndent: true) expect(buffer.lineForRow(2)).toBe " " + + describe ".fold()", -> + it "folds the buffer range spanned by the selection", -> + selection.setBufferRange([[0, 3], [1, 6]]) + selection.fold() + + expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]]) + expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]]) + expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {" + expect(editor.isFoldedAtBufferRow(0)).toBe(true) + + it "doesn't create a fold when the selection is empty", -> + selection.setBufferRange([[0, 3], [0, 3]]) + selection.fold() + + expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]]) + expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]]) + expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + expect(editor.isFoldedAtBufferRow(0)).toBe(false) diff --git a/src/selection.coffee b/src/selection.coffee index ea8ca187a..7ecbb3fbc 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -628,8 +628,9 @@ class Selection extends Model # Public: Creates a fold containing the current selection. fold: -> range = @getBufferRange() - @editor.foldBufferRange(range) - @cursor.setBufferPosition(range.end) + unless range.isEmpty() + @editor.foldBufferRange(range) + @cursor.setBufferPosition(range.end) # Private: Increase the indentation level of the given text by given number # of levels. Leaves the first line unchanged. From 76d8421963e1712f13c2808f1a2725011322080e Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 28 Apr 2016 09:50:01 -0400 Subject: [PATCH 224/262] Inline the no-op assert. --- src/text-editor.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index e3300a3b2..8779cd973 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -101,7 +101,7 @@ class TextEditor extends Model throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? - @assert ?= @defaultAssert.bind(this) + @assert ?= (condition) -> condition @firstVisibleScreenRow ?= 0 @firstVisibleScreenColumn ?= 0 @emitter = new Emitter @@ -3364,9 +3364,6 @@ class TextEditor extends Model @emitter.emit 'will-insert-text', willInsertEvent result - defaultAssert: (condition, message, callback) -> - condition - ### Section: Language Mode Delegated Methods ### From 2c5340c70ff3261af9e78addb5c1c94d4c7eede4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 28 Apr 2016 17:29:59 +0200 Subject: [PATCH 225/262] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index abbcd7b70..be2613aac 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta6", + "text-buffer": "9.0.0-beta7", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 2f1268dc7ca9ab773837499d4a938064978919ac Mon Sep 17 00:00:00 2001 From: joshaber Date: Thu, 28 Apr 2016 11:36:17 -0400 Subject: [PATCH 226/262] Move copyPathToClipboard to the default commands. --- src/atom-environment.coffee | 2 +- src/register-default-commands.coffee | 11 ++++++++--- src/text-editor.coffee | 6 ------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index cf54ee0bc..f50ae9d5b 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -255,7 +255,7 @@ class AtomEnvironment extends Model @deserializers.add(TextBuffer) registerDefaultCommands: -> - registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications}) + registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications, @project, @clipboard}) registerDefaultViewProviders: -> @views.addViewProvider Workspace, (model, env) -> diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 035750363..1cb3d99b5 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -1,6 +1,6 @@ {ipcRenderer} = require 'electron' -module.exports = ({commandRegistry, commandInstaller, config, notificationManager}) -> +module.exports = ({commandRegistry, commandInstaller, config, notificationManager, project, clipboard}) -> commandRegistry.add 'atom-workspace', 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() @@ -188,8 +188,8 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) 'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager) - 'editor:copy-path': -> @copyPathToClipboard(false) - 'editor:copy-project-path': -> @copyPathToClipboard(true) + 'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false) + 'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true) 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers')) 'editor:scroll-to-cursor': -> @scrollToCursorPosition() @@ -239,3 +239,8 @@ showCursorScope = (descriptor, notificationManager) -> content = "Scopes at Cursor\n#{list.join('\n')}" notificationManager.addInfo(content, dismissable: true) + +copyPathToClipboard = (editor, project, clipboard, relative) -> + if filePath = editor.getPath() + filePath = project.relativize(filePath) if relative + clipboard.write(filePath) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 8779cd973..9976e5906 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -669,12 +669,6 @@ class TextEditor extends Model # Essential: Returns {Boolean} `true` if this editor has no content. isEmpty: -> @buffer.isEmpty() - # Copies the current file path to the native clipboard. - copyPathToClipboard: (relative = false) -> - if filePath = @getPath() - filePath = atom.project.relativize(filePath) if relative - @clipboard.write(filePath) - ### Section: File Operations ### From 398ae4491ec8b4a03f3d47343c410c46788975ef Mon Sep 17 00:00:00 2001 From: David Elliott Date: Thu, 28 Apr 2016 11:13:07 -0700 Subject: [PATCH 227/262] :art: Removed application:open-dev and application:open-safe from new method. --- src/browser/atom-application.coffee | 7 ++++--- src/register-default-commands.coffee | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index b40dbfaad..f39c50ce4 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -170,6 +170,8 @@ class AtomApplication @on 'application:quit', -> app.quit() @on 'application:new-window', -> @openPath(getLoadSettings()) @on 'application:new-file', -> (@focusedWindow() ? this).openPath() + @on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true) + @on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true) @on 'application:inspect', ({x, y, atomWindow}) -> atomWindow ?= @focusedWindow() atomWindow?.browserWindow.inspectElement(x, y) @@ -251,12 +253,11 @@ class AtomApplication @emit(command) ipcMain.on 'open-command', (event, command, args...) => + defaultPath = args[0] if args.length > 0 switch command when 'application:open' then @promptForPathToOpen('all', getLoadSettings()) - when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), args[0]) + when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath) when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings()) - when 'application:open-dev' then @promptForPathToOpen('all', devMode: true) - when 'application:open-safe' then @promptForPathToOpen('all', safeMode: true) else console.log "Invalid open-command received: " + command ipcMain.on 'window-command', (event, command, args...) -> diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index e0b08abdc..b2dc1fd68 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -36,8 +36,8 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-command', 'application:open-file', defaultPath) 'application:open-folder': -> ipcRenderer.send('open-command', 'application:open-folder') - 'application:open-dev': -> ipcRenderer.send('open-command', 'application:open-dev') - 'application:open-safe': -> ipcRenderer.send('open-command', 'application:open-safe') + 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') + 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe') 'application:add-project-folder': -> atom.addProjectFolder() 'application:minimize': -> ipcRenderer.send('command', 'application:minimize') 'application:zoom': -> ipcRenderer.send('command', 'application:zoom') From d3ee21941a78c16695efe9b8e634a8f2921acf47 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 28 Apr 2016 12:23:44 -0700 Subject: [PATCH 228/262] Fix some specs on Windows and honor options.shell --- spec/buffered-process-spec.coffee | 23 +++++++++-------------- src/buffered-process.coffee | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index 643a2d411..ec01b40d7 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -16,9 +16,9 @@ describe "BufferedProcess", -> describe "when an error event is emitted by the process", -> it "calls the error handler and does not throw an exception", -> process = new BufferedProcess - command: 'bad-command-nope' + command: 'bad-command-nope1' args: ['nothing'] - options: {} + options: {shell: false} errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() process.onWillThrowError(errorSpy) @@ -28,7 +28,7 @@ describe "BufferedProcess", -> runs -> expect(window.onerror).not.toHaveBeenCalled() expect(errorSpy).toHaveBeenCalled() - expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT' + expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope1 ENOENT' describe "when an error is thrown spawning the process", -> it "calls the error handler and does not throw an exception", -> @@ -53,17 +53,17 @@ describe "BufferedProcess", -> expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'Something is really wrong' describe "when there is not an error handler specified", -> - it "calls the error handler and does not throw an exception", -> + it "does throw an exception", -> process = new BufferedProcess - command: 'bad-command-nope' + command: 'bad-command-nope2' args: ['nothing'] - options: {} + options: {shell: false} waitsFor -> window.onerror.callCount > 0 runs -> expect(window.onerror).toHaveBeenCalled() - expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope`' + expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope2`' expect(window.onerror.mostRecentCall.args[4].name).toBe 'BufferedProcessError' describe "on Windows", -> @@ -72,13 +72,7 @@ describe "BufferedProcess", -> beforeEach -> # Prevent any commands from actually running and affecting the host originalSpawn = ChildProcess.spawn - spyOn(ChildProcess, 'spawn').andCallFake -> - # Just spawn something that won't actually modify the host - if originalPlatform is 'win32' - originalSpawn('dir') - else - originalSpawn('ls') - + spyOn(ChildProcess, 'spawn') originalPlatform = process.platform Object.defineProperty process, 'platform', value: 'win32' @@ -117,6 +111,7 @@ describe "BufferedProcess", -> expect(stdout).toEqual '' it "calls the specified stdout callback only with whole lines", -> + # WINSPEC: Fails because command is too long, /bin/echo is *nix only, odd newlining and quoting exitCallback = jasmine.createSpy('exit callback') baseContent = "There are dozens of us! Dozens! It's as Ann as the nose on Plain's face. Can you believe that the only reason the club is going under is because it's in a terrifying neighborhood? She calls it a Mayonegg. Waiting for the Emmys. BTW did you know won 6 Emmys and was still canceled early by Fox? COME ON. I'll buy you a hundred George Michaels that you can teach to drive! Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'" content = (baseContent for _ in [1..200]).join('\n') diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index 59f2a7e9a..07fcfb664 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -50,7 +50,7 @@ class BufferedProcess options ?= {} @command = command # Related to joyent/node#2318 - if process.platform is 'win32' + if process.platform is 'win32' and not options.shell? # Quote all arguments and escapes inner quotes if args? cmdArgs = args.filter (arg) -> arg? From c6bd9bc8c0c8079044ea5c057ce4adebe3249f68 Mon Sep 17 00:00:00 2001 From: Rahat Ahmed Date: Thu, 28 Apr 2016 15:28:54 -0500 Subject: [PATCH 229/262] Adjustments for @as-cii --- package.json | 1 - src/lines-yardstick.coffee | 67 +++++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 31b8bb722..ffe6d30e0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "async": "0.2.6", "atom-keymap": "6.3.2", "babel-core": "^5.8.21", - "binary-search-with-index": "^1.3.0", "bootstrap": "^3.3.4", "cached-run-in-this-context": "0.4.1", "clear-cut": "^2.0.1", diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index f865a039c..4642a53a6 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -1,6 +1,5 @@ {Point} = require 'text-buffer' {isPairedCharacter} = require './text-utils' -binarySearch = require 'binary-search-with-index' module.exports = class LinesYardstick @@ -20,8 +19,8 @@ class LinesYardstick targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() - return Point(0, 0) if targetTop < 0 row = @lineTopIndex.rowForPixelPosition(targetTop) + targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) @@ -33,26 +32,40 @@ class LinesYardstick lineOffset = lineNode.getBoundingClientRect().left targetLeft += lineOffset - textNodeComparator = (textNode, position) => + textNodeIndex = @_binarySearch(textNodes, (textNode) => {length: textNodeLength} = textNode rangeRect = @clientRectForRange(textNode, 0, textNodeLength) - return -1 if rangeRect.right < position - return 1 if rangeRect.left > position + return -1 if rangeRect.right < targetLeft + return 1 if rangeRect.left > targetLeft return 0 + ) - textNodeIndex = binarySearch(textNodes, targetLeft, textNodeComparator) + textNodeStartColumn = 0 if textNodeIndex >= 0 - textNodeStartColumn = textNodes - .slice(0, textNodeIndex) - .reduce(((totalLength, node) -> totalLength + node.length), 0) - charIndex = @charIndexForScreenPosition(textNodes[textNodeIndex], targetLeft) + textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] - return Point(row, textNodeStartColumn + charIndex) + textNode = textNodes[textNodeIndex] + {textContent: textNodeContent} = textNode + rangeRect = null + nextCharIndex = -1 - textNodeStartColumn = textNodes - .reduce(((totalLength, node) -> totalLength + node.length), 0) + characterIndex = @_binarySearch(textNodeContent, (char, charIndex) => + if isPairedCharacter(textNodeContent, charIndex) + nextCharIndex = charIndex + 2 + else + nextCharIndex = charIndex + 1 + rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex) + return -1 if rangeRect.right < targetLeft + return 1 if rangeRect.left > targetLeft + return 0 + ) + if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) + return Point(row, textNodeStartColumn + characterIndex) + return Point(row, textNodeStartColumn + nextCharIndex) + + textNodeStartColumn += node.length for node in textNodes return Point(row, textNodeStartColumn) pixelPositionForScreenPosition: (screenPosition) -> @@ -104,21 +117,17 @@ class LinesYardstick @rangeForMeasurement.setEnd(textNode, endIndex) @rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect() - charIndexForScreenPosition: (textNode, targetLeft) -> - {textContent: textNodeContent} = textNode - rangeRect = null - nextCharIndex = -1 - characterComparator = (char, position, charIndex) => - if isPairedCharacter(textNodeContent, charIndex) - nextCharIndex = charIndex + 2 + _binarySearch: (array, compare) -> + low = 0 + high = array.length - 1 + while low <= high + mid = low + (high - low >> 1) + comparison = compare(array[mid], mid) + if comparison < 0 + low = mid + 1 + else if comparison > 0 + high = mid - 1 else - nextCharIndex = charIndex + 1 - rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex) - return -1 if rangeRect.right < position - return 1 if rangeRect.left > position - return 0 + return mid - characterIndex = binarySearch(textNodeContent, targetLeft, characterComparator) - if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) - return characterIndex - return nextCharIndex + return -1 From 7d463d9dd02fb60990690b8e32d16b75ea820137 Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Thu, 28 Apr 2016 15:30:38 -0700 Subject: [PATCH 230/262] :arrow_up: about@1.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1802e8bd1..87083ddd7 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "one-light-syntax": "1.2.0", "solarized-dark-syntax": "1.0.2", "solarized-light-syntax": "1.0.2", - "about": "1.5.1", + "about": "1.5.2", "archive-view": "0.61.1", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.1", From 50e3f1f789d5432153ce41f2eae5752dc95c2abc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Apr 2016 14:06:05 +0200 Subject: [PATCH 231/262] :arrow_up: text-bufffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be2613aac..6901d930c 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta7", + "text-buffer": "9.0.0-beta8", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From c5a76d4a7d9b7de9c78a534091a1b775bc5e9c9a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Apr 2016 14:41:34 +0200 Subject: [PATCH 232/262] Persist the entire state on reload This fixes an annoying problem that prevented the state of marker layers from being saved when the window was reloaded either via `Cmd+R` in DevTools or via `Ctrl+Option+Cmd+L` in Atom. The issue was that we were *always* scheduling `saveState` on an idle callback: `window.onbeforeunload`, however, doesn't wait for that event before closing the window, and thus that state was never saved in those situations. The solution is to use idle callbacks only during the critical code path (i.e. on mousedown and keydown), but save it synchronously otherwise. Saving something to IndexedDB is actually asynchronous too, but it seems like Chrome fulfills `put` requests that get executed right during `onbeforeunload`. --- spec/atom-environment-spec.coffee | 29 ++++++++++++++++++++++++----- src/atom-environment.coffee | 31 ++++++++++++++++--------------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 846083b0e..a78190ab7 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -208,12 +208,15 @@ describe "AtomEnvironment", -> waitsForPromise -> atom.loadState().then (state) -> expect(state).toEqual(serializedState) - it "saves state on keydown, mousedown, and when the editor window unloads", -> + it "saves state when the CPU is idle after a keydown or mousedown event", -> spyOn(atom, 'saveState') + idleCallbacks = [] + spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback) keydown = new KeyboardEvent('keydown') atom.document.dispatchEvent(keydown) advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false}) expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true}) @@ -221,17 +224,33 @@ describe "AtomEnvironment", -> mousedown = new MouseEvent('mousedown') atom.document.dispatchEvent(mousedown) advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false}) expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true}) - atom.saveState.reset() + it "saves state immediately when unloading the editor window, ignoring pending and successive mousedown/keydown events", -> + spyOn(atom, 'saveState') + idleCallbacks = [] + spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback) + + mousedown = new MouseEvent('mousedown') + atom.document.dispatchEvent(mousedown) atom.unloadEditorWindow() - mousedown = new MouseEvent('mousedown') - atom.document.dispatchEvent(mousedown) - advanceClock atom.saveStateDebounceInterval expect(atom.saveState).toHaveBeenCalledWith({isUnloading: true}) expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: false}) + atom.saveState.reset() + advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() + expect(atom.saveState).not.toHaveBeenCalled() + + atom.saveState.reset() + mousedown = new MouseEvent('mousedown') + atom.document.dispatchEvent(mousedown) + advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() + expect(atom.saveState).not.toHaveBeenCalled() + it "serializes the project state with all the options supplied in saveState", -> spyOn(atom.project, 'serialize').andReturn({foo: 42}) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index f50ae9d5b..da3e989f2 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -234,13 +234,14 @@ class AtomEnvironment extends Model checkPortableHomeWritable() attachSaveStateListeners: -> - saveState = => @saveState({isUnloading: false}) unless @unloaded - debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval) - @document.addEventListener('mousedown', debouncedSaveState, true) - @document.addEventListener('keydown', debouncedSaveState, true) + saveState = _.debounce((=> + window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded + ), @saveStateDebounceInterval) + @document.addEventListener('mousedown', saveState, true) + @document.addEventListener('keydown', saveState, true) @disposables.add new Disposable => - @document.removeEventListener('mousedown', debouncedSaveState, true) - @document.removeEventListener('keydown', debouncedSaveState, true) + @document.removeEventListener('mousedown', saveState, true) + @document.removeEventListener('keydown', saveState, true) setConfigSchema: -> @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} @@ -655,6 +656,7 @@ class AtomEnvironment extends Model # Call this method when establishing a real application window. startEditorWindow: -> + @unloaded = false @loadState().then (state) => @windowDimensions = state?.windowDimensions @displayWindow().then => @@ -842,16 +844,15 @@ class AtomEnvironment extends Model return Promise.resolve() unless @enablePersistence new Promise (resolve, reject) => - window.requestIdleCallback => - return if not @project + return if not @project - state = @serialize(options) - savePromise = - if storageKey = @getStateKey(@project?.getPaths()) - @stateStore.save(storageKey, state) - else - @applicationDelegate.setTemporaryWindowState(state) - savePromise.catch(reject).then(resolve) + state = @serialize(options) + savePromise = + if storageKey = @getStateKey(@project?.getPaths()) + @stateStore.save(storageKey, state) + else + @applicationDelegate.setTemporaryWindowState(state) + savePromise.catch(reject).then(resolve) loadState: -> if @enablePersistence From 7b2f049cbb0a0550f4b9b29d730814ff4e3f18e1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Apr 2016 16:44:12 +0200 Subject: [PATCH 233/262] :art: Refine binary search algorithm --- spec/lines-yardstick-spec.coffee | 4 ++ src/lines-yardstick.coffee | 84 +++++++++++++++----------------- src/text-editor-component.coffee | 2 +- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 2f7233e2f..72609143e 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -170,3 +170,7 @@ describe "LinesYardstick", -> expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2] expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0] + + it "clips negative horizontal pixel positions", -> + expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: -10)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: 1 * 14, left: -10)).toEqual [1, 0] diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 4642a53a6..39e16a8e0 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -20,7 +20,7 @@ class LinesYardstick targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() row = @lineTopIndex.rowForPixelPosition(targetTop) - targetLeft = 0 if targetTop < 0 + targetLeft = 0 if targetTop < 0 or targetLeft < 0 targetLeft = Infinity if row > @model.getLastScreenRow() row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) @@ -32,41 +32,52 @@ class LinesYardstick lineOffset = lineNode.getBoundingClientRect().left targetLeft += lineOffset - textNodeIndex = @_binarySearch(textNodes, (textNode) => - {length: textNodeLength} = textNode - rangeRect = @clientRectForRange(textNode, 0, textNodeLength) - return -1 if rangeRect.right < targetLeft - return 1 if rangeRect.left > targetLeft - return 0 - ) - - textNodeStartColumn = 0 - - if textNodeIndex >= 0 - textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] + textNodeIndex = -1 + low = 0 + high = textNodes.length - 1 + while low <= high + mid = low + (high - low >> 1) + textNode = textNodes[mid] + rangeRect = @clientRectForRange(textNode, 0, textNode.length) + if targetLeft < rangeRect.left + high = mid - 1 + else if targetLeft > rangeRect.right + low = mid + 1 + else + textNodeIndex = mid + break + if textNodeIndex is -1 + textNodesExtent = 0 + textNodesExtent += textContent.length for {textContent} in textNodes + Point(row, textNodesExtent) + else textNode = textNodes[textNodeIndex] - {textContent: textNodeContent} = textNode - rangeRect = null - nextCharIndex = -1 - - characterIndex = @_binarySearch(textNodeContent, (char, charIndex) => - if isPairedCharacter(textNodeContent, charIndex) + characterIndex = -1 + low = 0 + high = textNode.textContent.length - 1 + while low <= high + charIndex = low + (high - low >> 1) + if isPairedCharacter(textNode.textContent, charIndex) nextCharIndex = charIndex + 2 else nextCharIndex = charIndex + 1 + rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex) - return -1 if rangeRect.right < targetLeft - return 1 if rangeRect.left > targetLeft - return 0 - ) + if targetLeft < rangeRect.left + high = charIndex - 1 + else if targetLeft > rangeRect.right + low = nextCharIndex + else + if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) + characterIndex = charIndex + else + characterIndex = nextCharIndex + break - if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) - return Point(row, textNodeStartColumn + characterIndex) - return Point(row, textNodeStartColumn + nextCharIndex) - - textNodeStartColumn += node.length for node in textNodes - return Point(row, textNodeStartColumn) + textNodeStartColumn = 0 + textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1 + Point(row, textNodeStartColumn + characterIndex) pixelPositionForScreenPosition: (screenPosition) -> targetRow = screenPosition.row @@ -116,18 +127,3 @@ class LinesYardstick @rangeForMeasurement.setStart(textNode, startIndex) @rangeForMeasurement.setEnd(textNode, endIndex) @rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect() - - _binarySearch: (array, compare) -> - low = 0 - high = array.length - 1 - while low <= high - mid = low + (high - low >> 1) - comparison = compare(array[mid], mid) - if comparison < 0 - low = mid + 1 - else if comparison > 0 - high = mid - 1 - else - return mid - - return -1 diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 3f9bb2029..8c20055ed 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -947,7 +947,7 @@ class TextEditorComponent screenPositionForMouseEvent: (event, linesClientRect) -> pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect) - @screenPositionForPixelPosition(pixelPosition, true) + @screenPositionForPixelPosition(pixelPosition) pixelPositionForMouseEvent: (event, linesClientRect) -> {clientX, clientY} = event From e2fa948ac8e541ecefe7fcdfac4f87535486a004 Mon Sep 17 00:00:00 2001 From: David Elliott Date: Fri, 29 Apr 2016 08:20:52 -0700 Subject: [PATCH 234/262] :bug: Add support for Mac and Open Folder dialog. --- src/browser/atom-application.coffee | 6 +++--- src/register-default-commands.coffee | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index f39c50ce4..f90afad83 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -255,9 +255,9 @@ class AtomApplication ipcMain.on 'open-command', (event, command, args...) => defaultPath = args[0] if args.length > 0 switch command - when 'application:open' then @promptForPathToOpen('all', getLoadSettings()) + when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath) when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath) - when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings()) + when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath) else console.log "Invalid open-command received: " + command ipcMain.on 'window-command', (event, command, args...) -> @@ -688,7 +688,7 @@ class AtomApplication else 'Open' # File dialog defaults to project directory of currently active editor - if path? and type is 'file' + if path? openOptions.defaultPath = path dialog.showOpenDialog(parentWindow, openOptions, callback) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index b2dc1fd68..74da2786e 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -1,6 +1,7 @@ {ipcRenderer} = require 'electron' module.exports = ({commandRegistry, commandInstaller, config, notificationManager}) -> + commandRegistry.add 'atom-workspace', 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() @@ -31,11 +32,15 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications') 'application:new-window': -> ipcRenderer.send('command', 'application:new-window') 'application:new-file': -> ipcRenderer.send('command', 'application:new-file') - 'application:open': -> ipcRenderer.send('open-command', 'application:open') - 'application:open-file': -> + 'application:open': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open', defaultPath) + 'application:open-file': -> defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-command', 'application:open-file', defaultPath) - 'application:open-folder': -> ipcRenderer.send('open-command', 'application:open-folder') + 'application:open-folder': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open-folder', defaultPath) 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe') 'application:add-project-folder': -> atom.addProjectFolder() From 6c9d7ecc2884075655e1583129afc99aa75dea47 Mon Sep 17 00:00:00 2001 From: David Elliott Date: Fri, 29 Apr 2016 08:39:14 -0700 Subject: [PATCH 235/262] Remove random space --- src/register-default-commands.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 74da2786e..086a55497 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -1,7 +1,6 @@ {ipcRenderer} = require 'electron' module.exports = ({commandRegistry, commandInstaller, config, notificationManager}) -> - commandRegistry.add 'atom-workspace', 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() @@ -32,13 +31,13 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications') 'application:new-window': -> ipcRenderer.send('command', 'application:new-window') 'application:new-file': -> ipcRenderer.send('command', 'application:new-file') - 'application:open': -> + 'application:open': -> defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-command', 'application:open', defaultPath) - 'application:open-file': -> + 'application:open-file': -> defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-command', 'application:open-file', defaultPath) - 'application:open-folder': -> + 'application:open-folder': -> defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-command', 'application:open-folder', defaultPath) 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') From b975c1ffc188a3b3fcee112a7b22a54c76089325 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 29 Apr 2016 18:15:41 -0400 Subject: [PATCH 236/262] :arrow_up: language-sass@0.48.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87083ddd7..e19a1c8f8 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-python": "0.43.1", "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", - "language-sass": "0.47.0", + "language-sass": "0.48.0", "language-shellscript": "0.22.0", "language-source": "0.9.0", "language-sql": "0.21.0", From 8d9324e234d8a73ff5681cab1129f9f796f6a529 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 29 Apr 2016 18:30:19 -0400 Subject: [PATCH 237/262] :arrow_up: language-sass@0.49.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e19a1c8f8..8aad494e4 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-python": "0.43.1", "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", - "language-sass": "0.48.0", + "language-sass": "0.49.0", "language-shellscript": "0.22.0", "language-source": "0.9.0", "language-sql": "0.21.0", From 539fc4090bbdb1269dfdd56633d8ba7b4365975b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 30 Apr 2016 01:34:49 +0200 Subject: [PATCH 238/262] :arrow_up: bookmarks --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8aad494e4..8142a3e5f 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "autoflow": "0.27.0", "autosave": "0.23.1", "background-tips": "0.26.0", - "bookmarks": "0.39.0", + "bookmarks": "0.41.0", "bracket-matcher": "0.82.0", "command-palette": "0.38.0", "deprecation-cop": "0.54.1", From 334b4c110434d88e1cc0e4bef2e9fc43fb330170 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 30 Apr 2016 11:51:54 +0200 Subject: [PATCH 239/262] Overshoot to the end of the text node when the position cannot be found ...because the only possible scenario when a logical position in a text node cannot be found is when the requested pixel position is exactly at the end of the node. --- spec/lines-yardstick-spec.coffee | 7 ++++--- src/lines-yardstick.coffee | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 72609143e..0cbb6e312 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -155,9 +155,10 @@ describe "LinesYardstick", -> expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14]) expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3]) expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9]) - expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 99.9})).toEqual([5, 14]) - expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 224.2365234375})).toEqual([5, 29]) - expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 225})).toEqual([5, 30]) + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 99.9})).toEqual([5, 14]) + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29]) + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30]) + expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33]) it "clips pixel positions above buffer start", -> expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 39e16a8e0..af12242a3 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -75,6 +75,7 @@ class LinesYardstick characterIndex = nextCharIndex break + characterIndex = textNode.textContent.length if characterIndex is -1 textNodeStartColumn = 0 textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1 Point(row, textNodeStartColumn + characterIndex) From 4f5efe98ffb049b982de1ce506b882d880439c21 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 1 May 2016 11:05:14 +0200 Subject: [PATCH 240/262] Overshoot to the nearest character when text nodes are not contiguous --- spec/lines-yardstick-spec.coffee | 28 +++++++++++++++ src/lines-yardstick.coffee | 58 +++++++++++++++----------------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 0cbb6e312..bb0294b54 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -160,6 +160,34 @@ describe "LinesYardstick", -> expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30]) expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33]) + it "overshoots to the nearest character when text nodes are not spatially contiguous", -> + atom.styles.addStyleSheet """ + * { + font-size: 12px; + font-family: monospace; + } + """ + + buildLineNode = (screenRow) -> + lineNode = document.createElement("div") + lineNode.style.whiteSpace = "pre" + lineNode.innerHTML = 'foobar' + jasmine.attachToDOM(lineNode) + createdLineNodes.push(lineNode) + lineNode + editor.setText("foobar") + + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6]) + it "clips pixel positions above buffer start", -> expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index af12242a3..cfc954cf2 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -32,7 +32,7 @@ class LinesYardstick lineOffset = lineNode.getBoundingClientRect().left targetLeft += lineOffset - textNodeIndex = -1 + textNodeIndex = 0 low = 0 high = textNodes.length - 1 while low <= high @@ -41,44 +41,42 @@ class LinesYardstick rangeRect = @clientRectForRange(textNode, 0, textNode.length) if targetLeft < rangeRect.left high = mid - 1 + textNodeIndex = Math.max(0, mid - 1) else if targetLeft > rangeRect.right low = mid + 1 + textNodeIndex = Math.min(textNodes.length - 1, mid + 1) else textNodeIndex = mid break - if textNodeIndex is -1 - textNodesExtent = 0 - textNodesExtent += textContent.length for {textContent} in textNodes - Point(row, textNodesExtent) - else - textNode = textNodes[textNodeIndex] - characterIndex = -1 - low = 0 - high = textNode.textContent.length - 1 - while low <= high - charIndex = low + (high - low >> 1) - if isPairedCharacter(textNode.textContent, charIndex) - nextCharIndex = charIndex + 2 - else - nextCharIndex = charIndex + 1 + textNode = textNodes[textNodeIndex] + characterIndex = 0 + low = 0 + high = textNode.textContent.length - 1 + while low <= high + charIndex = low + (high - low >> 1) + if isPairedCharacter(textNode.textContent, charIndex) + nextCharIndex = charIndex + 2 + else + nextCharIndex = charIndex + 1 - rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex) - if targetLeft < rangeRect.left - high = charIndex - 1 - else if targetLeft > rangeRect.right - low = nextCharIndex + rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex) + if targetLeft < rangeRect.left + high = charIndex - 1 + characterIndex = Math.max(0, charIndex - 1) + else if targetLeft > rangeRect.right + low = nextCharIndex + characterIndex = Math.min(textNode.textContent.length, nextCharIndex) + else + if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) + characterIndex = charIndex else - if targetLeft <= ((rangeRect.left + rangeRect.right) / 2) - characterIndex = charIndex - else - characterIndex = nextCharIndex - break + characterIndex = nextCharIndex + break - characterIndex = textNode.textContent.length if characterIndex is -1 - textNodeStartColumn = 0 - textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1 - Point(row, textNodeStartColumn + characterIndex) + textNodeStartColumn = 0 + textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1 + Point(row, textNodeStartColumn + characterIndex) pixelPositionForScreenPosition: (screenPosition) -> targetRow = screenPosition.row From 1c694df03b5c8a9852433c0d3ea464a79e0107c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 1 May 2016 11:42:28 +0200 Subject: [PATCH 241/262] Don't show indent guides for mini editors --- spec/text-editor-spec.coffee | 14 ++++++++++++++ src/text-editor.coffee | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 1298727a9..8affb9b5f 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5967,6 +5967,20 @@ describe "TextEditor", -> atom.config.set('editor.showInvisibles', true) expect(editor.lineTextForScreenRow(0).indexOf(atom.config.get('editor.invisibles.eol'))).toBe(-1) + describe "indent guides", -> + it "shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini", -> + editor.setText(" foo") + atom.config.set('editor.tabLength', 2) + + atom.config.set('editor.showIndentGuide', false) + expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace'] + + atom.config.set('editor.showIndentGuide', true) + expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace indent-guide'] + + editor.setMini(true) + expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace'] + describe "when the editor is constructed with the grammar option set", -> beforeEach -> atom.workspace.destroyActivePane() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a0d384946..e3902b88c 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -257,7 +257,7 @@ class TextEditor extends Model @displayLayer.reset({ invisibles: @getInvisibles(), softWrapColumn: @getSoftWrapColumn(), - showIndentGuides: @config.get('editor.showIndentGuide', scope: @getRootScopeDescriptor()), + showIndentGuides: not @isMini() and @config.get('editor.showIndentGuide', scope: @getRootScopeDescriptor()), atomicSoftTabs: @config.get('editor.atomicSoftTabs', scope: @getRootScopeDescriptor()), tabLength: @getTabLength(), ratioForCharacter: @ratioForCharacter.bind(this), @@ -589,7 +589,8 @@ class TextEditor extends Model setMini: (mini) -> if mini isnt @mini @mini = mini - @setIgnoreInvisibles(@mini) + @ignoreInvisibles = @mini + @resetDisplayLayer() @emitter.emit 'did-change-mini', @mini @mini From f4a31261d09de94502339e8efad7775a05eb4178 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 1 May 2016 11:48:39 +0200 Subject: [PATCH 242/262] Delete indent guides code from the presenter and the component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …because we're handling that behavior in `TextEditor` and `DisplayLayer` now. --- spec/text-editor-presenter-spec.coffee | 47 -------------------------- src/lines-component.coffee | 4 +-- src/lines-tile-component.coffee | 3 -- src/text-editor-presenter.coffee | 6 ---- 4 files changed, 1 insertion(+), 59 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 9318809d7..8cdc4e61a 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1143,53 +1143,6 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setScrollLeft(-300) expect(getState(presenter).content.scrollLeft).toBe 0 - describe ".indentGuidesVisible", -> - it "is initialized based on the editor.showIndentGuide config setting", -> - presenter = buildPresenter() - expect(getState(presenter).content.indentGuidesVisible).toBe false - - atom.config.set('editor.showIndentGuide', true) - presenter = buildPresenter() - expect(getState(presenter).content.indentGuidesVisible).toBe true - - it "updates when the editor.showIndentGuide config setting changes", -> - presenter = buildPresenter() - expect(getState(presenter).content.indentGuidesVisible).toBe false - - expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', true) - expect(getState(presenter).content.indentGuidesVisible).toBe true - - expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', false) - expect(getState(presenter).content.indentGuidesVisible).toBe false - - it "updates when the editor's grammar changes", -> - atom.config.set('editor.showIndentGuide', true, scopeSelector: ".source.js") - - presenter = buildPresenter() - expect(getState(presenter).content.indentGuidesVisible).toBe false - - stateUpdated = false - presenter.onDidUpdateState -> stateUpdated = true - - waitsForPromise -> atom.packages.activatePackage('language-javascript') - - runs -> - expect(stateUpdated).toBe true - expect(getState(presenter).content.indentGuidesVisible).toBe true - - expectStateUpdate presenter, -> editor.setGrammar(atom.grammars.selectGrammar('.txt')) - expect(getState(presenter).content.indentGuidesVisible).toBe false - - it "is always false when the editor is mini", -> - atom.config.set('editor.showIndentGuide', true) - editor.setMini(true) - presenter = buildPresenter() - expect(getState(presenter).content.indentGuidesVisible).toBe false - editor.setMini(false) - expect(getState(presenter).content.indentGuidesVisible).toBe true - editor.setMini(true) - expect(getState(presenter).content.indentGuidesVisible).toBe false - describe ".backgroundColor", -> it "is assigned to ::backgroundColor unless the editor is mini", -> presenter = buildPresenter() diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 9a7ce7f44..88645589a 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -43,7 +43,7 @@ class LinesComponent extends TiledComponent @domNode shouldRecreateAllTilesOnUpdate: -> - @oldState.indentGuidesVisible isnt @newState.indentGuidesVisible or @newState.continuousReflow + @newState.continuousReflow beforeUpdateSync: (state) -> if @newState.maxHeight isnt @oldState.maxHeight @@ -70,8 +70,6 @@ class LinesComponent extends TiledComponent @cursorsComponent.updateSync(state) - @oldState.indentGuidesVisible = @newState.indentGuidesVisible - buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars}) buildEmptyState: -> diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 3d82d9d9f..6844f21de 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -68,13 +68,10 @@ class LinesTileComponent @oldTileState.top = @newTileState.top @oldTileState.left = @newTileState.left - @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible @updateLineNodes() @highlightsComponent.updateSync(@newTileState) - @oldState.indentGuidesVisible = @newState.indentGuidesVisible - removeLineNodes: -> @removeLineNode(id) for id of @oldTileState.lines return diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 85c85c655..01a0293f6 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -175,7 +175,6 @@ class TextEditorPresenter @scrollPastEnd = @config.get('editor.scrollPastEnd', configParams) @showLineNumbers = @config.get('editor.showLineNumbers', configParams) - @showIndentGuide = @config.get('editor.showIndentGuide', configParams) if @configDisposables? @configDisposables?.dispose() @@ -184,10 +183,6 @@ class TextEditorPresenter @configDisposables = new CompositeDisposable @disposables.add(@configDisposables) - @configDisposables.add @config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) => - @showIndentGuide = newValue - - @emitDidUpdateState() @configDisposables.add @config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) => @scrollPastEnd = newValue @updateScrollHeight() @@ -295,7 +290,6 @@ class TextEditorPresenter @state.content.width = Math.max(@contentWidth + @verticalScrollbarWidth, @contentFrameWidth) @state.content.scrollWidth = @scrollWidth @state.content.scrollLeft = @scrollLeft - @state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null From 57bf8f797bf15cf25189acb709c4a78d9b2597fc Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Sun, 1 May 2016 10:32:21 -0700 Subject: [PATCH 243/262] :arrow_up: encoding-selector@0.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 535294933..af906359f 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "command-palette": "0.38.0", "deprecation-cop": "0.54.1", "dev-live-reload": "0.47.0", - "encoding-selector": "0.21.0", + "encoding-selector": "0.22.0", "exception-reporting": "0.38.1", "fuzzy-finder": "1.0.5", "git-diff": "1.0.1", From 73748ef768f8691c0f22a9d7a846370de4ec4dfb Mon Sep 17 00:00:00 2001 From: Willem Van Lint Date: Sun, 17 Apr 2016 17:06:22 -0700 Subject: [PATCH 244/262] Order listeners by reverse registration order --- spec/command-registry-spec.coffee | 7 +++++++ src/command-registry.coffee | 11 +++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index ecdd42fd6..aaf044b1d 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -74,6 +74,13 @@ describe "CommandRegistry", -> grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) expect(calls).toEqual ['.foo.bar', '.bar', '.foo'] + it "orders inline listeners by reverse registration order", -> + calls = [] + registry.add child, 'command', -> calls.push('child1') + registry.add child, 'command', -> calls.push('child2') + child.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['child2', 'child1'] + it "stops bubbling through ancestors when .stopPropagation() is called on the event", -> calls = [] diff --git a/src/command-registry.coffee b/src/command-registry.coffee index db2cf498d..955a1b540 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -244,11 +244,14 @@ class CommandRegistry (@selectorBasedListenersByCommandName[event.type] ? []) .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) .sort (a, b) -> a.compare(b) - listeners = listeners.concat(selectorBasedListeners) + listeners = selectorBasedListeners.concat(listeners) matched = true if listeners.length > 0 - for listener in listeners + # Call inline listeners first in reverse registration order, + # and selector-based listeners by specificity and reverse + # registration order. + for listener in listeners by -1 break if immediatePropagationStopped listener.callback.call(currentTarget, dispatchedEvent) @@ -271,8 +274,8 @@ class SelectorBasedListener @sequenceNumber = SequenceCount++ compare: (other) -> - other.specificity - @specificity or - other.sequenceNumber - @sequenceNumber + @specificity - other.specificity or + @sequenceNumber - other.sequenceNumber class InlineListener constructor: (@callback) -> From 671334993f5984646f47755e783272440c6bb6ab Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Sun, 1 May 2016 22:16:36 -0700 Subject: [PATCH 245/262] :arrow_up: apm@1.10.0 --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index 4ddb4dabe..d4fcc851a 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.9.3" + "atom-package-manager": "1.10.0" } } From 874be7d242ce6e88d32f63d5e7455ac9c20c9331 Mon Sep 17 00:00:00 2001 From: Timothy Cyrus Date: Mon, 2 May 2016 10:47:49 -0400 Subject: [PATCH 246/262] Update package.json Update `marked` to fix sanitization issue on david-dm --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af906359f..04ee2034c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.23", "line-top-index": "0.2.0", - "marked": "^0.3.4", + "marked": "^0.3.5", "normalize-package-data": "^2.0.0", "nslog": "^3", "ohnogit": "0.0.11", From 9adc822822d42877151bda2fe1f3051205d76b98 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 2 May 2016 11:32:29 -0400 Subject: [PATCH 247/262] Mark text editors as being registered. --- src/text-editor-registry.coffee | 14 +++++++++++++- src/text-editor.coffee | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/text-editor-registry.coffee b/src/text-editor-registry.coffee index 8a17335d4..e31630fee 100644 --- a/src/text-editor-registry.coffee +++ b/src/text-editor-registry.coffee @@ -26,8 +26,20 @@ class TextEditorRegistry # editor is destroyed. add: (editor) -> @editors.add(editor) + editor.registered = true + @emitter.emit 'did-add-editor', editor - new Disposable => @editors.delete(editor) + new Disposable => @remove(editor) + + # Remove a `TextEditor`. + # + # * `editor` The editor to remove. + # + # Returns a {Boolean} indicating whether the editor was successfully removed. + remove: (editor) -> + removed = @editors.delete(editor) + editor.registered = false + removed # Invoke the given callback with all the current and future registered # `TextEditors`. diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 9976e5906..bdc283d8f 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -62,6 +62,7 @@ class TextEditor extends Model selectionFlashDuration: 500 gutterContainer: null editorElement: null + registered: false Object.defineProperty @prototype, "element", get: -> @getElement() @@ -157,7 +158,7 @@ class TextEditor extends Model firstVisibleScreenColumn: @getFirstVisibleScreenColumn() displayBuffer: @displayBuffer.serialize() selectionsMarkerLayerId: @selectionsMarkerLayer.id - registered: atom.textEditors.editors.has this + registered: @registered subscribeToBuffer: -> @buffer.retain() From d325e02def5e7819030e09eab09fcb62e5d701b9 Mon Sep 17 00:00:00 2001 From: joshaber Date: Mon, 2 May 2016 11:32:38 -0400 Subject: [PATCH 248/262] Test it. --- spec/text-editor-registry-spec.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/text-editor-registry-spec.coffee b/spec/text-editor-registry-spec.coffee index 04665bef2..80f29f897 100644 --- a/spec/text-editor-registry-spec.coffee +++ b/spec/text-editor-registry-spec.coffee @@ -10,6 +10,7 @@ describe "TextEditorRegistry", -> it "gets added to the list of registered editors", -> editor = {} registry.add(editor) + expect(editor.registered).toBe true expect(registry.editors.size).toBe 1 expect(registry.editors.has(editor)).toBe(true) @@ -19,6 +20,16 @@ describe "TextEditorRegistry", -> expect(registry.editors.size).toBe 1 disposable.dispose() expect(registry.editors.size).toBe 0 + expect(editor.registered).toBe false + + it "can be removed", -> + editor = {} + registry.add(editor) + expect(registry.editors.size).toBe 1 + success = registry.remove(editor) + expect(success).toBe true + expect(registry.editors.size).toBe 0 + expect(editor.registered).toBe false describe "when the registry is observed", -> it "calls the callback for current and future editors until unsubscribed", -> From dd24e3b22304b495625140f74be9d221238074ab Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Mon, 2 May 2016 13:19:19 -0700 Subject: [PATCH 249/262] :arrow_up: tabs@0.93.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af906359f..8b4d137ff 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "status-bar": "1.2.6", "styleguide": "0.45.2", "symbols-view": "0.112.0", - "tabs": "0.93.1", + "tabs": "0.93.2", "timecop": "0.33.1", "tree-view": "0.206.2", "update-package-dependencies": "0.10.0", From 7542a401fff99d6ca93e7b1254bd8fc4d3672559 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 2 May 2016 15:37:36 -0700 Subject: [PATCH 250/262] BufferedProcess whole-string spec now cross-platform --- spec/buffered-process-spec.coffee | 29 +++++++++++++---------------- spec/fixtures/lorem.txt | 3 +++ spec/fixtures/shebang | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 spec/fixtures/lorem.txt diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index ec01b40d7..40b96e3e1 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -1,5 +1,7 @@ ChildProcess = require 'child_process' path = require 'path' +fs = require 'fs-plus' +platform = require './spec-helper-platform' BufferedProcess = require '../src/buffered-process' describe "BufferedProcess", -> @@ -110,30 +112,25 @@ describe "BufferedProcess", -> expect(stderr).toContain 'apm - Atom Package Manager' expect(stdout).toEqual '' - it "calls the specified stdout callback only with whole lines", -> - # WINSPEC: Fails because command is too long, /bin/echo is *nix only, odd newlining and quoting + it "calls the specified stdout callback with whole lines", -> exitCallback = jasmine.createSpy('exit callback') - baseContent = "There are dozens of us! Dozens! It's as Ann as the nose on Plain's face. Can you believe that the only reason the club is going under is because it's in a terrifying neighborhood? She calls it a Mayonegg. Waiting for the Emmys. BTW did you know won 6 Emmys and was still canceled early by Fox? COME ON. I'll buy you a hundred George Michaels that you can teach to drive! Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'" - content = (baseContent for _ in [1..200]).join('\n') + loremPath = require.resolve("./fixtures/lorem.txt") + content = fs.readFileSync(loremPath).toString() + baseContent = content.split('\n') stdout = '' - endLength = 10 - outputAlwaysEndsWithStew = true + allLinesEndWithNewline = true process = new BufferedProcess - command: '/bin/echo' - args: [content] + command: if platform.isWindows() then 'type' else 'cat' + args: [loremPath] options: {} stdout: (lines) -> + endsWithNewline = (lines.charAt lines.length - 1) is '\n' + if not endsWithNewline then allLinesEndWithNewline = false stdout += lines - - end = baseContent.substr(baseContent.length - endLength, endLength) - lineEndsWithStew = lines.substr(lines.length - endLength, endLength) is end - expect(lineEndsWithStew).toBeTrue - - outputAlwaysEndsWithStew = outputAlwaysEndsWithStew and lineEndsWithStew exit: exitCallback waitsFor -> exitCallback.callCount is 1 runs -> - expect(outputAlwaysEndsWithStew).toBeTrue - expect(stdout).toBe content += '\n' + expect(allLinesEndWithNewline).toBeTrue + expect(stdout).toBe content diff --git a/spec/fixtures/lorem.txt b/spec/fixtures/lorem.txt new file mode 100644 index 000000000..be8db8ab8 --- /dev/null +++ b/spec/fixtures/lorem.txt @@ -0,0 +1,3 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ultricies nulla id nibh aliquam, vitae euismod ipsum scelerisque. Vestibulum vulputate facilisis nisi, eu rhoncus turpis pretium ut. Curabitur facilisis urna in diam efficitur, vel maximus tellus consectetur. Suspendisse pulvinar felis sed metus tristique, a posuere dui suscipit. Ut vehicula, tellus ac blandit consequat, libero dui hendrerit elit, non pretium metus odio sed dolor. Vivamus quis volutpat ipsum. In convallis magna nec nunc tristique malesuada. Sed sed hendrerit lacus. Etiam arcu dui, consequat vel neque vitae, iaculis egestas justo. Donec lacinia odio nulla, condimentum porta erat accumsan at. Nunc vulputate nulla vel nunc fermentum egestas. +Duis ultricies libero elit, nec facilisis mi rhoncus ornare. Aliquam aliquet libero vitae arcu porttitor mattis. Vestibulum ultricies consectetur arcu, non gravida magna eleifend vel. Phasellus varius mattis ultricies. Vestibulum placerat lacus non consectetur fringilla. Duis congue, arcu iaculis vehicula hendrerit, purus odio faucibus ipsum, et fermentum massa tellus euismod nulla. Vivamus pellentesque blandit massa, sit amet hendrerit turpis congue eu. Suspendisse diam dui, vestibulum nec semper varius, maximus eu nunc. Vivamus facilisis pulvinar viverra. Praesent luctus lectus id est porttitor volutpat. Suspendisse est augue, mattis a tincidunt id, condimentum in turpis. Curabitur at erat commodo orci interdum tincidunt. Sed sodales elit odio, a placerat ipsum luctus nec. Sed maximus, justo ut pharetra pellentesque, orci mi faucibus enim, quis viverra arcu dui sed nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent quis velit libero. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus a rutrum tortor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce bibendum odio et neque vestibulum rutrum. Vestibulum commodo, nibh non sodales lobortis, dui ex consectetur leo, a finibus libero lectus ac diam. Etiam dui nunc, bibendum a tempor vel, vestibulum lacinia neque. Mauris consectetur odio sit amet maximus pretium. Sed rutrum nunc at ante ullamcorper fermentum. Proin at quam a mauris pellentesque viverra. Nunc pretium pulvinar ipsum. Vestibulum eu nibh ut ex gravida tempus. Praesent ut elit ut ligula tristique dapibus ut sit amet leo. Proin non molestie erat. diff --git a/spec/fixtures/shebang b/spec/fixtures/shebang index f15429b13..f343f6833 100644 --- a/spec/fixtures/shebang +++ b/spec/fixtures/shebang @@ -1,3 +1,3 @@ #!/usr/bin/ruby -puts "America – fuck yeah!" \ No newline at end of file +puts "Atom fixture test" From 36bcb542a81bb75d3ea1653976115c57c618384f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 3 May 2016 11:50:02 +0200 Subject: [PATCH 251/262] Don't move down a line if it's the last buffer row --- spec/text-editor-spec.coffee | 7 +++++++ src/text-editor.coffee | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 8affb9b5f..c8156cdd0 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -2875,6 +2875,13 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(1)).toBe "1" expect(editor.lineTextForBufferRow(2)).toBe "2" + describe "when the line is the last buffer row", -> + it "doesn't move it", -> + editor.setText("abc\ndef") + editor.setCursorBufferPosition([1, 0]) + editor.moveLineDown() + expect(editor.getText()).toBe("abc\ndef") + describe ".insertText(text)", -> describe "when there is a single selection", -> beforeEach -> diff --git a/src/text-editor.coffee b/src/text-editor.coffee index e3902b88c..b778491d1 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1068,7 +1068,7 @@ class TextEditor extends Model # Delete lines spanned by selection and insert them on the following correct buffer row lines = @buffer.getTextInRange(linesRange) - if linesRange.end.row is @buffer.getLastRow() + if followingRow - 1 is @buffer.getLastRow() lines = "\n#{lines}" @buffer.insert([followingRow, 0], lines) From 6b4c082bbf3162735cd7f904be8ba057171f92d8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 3 May 2016 13:06:16 +0200 Subject: [PATCH 252/262] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3fb2c2b5..6d390354d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta8", + "text-buffer": "9.0.0-beta9", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 004bb6122cd28fd89f1f867160d44404664c0646 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 3 May 2016 13:24:28 +0200 Subject: [PATCH 253/262] :art: :racehorse: --- src/tokenized-buffer-iterator.coffee | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee index ad1834f09..780156e42 100644 --- a/src/tokenized-buffer-iterator.coffee +++ b/src/tokenized-buffer-iterator.coffee @@ -1,5 +1,4 @@ {Point} = require 'text-buffer' -{isEqual} = require 'underscore-plus' module.exports = class TokenizedBufferIterator @@ -58,7 +57,7 @@ class TokenizedBufferIterator @moveToNextLine() @openTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id) @shouldMoveToNextLine = false - else if @hasNextLine() and not isEqual(@containingTags, @nextLineOpeningScopes()) + else if @nextLineHasMismatchedContainingTags() @closeTags = @containingTags.slice().reverse() @containingTags = [] @shouldMoveToNextLine = true @@ -97,12 +96,16 @@ class TokenizedBufferIterator Section: Private Methods ### - hasNextLine: -> - @tokenizedBuffer.tokenizedLineForRow(@position.row + 1)? + nextLineHasMismatchedContainingTags: -> + if line = @tokenizedBuffer.tokenizedLineForRow(@position.row + 1) + return true if line.openScopes.length isnt @containingTags.length - nextLineOpeningScopes: -> - line = @tokenizedBuffer.tokenizedLineForRow(@position.row + 1) - line.openScopes.map (id) => @grammarRegistry.scopeForId(id) + for i in [0...@containingTags.length] by 1 + if @containingTags[i] isnt @grammarRegistry.scopeForId(line.openScopes[i]) + return true + false + else + false moveToNextLine: -> @position = Point(@position.row + 1, 0) From d6d47c422c07b71421471720f7d96b52890feeb0 Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Tue, 3 May 2016 09:20:47 -0700 Subject: [PATCH 254/262] :arrow_up: settings-view@0.236.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d007eefa8..5a752cd6d 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "notifications": "0.63.2", "open-on-github": "1.1.0", "package-generator": "1.0.0", - "settings-view": "0.235.1", + "settings-view": "0.236.0", "snippets": "1.0.2", "spell-check": "0.67.1", "status-bar": "1.2.6", From 48703864c87c231eb1eb3675254f7a44a9c57d9c Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 3 May 2016 15:22:13 -0700 Subject: [PATCH 255/262] Switch to process.platform, stop clobbering process var and move windows block --- spec/buffered-process-spec.coffee | 69 +++++++++++++++---------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index 40b96e3e1..84d8b0440 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -1,7 +1,6 @@ ChildProcess = require 'child_process' path = require 'path' fs = require 'fs-plus' -platform = require './spec-helper-platform' BufferedProcess = require '../src/buffered-process' describe "BufferedProcess", -> @@ -17,13 +16,13 @@ describe "BufferedProcess", -> describe "when there is an error handler specified", -> describe "when an error event is emitted by the process", -> it "calls the error handler and does not throw an exception", -> - process = new BufferedProcess + bufferedProcess = new BufferedProcess command: 'bad-command-nope1' args: ['nothing'] options: {shell: false} errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() - process.onWillThrowError(errorSpy) + bufferedProcess.onWillThrowError(errorSpy) waitsFor -> errorSpy.callCount > 0 @@ -39,13 +38,13 @@ describe "BufferedProcess", -> error.code = 'EAGAIN' throw error - process = new BufferedProcess + bufferedProcess = new BufferedProcess command: 'ls' args: [] options: {} errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() - process.onWillThrowError(errorSpy) + bufferedProcess.onWillThrowError(errorSpy) waitsFor -> errorSpy.callCount > 0 @@ -56,7 +55,7 @@ describe "BufferedProcess", -> describe "when there is not an error handler specified", -> it "does throw an exception", -> - process = new BufferedProcess + new BufferedProcess command: 'bad-command-nope2' args: ['nothing'] options: {shell: false} @@ -68,37 +67,11 @@ describe "BufferedProcess", -> expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope2`' expect(window.onerror.mostRecentCall.args[4].name).toBe 'BufferedProcessError' - describe "on Windows", -> - originalPlatform = null - - beforeEach -> - # Prevent any commands from actually running and affecting the host - originalSpawn = ChildProcess.spawn - spyOn(ChildProcess, 'spawn') - originalPlatform = process.platform - Object.defineProperty process, 'platform', value: 'win32' - - afterEach -> - Object.defineProperty process, 'platform', value: originalPlatform - - describe "when the explorer command is spawned on Windows", -> - it "doesn't quote arguments of the form /root,C...", -> - new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']}) - expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"' - - it "spawns the command using a cmd.exe wrapper", -> - new BufferedProcess({command: 'dir'}) - expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe' - expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s' - expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d' - expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c' - expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"' - it "calls the specified stdout, stderr, and exit callbacks", -> stdout = '' stderr = '' exitCallback = jasmine.createSpy('exit callback') - process = new BufferedProcess + new BufferedProcess command: atom.packages.getApmPath() args: ['-h'] options: {} @@ -119,8 +92,8 @@ describe "BufferedProcess", -> baseContent = content.split('\n') stdout = '' allLinesEndWithNewline = true - process = new BufferedProcess - command: if platform.isWindows() then 'type' else 'cat' + new BufferedProcess + command: if process.platform is 'win32' then 'type' else 'cat' args: [loremPath] options: {} stdout: (lines) -> @@ -134,3 +107,29 @@ describe "BufferedProcess", -> runs -> expect(allLinesEndWithNewline).toBeTrue expect(stdout).toBe content + + describe "on Windows", -> + originalPlatform = null + + beforeEach -> + # Prevent any commands from actually running and affecting the host + originalSpawn = ChildProcess.spawn + spyOn(ChildProcess, 'spawn') + originalPlatform = process.platform + Object.defineProperty process, 'platform', value: 'win32' + + afterEach -> + Object.defineProperty process, 'platform', value: originalPlatform + + describe "when the explorer command is spawned on Windows", -> + it "doesn't quote arguments of the form /root,C...", -> + new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']}) + expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"' + + it "spawns the command using a cmd.exe wrapper when options.shell is undefined", -> + new BufferedProcess({command: 'dir'}) + expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe' + expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s' + expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d' + expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c' + expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"' From 0e194f1021935deb7acc7237bda43a63382ae5a1 Mon Sep 17 00:00:00 2001 From: Lee Dohm Date: Tue, 3 May 2016 16:04:04 -0700 Subject: [PATCH 256/262] :arrow_up: line-ending-selector@0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a752cd6d..e76dce85e 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "image-view": "0.57.0", "incompatible-packages": "0.26.1", "keybinding-resolver": "0.35.0", - "line-ending-selector": "0.4.1", + "line-ending-selector": "0.5.0", "link": "0.31.1", "markdown-preview": "0.158.0", "metrics": "0.53.1", From c56cbe3ce364ac0e2d6ad4c4818bdbca935889db Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 3 May 2016 16:50:06 -0700 Subject: [PATCH 257/262] :shirt: Change fat arrow to thin arrow for linter warning --- src/workspace.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index f75f00bc6..5e9de93dd 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -1092,7 +1092,7 @@ class Workspace extends Model if editor.getPath() checkoutHead = => @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) - .then (repository) => + .then (repository) -> repository?.async.checkoutHeadForEditor(editor) if @config.get('editor.confirmCheckoutHeadRevision') From d29e96e358970ccea1aa0bd5305ba61b04559afb Mon Sep 17 00:00:00 2001 From: Wliu Date: Tue, 3 May 2016 20:47:02 -0400 Subject: [PATCH 258/262] :arrow_up: language-sass@0.50.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e76dce85e..29d08589b 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-python": "0.43.1", "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", - "language-sass": "0.49.0", + "language-sass": "0.50.0", "language-shellscript": "0.22.0", "language-source": "0.9.0", "language-sql": "0.21.0", From b9dbb81bd5404294bb33b2b30ffe1b464a2d2eac Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 4 May 2016 12:05:04 +0900 Subject: [PATCH 259/262] Revert ":arrow_up: language-sass@0.50.0" This reverts commit d29e96e358970ccea1aa0bd5305ba61b04559afb. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29d08589b..e76dce85e 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-python": "0.43.1", "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", - "language-sass": "0.50.0", + "language-sass": "0.49.0", "language-shellscript": "0.22.0", "language-source": "0.9.0", "language-sql": "0.21.0", From 6c6f8fc34154f2900e003c5956465119c66618c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 4 May 2016 10:15:02 +0200 Subject: [PATCH 260/262] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e76dce85e..7f7a5dfe8 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.1", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.30.0", + "autocomplete-plus": "2.31.0", "autocomplete-snippets": "1.10.0", "autoflow": "0.27.0", "autosave": "0.23.1", From 512f1fb540700ed455e4aca3821bf6e0ee6ed6f9 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 4 May 2016 11:56:17 +0200 Subject: [PATCH 261/262] :arrow_up: symbols-view@0.113.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f7a5dfe8..f5a432a2d 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "spell-check": "0.67.1", "status-bar": "1.2.6", "styleguide": "0.45.2", - "symbols-view": "0.112.0", + "symbols-view": "0.113.0", "tabs": "0.93.2", "timecop": "0.33.1", "tree-view": "0.206.2", From 89d6b1fccec244ca1223f8aa091611c1e99f33b4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 4 May 2016 18:57:07 +0200 Subject: [PATCH 262/262] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 814ba0033..4a8591ae5 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.0.0-beta9", + "text-buffer": "9.0.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0"