mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
Merge pull request #11414 from atom/ns-switch-to-display-layers
Use display layers facility of text-buffer; delete all the code they replace
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"service-hub": "^0.7.0",
|
||||
"source-map-support": "^0.3.2",
|
||||
"temp": "0.8.1",
|
||||
"text-buffer": "8.5.0",
|
||||
"text-buffer": "9.0.0",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"yargs": "^3.23.0"
|
||||
|
||||
85
spec/decoration-manager-spec.coffee
Normal file
85
spec/decoration-manager-spec.coffee
Normal file
@@ -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'
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) ->
|
||||
@@ -24,31 +26,38 @@ class FakeLinesYardstick
|
||||
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
baseCharacterWidth = @model.getDefaultCharWidth()
|
||||
|
||||
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
|
||||
left = 0
|
||||
column = 0
|
||||
|
||||
iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator()
|
||||
while iterator.next()
|
||||
characterWidths = @getScopedCharacterWidths(iterator.getScopes())
|
||||
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
|
||||
text = iterator.getText()
|
||||
while valueIndex < text.length
|
||||
if iterator.isPairedCharacter()
|
||||
char = text
|
||||
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}
|
||||
|
||||
@@ -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", ->
|
||||
|
||||
@@ -19,36 +19,45 @@ 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) ->
|
||||
buildLineNode(screenRow)
|
||||
lineNodesById: {}
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
editor.screenLineForScreenRow(screenRow).id
|
||||
|
||||
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
|
||||
lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow)
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
@lineNodesById[@lineIdForScreenRow(screenRow)] ?= buildLineNode(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 +78,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 +91,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 +101,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 +111,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.
|
||||
@@ -163,9 +155,38 @@ 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 "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 = '<span>foo</span><span style="margin-left: 40px">bar</span>'
|
||||
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]
|
||||
@@ -178,3 +199,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]
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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, ->
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 === '') {
|
||||
expect(lineNode.innerHTML).toBe(' ')
|
||||
if (text === '') {
|
||||
expect(lineNode.textContent).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 () {
|
||||
@@ -361,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 () {
|
||||
@@ -429,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 () {
|
||||
@@ -484,7 +484,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('<span class="source js"><span class="storage type var js">let</span></span><span class="invisible-character">' + invisibles.eol + '</span>')
|
||||
expect(component.lineNodeForScreenRow(0).innerHTML).toBe('<span class="source js"><span class="storage type var js">let</span><span class="invisible-character eol">' + invisibles.eol + '</span></span>')
|
||||
})
|
||||
|
||||
it('displays trailing carriage returns using a visible, non-empty value', async function () {
|
||||
@@ -497,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 () {
|
||||
@@ -518,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('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
|
||||
|
||||
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span><span class="invisible-character">E</span></span>')
|
||||
editor.setTabLength(3)
|
||||
await nextViewUpdatePromise()
|
||||
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
|
||||
|
||||
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span><span class="invisible-character">E</span> </span>')
|
||||
editor.setTabLength(1)
|
||||
await nextViewUpdatePromise()
|
||||
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
|
||||
|
||||
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span></span><span class="indent-guide"><span class="invisible-character">E</span></span>')
|
||||
editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ')
|
||||
editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ')
|
||||
await nextViewUpdatePromise()
|
||||
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span></span><span class="invisible-character">E</span>')
|
||||
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
|
||||
})
|
||||
|
||||
describe('when soft wrapping is enabled', function () {
|
||||
@@ -550,8 +551,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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -986,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 () {
|
||||
@@ -1051,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()
|
||||
})
|
||||
@@ -1060,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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1082,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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1106,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))
|
||||
@@ -1200,7 +1241,7 @@ 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()
|
||||
let range = document.createRange(cursorLocationTextNode)
|
||||
range.setStart(cursorLocationTextNode, 0)
|
||||
range.setEnd(cursorLocationTextNode, 1)
|
||||
let rangeRect = range.getBoundingClientRect()
|
||||
@@ -1208,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])
|
||||
@@ -1475,7 +1527,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'
|
||||
@@ -1887,7 +1939,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, {
|
||||
@@ -2082,7 +2134,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, {
|
||||
@@ -2104,7 +2156,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, {
|
||||
@@ -2125,7 +2177,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, {
|
||||
@@ -2171,7 +2223,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, {
|
||||
@@ -2753,20 +2805,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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3101,7 +3193,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]])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3175,7 +3267,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 () {
|
||||
@@ -3189,7 +3281,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]]])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3204,7 +3296,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]])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4966,7 +5058,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -1229,9 +1182,19 @@ 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]
|
||||
|
||||
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
|
||||
|
||||
@@ -1241,73 +1204,12 @@ 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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", ->
|
||||
@@ -1315,34 +1217,20 @@ 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
|
||||
}
|
||||
|
||||
line2 = editor.tokenizedLineForScreenRow(2)
|
||||
expectValues lineStateForScreenRow(presenter, 2), {
|
||||
text: line2.text
|
||||
tags: line2.tags
|
||||
}
|
||||
|
||||
line3 = editor.tokenizedLineForScreenRow(3)
|
||||
expectValues lineStateForScreenRow(presenter, 3), {
|
||||
text: line3.text
|
||||
tags: line3.tags
|
||||
}
|
||||
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(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toBeNull()
|
||||
expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toBeNull()
|
||||
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(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(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", ->
|
||||
@@ -2905,12 +2793,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 +2804,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()
|
||||
@@ -3184,6 +3069,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()
|
||||
|
||||
@@ -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", ->
|
||||
@@ -314,7 +311,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])
|
||||
@@ -495,7 +492,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)", ->
|
||||
@@ -683,7 +680,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", ->
|
||||
@@ -1798,22 +1795,22 @@ 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()
|
||||
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", ->
|
||||
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()
|
||||
@@ -2225,7 +2222,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]]
|
||||
|
||||
@@ -2253,7 +2250,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()
|
||||
@@ -2291,7 +2288,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()
|
||||
@@ -2319,7 +2316,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()
|
||||
@@ -2363,7 +2360,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()
|
||||
@@ -2403,7 +2400,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]]
|
||||
@@ -2441,7 +2438,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()
|
||||
@@ -2468,8 +2465,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]],
|
||||
@@ -2553,7 +2550,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()
|
||||
@@ -2579,7 +2576,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()
|
||||
@@ -2633,7 +2630,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()
|
||||
@@ -2661,7 +2658,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()
|
||||
@@ -2691,7 +2688,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()
|
||||
@@ -2733,8 +2730,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]],
|
||||
@@ -2763,7 +2760,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()
|
||||
@@ -2786,7 +2783,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()
|
||||
@@ -2811,7 +2808,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]]
|
||||
@@ -2878,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 ->
|
||||
@@ -2949,10 +2953,10 @@ 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()
|
||||
expect(editor.isFoldedAtScreenRow(2)).toBeFalsy()
|
||||
|
||||
describe "when there are ::onWillInsertText and ::onDidInsertText observers", ->
|
||||
beforeEach ->
|
||||
@@ -3246,15 +3250,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", ->
|
||||
@@ -3267,14 +3270,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", ->
|
||||
@@ -3341,7 +3343,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", ->
|
||||
@@ -3514,16 +3516,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", ->
|
||||
@@ -3535,20 +3537,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.createFold(2, 4)
|
||||
editor.createFold(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", ->
|
||||
@@ -3805,10 +3808,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", ->
|
||||
@@ -4693,7 +4696,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", ->
|
||||
@@ -5050,11 +5054,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)
|
||||
@@ -5063,21 +5069,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", ->
|
||||
@@ -5113,11 +5123,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)", ->
|
||||
@@ -5261,7 +5271,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()
|
||||
@@ -5345,10 +5355,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) {" + 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", ->
|
||||
editor.foldBufferRow(4)
|
||||
@@ -5544,17 +5554,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", ->
|
||||
@@ -5564,25 +5572,22 @@ 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.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.tokenizedLineForScreenRow(0)
|
||||
expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
|
||||
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-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.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", ->
|
||||
@@ -5710,6 +5715,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(
|
||||
@@ -5855,6 +5873,7 @@ describe "TextEditor", ->
|
||||
expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual {
|
||||
properties: {type: 'highlight', class: 'foo'}
|
||||
screenRange: marker.getScreenRange(),
|
||||
bufferRange: marker.getBufferRange(),
|
||||
rangeIsReversed: false
|
||||
}
|
||||
|
||||
@@ -5875,26 +5894,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
|
||||
}
|
||||
|
||||
@@ -5906,16 +5930,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
|
||||
}
|
||||
|
||||
@@ -5924,6 +5951,7 @@ describe "TextEditor", ->
|
||||
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
|
||||
properties: {type: 'highlight', class: 'quux'},
|
||||
screenRange: marker1.getRange(),
|
||||
bufferRange: marker1.getRange(),
|
||||
rangeIsReversed: false
|
||||
}
|
||||
|
||||
@@ -5932,6 +5960,7 @@ describe "TextEditor", ->
|
||||
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
|
||||
properties: {type: 'highlight', class: 'bar'},
|
||||
screenRange: marker1.getRange(),
|
||||
bufferRange: marker1.getRange(),
|
||||
rangeIsReversed: false
|
||||
}
|
||||
|
||||
@@ -5943,8 +5972,21 @@ 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 "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 ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
103
spec/tokenized-buffer-iterator-spec.js
Normal file
103
spec/tokenized-buffer-iterator-spec.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/** @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([])
|
||||
})
|
||||
|
||||
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([])
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
TokenizedBuffer = require '../src/tokenized-buffer'
|
||||
TextBuffer = require 'text-buffer'
|
||||
{Point} = TextBuffer = require 'text-buffer'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
describe "TokenizedBuffer", ->
|
||||
@@ -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,132 +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 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(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
|
||||
@@ -574,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)
|
||||
@@ -587,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
|
||||
@@ -605,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()
|
||||
@@ -682,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({
|
||||
@@ -817,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", ->
|
||||
@@ -861,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
|
||||
@@ -890,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
|
||||
@@ -1049,3 +769,107 @@ 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))
|
||||
|
||||
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.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"]}
|
||||
{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.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))
|
||||
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.decimal.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)
|
||||
|
||||
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)
|
||||
|
||||
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']
|
||||
|
||||
@@ -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
|
||||
@@ -429,7 +429,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", ->
|
||||
@@ -454,7 +454,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", ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -261,11 +261,11 @@ 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
|
||||
@setScreenPosition({row, column}, clip: 'backward')
|
||||
@setScreenPosition({row, column}, clipDirection: 'backward')
|
||||
|
||||
# Public: Moves the cursor right one screen column.
|
||||
#
|
||||
@@ -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,11 +288,11 @@ 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
|
||||
@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: ->
|
||||
|
||||
181
src/decoration-manager.coffee
Normal file
181
src/decoration-manager.coffee
Normal file
@@ -0,0 +1,181 @@
|
||||
{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()
|
||||
bufferRange = marker.getBufferRange()
|
||||
rangeIsReversed = marker.isReversed()
|
||||
|
||||
if decorations = @decorationsByMarkerId[marker.id]
|
||||
for decoration in decorations
|
||||
decorationsState[decoration.id] = {
|
||||
properties: decoration.properties
|
||||
screenRange, bufferRange, rangeIsReversed
|
||||
}
|
||||
|
||||
if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
|
||||
for layerDecoration in layerDecorations
|
||||
decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
|
||||
properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
|
||||
screenRange, bufferRange, 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]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -71,14 +71,14 @@ 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
|
||||
@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'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
@@ -90,30 +90,36 @@ 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?
|
||||
@editor.createFold(startRow, endRow)
|
||||
continue if foldedRowRanges[rowRange]
|
||||
|
||||
@editor.foldBufferRowRange(startRow, endRow)
|
||||
foldedRowRanges[rowRange] = true
|
||||
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
|
||||
#
|
||||
# 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.createFold(startRow, endRow)
|
||||
@editor.foldBufferRowRange(startRow, endRow)
|
||||
foldedRowRanges[rowRange] = true
|
||||
return
|
||||
|
||||
# Given a buffer row, creates a fold at it.
|
||||
@@ -125,8 +131,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.isFoldedAtBufferRow(startRow)
|
||||
return @editor.foldBufferRowRange(startRow, endRow)
|
||||
|
||||
# Find the row range for a fold at a given bufferRow. Will handle comments
|
||||
# and code.
|
||||
@@ -140,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
|
||||
@@ -175,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
|
||||
@@ -234,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) ->
|
||||
|
||||
@@ -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,11 +44,11 @@ class LayerDecoration
|
||||
setProperties: (newProperties) ->
|
||||
return if @destroyed
|
||||
@properties = newProperties
|
||||
@displayBuffer.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
|
||||
# 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.
|
||||
@@ -58,4 +58,4 @@ class LayerDecoration
|
||||
@overridePropertiesByMarkerId[marker.id] = properties
|
||||
else
|
||||
delete @overridePropertiesByMarkerId[marker.id]
|
||||
@displayBuffer.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: ->
|
||||
@@ -97,10 +95,14 @@ class LinesComponent extends TiledComponent
|
||||
@presenter.setLineHeight(lineHeightInPixels)
|
||||
@presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
|
||||
|
||||
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.lineNodeForLineId(lineId)
|
||||
@getComponentForTile(tile)?.lineIdForScreenRow(screenRow)
|
||||
|
||||
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.textNodesForLineId(lineId)
|
||||
@getComponentForTile(tile)?.lineNodeForScreenRow(screenRow)
|
||||
|
||||
textNodesForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.textNodesForScreenRow(screenRow)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
HighlightsComponent = require './highlights-component'
|
||||
TokenIterator = require './token-iterator'
|
||||
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
|
||||
TokenTextEscapeRegex = /[&"'<>]/g
|
||||
MaxTokenLength = 20000
|
||||
ZERO_WIDTH_NBSP = '\ufeff'
|
||||
|
||||
cloneObject = (object) ->
|
||||
clone = {}
|
||||
@@ -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 = {}
|
||||
@@ -69,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
|
||||
@@ -195,8 +191,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]
|
||||
{lineText, tagCodes, screenRow, decorationClasses} = @newTileState.lines[id]
|
||||
|
||||
lineNode = @domElementPool.buildElement("div", "line")
|
||||
lineNode.dataset.screenRow = screenRow
|
||||
@@ -205,185 +200,40 @@ class LinesTileComponent
|
||||
for decorationClass in decorationClasses
|
||||
lineNode.classList.add(decorationClass)
|
||||
|
||||
@currentLineTextNodes = []
|
||||
if text is ""
|
||||
@setEmptyLineInnerNodes(id, lineNode)
|
||||
else
|
||||
@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) ->
|
||||
lineState = @newTileState.lines[id]
|
||||
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
|
||||
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
|
||||
|
||||
@tokenIterator.reset(lineState)
|
||||
textNodes = []
|
||||
lineLength = 0
|
||||
startIndex = 0
|
||||
openScopeNode = lineNode
|
||||
|
||||
while @tokenIterator.next()
|
||||
for scope in @tokenIterator.getScopeEnds()
|
||||
for tagCode in tagCodes when tagCode isnt 0
|
||||
if @presenter.isCloseTagCode(tagCode)
|
||||
openScopeNode = openScopeNode.parentElement
|
||||
|
||||
for scope in @tokenIterator.getScopeStarts()
|
||||
else if @presenter.isOpenTagCode(tagCode)
|
||||
scope = @presenter.tagForCode(tagCode)
|
||||
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
|
||||
textNode = @domElementPool.buildText(lineText.substr(startIndex, tagCode))
|
||||
startIndex += tagCode
|
||||
openScopeNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
|
||||
if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
|
||||
tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
|
||||
else
|
||||
tokenFirstTrailingWhitespaceIndex = null
|
||||
if startIndex is 0
|
||||
textNode = @domElementPool.buildText(' ')
|
||||
lineNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
|
||||
hasIndentGuide =
|
||||
@newState.indentGuidesVisible and
|
||||
(hasLeadingWhitespace or lineIsWhitespaceOnly)
|
||||
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)
|
||||
|
||||
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)
|
||||
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]
|
||||
@@ -436,3 +286,9 @@ class LinesTileComponent
|
||||
|
||||
textNodesForLineId: (lineId) ->
|
||||
@textNodesByLineId[lineId].slice()
|
||||
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
@lineIdsByScreenRow[screenRow]
|
||||
|
||||
textNodesForScreenRow: (screenRow) ->
|
||||
@textNodesByLineId[@lineIdsByScreenRow[screenRow]]?.slice()
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
TokenIterator = require './token-iterator'
|
||||
{Point} = require 'text-buffer'
|
||||
{isPairedCharacter} = require './text-utils'
|
||||
|
||||
module.exports =
|
||||
class LinesYardstick
|
||||
constructor: (@model, @lineNodesProvider, @lineTopIndex, grammarRegistry) ->
|
||||
@tokenIterator = new TokenIterator({grammarRegistry})
|
||||
@rangeForMeasurement = document.createRange()
|
||||
@invalidateCache()
|
||||
|
||||
invalidateCache: ->
|
||||
@pixelPositionsByLineIdAndColumn = {}
|
||||
@leftPixelPositionCache = {}
|
||||
|
||||
measuredRowForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
@@ -21,61 +20,63 @@ 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)
|
||||
|
||||
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
|
||||
textNodeIndex = 0
|
||||
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
|
||||
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
|
||||
|
||||
@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
|
||||
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
|
||||
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
|
||||
char = text[textIndex]
|
||||
charLength = 1
|
||||
textIndex++
|
||||
characterIndex = nextCharIndex
|
||||
break
|
||||
|
||||
unless textNode?
|
||||
textNode = textNodes.shift()
|
||||
textNodeLength = textNode.textContent.length
|
||||
textNodeIndex = 0
|
||||
nextTextNodeIndex = textNodeLength
|
||||
|
||||
while nextTextNodeIndex <= column
|
||||
textNode = textNodes.shift()
|
||||
textNodeLength = textNode.textContent.length
|
||||
textNodeIndex = nextTextNodeIndex
|
||||
nextTextNodeIndex = textNodeIndex + textNodeLength
|
||||
|
||||
indexWithinTextNode = column - textNodeIndex
|
||||
left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode)
|
||||
charWidth = left - previousLeft
|
||||
|
||||
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)
|
||||
textNodeStartColumn = 0
|
||||
textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1
|
||||
Point(row, textNodeStartColumn + characterIndex)
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
targetRow = screenPosition.row
|
||||
@@ -87,76 +88,41 @@ 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)
|
||||
textNodeStartColumn = 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
|
||||
textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
|
||||
if textNodeEndColumn > column
|
||||
indexInTextNode = column - textNodeStartColumn
|
||||
break
|
||||
else
|
||||
textNodeStartColumn = textNodeEndColumn
|
||||
|
||||
if textNode?
|
||||
foundIndexWithinTextNode ?= textNode.textContent.length
|
||||
position = @leftPixelPositionForCharInTextNode(
|
||||
lineNode, textNode, foundIndexWithinTextNode
|
||||
)
|
||||
@pixelPositionsByLineIdAndColumn[line.id] ?= {}
|
||||
@pixelPositionsByLineIdAndColumn[line.id][column] = position
|
||||
position
|
||||
indexInTextNode ?= textNode.textContent.length
|
||||
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
|
||||
leftPixelPosition
|
||||
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.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -394,7 +393,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) if wasReversed
|
||||
|
||||
if autoIndentFirstLine
|
||||
@editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
|
||||
@@ -411,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
|
||||
@@ -446,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
|
||||
@@ -483,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)
|
||||
|
||||
@@ -516,7 +509,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]
|
||||
@@ -635,8 +628,9 @@ class Selection extends Model
|
||||
# Public: Creates a fold containing the current selection.
|
||||
fold: ->
|
||||
range = @getBufferRange()
|
||||
@editor.createFold(range.start.row, range.end.row)
|
||||
@cursor.setBufferPosition([range.end.row + 1, 0])
|
||||
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.
|
||||
@@ -690,7 +684,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()]
|
||||
@@ -703,14 +697,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]
|
||||
@@ -723,7 +718,8 @@ class Selection extends Model
|
||||
else
|
||||
continue if clippedRange.isEmpty()
|
||||
|
||||
@editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
|
||||
selection = @editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
break
|
||||
|
||||
return
|
||||
@@ -762,6 +758,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
|
||||
@@ -832,7 +834,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)
|
||||
|
||||
@@ -494,7 +494,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)
|
||||
@@ -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
|
||||
@@ -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) =>
|
||||
@@ -890,10 +890,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)
|
||||
@@ -950,7 +947,7 @@ class TextEditorComponent
|
||||
|
||||
screenPositionForMouseEvent: (event, linesClientRect) ->
|
||||
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
|
||||
@screenPositionForPixelPosition(pixelPosition, true)
|
||||
@screenPositionForPixelPosition(pixelPosition)
|
||||
|
||||
pixelPositionForMouseEvent: (event, linesClientRect) ->
|
||||
{clientX, clientY} = event
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -16,6 +16,7 @@ class TextEditorPresenter
|
||||
{@model, @config, @lineTopIndex, scrollPastEnd} = params
|
||||
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
|
||||
{@contentFrameWidth} = params
|
||||
{@displayLayer} = @model
|
||||
|
||||
@gutterWidth = 0
|
||||
@tileSize ?= 6
|
||||
@@ -23,6 +24,7 @@ class TextEditorPresenter
|
||||
@realScrollLeft = @scrollLeft
|
||||
@disposables = new CompositeDisposable
|
||||
@emitter = new Emitter
|
||||
@linesByScreenRow = new Map
|
||||
@visibleHighlights = {}
|
||||
@characterWidthsByScope = {}
|
||||
@lineDecorationsByScreenRow = {}
|
||||
@@ -87,6 +89,8 @@ class TextEditorPresenter
|
||||
@updateCommonGutterState()
|
||||
@updateReflowState()
|
||||
|
||||
@updateLines()
|
||||
|
||||
if @shouldUpdateDecorations
|
||||
@fetchDecorations()
|
||||
@updateLineDecorations()
|
||||
@@ -106,6 +110,8 @@ class TextEditorPresenter
|
||||
@clearPendingScrollPosition()
|
||||
@updateRowsPerPage()
|
||||
|
||||
@updateLines()
|
||||
|
||||
@updateFocusedState()
|
||||
@updateHeightState()
|
||||
@updateVerticalScrollState()
|
||||
@@ -132,8 +138,11 @@ class TextEditorPresenter
|
||||
@shouldUpdateDecorations = true
|
||||
|
||||
observeModel: ->
|
||||
@disposables.add @model.onDidChange ({start, end, screenDelta}) =>
|
||||
@spliceBlockDecorationsInRange(start, end, screenDelta)
|
||||
@disposables.add @model.displayLayer.onDidChangeSync (changes) =>
|
||||
for change in changes
|
||||
startRow = change.start.row
|
||||
endRow = startRow + change.oldExtent.row
|
||||
@spliceBlockDecorationsInRange(startRow, endRow, change.newExtent.row - change.oldExtent.row)
|
||||
@shouldUpdateDecorations = true
|
||||
@emitDidUpdateState()
|
||||
|
||||
@@ -166,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()
|
||||
@@ -175,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()
|
||||
@@ -286,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
|
||||
|
||||
@@ -297,15 +300,15 @@ class TextEditorPresenter
|
||||
Math.max(0, Math.min(row, @model.getScreenLineCount()))
|
||||
|
||||
getStartTileRow: ->
|
||||
@constrainRow(@tileForRow(@startRow))
|
||||
@constrainRow(@tileForRow(@startRow ? 0))
|
||||
|
||||
getEndTileRow: ->
|
||||
@constrainRow(@tileForRow(@endRow))
|
||||
@constrainRow(@tileForRow(@endRow ? 0))
|
||||
|
||||
isValidScreenRow: (screenRow) ->
|
||||
screenRow >= 0 and screenRow < @model.getScreenLineCount()
|
||||
|
||||
getScreenRows: ->
|
||||
getScreenRowsToRender: ->
|
||||
startRow = @getStartTileRow()
|
||||
endRow = @constrainRow(@getEndTileRow() + @tileSize)
|
||||
|
||||
@@ -320,6 +323,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
|
||||
|
||||
@@ -332,7 +351,7 @@ class TextEditorPresenter
|
||||
updateTilesState: ->
|
||||
return unless @startRow? and @endRow? and @lineHeight?
|
||||
|
||||
screenRows = @getScreenRows()
|
||||
screenRows = @getScreenRowsToRender()
|
||||
visibleTiles = {}
|
||||
startRow = screenRows[0]
|
||||
endRow = screenRows[screenRows.length - 1]
|
||||
@@ -375,7 +394,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]?
|
||||
@@ -393,7 +412,7 @@ class TextEditorPresenter
|
||||
tileState.lines ?= {}
|
||||
visibleLineIds = {}
|
||||
for screenRow in screenRows
|
||||
line = @model.tokenizedLineForScreenRow(screenRow)
|
||||
line = @linesByScreenRow.get(screenRow)
|
||||
unless line?
|
||||
throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}")
|
||||
|
||||
@@ -411,18 +430,8 @@ 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
|
||||
lineText: line.lineText
|
||||
tagCodes: line.tagCodes
|
||||
decorationClasses: @lineDecorationClassesForRow(screenRow)
|
||||
precedingBlockDecorations: precedingBlockDecorations
|
||||
followingBlockDecorations: followingBlockDecorations
|
||||
@@ -618,7 +627,7 @@ class TextEditorPresenter
|
||||
softWrapped = false
|
||||
|
||||
screenRow = startRow + i
|
||||
line = @model.tokenizedLineForScreenRow(screenRow)
|
||||
lineId = @linesByScreenRow.get(screenRow).id
|
||||
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
|
||||
blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow)
|
||||
blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight
|
||||
@@ -626,8 +635,8 @@ class TextEditorPresenter
|
||||
blockDecorationsAfterPreviousScreenRowHeight = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - @lineHeight - @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow - 1)
|
||||
blockDecorationsHeight += blockDecorationsAfterPreviousScreenRowHeight
|
||||
|
||||
tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight}
|
||||
visibleLineNumberIds[line.id] = true
|
||||
tileState.lineNumbers[lineId] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight}
|
||||
visibleLineNumberIds[lineId] = true
|
||||
|
||||
for id of tileState.lineNumbers
|
||||
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
|
||||
@@ -687,9 +696,7 @@ 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()
|
||||
@contentWidth = @pixelPositionForScreenPosition(rightmostPosition).left
|
||||
@contentWidth += @scrollLeft
|
||||
@contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
|
||||
@@ -1057,6 +1064,16 @@ class TextEditorPresenter
|
||||
rect.height = Math.round(rect.height)
|
||||
rect
|
||||
|
||||
updateLines: ->
|
||||
@linesByScreenRow.clear()
|
||||
|
||||
for [startRow, endRow] in @getScreenRangesToRender()
|
||||
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)
|
||||
@@ -1104,9 +1121,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] ?= {}
|
||||
@@ -1127,7 +1144,7 @@ class TextEditorPresenter
|
||||
|
||||
return
|
||||
|
||||
addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) ->
|
||||
addToLineDecorationCaches: (decorationId, properties, bufferRange, screenRange, rangeIsReversed) ->
|
||||
if screenRange.isEmpty()
|
||||
return if properties.onlyNonEmpty
|
||||
else
|
||||
@@ -1135,21 +1152,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
|
||||
|
||||
@@ -1529,5 +1553,11 @@ class TextEditorPresenter
|
||||
isRowVisible: (row) ->
|
||||
@startRow <= row < @endRow
|
||||
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
@model.tokenizedLineForScreenRow(screenRow)?.id
|
||||
isOpenTagCode: (tagCode) ->
|
||||
@displayLayer.isOpenTagCode(tagCode)
|
||||
|
||||
isCloseTagCode: (tagCode) ->
|
||||
@displayLayer.isCloseTagCode(tagCode)
|
||||
|
||||
tagForCode: (tagCode) ->
|
||||
@displayLayer.tagForCode(tagCode)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,106 +1,57 @@
|
||||
{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
|
||||
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter} = require './text-utils'
|
||||
|
||||
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
|
||||
if @isHardTab()
|
||||
@screenEnd = @screenStart + tag
|
||||
@bufferEnd = @bufferStart + 1
|
||||
else if @isSoftWrapIndentation()
|
||||
@screenEnd = @screenStart + tag
|
||||
@bufferEnd = @bufferStart + 0
|
||||
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
|
||||
getScopes: -> @scopes
|
||||
|
||||
getScopeStarts: -> @scopeStarts
|
||||
getScopeEnds: -> @scopeEnds
|
||||
|
||||
getScopes: -> @scopes
|
||||
getScopeEnds: -> @scopeEnds
|
||||
|
||||
getText: -> @text
|
||||
|
||||
isSoftTab: ->
|
||||
@line.specialTokens[@index] is SoftTab
|
||||
getBufferStart: -> @startColumn
|
||||
|
||||
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()
|
||||
getBufferEnd: -> @endColumn
|
||||
|
||||
@@ -7,41 +7,20 @@ 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
|
||||
@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))
|
||||
|
||||
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
|
||||
|
||||
122
src/tokenized-buffer-iterator.coffee
Normal file
122
src/tokenized-buffer-iterator.coffee
Normal file
@@ -0,0 +1,122 @@
|
||||
{Point} = require 'text-buffer'
|
||||
|
||||
module.exports =
|
||||
class TokenizedBufferIterator
|
||||
constructor: (@tokenizedBuffer, @grammarRegistry) ->
|
||||
@openTags = null
|
||||
@closeTags = null
|
||||
@containingTags = null
|
||||
|
||||
seek: (position) ->
|
||||
@openTags = []
|
||||
@closeTags = []
|
||||
@tagIndex = null
|
||||
|
||||
currentLine = @tokenizedBuffer.tokenizedLineForRow(position.row)
|
||||
@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
|
||||
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
|
||||
if @openTags.length > 0
|
||||
@tagIndex = index
|
||||
break
|
||||
else
|
||||
@closeTags.push(scopeName)
|
||||
else
|
||||
@openTags.push(scopeName)
|
||||
|
||||
@tagIndex ?= @currentTags.length
|
||||
@position = Point(position.row, Math.min(@currentLineLength, currentColumn))
|
||||
@containingTags.slice()
|
||||
|
||||
moveToSuccessor: ->
|
||||
@containingTags.pop() for tag in @closeTags
|
||||
@containingTags.push(tag) for tag in @openTags
|
||||
@openTags = []
|
||||
@closeTags = []
|
||||
|
||||
loop
|
||||
if @tagIndex is @currentTags.length
|
||||
if @isAtTagBoundary()
|
||||
break
|
||||
else
|
||||
if @shouldMoveToNextLine
|
||||
@moveToNextLine()
|
||||
@openTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id)
|
||||
@shouldMoveToNextLine = false
|
||||
else if @nextLineHasMismatchedContainingTags()
|
||||
@closeTags = @containingTags.slice().reverse()
|
||||
@containingTags = []
|
||||
@shouldMoveToNextLine = true
|
||||
else
|
||||
return false unless @moveToNextLine()
|
||||
else
|
||||
tag = @currentTags[@tagIndex]
|
||||
if tag >= 0
|
||||
if @isAtTagBoundary()
|
||||
break
|
||||
else
|
||||
@position = Point(@position.row, Math.min(@currentLineLength, @position.column + @currentTags[@tagIndex]))
|
||||
else
|
||||
scopeName = @grammarRegistry.scopeForId(tag)
|
||||
if tag % 2 is 0
|
||||
if @openTags.length > 0
|
||||
break
|
||||
else
|
||||
@closeTags.push(scopeName)
|
||||
else
|
||||
@openTags.push(scopeName)
|
||||
@tagIndex++
|
||||
|
||||
true
|
||||
|
||||
getPosition: ->
|
||||
@position
|
||||
|
||||
getCloseTags: ->
|
||||
@closeTags.slice()
|
||||
|
||||
getOpenTags: ->
|
||||
@openTags.slice()
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
nextLineHasMismatchedContainingTags: ->
|
||||
if line = @tokenizedBuffer.tokenizedLineForRow(@position.row + 1)
|
||||
return true if line.openScopes.length isnt @containingTags.length
|
||||
|
||||
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)
|
||||
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
|
||||
@@ -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
|
||||
@@ -34,7 +35,7 @@ class TokenizedBuffer extends Model
|
||||
|
||||
constructor: (params) ->
|
||||
{
|
||||
@buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config,
|
||||
@buffer, @tabLength, @largeFileMode, @config,
|
||||
@grammarRegistry, @assert, grammarScopeName
|
||||
} = params
|
||||
|
||||
@@ -57,13 +58,24 @@ class TokenizedBuffer extends Model
|
||||
destroyed: ->
|
||||
@disposables.dispose()
|
||||
|
||||
buildIterator: ->
|
||||
new TokenizedBufferIterator(this, @grammarRegistry)
|
||||
|
||||
getInvalidatedRanges: ->
|
||||
if @invalidatedRange?
|
||||
[@invalidatedRange]
|
||||
else
|
||||
[]
|
||||
|
||||
onDidInvalidateRange: (fn) ->
|
||||
@emitter.on 'did-invalidate-range', fn
|
||||
|
||||
serialize: ->
|
||||
state = {
|
||||
deserializer: 'TokenizedBuffer'
|
||||
bufferPath: @buffer.getPath()
|
||||
bufferId: @buffer.getId()
|
||||
tabLength: @tabLength
|
||||
ignoreInvisibles: @ignoreInvisibles
|
||||
largeFileMode: @largeFileMode
|
||||
}
|
||||
state.grammarScopeName = @grammar?.scopeName unless @buffer.getPath()
|
||||
@@ -104,24 +116,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()
|
||||
['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()
|
||||
@@ -162,13 +164,6 @@ class TokenizedBuffer extends Model
|
||||
return if tabLength is @tabLength
|
||||
|
||||
@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()
|
||||
@@ -211,6 +206,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()
|
||||
@@ -261,26 +257,15 @@ 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)
|
||||
|
||||
@invalidatedRange = Range(start, end)
|
||||
|
||||
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
|
||||
@@ -345,26 +330,16 @@ class TokenizedBuffer extends Model
|
||||
openScopes = [@grammar.startIdForScope(@grammar.scopeName)]
|
||||
text = @buffer.lineForRow(row)
|
||||
tags = [text.length]
|
||||
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, 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()
|
||||
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, lineEnding, @tokenIterator})
|
||||
|
||||
tokenizedLineForRow: (bufferRow) ->
|
||||
if 0 <= bufferRow < @tokenizedLines.length
|
||||
@@ -405,6 +380,7 @@ class TokenizedBuffer extends Model
|
||||
filePath: @buffer.getPath()
|
||||
fileContents: @buffer.getText()
|
||||
}
|
||||
break
|
||||
scopes
|
||||
|
||||
indentLevelForRow: (bufferRow) ->
|
||||
|
||||
@@ -1,187 +1,18 @@
|
||||
_ = 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
|
||||
|
||||
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
|
||||
lineIsWhitespaceOnly: false
|
||||
firstNonWhitespaceIndex: 0
|
||||
|
||||
constructor: (properties) ->
|
||||
@id = idCounter++
|
||||
|
||||
return unless properties?
|
||||
|
||||
@specialTokens = {}
|
||||
{@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties
|
||||
{@startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles} = properties
|
||||
|
||||
@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
|
||||
{@openScopes, @text, @tags, @ruleStack, @tokenIterator} = properties
|
||||
|
||||
getTokenIterator: -> @tokenIterator.reset(this, arguments...)
|
||||
|
||||
@@ -190,285 +21,21 @@ 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
|
||||
|
||||
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.indentLevel = @indentLevel
|
||||
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.
|
||||
#
|
||||
# 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
|
||||
|
||||
# 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)]
|
||||
|
||||
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) ->
|
||||
@@ -479,17 +46,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?
|
||||
|
||||
@@ -505,9 +61,6 @@ class TokenizedLine
|
||||
break
|
||||
@isCommentLine
|
||||
|
||||
isOnlyWhitespace: ->
|
||||
@lineIsWhitespaceOnly
|
||||
|
||||
tokenAtIndex: (index) ->
|
||||
@tokens[index]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user