diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee
index 544b09753..ff61d2ba8 100644
--- a/build/tasks/build-task.coffee
+++ b/build/tasks/build-task.coffee
@@ -133,7 +133,6 @@ module.exports = (grunt) ->
ignoredPaths.push "#{_.escapeRegExp(path.join('scrollbar-style', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('spellchecker', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('cached-run-in-this-context', 'src') + path.sep)}.*\\.(cc|h)?"
- ignoredPaths.push "#{_.escapeRegExp(path.join('marker-index', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('keyboard-layout', 'src') + path.sep)}.*\\.(cc|h|mm)*"
# Ignore build files
diff --git a/package.json b/package.json
index f5a432a2d..4a8591ae5 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
- "text-buffer": "8.5.0",
+ "text-buffer": "9.0.0",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee
new file mode 100644
index 000000000..c428df8cf
--- /dev/null
+++ b/spec/decoration-manager-spec.coffee
@@ -0,0 +1,85 @@
+DecorationManager = require '../src/decoration-manager'
+_ = require 'underscore-plus'
+
+describe "DecorationManager", ->
+ [decorationManager, buffer, defaultMarkerLayer] = []
+
+ beforeEach ->
+ buffer = atom.project.bufferForPathSync('sample.js')
+ displayLayer = buffer.addDisplayLayer()
+ defaultMarkerLayer = displayLayer.addMarkerLayer()
+ decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer)
+
+ waitsForPromise ->
+ atom.packages.activatePackage('language-javascript')
+
+ afterEach ->
+ decorationManager.destroy()
+ buffer.release()
+
+ describe "decorations", ->
+ [marker, decoration, decorationProperties] = []
+ beforeEach ->
+ marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]])
+ decorationProperties = {type: 'line-number', class: 'one'}
+ decoration = decorationManager.decorateMarker(marker, decorationProperties)
+
+ it "can add decorations associated with markers and remove them", ->
+ expect(decoration).toBeDefined()
+ expect(decoration.getProperties()).toBe decorationProperties
+ expect(decorationManager.decorationForId(decoration.id)).toBe decoration
+ expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
+
+ decoration.destroy()
+ expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
+ expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
+
+ it "will not fail if the decoration is removed twice", ->
+ decoration.destroy()
+ decoration.destroy()
+ expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
+
+ it "does not allow destroyed markers to be decorated", ->
+ marker.destroy()
+ expect(->
+ decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
+ ).toThrow("Cannot decorate a destroyed marker")
+ expect(decorationManager.getOverlayDecorations()).toEqual []
+
+ describe "when a decoration is updated via Decoration::update()", ->
+ it "emits an 'updated' event containing the new and old params", ->
+ decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
+ decoration.setProperties type: 'line-number', class: 'two'
+
+ {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
+ expect(oldProperties).toEqual decorationProperties
+ expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
+
+ describe "::getDecorations(properties)", ->
+ it "returns decorations matching the given optional properties", ->
+ expect(decorationManager.getDecorations()).toEqual [decoration]
+ expect(decorationManager.getDecorations(class: 'two').length).toEqual 0
+ expect(decorationManager.getDecorations(class: 'one').length).toEqual 1
+
+ describe "::decorateMarker", ->
+ describe "when decorating gutters", ->
+ [marker] = []
+
+ beforeEach ->
+ marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]])
+
+ it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
+ decorationProperties = {type: 'line-number', class: 'one'}
+ decoration = decorationManager.decorateMarker(marker, decorationProperties)
+ expect(decoration.isType('line-number')).toBe true
+ expect(decoration.isType('gutter')).toBe true
+ expect(decoration.getProperties().gutterName).toBe 'line-number'
+ expect(decoration.getProperties().class).toBe 'one'
+
+ it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
+ decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
+ decoration = decorationManager.decorateMarker(marker, decorationProperties)
+ expect(decoration.isType('gutter')).toBe true
+ expect(decoration.isType('line-number')).toBe false
+ expect(decoration.getProperties().gutterName).toBe 'test-gutter'
+ expect(decoration.getProperties().class).toBe 'one'
diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee
deleted file mode 100644
index 0246008a4..000000000
--- a/spec/display-buffer-spec.coffee
+++ /dev/null
@@ -1,1312 +0,0 @@
-DisplayBuffer = require '../src/display-buffer'
-_ = require 'underscore-plus'
-
-describe "DisplayBuffer", ->
- [displayBuffer, buffer, changeHandler, tabLength] = []
- beforeEach ->
- tabLength = 2
-
- buffer = atom.project.bufferForPathSync('sample.js')
- displayBuffer = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- changeHandler = jasmine.createSpy 'changeHandler'
- displayBuffer.onDidChange changeHandler
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- afterEach ->
- displayBuffer.destroy()
- buffer.release()
-
- describe "::copy()", ->
- it "creates a new DisplayBuffer with the same initial state", ->
- marker1 = displayBuffer.markBufferRange([[1, 2], [3, 4]], id: 1)
- marker2 = displayBuffer.markBufferRange([[2, 3], [4, 5]], reversed: true, id: 2)
- marker3 = displayBuffer.markBufferPosition([5, 6], id: 3)
- displayBuffer.createFold(3, 5)
-
- displayBuffer2 = displayBuffer.copy()
- expect(displayBuffer2.id).not.toBe displayBuffer.id
- expect(displayBuffer2.buffer).toBe displayBuffer.buffer
- expect(displayBuffer2.getTabLength()).toBe displayBuffer.getTabLength()
-
- expect(displayBuffer2.getMarkerCount()).toEqual displayBuffer.getMarkerCount()
- expect(displayBuffer2.findMarker(id: 1)).toEqual marker1
- expect(displayBuffer2.findMarker(id: 2)).toEqual marker2
- expect(displayBuffer2.findMarker(id: 3)).toEqual marker3
- expect(displayBuffer2.isFoldedAtBufferRow(3)).toBeTruthy()
-
- # can diverge from origin
- displayBuffer2.unfoldBufferRow(3)
- expect(displayBuffer2.isFoldedAtBufferRow(3)).not.toBe displayBuffer.isFoldedAtBufferRow(3)
-
- describe "when the buffer changes", ->
- it "renders line numbers correctly", ->
- originalLineCount = displayBuffer.getLineCount()
- oneHundredLines = [0..100].join("\n")
- buffer.insert([0, 0], oneHundredLines)
- expect(displayBuffer.getLineCount()).toBe 100 + originalLineCount
-
- it "updates the display buffer prior to invoking change handlers registered on the buffer", ->
- buffer.onDidChange -> expect(displayBuffer2.tokenizedLineForScreenRow(0).text).toBe "testing"
- displayBuffer2 = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- buffer.setText("testing")
-
- describe "soft wrapping", ->
- beforeEach ->
- displayBuffer.setEditorWidthInChars(50)
- displayBuffer.setSoftWrapped(true)
- displayBuffer.setDefaultCharWidth(1)
- changeHandler.reset()
-
- describe "rendering of soft-wrapped lines", ->
- describe "when there are double width characters", ->
- it "takes them into account when finding the soft wrap column", ->
- buffer.setText("私たちのフ是一个地方,数千名学生12345业余爱们的板作为hello world this is a pretty long latin line")
- displayBuffer.setDefaultCharWidth(1, 5, 0, 0)
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("私たちのフ是一个地方")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe(",数千名学生12345业余爱")
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("们的板作为hello world this is a ")
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe("pretty long latin line")
-
- describe "when there are half width characters", ->
- it "takes them into account when finding the soft wrap column", ->
- displayBuffer.setDefaultCharWidth(1, 0, 5, 0)
- buffer.setText("abcᆰᆱᆲネヌネノハヒフヒフヌᄡ○○○hello world this is a pretty long line")
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("abcᆰᆱᆲネヌネノハヒ")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe("フヒフヌᄡ○○○hello ")
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("world this is a pretty long line")
-
- describe "when there are korean characters", ->
- it "takes them into account when finding the soft wrap column", ->
- displayBuffer.setDefaultCharWidth(1, 0, 0, 10)
- buffer.setText("1234세계를향한대화,유니코제10회유니코드국제")
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("1234세계를향")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe("한대화,유")
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("니코제10회")
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe("유니코드국")
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe("제")
-
- describe "when editor.softWrapAtPreferredLineLength is set", ->
- it "uses the preferred line length as the soft wrap column when it is less than the configured soft wrap column", ->
- atom.config.set('editor.preferredLineLength', 100)
- atom.config.set('editor.softWrapAtPreferredLineLength', true)
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return '
-
- atom.config.set('editor.preferredLineLength', 5)
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' fun'
-
- atom.config.set('editor.softWrapAtPreferredLineLength', false)
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return '
-
- describe "when editor width is negative", ->
- it "does not hang while wrapping", ->
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setWidth(-1)
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe " "
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe " var sort = function(items) {"
-
- describe "when the line is shorter than the max line length", ->
- it "renders the line unchanged", ->
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0)
-
- describe "when the line is empty", ->
- it "renders the empty line", ->
- expect(displayBuffer.tokenizedLineForScreenRow(13).text).toBe ''
-
- describe "when there is a non-whitespace character at the max length boundary", ->
- describe "when there is whitespace before the boundary", ->
- it "wraps the line at the end of the first whitespace preceding the boundary", ->
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return '
- expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe ' sort(left).concat(pivot).concat(sort(right));'
-
- it "wraps the line at the first CJK character before the boundary", ->
- displayBuffer.setEditorWidthInChars(10)
-
- buffer.setTextInRange([[0, 0], [1, 0]], 'abcd efg유私フ业余爱\n')
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcd efg유私'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'フ业余爱'
-
- buffer.setTextInRange([[0, 0], [1, 0]], 'abcd ef유gef业余爱\n')
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcd ef유'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'gef业余爱'
-
- describe "when there is no whitespace before the boundary", ->
- it "wraps the line at the first CJK character before the boundary", ->
- buffer.setTextInRange([[0, 0], [1, 0]], '私たちのabcdefghij\n')
- displayBuffer.setEditorWidthInChars(10)
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe '私たちの'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'abcdefghij'
-
- it "wraps the line exactly at the boundary when no CJK character is found, since there's no more graceful place to wrap it", ->
- buffer.setTextInRange([[0, 0], [1, 0]], 'abcdefghijklmnopqrstuvwxyz\n')
- displayBuffer.setEditorWidthInChars(10)
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcdefghij'
- expect(displayBuffer.tokenizedLineForScreenRow(0).bufferDelta).toBe 'abcdefghij'.length
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'klmnopqrst'
- expect(displayBuffer.tokenizedLineForScreenRow(1).bufferDelta).toBe 'klmnopqrst'.length
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe 'uvwxyz'
- expect(displayBuffer.tokenizedLineForScreenRow(2).bufferDelta).toBe 'uvwxyz'.length
-
- it "closes all scopes at the wrap boundary", ->
- displayBuffer.setEditorWidthInChars(10)
- buffer.setText("`aaa${1+2}aaa`")
- iterator = displayBuffer.tokenizedLineForScreenRow(1).getTokenIterator()
- scopes = iterator.getScopes()
- expect(scopes[scopes.length - 1]).not.toBe 'punctuation.section.embedded.js'
-
- describe "when there is a whitespace character at the max length boundary", ->
- it "wraps the line at the first non-whitespace character following the boundary", ->
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items.shift(), current, left = [], '
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' right = [];'
-
- describe "when the only whitespace characters are at the beginning of the line", ->
- beforeEach ->
- displayBuffer.setEditorWidthInChars(10)
-
- it "wraps the line at the max length when indented with tabs", ->
- buffer.setTextInRange([[0, 0], [1, 0]], '\t\tabcdefghijklmnopqrstuvwxyz')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr'
-
- it "wraps the line at the max length when indented with spaces", ->
- buffer.setTextInRange([[0, 0], [1, 0]], ' abcdefghijklmnopqrstuvwxyz')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr'
-
- describe "when there are hard tabs", ->
- beforeEach ->
- buffer.setText(buffer.getText().replace(new RegExp(' ', 'g'), '\t'))
-
- it "correctly tokenizes the hard tabs", ->
- expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[0].isHardTab).toBeTruthy()
- expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy()
-
- describe "when a line is wrapped", ->
- it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", ->
- tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4)
-
- expect(tokenizedLine.tokens[0].value).toBe(" ")
- expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[1].value).toBe(" ")
- expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeFalsy()
-
- describe "when editor.softWrapHangingIndent is set", ->
- beforeEach ->
- atom.config.set('editor.softWrapHangingIndent', 3)
-
- it "further indents wrapped lines", ->
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe " return "
- expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe " sort(left).concat(pivot).concat(sort(right)"
- expect(displayBuffer.tokenizedLineForScreenRow(12).text).toBe " );"
-
- it "includes hanging indent when breaking soft-wrap indentation into tokens", ->
- tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4)
-
- expect(tokenizedLine.tokens[0].value).toBe(" ")
- expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[1].value).toBe(" ")
- expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[2].value).toBe(" ") # hanging indent
- expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[3].value).toBe(" ") # odd space
- expect(tokenizedLine.tokens[3].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[4].isSoftWrapIndentation).toBeFalsy()
-
- describe "when the buffer changes", ->
- describe "when buffer lines are updated", ->
- describe "when whitespace is added after the max line length", ->
- it "adds whitespace to the end of the current line and wraps an empty line", ->
- fiftyCharacters = _.multiplyString("x", 50)
- buffer.setText(fiftyCharacters)
- buffer.insert([0, 51], " ")
-
- describe "when the update makes a soft-wrapped line shorter than the max line length", ->
- it "rewraps the line and emits a change event", ->
- buffer.delete([[6, 24], [6, 42]])
- expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? : right.push(current);'
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' }'
-
- expect(changeHandler).toHaveBeenCalled()
- [[event]]= changeHandler.argsForCall
-
- expect(event).toEqual(start: 7, end: 8, screenDelta: -1, bufferDelta: 0)
-
- describe "when the update causes a line to soft wrap an additional time", ->
- it "rewraps the line and emits a change event", ->
- buffer.insert([6, 28], '1234567890')
- expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? '
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' left1234567890.push(current) : '
- expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe ' right.push(current);'
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' }'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, screenDelta: 1, bufferDelta: 0)
-
- describe "when buffer lines are inserted", ->
- it "inserts / updates wrapped lines and emits a change event", ->
- buffer.insert([6, 21], '1234567890 abcdefghij 1234567890\nabcdefghij')
- expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot1234567890 abcdefghij '
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' 1234567890'
- expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe 'abcdefghij ? left.push(current) : '
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe 'right.push(current);'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, screenDelta: 2, bufferDelta: 1)
-
- describe "when buffer lines are removed", ->
- it "removes lines and emits a change event", ->
- buffer.setTextInRange([[3, 21], [7, 5]], ';')
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items;'
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' return '
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toBe ' sort(left).concat(pivot).concat(sort(right));'
- expect(displayBuffer.tokenizedLineForScreenRow(6).text).toBe ' };'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 9, screenDelta: -6, bufferDelta: -4)
-
- describe "when a newline is inserted, deleted, and re-inserted at the end of a wrapped line (regression)", ->
- it "correctly renders the original wrapped line", ->
- buffer = atom.project.buildBufferSync(null, '')
- displayBuffer = new DisplayBuffer({
- buffer, tabLength, editorWidthInChars: 30, config: atom.config,
- grammarRegistry: atom.grammars, packageManager: atom.packages, assert: ->
- })
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setSoftWrapped(true)
-
- buffer.insert([0, 0], "the quick brown fox jumps over the lazy dog.")
- buffer.insert([0, Infinity], '\n')
- buffer.delete([[0, Infinity], [1, 0]])
- buffer.insert([0, Infinity], '\n')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "the quick brown fox jumps over "
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "the lazy dog."
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ""
-
- describe "position translation", ->
- it "translates positions accounting for wrapped lines", ->
- # before any wrapped lines
- expect(displayBuffer.screenPositionForBufferPosition([0, 5])).toEqual([0, 5])
- expect(displayBuffer.bufferPositionForScreenPosition([0, 5])).toEqual([0, 5])
- expect(displayBuffer.screenPositionForBufferPosition([0, 29])).toEqual([0, 29])
- expect(displayBuffer.bufferPositionForScreenPosition([0, 29])).toEqual([0, 29])
-
- # on a wrapped line
- expect(displayBuffer.screenPositionForBufferPosition([3, 5])).toEqual([3, 5])
- expect(displayBuffer.bufferPositionForScreenPosition([3, 5])).toEqual([3, 5])
- expect(displayBuffer.screenPositionForBufferPosition([3, 50])).toEqual([3, 50])
- expect(displayBuffer.screenPositionForBufferPosition([3, 51])).toEqual([3, 50])
- expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual([3, 50])
- expect(displayBuffer.bufferPositionForScreenPosition([3, 50])).toEqual([3, 50])
- expect(displayBuffer.screenPositionForBufferPosition([3, 62])).toEqual([4, 15])
- expect(displayBuffer.bufferPositionForScreenPosition([4, 11])).toEqual([3, 58])
-
- # following a wrapped line
- expect(displayBuffer.screenPositionForBufferPosition([4, 5])).toEqual([5, 5])
- expect(displayBuffer.bufferPositionForScreenPosition([5, 5])).toEqual([4, 5])
-
- # clip screen position inputs before translating
- expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0])
- expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([12, 2])
- expect(displayBuffer.bufferPositionForScreenPosition([3, -5])).toEqual([3, 0])
- expect(displayBuffer.bufferPositionForScreenPosition([3, Infinity])).toEqual([3, 50])
-
- describe ".setEditorWidthInChars(length)", ->
- it "changes the length at which lines are wrapped and emits a change event for all screen lines", ->
- tokensText = (tokens) ->
- _.pluck(tokens, 'value').join('')
-
- displayBuffer.setEditorWidthInChars(40)
- expect(tokensText displayBuffer.tokenizedLineForScreenRow(4).tokens).toBe ' left = [], right = [];'
- expect(tokensText displayBuffer.tokenizedLineForScreenRow(5).tokens).toBe ' while(items.length > 0) {'
- expect(tokensText displayBuffer.tokenizedLineForScreenRow(12).tokens).toBe ' sort(left).concat(pivot).concat(sort'
- expect(changeHandler).toHaveBeenCalledWith(start: 0, end: 15, screenDelta: 3, bufferDelta: 0)
-
- it "only allows positive widths to be assigned", ->
- displayBuffer.setEditorWidthInChars(0)
- expect(displayBuffer.editorWidthInChars).not.toBe 0
- displayBuffer.setEditorWidthInChars(-1)
- expect(displayBuffer.editorWidthInChars).not.toBe -1
-
- describe "primitive folding", ->
- beforeEach ->
- displayBuffer.destroy()
- buffer.release()
- buffer = atom.project.bufferForPathSync('two-hundred.txt')
- displayBuffer = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- displayBuffer.onDidChange changeHandler
-
- describe "when folds are created and destroyed", ->
- describe "when a fold spans multiple lines", ->
- it "replaces the lines spanned by the fold with a placeholder that references the fold object", ->
- fold = displayBuffer.createFold(4, 7)
- expect(fold).toBeDefined()
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe fold
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '8'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 7, screenDelta: -3, bufferDelta: 0)
- changeHandler.reset()
-
- fold.destroy()
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBeUndefined()
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 3, bufferDelta: 0)
-
- describe "when a fold spans a single line", ->
- it "renders a fold placeholder for the folded line but does not skip any lines", ->
- fold = displayBuffer.createFold(4, 4)
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe fold
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 0, bufferDelta: 0)
-
- # Line numbers don't actually change, but it's not worth the complexity to have this
- # be false for single line folds since they are so rare
- changeHandler.reset()
-
- fold.destroy()
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBeUndefined()
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 0, bufferDelta: 0)
-
- describe "when a fold is nested within another fold", ->
- it "does not render the placeholder for the inner fold until the outer fold is destroyed", ->
- innerFold = displayBuffer.createFold(6, 7)
- outerFold = displayBuffer.createFold(4, 8)
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe outerFold
- expect(line4.text).toMatch /4-+/
- expect(line5.text).toMatch /9-+/
-
- outerFold.destroy()
- [line4, line5, line6, line7] = displayBuffer.tokenizedLinesForScreenRows(4, 7)
- expect(line4.fold).toBeUndefined()
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
- expect(line6.fold).toBe innerFold
- expect(line6.text).toBe '6'
- expect(line7.text).toBe '8'
-
- it "allows the outer fold to start at the same location as the inner fold", ->
- innerFold = displayBuffer.createFold(4, 6)
- outerFold = displayBuffer.createFold(4, 8)
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe outerFold
- expect(line4.text).toMatch /4-+/
- expect(line5.text).toMatch /9-+/
-
- describe "when creating a fold where one already exists", ->
- it "returns existing fold and does't create new fold", ->
- fold = displayBuffer.createFold(0, 10)
- expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
-
- newFold = displayBuffer.createFold(0, 10)
- expect(newFold).toBe fold
- expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
-
- describe "when a fold is created inside an existing folded region", ->
- it "creates/destroys the fold, but does not trigger change event", ->
- outerFold = displayBuffer.createFold(0, 10)
- changeHandler.reset()
-
- innerFold = displayBuffer.createFold(2, 5)
- expect(changeHandler).not.toHaveBeenCalled()
- [line0, line1] = displayBuffer.tokenizedLinesForScreenRows(0, 1)
- expect(line0.fold).toBe outerFold
- expect(line1.fold).toBeUndefined()
-
- changeHandler.reset()
- innerFold.destroy()
- expect(changeHandler).not.toHaveBeenCalled()
- [line0, line1] = displayBuffer.tokenizedLinesForScreenRows(0, 1)
- expect(line0.fold).toBe outerFold
- expect(line1.fold).toBeUndefined()
-
- describe "when a fold ends where another fold begins", ->
- it "continues to hide the lines inside the second fold", ->
- fold2 = displayBuffer.createFold(4, 9)
- fold1 = displayBuffer.createFold(0, 4)
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toMatch /^0/
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toMatch /^10/
-
- describe "when there is another display buffer pointing to the same buffer", ->
- it "does not consider folds to be nested inside of folds from the other display buffer", ->
- otherDisplayBuffer = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- otherDisplayBuffer.createFold(1, 5)
-
- displayBuffer.createFold(2, 4)
- expect(otherDisplayBuffer.foldsStartingAtBufferRow(2).length).toBe 0
-
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe '5'
-
- describe "when the buffer changes", ->
- [fold1, fold2] = []
- beforeEach ->
- fold1 = displayBuffer.createFold(2, 4)
- fold2 = displayBuffer.createFold(6, 8)
- changeHandler.reset()
-
- describe "when the old range surrounds a fold", ->
- beforeEach ->
- buffer.setTextInRange([[1, 0], [5, 1]], 'party!')
-
- it "removes the fold and replaces the selection with the new text", ->
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "0"
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "party!"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 3, screenDelta: -2, bufferDelta: -4)
-
- describe "when the changes is subsequently undone", ->
- xit "restores destroyed folds", ->
- buffer.undo()
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe '5'
-
- describe "when the old range surrounds two nested folds", ->
- it "removes both folds and replaces the selection with the new text", ->
- displayBuffer.createFold(2, 9)
- changeHandler.reset()
-
- buffer.setTextInRange([[1, 0], [10, 0]], 'goodbye')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "0"
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "goodbye10"
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "11"
-
- expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 3, screenDelta: -2, bufferDelta: -9)
-
- describe "when multiple changes happen above the fold", ->
- it "repositions folds correctly", ->
- buffer.delete([[1, 1], [2, 0]])
- buffer.insert([0, 1], "\nnew")
-
- expect(fold1.getStartRow()).toBe 2
- expect(fold1.getEndRow()).toBe 4
-
- describe "when the old range precedes lines with a fold", ->
- describe "when the new range precedes lines with a fold", ->
- it "updates the buffer and re-positions subsequent folds", ->
- buffer.setTextInRange([[0, 0], [1, 1]], 'abc')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "abc"
- expect(displayBuffer.tokenizedLineForScreenRow(1).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(3).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 0, end: 1, screenDelta: -1, bufferDelta: -1)
- changeHandler.reset()
-
- fold1.destroy()
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "abc"
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "2"
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch /^4-+/
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(5).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(6).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 1, screenDelta: 2, bufferDelta: 0)
-
- describe "when the old range straddles the beginning of a fold", ->
- it "destroys the fold", ->
- buffer.setTextInRange([[1, 1], [3, 0]], "a\nb\nc\nd\n")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1a'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe 'b'
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBeUndefined()
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe 'c'
-
- describe "when the old range follows a fold", ->
- it "re-positions the screen ranges for the change event based on the preceding fold", ->
- buffer.setTextInRange([[10, 0], [11, 0]], 'abc')
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 6, end: 7, screenDelta: -1, bufferDelta: -1)
-
- describe "when the old range is inside a fold", ->
- describe "when the end of the new range precedes the end of the fold", ->
- it "updates the fold and ensures the change is present when the fold is destroyed", ->
- buffer.insert([3, 0], '\n')
- expect(fold1.getStartRow()).toBe 2
- expect(fold1.getEndRow()).toBe 5
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "2"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "5"
- expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 2, screenDelta: 0, bufferDelta: 1)
-
- describe "when the end of the new range exceeds the end of the fold", ->
- it "expands the fold to contain all the inserted lines", ->
- buffer.setTextInRange([[3, 0], [4, 0]], 'a\nb\nc\nd\n')
- expect(fold1.getStartRow()).toBe 2
- expect(fold1.getEndRow()).toBe 7
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "2"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "5"
- expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 2, screenDelta: 0, bufferDelta: 3)
-
- describe "when the old range straddles the end of the fold", ->
- describe "when the end of the new range precedes the end of the fold", ->
- it "destroys the fold", ->
- fold2.destroy()
- buffer.setTextInRange([[3, 0], [6, 0]], 'a\n')
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBeUndefined()
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe 'a'
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe '6'
-
- describe "when the old range is contained to a single line in-between two folds", ->
- it "re-renders the line with the placeholder and re-positions the second fold", ->
- buffer.insert([5, 0], 'abc\n')
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "abc"
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(5).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(6).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 3, screenDelta: 1, bufferDelta: 1)
-
- describe "when the change starts at the beginning of a fold but does not extend to the end (regression)", ->
- it "preserves a proper mapping between buffer and screen coordinates", ->
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [4, 0]
- buffer.setTextInRange([[2, 0], [3, 0]], "\n")
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [4, 0]
-
- describe "position translation", ->
- it "translates positions to account for folded lines and characters and the placeholder", ->
- fold = displayBuffer.createFold(4, 7)
-
- # preceding fold: identity
- expect(displayBuffer.screenPositionForBufferPosition([3, 0])).toEqual [3, 0]
- expect(displayBuffer.screenPositionForBufferPosition([4, 0])).toEqual [4, 0]
-
- expect(displayBuffer.bufferPositionForScreenPosition([3, 0])).toEqual [3, 0]
- expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual [4, 0]
-
- # inside of fold: translate to the start of the fold
- expect(displayBuffer.screenPositionForBufferPosition([4, 35])).toEqual [4, 0]
- expect(displayBuffer.screenPositionForBufferPosition([5, 5])).toEqual [4, 0]
-
- # following fold
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [5, 0]
- expect(displayBuffer.screenPositionForBufferPosition([11, 2])).toEqual [8, 2]
-
- expect(displayBuffer.bufferPositionForScreenPosition([5, 0])).toEqual [8, 0]
- expect(displayBuffer.bufferPositionForScreenPosition([9, 2])).toEqual [12, 2]
-
- # clip screen positions before translating
- expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0])
- expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([200, 0])
-
- # after fold is destroyed
- fold.destroy()
-
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [8, 0]
- expect(displayBuffer.screenPositionForBufferPosition([11, 2])).toEqual [11, 2]
-
- expect(displayBuffer.bufferPositionForScreenPosition([5, 0])).toEqual [5, 0]
- expect(displayBuffer.bufferPositionForScreenPosition([9, 2])).toEqual [9, 2]
-
- describe ".unfoldBufferRow(row)", ->
- it "destroys all folds containing the given row", ->
- displayBuffer.createFold(2, 4)
- displayBuffer.createFold(2, 6)
- displayBuffer.createFold(7, 8)
- displayBuffer.createFold(1, 9)
- displayBuffer.createFold(11, 12)
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '10'
-
- displayBuffer.unfoldBufferRow(2)
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(7).fold).toBeDefined()
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toMatch /^9-+/
- expect(displayBuffer.tokenizedLineForScreenRow(10).fold).toBeDefined()
-
- describe ".outermostFoldsInBufferRowRange(startRow, endRow)", ->
- it "returns the outermost folds entirely contained in the given row range, exclusive of end row", ->
- fold1 = displayBuffer.createFold(4, 7)
- fold2 = displayBuffer.createFold(5, 6)
- fold3 = displayBuffer.createFold(11, 15)
- fold4 = displayBuffer.createFold(12, 13)
- fold5 = displayBuffer.createFold(16, 17)
-
- expect(displayBuffer.outermostFoldsInBufferRowRange(3, 18)).toEqual [fold1, fold3, fold5]
- expect(displayBuffer.outermostFoldsInBufferRowRange(5, 16)).toEqual [fold3]
-
- describe "::clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, clip: 'closest')", ->
- beforeEach ->
- tabLength = 4
-
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setTabLength(tabLength)
- displayBuffer.setSoftWrapped(true)
- displayBuffer.setEditorWidthInChars(50)
-
- it "allows valid positions", ->
- expect(displayBuffer.clipScreenPosition([4, 5])).toEqual [4, 5]
- expect(displayBuffer.clipScreenPosition([4, 11])).toEqual [4, 11]
-
- it "disallows negative positions", ->
- expect(displayBuffer.clipScreenPosition([-1, -1])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([-1, 10])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, -1])).toEqual [0, 0]
-
- it "disallows positions beyond the last row", ->
- expect(displayBuffer.clipScreenPosition([1000, 0])).toEqual [15, 2]
- expect(displayBuffer.clipScreenPosition([1000, 1000])).toEqual [15, 2]
-
- describe "when wrapBeyondNewlines is false (the default)", ->
- it "wraps positions beyond the end of hard newlines to the end of the line", ->
- expect(displayBuffer.clipScreenPosition([1, 10000])).toEqual [1, 30]
- expect(displayBuffer.clipScreenPosition([4, 30])).toEqual [4, 15]
- expect(displayBuffer.clipScreenPosition([4, 1000])).toEqual [4, 15]
-
- describe "when wrapBeyondNewlines is true", ->
- it "wraps positions past the end of hard newlines to the next line", ->
- expect(displayBuffer.clipScreenPosition([0, 29], wrapBeyondNewlines: true)).toEqual [0, 29]
- expect(displayBuffer.clipScreenPosition([0, 30], wrapBeyondNewlines: true)).toEqual [1, 0]
- expect(displayBuffer.clipScreenPosition([0, 1000], wrapBeyondNewlines: true)).toEqual [1, 0]
-
- it "wraps positions in the middle of fold lines to the next screen line", ->
- displayBuffer.createFold(3, 5)
- expect(displayBuffer.clipScreenPosition([3, 5], wrapBeyondNewlines: true)).toEqual [4, 0]
-
- describe "when skipSoftWrapIndentation is false (the default)", ->
- it "wraps positions at the end of previous soft-wrapped line", ->
- expect(displayBuffer.clipScreenPosition([4, 0])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([4, 1])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([4, 3])).toEqual [3, 50]
-
- describe "when skipSoftWrapIndentation is true", ->
- it "clips positions to the beginning of the line", ->
- expect(displayBuffer.clipScreenPosition([4, 0], skipSoftWrapIndentation: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([4, 1], skipSoftWrapIndentation: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([4, 3], skipSoftWrapIndentation: true)).toEqual [4, 4]
-
- describe "when wrapAtSoftNewlines is false (the default)", ->
- it "clips positions at the end of soft-wrapped lines to the character preceding the end of the line", ->
- expect(displayBuffer.clipScreenPosition([3, 50])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 51])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 58])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 1000])).toEqual [3, 50]
-
- describe "when wrapAtSoftNewlines is true", ->
- it "wraps positions at the end of soft-wrapped lines to the next screen line", ->
- expect(displayBuffer.clipScreenPosition([3, 50], wrapAtSoftNewlines: true)).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 51], wrapAtSoftNewlines: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([3, 58], wrapAtSoftNewlines: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([3, 1000], wrapAtSoftNewlines: true)).toEqual [4, 4]
-
- describe "when clip is 'closest' (the default)", ->
- it "clips screen positions in the middle of atomic tab characters to the closest edge of the character", ->
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.clipScreenPosition([0, 0])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, 1])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, 2])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, tabLength-1])).toEqual [0, tabLength]
- expect(displayBuffer.clipScreenPosition([0, tabLength])).toEqual [0, tabLength]
-
- describe "when clip is 'backward'", ->
- it "clips screen positions in the middle of atomic tab characters to the beginning of the character", ->
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.clipScreenPosition([0, 0], clip: 'backward')).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, tabLength-1], clip: 'backward')).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'backward')).toEqual [0, tabLength]
-
- describe "when clip is 'forward'", ->
- it "clips screen positions in the middle of atomic tab characters to the end of the character", ->
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.clipScreenPosition([0, 0], clip: 'forward')).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, 1], clip: 'forward')).toEqual [0, tabLength]
- expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'forward')).toEqual [0, tabLength]
-
- describe "::screenPositionForBufferPosition(bufferPosition, options)", ->
- it "clips the specified buffer position", ->
- expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 2]
- expect(displayBuffer.screenPositionForBufferPosition([0, 100000])).toEqual [0, 29]
- expect(displayBuffer.screenPositionForBufferPosition([100000, 0])).toEqual [12, 2]
- expect(displayBuffer.screenPositionForBufferPosition([100000, 100000])).toEqual [12, 2]
-
- it "clips to the (left or right) edge of an atomic token without simply rounding up", ->
- tabLength = 4
- displayBuffer.setTabLength(tabLength)
-
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, tabLength]
-
- it "clips to the edge closest to the given position when it's inside a soft tab", ->
- tabLength = 4
- displayBuffer.setTabLength(tabLength)
-
- buffer.insert([0, 0], ' ')
- expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 3])).toEqual [0, 4]
- expect(displayBuffer.screenPositionForBufferPosition([0, 4])).toEqual [0, 4]
-
- describe "position translation in the presence of hard tabs", ->
- it "correctly translates positions on either side of a tab", ->
- buffer.setText('\t')
- expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, 2]
- expect(displayBuffer.bufferPositionForScreenPosition([0, 2])).toEqual [0, 1]
-
- it "correctly translates positions on soft wrapped lines containing tabs", ->
- buffer.setText('\t\taa bb cc dd ee ff gg')
- displayBuffer.setSoftWrapped(true)
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setEditorWidthInChars(10)
- expect(displayBuffer.screenPositionForBufferPosition([0, 10], wrapAtSoftNewlines: true)).toEqual [1, 4]
- expect(displayBuffer.bufferPositionForScreenPosition([1, 0])).toEqual [0, 9]
-
- describe "::getMaxLineLength()", ->
- it "returns the length of the longest screen line", ->
- expect(displayBuffer.getMaxLineLength()).toBe 65
- buffer.delete([[6, 0], [6, 65]])
- expect(displayBuffer.getMaxLineLength()).toBe 62
-
- it "correctly updates the location of the longest screen line when changes occur", ->
- expect(displayBuffer.getLongestScreenRow()).toBe 6
- buffer.delete([[3, 0], [5, 0]])
- expect(displayBuffer.getLongestScreenRow()).toBe 4
-
- buffer.delete([[4, 0], [5, 0]])
- expect(displayBuffer.getLongestScreenRow()).toBe 5
- expect(displayBuffer.getMaxLineLength()).toBe 56
-
- buffer.delete([[6, 0], [8, 0]])
- expect(displayBuffer.getLongestScreenRow()).toBe 5
- expect(displayBuffer.getMaxLineLength()).toBe 56
-
- describe "::destroy()", ->
- it "unsubscribes all display buffer markers from their underlying buffer marker (regression)", ->
- marker = displayBuffer.markBufferPosition([12, 2])
- displayBuffer.destroy()
- expect( -> buffer.insert([12, 2], '\n')).not.toThrow()
-
- describe "markers", ->
- beforeEach ->
- displayBuffer.createFold(4, 7)
-
- describe "marker creation and manipulation", ->
- it "allows markers to be created in terms of both screen and buffer coordinates", ->
- marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker2 = displayBuffer.markBufferRange([[8, 4], [8, 10]])
- expect(marker1.getBufferRange()).toEqual [[8, 4], [8, 10]]
- expect(marker2.getScreenRange()).toEqual [[5, 4], [5, 10]]
-
- it "emits a 'marker-created' event on the DisplayBuffer whenever a marker is created", ->
- displayBuffer.onDidCreateMarker markerCreatedHandler = jasmine.createSpy("markerCreatedHandler")
-
- marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- expect(markerCreatedHandler).toHaveBeenCalledWith(marker1)
- markerCreatedHandler.reset()
-
- marker2 = buffer.markRange([[5, 4], [5, 10]])
- expect(markerCreatedHandler).toHaveBeenCalledWith(displayBuffer.getMarker(marker2.id))
-
- it "allows marker head and tail positions to be manipulated in both screen and buffer coordinates", ->
- marker = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker.setHeadScreenPosition([5, 4])
- marker.setTailBufferPosition([5, 4])
- expect(marker.isReversed()).toBeFalsy()
- expect(marker.getBufferRange()).toEqual [[5, 4], [8, 4]]
- marker.setHeadBufferPosition([5, 4])
- marker.setTailScreenPosition([5, 4])
- expect(marker.isReversed()).toBeTruthy()
- expect(marker.getBufferRange()).toEqual [[5, 4], [8, 4]]
-
- it "returns whether a position changed when it is assigned", ->
- marker = displayBuffer.markScreenRange([[0, 0], [0, 0]])
- expect(marker.setHeadScreenPosition([5, 4])).toBeTruthy()
- expect(marker.setHeadScreenPosition([5, 4])).toBeFalsy()
- expect(marker.setHeadBufferPosition([1, 0])).toBeTruthy()
- expect(marker.setHeadBufferPosition([1, 0])).toBeFalsy()
- expect(marker.setTailScreenPosition([5, 4])).toBeTruthy()
- expect(marker.setTailScreenPosition([5, 4])).toBeFalsy()
- expect(marker.setTailBufferPosition([1, 0])).toBeTruthy()
- expect(marker.setTailBufferPosition([1, 0])).toBeFalsy()
-
- describe "marker change events", ->
- [markerChangedHandler, marker] = []
-
- beforeEach ->
- marker = displayBuffer.addMarkerLayer(maintainHistory: true).markScreenRange([[5, 4], [5, 10]])
- marker.onDidChange markerChangedHandler = jasmine.createSpy("markerChangedHandler")
-
- it "triggers the 'changed' event whenever the markers head's screen position changes in the buffer or on screen", ->
- marker.setHeadScreenPosition([8, 20])
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [8, 20]
- newHeadBufferPosition: [11, 20]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: false
- isValid: true
- }
- markerChangedHandler.reset()
-
- buffer.insert([11, 0], '...')
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [8, 20]
- oldHeadBufferPosition: [11, 20]
- newHeadScreenPosition: [8, 23]
- newHeadBufferPosition: [11, 23]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: true
- isValid: true
- }
- markerChangedHandler.reset()
-
- displayBuffer.unfoldBufferRow(4)
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [8, 23]
- oldHeadBufferPosition: [11, 23]
- newHeadScreenPosition: [11, 23]
- newHeadBufferPosition: [11, 23]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [8, 4]
- newTailBufferPosition: [8, 4]
- textChanged: false
- isValid: true
- }
- markerChangedHandler.reset()
-
- displayBuffer.createFold(4, 7)
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [11, 23]
- oldHeadBufferPosition: [11, 23]
- newHeadScreenPosition: [8, 23]
- newHeadBufferPosition: [11, 23]
- oldTailScreenPosition: [8, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: false
- isValid: true
- }
-
- it "triggers the 'changed' event whenever the marker tail's position changes in the buffer or on screen", ->
- marker.setTailScreenPosition([8, 20])
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [5, 10]
- newHeadBufferPosition: [8, 10]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [8, 20]
- newTailBufferPosition: [11, 20]
- textChanged: false
- isValid: true
- }
- markerChangedHandler.reset()
-
- buffer.insert([11, 0], '...')
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [5, 10]
- newHeadBufferPosition: [8, 10]
- oldTailScreenPosition: [8, 20]
- oldTailBufferPosition: [11, 20]
- newTailScreenPosition: [8, 23]
- newTailBufferPosition: [11, 23]
- textChanged: true
- isValid: true
- }
-
- it "triggers the 'changed' event whenever the marker is invalidated or revalidated", ->
- buffer.deleteRow(8)
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [5, 0]
- newHeadBufferPosition: [8, 0]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 0]
- newTailBufferPosition: [8, 0]
- textChanged: true
- isValid: false
- }
-
- markerChangedHandler.reset()
- buffer.undo()
-
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 0]
- oldHeadBufferPosition: [8, 0]
- newHeadScreenPosition: [5, 10]
- newHeadBufferPosition: [8, 10]
- oldTailScreenPosition: [5, 0]
- oldTailBufferPosition: [8, 0]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: true
- isValid: true
- }
-
- it "does not call the callback for screen changes that don't change the position of the marker", ->
- displayBuffer.createFold(10, 11)
- expect(markerChangedHandler).not.toHaveBeenCalled()
-
- it "updates markers before emitting buffer change events, but does not notify their observers until the change event", ->
- marker2 = displayBuffer.addMarkerLayer(maintainHistory: true).markBufferRange([[8, 1], [8, 1]])
- marker2.onDidChange marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler")
- displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange()
-
- # New change ----
-
- onDisplayBufferChange = ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- expect(marker2ChangedHandler).not.toHaveBeenCalled()
- # but still updates the markers
- expect(marker.getScreenRange()).toEqual [[5, 7], [5, 13]]
- expect(marker.getHeadScreenPosition()).toEqual [5, 13]
- expect(marker.getTailScreenPosition()).toEqual [5, 7]
- expect(marker2.isValid()).toBeFalsy()
-
- buffer.setTextInRange([[8, 0], [8, 2]], ".....")
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(marker2ChangedHandler).toHaveBeenCalled()
-
- # Undo change ----
-
- changeHandler.reset()
- markerChangedHandler.reset()
- marker2ChangedHandler.reset()
-
- marker3 = displayBuffer.markBufferRange([[8, 1], [8, 2]])
- marker3.onDidChange marker3ChangedHandler = jasmine.createSpy("marker3ChangedHandler")
-
- onDisplayBufferChange = ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- expect(marker2ChangedHandler).not.toHaveBeenCalled()
- expect(marker3ChangedHandler).not.toHaveBeenCalled()
-
- # markers positions are updated based on the text change
- expect(marker.getScreenRange()).toEqual [[5, 4], [5, 10]]
- expect(marker.getHeadScreenPosition()).toEqual [5, 10]
- expect(marker.getTailScreenPosition()).toEqual [5, 4]
-
- buffer.undo()
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(marker2ChangedHandler).toHaveBeenCalled()
- expect(marker3ChangedHandler).toHaveBeenCalled()
- expect(marker2.isValid()).toBeTruthy()
- expect(marker3.isValid()).toBeFalsy()
-
- # Redo change ----
-
- changeHandler.reset()
- markerChangedHandler.reset()
- marker2ChangedHandler.reset()
- marker3ChangedHandler.reset()
-
- onDisplayBufferChange = ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- expect(marker2ChangedHandler).not.toHaveBeenCalled()
- expect(marker3ChangedHandler).not.toHaveBeenCalled()
-
- # markers positions are updated based on the text change
- expect(marker.getScreenRange()).toEqual [[5, 7], [5, 13]]
- expect(marker.getHeadScreenPosition()).toEqual [5, 13]
- expect(marker.getTailScreenPosition()).toEqual [5, 7]
-
- # but marker snapshots are not restored until the end of the undo.
- expect(marker2.isValid()).toBeFalsy()
- expect(marker3.isValid()).toBeFalsy()
-
- buffer.redo()
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(marker2ChangedHandler).toHaveBeenCalled()
- expect(marker3ChangedHandler).toHaveBeenCalled()
-
- it "updates the position of markers before emitting change events that aren't caused by a buffer change", ->
- displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- # but still updates the markers
- expect(marker.getScreenRange()).toEqual [[8, 4], [8, 10]]
- expect(marker.getHeadScreenPosition()).toEqual [8, 10]
- expect(marker.getTailScreenPosition()).toEqual [8, 4]
-
- displayBuffer.unfoldBufferRow(4)
-
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
-
- it "emits the correct events when markers are mutated inside event listeners", ->
- marker.onDidChange ->
- if marker.getHeadScreenPosition().isEqual([5, 9])
- marker.setHeadScreenPosition([5, 8])
-
- marker.setHeadScreenPosition([5, 9])
-
- headChanges = for [event] in markerChangedHandler.argsForCall
- {old: event.oldHeadScreenPosition, new: event.newHeadScreenPosition}
-
- expect(headChanges).toEqual [
- {old: [5, 10], new: [5, 9]}
- {old: [5, 9], new: [5, 8]}
- ]
-
- describe "::findMarkers(attributes)", ->
- it "allows the startBufferRow and endBufferRow to be specified", ->
- marker1 = displayBuffer.markBufferRange([[0, 0], [3, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[0, 0], [5, 0]], class: 'a')
- marker3 = displayBuffer.markBufferRange([[9, 0], [10, 0]], class: 'b')
-
- expect(displayBuffer.findMarkers(class: 'a', startBufferRow: 0)).toEqual [marker2, marker1]
- expect(displayBuffer.findMarkers(class: 'a', startBufferRow: 0, endBufferRow: 3)).toEqual [marker1]
- expect(displayBuffer.findMarkers(endBufferRow: 10)).toEqual [marker3]
-
- it "allows the startScreenRow and endScreenRow to be specified", ->
- marker1 = displayBuffer.markBufferRange([[6, 0], [7, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[9, 0], [10, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', startScreenRow: 6, endScreenRow: 7)).toEqual [marker2]
-
- it "allows intersectsBufferRowRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRowRange: [5, 6])).toEqual [marker1]
-
- it "allows intersectsScreenRowRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 10])).toEqual [marker2]
-
- it "allows containedInScreenRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', containedInScreenRange: [[5, 0], [7, 0]])).toEqual [marker2]
-
- it "allows intersectsBufferRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRange: [[5, 0], [6, 0]])).toEqual [marker1]
-
- it "allows intersectsScreenRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRange: [[5, 0], [10, 0]])).toEqual [marker2]
-
- describe "marker destruction", ->
- it "allows markers to be destroyed", ->
- marker = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker.destroy()
- expect(marker.isValid()).toBeFalsy()
- expect(displayBuffer.getMarker(marker.id)).toBeUndefined()
-
- it "notifies ::onDidDestroy observers when markers are destroyed", ->
- destroyedHandler = jasmine.createSpy("destroyedHandler")
- marker = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker.onDidDestroy destroyedHandler
- marker.destroy()
- expect(destroyedHandler).toHaveBeenCalled()
- destroyedHandler.reset()
-
- marker2 = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker2.onDidDestroy destroyedHandler
- buffer.getMarker(marker2.id).destroy()
- expect(destroyedHandler).toHaveBeenCalled()
-
- describe "Marker::copy(attributes)", ->
- it "creates a copy of the marker with the given attributes merged in", ->
- initialMarkerCount = displayBuffer.getMarkerCount()
- marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]], a: 1, b: 2)
- expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 1
-
- marker2 = marker1.copy(b: 3)
- expect(marker2.getBufferRange()).toEqual marker1.getBufferRange()
- expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 2
- expect(marker1.getProperties()).toEqual a: 1, b: 2
- expect(marker2.getProperties()).toEqual a: 1, b: 3
-
- describe 'when there are multiple DisplayBuffers for a buffer', ->
- describe 'when a marker is created', ->
- it 'the second display buffer will not emit a marker-created event when the marker has been deleted in the first marker-created event', ->
- displayBuffer2 = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- displayBuffer.onDidCreateMarker markerCreated1 = jasmine.createSpy().andCallFake (marker) -> marker.destroy()
- displayBuffer2.onDidCreateMarker markerCreated2 = jasmine.createSpy()
-
- displayBuffer.markBufferRange([[0, 0], [1, 5]], {})
-
- expect(markerCreated1).toHaveBeenCalled()
- expect(markerCreated2).not.toHaveBeenCalled()
-
- describe "decorations", ->
- [marker, decoration, decorationProperties] = []
- beforeEach ->
- marker = displayBuffer.markBufferRange([[2, 13], [3, 15]])
- decorationProperties = {type: 'line-number', class: 'one'}
- decoration = displayBuffer.decorateMarker(marker, decorationProperties)
-
- it "can add decorations associated with markers and remove them", ->
- expect(decoration).toBeDefined()
- expect(decoration.getProperties()).toBe decorationProperties
- expect(displayBuffer.decorationForId(decoration.id)).toBe decoration
- expect(displayBuffer.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
-
- decoration.destroy()
- expect(displayBuffer.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
- expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined()
-
- it "will not fail if the decoration is removed twice", ->
- decoration.destroy()
- decoration.destroy()
- expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined()
-
- it "does not allow destroyed markers to be decorated", ->
- marker.destroy()
- expect(->
- displayBuffer.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
- ).toThrow("Cannot decorate a destroyed marker")
- expect(displayBuffer.getOverlayDecorations()).toEqual []
-
- describe "when a decoration is updated via Decoration::update()", ->
- it "emits an 'updated' event containing the new and old params", ->
- decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
- decoration.setProperties type: 'line-number', class: 'two'
-
- {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
- expect(oldProperties).toEqual decorationProperties
- expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
-
- describe "::getDecorations(properties)", ->
- it "returns decorations matching the given optional properties", ->
- expect(displayBuffer.getDecorations()).toEqual [decoration]
- expect(displayBuffer.getDecorations(class: 'two').length).toEqual 0
- expect(displayBuffer.getDecorations(class: 'one').length).toEqual 1
-
- describe "::scrollToScreenPosition(position, [options])", ->
- it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", ->
- scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll")
- displayBuffer.onDidRequestAutoscroll(scrollSpy)
-
- displayBuffer.scrollToScreenPosition([8, 20])
- displayBuffer.scrollToScreenPosition([8, 20], center: true)
- displayBuffer.scrollToScreenPosition([8, 20], center: false, reversed: true)
-
- expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {})
- expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true})
- expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true})
-
- describe "::decorateMarker", ->
- describe "when decorating gutters", ->
- [marker] = []
-
- beforeEach ->
- marker = displayBuffer.markBufferRange([[1, 0], [1, 0]])
-
- it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
- decorationProperties = {type: 'line-number', class: 'one'}
- decoration = displayBuffer.decorateMarker(marker, decorationProperties)
- expect(decoration.isType('line-number')).toBe true
- expect(decoration.isType('gutter')).toBe true
- expect(decoration.getProperties().gutterName).toBe 'line-number'
- expect(decoration.getProperties().class).toBe 'one'
-
- it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
- decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
- decoration = displayBuffer.decorateMarker(marker, decorationProperties)
- expect(decoration.isType('gutter')).toBe true
- expect(decoration.isType('line-number')).toBe false
- expect(decoration.getProperties().gutterName).toBe 'test-gutter'
- expect(decoration.getProperties().class).toBe 'one'
diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee
index 38716ab3e..c3396ff9f 100644
--- a/spec/fake-lines-yardstick.coffee
+++ b/spec/fake-lines-yardstick.coffee
@@ -1,8 +1,10 @@
{Point} = require 'text-buffer'
+{isPairedCharacter} = require '../src/text-utils'
module.exports =
class FakeLinesYardstick
constructor: (@model, @lineTopIndex) ->
+ {@displayLayer} = @model
@characterWidthsByScope = {}
getScopedCharacterWidth: (scopeNames, char) ->
@@ -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}
diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee
index 26bb19b0e..d8f545abe 100644
--- a/spec/language-mode-spec.coffee
+++ b/spec/language-mode-spec.coffee
@@ -334,66 +334,56 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
- fold1 = editor.tokenizedLineForScreenRow(0).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 12]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(1).fold
- expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 9]
- fold2.destroy()
-
- fold3 = editor.tokenizedLineForScreenRow(4).fold
- expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [4, 7]
+ [fold1, fold2, fold3] = languageMode.unfoldAll()
+ expect([fold1.start.row, fold1.end.row]).toEqual [0, 12]
+ expect([fold2.start.row, fold2.end.row]).toEqual [1, 9]
+ expect([fold3.start.row, fold3.end.row]).toEqual [4, 7]
describe ".foldBufferRow(bufferRow)", ->
describe "when bufferRow can be folded", ->
it "creates a fold based on the syntactic region starting at the given row", ->
languageMode.foldBufferRow(1)
- fold = editor.tokenizedLineForScreenRow(1).fold
- expect(fold.getStartRow()).toBe 1
- expect(fold.getEndRow()).toBe 9
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when bufferRow can't be folded", ->
it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", ->
languageMode.foldBufferRow(8)
- fold = editor.tokenizedLineForScreenRow(1).fold
- expect(fold.getStartRow()).toBe 1
- expect(fold.getEndRow()).toBe 9
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when the bufferRow is already folded", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
languageMode.foldBufferRow(2)
- expect(editor.tokenizedLineForScreenRow(1).fold).toBeDefined()
- expect(editor.tokenizedLineForScreenRow(0).fold).not.toBeDefined()
+ expect(editor.isFoldedAtBufferRow(0)).toBe(false)
+ expect(editor.isFoldedAtBufferRow(1)).toBe(true)
languageMode.foldBufferRow(1)
- expect(editor.tokenizedLineForScreenRow(0).fold).toBeDefined()
+ expect(editor.isFoldedAtBufferRow(0)).toBe(true)
describe "when the bufferRow is in a multi-line comment", ->
it "searches upward and downward for surrounding comment lines and folds them as a single fold", ->
buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment")
languageMode.foldBufferRow(1)
- fold = editor.tokenizedLineForScreenRow(1).fold
- expect(fold.getStartRow()).toBe 1
- expect(fold.getEndRow()).toBe 3
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [1, 3]
describe "when the bufferRow is a single-line comment", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
buffer.insert([1, 0], " //this is a single line comment\n")
languageMode.foldBufferRow(1)
- fold = editor.tokenizedLineForScreenRow(0).fold
- expect(fold.getStartRow()).toBe 0
- expect(fold.getEndRow()).toBe 13
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [0, 13]
describe ".foldAllAtIndentLevel(indentLevel)", ->
it "folds blocks of text at the given indentation level", ->
languageMode.foldAllAtIndentLevel(0)
- expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
+ expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 0
languageMode.foldAllAtIndentLevel(1)
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
- expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {"
+ expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 4
languageMode.foldAllAtIndentLevel(2)
@@ -429,59 +419,35 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
- fold1 = editor.tokenizedLineForScreenRow(0).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(1).fold
- expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 4]
-
- fold3 = editor.tokenizedLineForScreenRow(2).fold.destroy()
-
- fold4 = editor.tokenizedLineForScreenRow(3).fold
- expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8]
-
- fold5 = editor.tokenizedLineForScreenRow(6).fold
- expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [11, 16]
- fold5.destroy()
-
- fold6 = editor.tokenizedLineForScreenRow(13).fold
- expect([fold6.getStartRow(), fold6.getEndRow()]).toEqual [21, 22]
- fold6.destroy()
+ folds = languageMode.unfoldAll()
+ expect(folds.length).toBe 8
+ expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
+ expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4]
+ expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27]
+ expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8]
+ expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16]
+ expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20]
+ expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22]
+ expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25]
describe ".foldAllAtIndentLevel()", ->
it "folds every foldable range at a given indentLevel", ->
languageMode.foldAllAtIndentLevel(2)
- fold1 = editor.tokenizedLineForScreenRow(6).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [6, 8]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(11).fold
- expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 16]
- fold2.destroy()
-
- fold3 = editor.tokenizedLineForScreenRow(17).fold
- expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [17, 20]
- fold3.destroy()
-
- fold4 = editor.tokenizedLineForScreenRow(21).fold
- expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [21, 22]
- fold4.destroy()
-
- fold5 = editor.tokenizedLineForScreenRow(24).fold
- expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [24, 25]
- fold5.destroy()
+ folds = languageMode.unfoldAll()
+ expect(folds.length).toBe 5
+ expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8]
+ expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16]
+ expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20]
+ expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22]
+ expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25]
it "does not fold anything but the indentLevel", ->
languageMode.foldAllAtIndentLevel(0)
- fold1 = editor.tokenizedLineForScreenRow(0).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(5).fold
- expect(fold2).toBeFalsy()
+ folds = languageMode.unfoldAll()
+ expect(folds.length).toBe 1
+ expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
describe ".isFoldableAtBufferRow(bufferRow)", ->
it "returns true if the line starts a multi-line comment", ->
diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee
index 46935510f..bb0294b54 100644
--- a/spec/lines-yardstick-spec.coffee
+++ b/spec/lines-yardstick-spec.coffee
@@ -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 = 'foobar'
+ jasmine.attachToDOM(lineNode)
+ createdLineNodes.push(lineNode)
+ lineNode
+ editor.setText("foobar")
+
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6])
it "clips pixel positions above buffer start", ->
expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0]
@@ -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]
diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee
index 953cf103e..17fea7360 100644
--- a/spec/project-spec.coffee
+++ b/spec/project-spec.coffee
@@ -87,7 +87,7 @@ describe "Project", ->
runs ->
bufferA = atom.project.getBuffers()[0]
- layerA = bufferA.addMarkerLayer(maintainHistory: true)
+ layerA = bufferA.addMarkerLayer(persistent: true)
markerA = layerA.markPosition([0, 3])
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee
index 3924a8412..cada2fe22 100644
--- a/spec/random-editor-spec.coffee
+++ b/spec/random-editor-spec.coffee
@@ -17,7 +17,7 @@ describe "TextEditor", ->
buffer = new TextBuffer
editor = atom.workspace.buildTextEditor({buffer})
editor.setEditorWidthInChars(80)
- tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
+ tokenizedBuffer = editor.tokenizedBuffer
steps = []
times 30, ->
@@ -33,8 +33,8 @@ describe "TextEditor", ->
logLines()
throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", )
- actualScreenLine = editor.tokenizedLineForScreenRow(screenRow)
- unless actualScreenLine.text is referenceScreenLine.text
+ actualScreenLine = editor.lineTextForScreenRow(screenRow)
+ unless actualScreenLine is referenceScreenLine
logLines()
throw new Error("Invalid line text at screen row #{screenRow}")
@@ -84,7 +84,8 @@ describe "TextEditor", ->
referenceEditor.setEditorWidthInChars(80)
referenceEditor.setText(editor.getText())
referenceEditor.setSoftWrapped(editor.isSoftWrapped())
- screenLines = referenceEditor.tokenizedLinesForScreenRows(0, referenceEditor.getLastScreenRow())
+
+ screenLines = [0..referenceEditor.getLastScreenRow()].map (row) => referenceEditor.lineTextForScreenRow(row)
bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow())
{screenLines, bufferRows}
diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee
index 18095d6f8..7511c4b39 100644
--- a/spec/selection-spec.coffee
+++ b/spec/selection-spec.coffee
@@ -101,3 +101,22 @@ describe "Selection", ->
selection.setBufferRange [[2, 0], [3, 0]]
selection.insertText("\r\n", autoIndent: true)
expect(buffer.lineForRow(2)).toBe " "
+
+ describe ".fold()", ->
+ it "folds the buffer range spanned by the selection", ->
+ selection.setBufferRange([[0, 3], [1, 6]])
+ selection.fold()
+
+ expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
+ expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
+ expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {"
+ expect(editor.isFoldedAtBufferRow(0)).toBe(true)
+
+ it "doesn't create a fold when the selection is empty", ->
+ selection.setBufferRange([[0, 3], [0, 3]])
+ selection.fold()
+
+ expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
+ expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
+ expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
+ expect(editor.isFoldedAtBufferRow(0)).toBe(false)
diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js
index 1b3a7b5c8..83f5d1b80 100644
--- a/spec/text-editor-component-spec.js
+++ b/spec/text-editor-component-spec.js
@@ -69,13 +69,12 @@ describe('TextEditorComponent', function () {
describe('line rendering', async function () {
function expectTileContainsRow (tileNode, screenRow, {top}) {
let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]')
- let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
-
+ let text = editor.lineTextForScreenRow(screenRow)
expect(lineNode.offsetTop).toBe(top)
- if (tokenizedLine.text === '') {
- 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('let' + invisibles.eol + '')
+ expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '')
})
it('displays trailing carriage returns using a visible, non-empty value', async function () {
@@ -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('CE')
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
editor.setTabLength(3)
await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ')
editor.setTabLength(1)
await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ')
editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ')
await nextViewUpdatePromise()
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
})
describe('when soft wrapping is enabled', function () {
@@ -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)
}
diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee
index f8117af09..8cdc4e61a 100644
--- a/spec/text-editor-presenter-spec.coffee
+++ b/spec/text-editor-presenter-spec.coffee
@@ -1143,53 +1143,6 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setScrollLeft(-300)
expect(getState(presenter).content.scrollLeft).toBe 0
- describe ".indentGuidesVisible", ->
- it "is initialized based on the editor.showIndentGuide config setting", ->
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- atom.config.set('editor.showIndentGuide', true)
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe true
-
- it "updates when the editor.showIndentGuide config setting changes", ->
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', true)
- expect(getState(presenter).content.indentGuidesVisible).toBe true
-
- expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', false)
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- it "updates when the editor's grammar changes", ->
- atom.config.set('editor.showIndentGuide', true, scopeSelector: ".source.js")
-
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- stateUpdated = false
- presenter.onDidUpdateState -> stateUpdated = true
-
- waitsForPromise -> atom.packages.activatePackage('language-javascript')
-
- runs ->
- expect(stateUpdated).toBe true
- expect(getState(presenter).content.indentGuidesVisible).toBe true
-
- expectStateUpdate presenter, -> editor.setGrammar(atom.grammars.selectGrammar('.txt'))
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- it "is always false when the editor is mini", ->
- atom.config.set('editor.showIndentGuide', true)
- editor.setMini(true)
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
- editor.setMini(false)
- expect(getState(presenter).content.indentGuidesVisible).toBe true
- editor.setMini(true)
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
describe ".backgroundColor", ->
it "is assigned to ::backgroundColor unless the editor is mini", ->
presenter = buildPresenter()
@@ -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()
diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee
index bf7543fff..8f4231323 100644
--- a/spec/text-editor-spec.coffee
+++ b/spec/text-editor-spec.coffee
@@ -39,21 +39,19 @@ describe "TextEditor", ->
it "preserves the invisibles setting", ->
atom.config.set('editor.showInvisibles', true)
- previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles
-
+ previousLineText = editor.lineTextForScreenRow(0)
editor2 = TextEditor.deserialize(editor.serialize(), atom)
-
- expect(previousInvisibles).toBeDefined()
- expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles
+ expect(editor2.lineTextForScreenRow(0)).toBe(previousLineText)
it "updates invisibles if the settings have changed between serialization and deserialization", ->
atom.config.set('editor.showInvisibles', true)
-
+ previousLineText = editor.lineTextForScreenRow(0)
state = editor.serialize()
atom.config.set('editor.invisibles', eol: '?')
editor2 = TextEditor.deserialize(state, atom)
- expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?'
+ expect(editor2.lineTextForScreenRow(0)).not.toBe(previousLineText)
+ expect(editor2.lineTextForScreenRow(0).endsWith('?')).toBe(true)
describe "when the editor is constructed with the largeFileMode option set to true", ->
it "loads the editor but doesn't tokenize", ->
@@ -64,15 +62,14 @@ describe "TextEditor", ->
runs ->
buffer = editor.getBuffer()
- expect(editor.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0)
- expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
- expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # soft tab
- expect(editor.tokenizedLineForScreenRow(12).text).toBe buffer.lineForRow(12)
- expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
+ expect(editor.lineTextForScreenRow(0)).toBe buffer.lineForRow(0)
+ expect(editor.tokensForScreenRow(0).length).toBe 1
+ expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab
+ expect(editor.lineTextForScreenRow(12)).toBe buffer.lineForRow(12)
expect(editor.getCursorScreenPosition()).toEqual [0, 0]
editor.insertText('hey"')
- expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
- expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # sof tab
+ expect(editor.tokensForScreenRow(0).length).toBe 1
+ expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab
describe ".copy()", ->
it "returns a different edit session with the same initial state", ->
@@ -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 ->
diff --git a/spec/text-utils-spec.coffee b/spec/text-utils-spec.coffee
index aa36c5003..bae7f5997 100644
--- a/spec/text-utils-spec.coffee
+++ b/spec/text-utils-spec.coffee
@@ -75,22 +75,23 @@ describe 'text utilities', ->
expect(textUtils.isKoreanCharacter("O")).toBe(false)
- describe ".isCJKCharacter(character)", ->
- it "returns true when the character is either a korean, half-width or double-width character", ->
- expect(textUtils.isCJKCharacter("我")).toBe(true)
- expect(textUtils.isCJKCharacter("私")).toBe(true)
- expect(textUtils.isCJKCharacter("B")).toBe(true)
- expect(textUtils.isCJKCharacter(",")).toBe(true)
- expect(textUtils.isCJKCharacter("¢")).toBe(true)
- expect(textUtils.isCJKCharacter("ハ")).toBe(true)
- expect(textUtils.isCJKCharacter("ヒ")).toBe(true)
- expect(textUtils.isCJKCharacter("ᆲ")).toBe(true)
- expect(textUtils.isCJKCharacter("■")).toBe(true)
- expect(textUtils.isCJKCharacter("우")).toBe(true)
- expect(textUtils.isCJKCharacter("가")).toBe(true)
- expect(textUtils.isCJKCharacter("ㅢ")).toBe(true)
- expect(textUtils.isCJKCharacter("ㄼ")).toBe(true)
+ describe ".isWrapBoundary(previousCharacter, character)", ->
+ it "returns true when the character is CJK or when the previous character is a space/tab", ->
+ anyCharacter = 'x'
+ expect(textUtils.isWrapBoundary(anyCharacter, "我")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "私")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "B")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, ",")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "¢")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ハ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ヒ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ᆲ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "■")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "우")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "가")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ㅢ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ㄼ")).toBe(true)
- expect(textUtils.isDoubleWidthCharacter("a")).toBe(false)
- expect(textUtils.isDoubleWidthCharacter("O")).toBe(false)
- expect(textUtils.isDoubleWidthCharacter("z")).toBe(false)
+ expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true)
+ expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true)
+ expect(textUtils.isWrapBoundary('a', 'h')).toBe(false)
diff --git a/spec/tokenized-buffer-iterator-spec.js b/spec/tokenized-buffer-iterator-spec.js
new file mode 100644
index 000000000..8d0e458f4
--- /dev/null
+++ b/spec/tokenized-buffer-iterator-spec.js
@@ -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([])
+ })
+})
diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee
index 7a1f4d221..ee418a386 100644
--- a/spec/tokenized-buffer-spec.coffee
+++ b/spec/tokenized-buffer-spec.coffee
@@ -1,5 +1,5 @@
TokenizedBuffer = require '../src/tokenized-buffer'
-TextBuffer = require 'text-buffer'
+{Point} = TextBuffer = require 'text-buffer'
_ = require 'underscore-plus'
describe "TokenizedBuffer", ->
@@ -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']
diff --git a/spec/tokenized-line-spec.coffee b/spec/tokenized-line-spec.coffee
deleted file mode 100644
index f1dce7b9e..000000000
--- a/spec/tokenized-line-spec.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-describe "TokenizedLine", ->
- editor = null
-
- beforeEach ->
- waitsForPromise -> atom.packages.activatePackage('language-coffee-script')
-
- describe "::isOnlyWhitespace()", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('coffee.coffee').then (o) -> editor = o
-
- it "returns true when the line is only whitespace", ->
- expect(editor.tokenizedLineForScreenRow(3).isOnlyWhitespace()).toBe true
- expect(editor.tokenizedLineForScreenRow(7).isOnlyWhitespace()).toBe true
- expect(editor.tokenizedLineForScreenRow(23).isOnlyWhitespace()).toBe true
-
- it "returns false when the line is not only whitespace", ->
- expect(editor.tokenizedLineForScreenRow(0).isOnlyWhitespace()).toBe false
- expect(editor.tokenizedLineForScreenRow(2).isOnlyWhitespace()).toBe false
diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee
index 6fa8001aa..f08e85558 100644
--- a/spec/workspace-spec.coffee
+++ b/spec/workspace-spec.coffee
@@ -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", ->
diff --git a/src/config-schema.coffee b/src/config-schema.coffee
index 346551ff5..ed6691380 100644
--- a/src/config-schema.coffee
+++ b/src/config-schema.coffee
@@ -155,6 +155,10 @@ module.exports =
type: 'boolean'
default: true
description: 'Show line numbers in the editor\'s gutter.'
+ atomicSoftTabs:
+ type: 'boolean'
+ default: true
+ description: 'Skip over tab-length runs of leading whitespace when moving the cursor.'
autoIndent:
type: 'boolean'
default: true
diff --git a/src/cursor.coffee b/src/cursor.coffee
index 9a8e9f6d0..b8f7c72b4 100644
--- a/src/cursor.coffee
+++ b/src/cursor.coffee
@@ -9,7 +9,7 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
# where text can be inserted.
#
# Cursors belong to {TextEditor}s and have some metadata attached in the form
-# of a {TextEditorMarker}.
+# of a {DisplayMarker}.
module.exports =
class Cursor extends Model
screenPosition: null
@@ -129,7 +129,7 @@ class Cursor extends Model
Section: Cursor Position Details
###
- # Public: Returns the underlying {TextEditorMarker} for the cursor.
+ # Public: Returns the underlying {DisplayMarker} for the cursor.
# Useful with overlay {Decoration}s.
getMarker: -> @marker
@@ -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: ->
diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee
new file mode 100644
index 000000000..edb9dfb33
--- /dev/null
+++ b/src/decoration-manager.coffee
@@ -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]
diff --git a/src/decoration.coffee b/src/decoration.coffee
index 11e32236d..63be29d86 100644
--- a/src/decoration.coffee
+++ b/src/decoration.coffee
@@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
decorationParams.gutterName = 'line-number'
decorationParams
-# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is
+# Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
# basically a visual representation of a marker. It allows you to add CSS
# classes to line numbers in the gutter, lines, and add selection-line regions
# around marked ranges of text.
@@ -25,7 +25,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
# ```
#
-# Best practice for destroying the decoration is by destroying the {TextEditorMarker}.
+# Best practice for destroying the decoration is by destroying the {DisplayMarker}.
#
# ```coffee
# marker.destroy()
@@ -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'
diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee
deleted file mode 100644
index 109b791a1..000000000
--- a/src/display-buffer.coffee
+++ /dev/null
@@ -1,1146 +0,0 @@
-_ = require 'underscore-plus'
-{CompositeDisposable, Emitter} = require 'event-kit'
-{Point, Range} = require 'text-buffer'
-TokenizedBuffer = require './tokenized-buffer'
-RowMap = require './row-map'
-Fold = require './fold'
-Model = require './model'
-Token = require './token'
-Decoration = require './decoration'
-LayerDecoration = require './layer-decoration'
-TextEditorMarkerLayer = require './text-editor-marker-layer'
-
-class BufferToScreenConversionError extends Error
- constructor: (@message, @metadata) ->
- super
- Error.captureStackTrace(this, BufferToScreenConversionError)
-
-module.exports =
-class DisplayBuffer extends Model
- verticalScrollMargin: 2
- horizontalScrollMargin: 6
- changeCount: 0
- softWrapped: null
- editorWidthInChars: null
- lineHeightInPixels: null
- defaultCharWidth: null
- height: null
- width: null
- didUpdateDecorationsEventScheduled: false
- updatedSynchronously: false
-
- @deserialize: (state, atomEnvironment) ->
- state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
- state.foldsMarkerLayer = state.tokenizedBuffer.buffer.getMarkerLayer(state.foldsMarkerLayerId)
- state.config = atomEnvironment.config
- state.assert = atomEnvironment.assert
- state.grammarRegistry = atomEnvironment.grammars
- new this(state)
-
- constructor: (params={}) ->
- super
-
- {
- tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer,
- ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry
- } = params
-
- @emitter = new Emitter
- @disposables = new CompositeDisposable
-
- @tokenizedBuffer ?= new TokenizedBuffer({
- tabLength, buffer, ignoreInvisibles, @largeFileMode, @config,
- @grammarRegistry, @assert
- })
- @buffer = @tokenizedBuffer.buffer
- @charWidthsByScope = {}
- @defaultMarkerLayer = new TextEditorMarkerLayer(this, @buffer.getDefaultMarkerLayer(), true)
- @customMarkerLayersById = {}
- @foldsByMarkerId = {}
- @decorationsById = {}
- @decorationsByMarkerId = {}
- @overlayDecorationsById = {}
- @layerDecorationsByMarkerLayerId = {}
- @decorationCountsByLayerId = {}
- @layerUpdateDisposablesByLayerId = {}
-
- @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
- @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange
- @disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker
-
- @foldsMarkerLayer ?= @buffer.addMarkerLayer()
- folds = (new Fold(this, marker) for marker in @foldsMarkerLayer.getMarkers())
- @updateAllScreenLines()
- @decorateFold(fold) for fold in folds
-
- subscribeToScopedConfigSettings: =>
- @scopedConfigSubscriptions?.dispose()
- @scopedConfigSubscriptions = subscriptions = new CompositeDisposable
-
- scopeDescriptor = @getRootScopeDescriptor()
-
- oldConfigSettings = @configSettings
- @configSettings =
- scrollPastEnd: @config.get('editor.scrollPastEnd', scope: scopeDescriptor)
- softWrap: @config.get('editor.softWrap', scope: scopeDescriptor)
- softWrapAtPreferredLineLength: @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
- softWrapHangingIndent: @config.get('editor.softWrapHangingIndent', scope: scopeDescriptor)
- preferredLineLength: @config.get('editor.preferredLineLength', scope: scopeDescriptor)
-
- subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.softWrap = newValue
- @updateWrappedScreenLines()
-
- subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.softWrapHangingIndent = newValue
- @updateWrappedScreenLines()
-
- subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.softWrapAtPreferredLineLength = newValue
- @updateWrappedScreenLines() if @isSoftWrapped()
-
- subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.preferredLineLength = newValue
- @updateWrappedScreenLines() if @isSoftWrapped() and @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
-
- subscriptions.add @config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) =>
- @configSettings.scrollPastEnd = value
-
- @updateWrappedScreenLines() if oldConfigSettings? and not _.isEqual(oldConfigSettings, @configSettings)
-
- serialize: ->
- deserializer: 'DisplayBuffer'
- id: @id
- softWrapped: @isSoftWrapped()
- editorWidthInChars: @editorWidthInChars
- tokenizedBuffer: @tokenizedBuffer.serialize()
- largeFileMode: @largeFileMode
- foldsMarkerLayerId: @foldsMarkerLayer.id
-
- copy: ->
- foldsMarkerLayer = @foldsMarkerLayer.copy()
- new DisplayBuffer({
- @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert,
- @grammarRegistry, foldsMarkerLayer
- })
-
- updateAllScreenLines: ->
- @maxLineLength = 0
- @screenLines = []
- @rowMap = new RowMap
- @updateScreenLines(0, @buffer.getLineCount(), null, suppressChangeEvent: true)
-
- onDidChangeSoftWrapped: (callback) ->
- @emitter.on 'did-change-soft-wrapped', callback
-
- onDidChangeGrammar: (callback) ->
- @tokenizedBuffer.onDidChangeGrammar(callback)
-
- onDidTokenize: (callback) ->
- @tokenizedBuffer.onDidTokenize(callback)
-
- onDidChange: (callback) ->
- @emitter.on 'did-change', callback
-
- onDidChangeCharacterWidths: (callback) ->
- @emitter.on 'did-change-character-widths', callback
-
- onDidRequestAutoscroll: (callback) ->
- @emitter.on 'did-request-autoscroll', callback
-
- observeDecorations: (callback) ->
- callback(decoration) for decoration in @getDecorations()
- @onDidAddDecoration(callback)
-
- onDidAddDecoration: (callback) ->
- @emitter.on 'did-add-decoration', callback
-
- onDidRemoveDecoration: (callback) ->
- @emitter.on 'did-remove-decoration', callback
-
- onDidCreateMarker: (callback) ->
- @emitter.on 'did-create-marker', callback
-
- onDidUpdateMarkers: (callback) ->
- @emitter.on 'did-update-markers', callback
-
- onDidUpdateDecorations: (callback) ->
- @emitter.on 'did-update-decorations', callback
-
- emitDidChange: (eventProperties, refreshMarkers=true) ->
- @emitter.emit 'did-change', eventProperties
- if refreshMarkers
- @refreshMarkerScreenPositions()
- @emitter.emit 'did-update-markers'
-
- updateWrappedScreenLines: ->
- start = 0
- end = @getLastRow()
- @updateAllScreenLines()
- screenDelta = @getLastRow() - end
- bufferDelta = 0
- @emitDidChange({start, end, screenDelta, bufferDelta})
-
- # Sets the visibility of the tokenized buffer.
- #
- # visible - A {Boolean} indicating of the tokenized buffer is shown
- setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
-
- setUpdatedSynchronously: (@updatedSynchronously) ->
-
- getVerticalScrollMargin: ->
- maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2)
- Math.min(@verticalScrollMargin, maxScrollMargin)
-
- setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
-
- getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2))
- setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
-
- getHeight: ->
- @height
-
- setHeight: (@height) ->
- @height
-
- getWidth: ->
- @width
-
- setWidth: (newWidth) ->
- oldWidth = @width
- @width = newWidth
- @updateWrappedScreenLines() if newWidth isnt oldWidth and @isSoftWrapped()
- @width
-
- getLineHeightInPixels: -> @lineHeightInPixels
- setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels
-
- getKoreanCharWidth: -> @koreanCharWidth
-
- getHalfWidthCharWidth: -> @halfWidthCharWidth
-
- getDoubleWidthCharWidth: -> @doubleWidthCharWidth
-
- getDefaultCharWidth: -> @defaultCharWidth
-
- setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
- doubleWidthCharWidth ?= defaultCharWidth
- halfWidthCharWidth ?= defaultCharWidth
- koreanCharWidth ?= defaultCharWidth
- if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth
- @defaultCharWidth = defaultCharWidth
- @doubleWidthCharWidth = doubleWidthCharWidth
- @halfWidthCharWidth = halfWidthCharWidth
- @koreanCharWidth = koreanCharWidth
- @updateWrappedScreenLines() if @isSoftWrapped() and @getEditorWidthInChars()?
- defaultCharWidth
-
- getCursorWidth: -> 1
-
- scrollToScreenRange: (screenRange, options = {}) ->
- scrollEvent = {screenRange, options}
- @emitter.emit "did-request-autoscroll", scrollEvent
-
- scrollToScreenPosition: (screenPosition, options) ->
- @scrollToScreenRange(new Range(screenPosition, screenPosition), options)
-
- scrollToBufferPosition: (bufferPosition, options) ->
- @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options)
-
- # Retrieves the current tab length.
- #
- # Returns a {Number}.
- getTabLength: ->
- @tokenizedBuffer.getTabLength()
-
- # Specifies the tab length.
- #
- # tabLength - A {Number} that defines the new tab length.
- setTabLength: (tabLength) ->
- @tokenizedBuffer.setTabLength(tabLength)
-
- setIgnoreInvisibles: (ignoreInvisibles) ->
- @tokenizedBuffer.setIgnoreInvisibles(ignoreInvisibles)
-
- setSoftWrapped: (softWrapped) ->
- if softWrapped isnt @softWrapped
- @softWrapped = softWrapped
- @updateWrappedScreenLines()
- softWrapped = @isSoftWrapped()
- @emitter.emit 'did-change-soft-wrapped', softWrapped
- softWrapped
- else
- @isSoftWrapped()
-
- isSoftWrapped: ->
- if @largeFileMode
- false
- else
- @softWrapped ? @configSettings.softWrap ? false
-
- # Set the number of characters that fit horizontally in the editor.
- #
- # editorWidthInChars - A {Number} of characters.
- setEditorWidthInChars: (editorWidthInChars) ->
- if editorWidthInChars > 0
- previousWidthInChars = @editorWidthInChars
- @editorWidthInChars = editorWidthInChars
- if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped()
- @updateWrappedScreenLines()
-
- # Returns the editor width in characters for soft wrap.
- getEditorWidthInChars: ->
- width = @getWidth()
- if width? and @defaultCharWidth > 0
- Math.max(0, Math.floor(width / @defaultCharWidth))
- else
- @editorWidthInChars
-
- getSoftWrapColumn: ->
- if @configSettings.softWrapAtPreferredLineLength
- Math.min(@getEditorWidthInChars(), @configSettings.preferredLineLength)
- else
- @getEditorWidthInChars()
-
- getSoftWrapColumnForTokenizedLine: (tokenizedLine) ->
- lineMaxWidth = @getSoftWrapColumn() * @getDefaultCharWidth()
-
- return if Number.isNaN(lineMaxWidth)
- return 0 if lineMaxWidth is 0
-
- iterator = tokenizedLine.getTokenIterator(false)
- column = 0
- currentWidth = 0
- while iterator.next()
- textIndex = 0
- text = iterator.getText()
- while textIndex < text.length
- if iterator.isPairedCharacter()
- charLength = 2
- else
- charLength = 1
-
- if iterator.hasDoubleWidthCharacterAt(textIndex)
- charWidth = @getDoubleWidthCharWidth()
- else if iterator.hasHalfWidthCharacterAt(textIndex)
- charWidth = @getHalfWidthCharWidth()
- else if iterator.hasKoreanCharacterAt(textIndex)
- charWidth = @getKoreanCharWidth()
- else
- charWidth = @getDefaultCharWidth()
-
- return column if currentWidth + charWidth > lineMaxWidth
-
- currentWidth += charWidth
- column += charLength
- textIndex += charLength
- column
-
- # Gets the screen line for the given screen row.
- #
- # * `screenRow` - A {Number} indicating the screen row.
- #
- # Returns {TokenizedLine}
- tokenizedLineForScreenRow: (screenRow) ->
- if @largeFileMode
- if line = @tokenizedBuffer.tokenizedLineForRow(screenRow)
- if line.text.length > @maxLineLength
- @maxLineLength = line.text.length
- @longestScreenRow = screenRow
- line
- else
- @screenLines[screenRow]
-
- # Gets the screen lines for the given screen row range.
- #
- # startRow - A {Number} indicating the beginning screen row.
- # endRow - A {Number} indicating the ending screen row.
- #
- # Returns an {Array} of {TokenizedLine}s.
- tokenizedLinesForScreenRows: (startRow, endRow) ->
- if @largeFileMode
- @tokenizedBuffer.tokenizedLinesForRows(startRow, endRow)
- else
- @screenLines[startRow..endRow]
-
- # Gets all the screen lines.
- #
- # Returns an {Array} of {TokenizedLine}s.
- getTokenizedLines: ->
- if @largeFileMode
- @tokenizedBuffer.tokenizedLinesForRows(0, @getLastRow())
- else
- new Array(@screenLines...)
-
- indentLevelForLine: (line) ->
- @tokenizedBuffer.indentLevelForLine(line)
-
- # Given starting and ending screen rows, this returns an array of the
- # buffer rows corresponding to every screen row in the range
- #
- # startScreenRow - The screen row {Number} to start at
- # endScreenRow - The screen row {Number} to end at (default: the last screen row)
- #
- # Returns an {Array} of buffer rows as {Numbers}s.
- bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
- if @largeFileMode
- [startScreenRow..endScreenRow]
- else
- for screenRow in [startScreenRow..endScreenRow]
- @rowMap.bufferRowRangeForScreenRow(screenRow)[0]
-
- # Creates a new fold between two row numbers.
- #
- # startRow - The row {Number} to start folding at
- # endRow - The row {Number} to end the fold
- #
- # Returns the new {Fold}.
- createFold: (startRow, endRow) ->
- unless @largeFileMode
- if foldMarker = @findFoldMarker({startRow, endRow})
- @foldForMarker(foldMarker)
- else
- foldMarker = @foldsMarkerLayer.markRange([[startRow, 0], [endRow, Infinity]])
- fold = new Fold(this, foldMarker)
- fold.updateDisplayBuffer()
- @decorateFold(fold)
- fold
-
- isFoldedAtBufferRow: (bufferRow) ->
- @largestFoldContainingBufferRow(bufferRow)?
-
- isFoldedAtScreenRow: (screenRow) ->
- @largestFoldContainingBufferRow(@bufferRowForScreenRow(screenRow))?
-
- isFoldableAtBufferRow: (row) ->
- @tokenizedBuffer.isFoldableAtRow(row)
-
- # Destroys the fold with the given id
- destroyFoldWithId: (id) ->
- @foldsByMarkerId[id]?.destroy()
-
- # Removes any folds found that contain the given buffer row.
- #
- # bufferRow - The buffer row {Number} to check against
- unfoldBufferRow: (bufferRow) ->
- fold.destroy() for fold in @foldsContainingBufferRow(bufferRow)
- return
-
- # Given a buffer row, this returns the largest fold that starts there.
- #
- # Largest is defined as the fold whose difference between its start and end points
- # are the greatest.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns a {Fold} or null if none exists.
- largestFoldStartingAtBufferRow: (bufferRow) ->
- @foldsStartingAtBufferRow(bufferRow)[0]
-
- # Public: Given a buffer row, this returns all folds that start there.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns an {Array} of {Fold}s.
- foldsStartingAtBufferRow: (bufferRow) ->
- for marker in @findFoldMarkers(startRow: bufferRow)
- @foldForMarker(marker)
-
- # Given a screen row, this returns the largest fold that starts there.
- #
- # Largest is defined as the fold whose difference between its start and end points
- # are the greatest.
- #
- # screenRow - A {Number} indicating the screen row
- #
- # Returns a {Fold}.
- largestFoldStartingAtScreenRow: (screenRow) ->
- @largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow))
-
- # Given a buffer row, this returns the largest fold that includes it.
- #
- # Largest is defined as the fold whose difference between its start and end rows
- # is the greatest.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns a {Fold}.
- largestFoldContainingBufferRow: (bufferRow) ->
- @foldsContainingBufferRow(bufferRow)[0]
-
- # Returns the folds in the given row range (exclusive of end row) that are
- # not contained by any other folds.
- outermostFoldsInBufferRowRange: (startRow, endRow) ->
- folds = []
- lastFoldEndRow = -1
-
- for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow])
- range = marker.getRange()
- if range.start.row > lastFoldEndRow
- lastFoldEndRow = range.end.row
- if startRow <= range.start.row <= range.end.row < endRow
- folds.push(@foldForMarker(marker))
-
- folds
-
- # Returns all the folds that intersect the given row range.
- foldsIntersectingBufferRowRange: (startRow, endRow) ->
- @foldForMarker(marker) for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow])
-
- # Public: Given a buffer row, this returns folds that include it.
- #
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns an {Array} of {Fold}s.
- foldsContainingBufferRow: (bufferRow) ->
- for marker in @findFoldMarkers(intersectsRow: bufferRow)
- @foldForMarker(marker)
-
- # Given a buffer row, this converts it into a screen row.
- #
- # bufferRow - A {Number} representing a buffer row
- #
- # Returns a {Number}.
- screenRowForBufferRow: (bufferRow) ->
- if @largeFileMode
- bufferRow
- else
- @rowMap.screenRowRangeForBufferRow(bufferRow)[0]
-
- lastScreenRowForBufferRow: (bufferRow) ->
- if @largeFileMode
- bufferRow
- else
- @rowMap.screenRowRangeForBufferRow(bufferRow)[1] - 1
-
- # Given a screen row, this converts it into a buffer row.
- #
- # screenRow - A {Number} representing a screen row
- #
- # Returns a {Number}.
- bufferRowForScreenRow: (screenRow) ->
- if @largeFileMode
- screenRow
- else
- @rowMap.bufferRowRangeForScreenRow(screenRow)[0]
-
- # Given a buffer range, this converts it into a screen position.
- #
- # bufferRange - The {Range} to convert
- #
- # Returns a {Range}.
- screenRangeForBufferRange: (bufferRange, options) ->
- bufferRange = Range.fromObject(bufferRange)
- start = @screenPositionForBufferPosition(bufferRange.start, options)
- end = @screenPositionForBufferPosition(bufferRange.end, options)
- new Range(start, end)
-
- # Given a screen range, this converts it into a buffer position.
- #
- # screenRange - The {Range} to convert
- #
- # Returns a {Range}.
- bufferRangeForScreenRange: (screenRange) ->
- screenRange = Range.fromObject(screenRange)
- start = @bufferPositionForScreenPosition(screenRange.start)
- end = @bufferPositionForScreenPosition(screenRange.end)
- new Range(start, end)
-
- # Gets the number of screen lines.
- #
- # Returns a {Number}.
- getLineCount: ->
- if @largeFileMode
- @tokenizedBuffer.getLineCount()
- else
- @screenLines.length
-
- # Gets the number of the last screen line.
- #
- # Returns a {Number}.
- getLastRow: ->
- @getLineCount() - 1
-
- # Gets the length of the longest screen line.
- #
- # Returns a {Number}.
- getMaxLineLength: ->
- @maxLineLength
-
- # Gets the row number of the longest screen line.
- #
- # Return a {}
- getLongestScreenRow: ->
- @longestScreenRow
-
- # Given a buffer position, this converts it into a screen position.
- #
- # bufferPosition - An object that represents a buffer position. It can be either
- # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
- # options - A hash of options with the following keys:
- # wrapBeyondNewlines:
- # wrapAtSoftNewlines:
- #
- # Returns a {Point}.
- screenPositionForBufferPosition: (bufferPosition, options) ->
- throw new Error("This TextEditor has been destroyed") if @isDestroyed()
-
- {row, column} = @buffer.clipPosition(bufferPosition)
- [startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row)
- for screenRow in [startScreenRow...endScreenRow]
- screenLine = @tokenizedLineForScreenRow(screenRow)
-
- unless screenLine?
- throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row",
- softWrapEnabled: @isSoftWrapped()
- foldCount: @findFoldMarkers().length
- lastBufferRow: @buffer.getLastRow()
- lastScreenRow: @getLastRow()
- bufferRow: row
- screenRow: screenRow
- displayBufferChangeCount: @changeCount
- tokenizedBufferChangeCount: @tokenizedBuffer.changeCount
- bufferChangeCount: @buffer.changeCount
-
- maxBufferColumn = screenLine.getMaxBufferColumn()
- if screenLine.isSoftWrapped() and column > maxBufferColumn
- continue
- else
- if column <= maxBufferColumn
- screenColumn = screenLine.screenColumnForBufferColumn(column)
- else
- screenColumn = Infinity
- break
-
- @clipScreenPosition([screenRow, screenColumn], options)
-
- # Given a buffer position, this converts it into a screen position.
- #
- # screenPosition - An object that represents a buffer position. It can be either
- # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
- # options - A hash of options with the following keys:
- # wrapBeyondNewlines:
- # wrapAtSoftNewlines:
- #
- # Returns a {Point}.
- bufferPositionForScreenPosition: (screenPosition, options) ->
- {row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options)
- [bufferRow] = @rowMap.bufferRowRangeForScreenRow(row)
- new Point(bufferRow, @tokenizedLineForScreenRow(row).bufferColumnForScreenColumn(column))
-
- # Retrieves the grammar's token scopeDescriptor for a buffer position.
- #
- # bufferPosition - A {Point} in the {TextBuffer}
- #
- # Returns a {ScopeDescriptor}.
- scopeDescriptorForBufferPosition: (bufferPosition) ->
- @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)
-
- bufferRangeForScopeAtPosition: (selector, position) ->
- @tokenizedBuffer.bufferRangeForScopeAtPosition(selector, position)
-
- # Retrieves the grammar's token for a buffer position.
- #
- # bufferPosition - A {Point} in the {TextBuffer}.
- #
- # Returns a {Token}.
- tokenForBufferPosition: (bufferPosition) ->
- @tokenizedBuffer.tokenForPosition(bufferPosition)
-
- # Get the grammar for this buffer.
- #
- # Returns the current {Grammar} or the {NullGrammar}.
- getGrammar: ->
- @tokenizedBuffer.grammar
-
- # Sets the grammar for the buffer.
- #
- # grammar - Sets the new grammar rules
- setGrammar: (grammar) ->
- @tokenizedBuffer.setGrammar(grammar)
-
- # Reloads the current grammar.
- reloadGrammar: ->
- @tokenizedBuffer.reloadGrammar()
-
- # Given a position, this clips it to a real position.
- #
- # For example, if `position`'s row exceeds the row count of the buffer,
- # or if its column goes beyond a line's length, this "sanitizes" the value
- # to a real position.
- #
- # position - The {Point} to clip
- # options - A hash with the following values:
- # wrapBeyondNewlines: if `true`, continues wrapping past newlines
- # wrapAtSoftNewlines: if `true`, continues wrapping past soft newlines
- # skipSoftWrapIndentation: if `true`, skips soft wrap indentation without wrapping to the previous line
- # screenLine: if `true`, indicates that you're using a line number, not a row number
- #
- # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed.
- clipScreenPosition: (screenPosition, options={}) ->
- {wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options
- {row, column} = Point.fromObject(screenPosition)
-
- if row < 0
- row = 0
- column = 0
- else if row > @getLastRow()
- row = @getLastRow()
- column = Infinity
- else if column < 0
- column = 0
-
- screenLine = @tokenizedLineForScreenRow(row)
- unless screenLine?
- error = new Error("Undefined screen line when clipping screen position")
- Error.captureStackTrace(error)
- error.metadata = {
- screenRow: row
- screenColumn: column
- maxScreenRow: @getLastRow()
- screenLinesDefined: @screenLines.map (sl) -> sl?
- displayBufferChangeCount: @changeCount
- tokenizedBufferChangeCount: @tokenizedBuffer.changeCount
- bufferChangeCount: @buffer.changeCount
- }
- throw error
-
- maxScreenColumn = screenLine.getMaxScreenColumn()
-
- if screenLine.isSoftWrapped() and column >= maxScreenColumn
- if wrapAtSoftNewlines
- row++
- column = @tokenizedLineForScreenRow(row).clipScreenColumn(0)
- else
- column = screenLine.clipScreenColumn(maxScreenColumn - 1)
- else if screenLine.isColumnInsideSoftWrapIndentation(column)
- if skipSoftWrapIndentation
- column = screenLine.clipScreenColumn(0)
- else
- row--
- column = @tokenizedLineForScreenRow(row).getMaxScreenColumn() - 1
- else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow()
- row++
- column = 0
- else
- column = screenLine.clipScreenColumn(column, options)
- new Point(row, column)
-
- # Clip the start and end of the given range to valid positions on screen.
- # See {::clipScreenPosition} for more information.
- #
- # * `range` The {Range} to clip.
- # * `options` (optional) See {::clipScreenPosition} `options`.
- # Returns a {Range}.
- clipScreenRange: (range, options) ->
- start = @clipScreenPosition(range.start, options)
- end = @clipScreenPosition(range.end, options)
-
- new Range(start, end)
-
- # Calculates a {Range} representing the start of the {TextBuffer} until the end.
- #
- # Returns a {Range}.
- rangeForAllLines: ->
- new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
-
- decorationForId: (id) ->
- @decorationsById[id]
-
- getDecorations: (propertyFilter) ->
- allDecorations = []
- for markerId, decorations of @decorationsByMarkerId
- allDecorations.push(decorations...) if decorations?
- if propertyFilter?
- allDecorations = allDecorations.filter (decoration) ->
- for key, value of propertyFilter
- return false unless decoration.properties[key] is value
- true
- allDecorations
-
- getLineDecorations: (propertyFilter) ->
- @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line')
-
- getLineNumberDecorations: (propertyFilter) ->
- @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number')
-
- getHighlightDecorations: (propertyFilter) ->
- @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight')
-
- getOverlayDecorations: (propertyFilter) ->
- result = []
- for id, decoration of @overlayDecorationsById
- result.push(decoration)
- if propertyFilter?
- result.filter (decoration) ->
- for key, value of propertyFilter
- return false unless decoration.properties[key] is value
- true
- else
- result
-
- decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
- decorationsByMarkerId = {}
- for marker in @findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
- if decorations = @decorationsByMarkerId[marker.id]
- decorationsByMarkerId[marker.id] = decorations
- decorationsByMarkerId
-
- decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
- decorationsState = {}
-
- for layerId of @decorationCountsByLayerId
- layer = @getMarkerLayer(layerId)
-
- for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
- screenRange = marker.getScreenRange()
- rangeIsReversed = marker.isReversed()
-
- if decorations = @decorationsByMarkerId[marker.id]
- for decoration in decorations
- decorationsState[decoration.id] = {
- properties: decoration.properties
- screenRange, rangeIsReversed
- }
-
- if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
- for layerDecoration in layerDecorations
- decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
- properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
- screenRange, rangeIsReversed
- }
-
- decorationsState
-
- decorateMarker: (marker, decorationParams) ->
- throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
- marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id)
- decoration = new Decoration(marker, this, decorationParams)
- @decorationsByMarkerId[marker.id] ?= []
- @decorationsByMarkerId[marker.id].push(decoration)
- @overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
- @decorationsById[decoration.id] = decoration
- @observeDecoratedLayer(marker.layer)
- @scheduleUpdateDecorationsEvent()
- @emitter.emit 'did-add-decoration', decoration
- decoration
-
- decorateMarkerLayer: (markerLayer, decorationParams) ->
- decoration = new LayerDecoration(markerLayer, this, decorationParams)
- @layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
- @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
- @observeDecoratedLayer(markerLayer)
- @scheduleUpdateDecorationsEvent()
- decoration
-
- decorationsForMarkerId: (markerId) ->
- @decorationsByMarkerId[markerId]
-
- # Retrieves a {TextEditorMarker} based on its id.
- #
- # id - A {Number} representing a marker id
- #
- # Returns the {TextEditorMarker} (if it exists).
- getMarker: (id) ->
- @defaultMarkerLayer.getMarker(id)
-
- # Retrieves the active markers in the buffer.
- #
- # Returns an {Array} of existing {TextEditorMarker}s.
- getMarkers: ->
- @defaultMarkerLayer.getMarkers()
-
- getMarkerCount: ->
- @buffer.getMarkerCount()
-
- # Public: Constructs a new marker at the given screen range.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markScreenRange: (screenRange, options) ->
- @defaultMarkerLayer.markScreenRange(screenRange, options)
-
- # Public: Constructs a new marker at the given buffer range.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markBufferRange: (bufferRange, options) ->
- @defaultMarkerLayer.markBufferRange(bufferRange, options)
-
- # Public: Constructs a new marker at the given screen position.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markScreenPosition: (screenPosition, options) ->
- @defaultMarkerLayer.markScreenPosition(screenPosition, options)
-
- # Public: Constructs a new marker at the given buffer position.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markBufferPosition: (bufferPosition, options) ->
- @defaultMarkerLayer.markBufferPosition(bufferPosition, options)
-
- # Finds the first marker satisfying the given attributes
- #
- # Refer to {DisplayBuffer::findMarkers} for details.
- #
- # Returns a {TextEditorMarker} or null
- findMarker: (params) ->
- @defaultMarkerLayer.findMarkers(params)[0]
-
- # Public: Find all markers satisfying a set of parameters.
- #
- # params - An {Object} containing parameters that all returned markers must
- # satisfy. Unreserved keys will be compared against the markers' custom
- # properties. There are also the following reserved keys with special
- # meaning for the query:
- # :startBufferRow - A {Number}. Only returns markers starting at this row in
- # buffer coordinates.
- # :endBufferRow - A {Number}. Only returns markers ending at this row in
- # buffer coordinates.
- # :containsBufferRange - A {Range} or range-compatible {Array}. Only returns
- # markers containing this range in buffer coordinates.
- # :containsBufferPosition - A {Point} or point-compatible {Array}. Only
- # returns markers containing this position in buffer coordinates.
- # :containedInBufferRange - A {Range} or range-compatible {Array}. Only
- # returns markers contained within this range.
- #
- # Returns an {Array} of {TextEditorMarker}s
- findMarkers: (params) ->
- @defaultMarkerLayer.findMarkers(params)
-
- addMarkerLayer: (options) ->
- bufferLayer = @buffer.addMarkerLayer(options)
- @getMarkerLayer(bufferLayer.id)
-
- getMarkerLayer: (id) ->
- if layer = @customMarkerLayersById[id]
- layer
- else if bufferLayer = @buffer.getMarkerLayer(id)
- @customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer)
-
- getDefaultMarkerLayer: -> @defaultMarkerLayer
-
- findFoldMarker: (params) ->
- @findFoldMarkers(params)[0]
-
- findFoldMarkers: (params) ->
- @foldsMarkerLayer.findMarkers(params)
-
- refreshMarkerScreenPositions: ->
- @defaultMarkerLayer.refreshMarkerScreenPositions()
- layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById
- return
-
- destroyed: ->
- @defaultMarkerLayer.destroy()
- @foldsMarkerLayer.destroy()
- @scopedConfigSubscriptions.dispose()
- @disposables.dispose()
- @tokenizedBuffer.destroy()
-
- logLines: (start=0, end=@getLastRow()) ->
- for row in [start..end]
- line = @tokenizedLineForScreenRow(row).text
- console.log row, @bufferRowForScreenRow(row), line, line.length
- return
-
- getRootScopeDescriptor: ->
- @tokenizedBuffer.rootScopeDescriptor
-
- handleTokenizedBufferChange: (tokenizedBufferChange) =>
- @changeCount = @tokenizedBuffer.changeCount
- {start, end, delta, bufferChange} = tokenizedBufferChange
- @updateScreenLines(start, end + 1, delta, refreshMarkers: false)
-
- updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) ->
- return if @largeFileMode
- return if @isDestroyed()
-
- startBufferRow = @rowMap.bufferRowRangeForBufferRow(startBufferRow)[0]
- endBufferRow = @rowMap.bufferRowRangeForBufferRow(endBufferRow - 1)[1]
- startScreenRow = @rowMap.screenRowRangeForBufferRow(startBufferRow)[0]
- endScreenRow = @rowMap.screenRowRangeForBufferRow(endBufferRow - 1)[1]
- {screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta)
- screenDelta = screenLines.length - (endScreenRow - startScreenRow)
-
- _.spliceWithArray(@screenLines, startScreenRow, endScreenRow - startScreenRow, screenLines, 10000)
-
- @checkScreenLinesInvariant()
-
- @rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions)
- @findMaxLineLength(startScreenRow, endScreenRow, screenLines, screenDelta)
-
- return if options.suppressChangeEvent
-
- changeEvent =
- start: startScreenRow
- end: endScreenRow - 1
- screenDelta: screenDelta
- bufferDelta: bufferDelta
-
- @emitDidChange(changeEvent, options.refreshMarkers)
-
- buildScreenLines: (startBufferRow, endBufferRow) ->
- screenLines = []
- regions = []
- rectangularRegion = null
-
- foldsByStartRow = {}
- for fold in @outermostFoldsInBufferRowRange(startBufferRow, endBufferRow)
- foldsByStartRow[fold.getStartRow()] = fold
-
- bufferRow = startBufferRow
- while bufferRow < endBufferRow
- tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(bufferRow)
-
- if fold = foldsByStartRow[bufferRow]
- foldLine = tokenizedLine.copy()
- foldLine.fold = fold
- screenLines.push(foldLine)
-
- if rectangularRegion?
- regions.push(rectangularRegion)
- rectangularRegion = null
-
- foldedRowCount = fold.getBufferRowCount()
- regions.push(bufferRows: foldedRowCount, screenRows: 1)
- bufferRow += foldedRowCount
- else
- softWraps = 0
- if @isSoftWrapped()
- while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumnForTokenizedLine(tokenizedLine))
- [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(
- wrapScreenColumn,
- @configSettings.softWrapHangingIndent
- )
- break if wrappedLine.hasOnlySoftWrapIndentation()
- screenLines.push(wrappedLine)
- softWraps++
- screenLines.push(tokenizedLine)
-
- if softWraps > 0
- if rectangularRegion?
- regions.push(rectangularRegion)
- rectangularRegion = null
- regions.push(bufferRows: 1, screenRows: softWraps + 1)
- else
- rectangularRegion ?= {bufferRows: 0, screenRows: 0}
- rectangularRegion.bufferRows++
- rectangularRegion.screenRows++
-
- bufferRow++
-
- if rectangularRegion?
- regions.push(rectangularRegion)
-
- {screenLines, regions}
-
- findMaxLineLength: (startScreenRow, endScreenRow, newScreenLines, screenDelta) ->
- oldMaxLineLength = @maxLineLength
-
- if startScreenRow <= @longestScreenRow < endScreenRow
- @longestScreenRow = 0
- @maxLineLength = 0
- maxLengthCandidatesStartRow = 0
- maxLengthCandidates = @screenLines
- else
- @longestScreenRow += screenDelta if endScreenRow <= @longestScreenRow
- maxLengthCandidatesStartRow = startScreenRow
- maxLengthCandidates = newScreenLines
-
- for screenLine, i in maxLengthCandidates
- screenRow = maxLengthCandidatesStartRow + i
- length = screenLine.text.length
- if length > @maxLineLength
- @longestScreenRow = screenRow
- @maxLineLength = length
-
- didCreateDefaultLayerMarker: (textBufferMarker) =>
- if marker = @getMarker(textBufferMarker.id)
- # The marker might have been removed in some other handler called before
- # this one. Only emit when the marker still exists.
- @emitter.emit 'did-create-marker', marker
-
- scheduleUpdateDecorationsEvent: ->
- if @updatedSynchronously
- @emitter.emit 'did-update-decorations'
- return
-
- unless @didUpdateDecorationsEventScheduled
- @didUpdateDecorationsEventScheduled = true
- process.nextTick =>
- @didUpdateDecorationsEventScheduled = false
- @emitter.emit 'did-update-decorations'
-
- decorateFold: (fold) ->
- @decorateMarker(fold.marker, type: 'line-number', class: 'folded')
-
- foldForMarker: (marker) ->
- @foldsByMarkerId[marker.id]
-
- decorationDidChangeType: (decoration) ->
- if decoration.isType('overlay')
- @overlayDecorationsById[decoration.id] = decoration
- 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]
-
- checkScreenLinesInvariant: ->
- return if @isSoftWrapped()
- return if _.size(@foldsByMarkerId) > 0
-
- screenLinesCount = @screenLines.length
- tokenizedLinesCount = @tokenizedBuffer.getLineCount()
- bufferLinesCount = @buffer.getLineCount()
-
- @assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) ->
- error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount}
-
- @assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) ->
- error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount}
diff --git a/src/fold.coffee b/src/fold.coffee
deleted file mode 100644
index 051be9f4c..000000000
--- a/src/fold.coffee
+++ /dev/null
@@ -1,83 +0,0 @@
-{Point, Range} = require 'text-buffer'
-
-# Represents a fold that collapses multiple buffer lines into a single
-# line on the screen.
-#
-# Their creation is managed by the {DisplayBuffer}.
-module.exports =
-class Fold
- id: null
- displayBuffer: null
- marker: null
-
- constructor: (@displayBuffer, @marker) ->
- @id = @marker.id
- @displayBuffer.foldsByMarkerId[@marker.id] = this
- @marker.onDidDestroy => @destroyed()
- @marker.onDidChange ({isValid}) => @destroy() unless isValid
-
- # Returns whether this fold is contained within another fold
- isInsideLargerFold: ->
- largestContainingFoldMarker = @displayBuffer.findFoldMarker(containsRange: @getBufferRange())
- largestContainingFoldMarker and
- not largestContainingFoldMarker.getRange().isEqual(@getBufferRange())
-
- # Destroys this fold
- destroy: ->
- @marker.destroy()
-
- # Returns the fold's {Range} in buffer coordinates
- #
- # includeNewline - A {Boolean} which, if `true`, includes the trailing newline
- #
- # Returns a {Range}.
- getBufferRange: ({includeNewline}={}) ->
- range = @marker.getRange()
-
- if range.end.row > range.start.row and nextFold = @displayBuffer.largestFoldStartingAtBufferRow(range.end.row)
- nextRange = nextFold.getBufferRange()
- range = new Range(range.start, nextRange.end)
-
- if includeNewline
- range = range.copy()
- range.end.row++
- range.end.column = 0
- range
-
- getBufferRowRange: ->
- {start, end} = @getBufferRange()
- [start.row, end.row]
-
- # Returns the fold's start row as a {Number}.
- getStartRow: ->
- @getBufferRange().start.row
-
- # Returns the fold's end row as a {Number}.
- getEndRow: ->
- @getBufferRange().end.row
-
- # Returns a {String} representation of the fold.
- inspect: ->
- "Fold(#{@getStartRow()}, #{@getEndRow()})"
-
- # Retrieves the number of buffer rows spanned by the fold.
- #
- # Returns a {Number}.
- getBufferRowCount: ->
- @getEndRow() - @getStartRow() + 1
-
- # Identifies if a fold is nested within a fold.
- #
- # fold - A {Fold} to check
- #
- # Returns a {Boolean}.
- isContainedByFold: (fold) ->
- @isContainedByRange(fold.getBufferRange())
-
- updateDisplayBuffer: ->
- unless @isInsideLargerFold()
- @displayBuffer.updateScreenLines(@getStartRow(), @getEndRow() + 1, 0, updateMarkers: true)
-
- destroyed: ->
- delete @displayBuffer.foldsByMarkerId[@marker.id]
- @updateDisplayBuffer()
diff --git a/src/gutter.coffee b/src/gutter.coffee
index f59fa7b6e..2fc362cbd 100644
--- a/src/gutter.coffee
+++ b/src/gutter.coffee
@@ -71,13 +71,13 @@ class Gutter
isVisible: ->
@visible
- # Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves,
+ # Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
#
# ## Arguments
#
- # * `marker` A {TextEditorMarker} you want this decoration to follow.
+ # * `marker` A {DisplayMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration. It is passed
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
# all options documented there.
diff --git a/src/language-mode.coffee b/src/language-mode.coffee
index 4824431bf..a0392acf6 100644
--- a/src/language-mode.coffee
+++ b/src/language-mode.coffee
@@ -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) ->
diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee
index 1f76140a3..e00e024cb 100644
--- a/src/layer-decoration.coffee
+++ b/src/layer-decoration.coffee
@@ -7,7 +7,7 @@ nextId = -> idCounter++
# layer. Created via {TextEditor::decorateMarkerLayer}.
module.exports =
class LayerDecoration
- constructor: (@markerLayer, @displayBuffer, @properties) ->
+ constructor: (@markerLayer, @decorationManager, @properties) ->
@id = nextId()
@destroyed = false
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
@@ -19,7 +19,7 @@ class LayerDecoration
@markerLayerDestroyedDisposable.dispose()
@markerLayerDestroyedDisposable = null
@destroyed = true
- @displayBuffer.didDestroyLayerDecoration(this)
+ @decorationManager.didDestroyLayerDecoration(this)
# Essential: Determine whether this decoration is destroyed.
#
@@ -44,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()
diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee
index bb66ff144..3a3c199c2 100644
--- a/src/line-number-gutter-component.coffee
+++ b/src/line-number-gutter-component.coffee
@@ -93,9 +93,9 @@ class LineNumberGutterComponent extends TiledComponent
{target} = event
lineNumber = target.parentNode
- if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
+ if target.classList.contains('icon-right')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
@editor.unfoldBufferRow(bufferRow)
- else
+ else if lineNumber.classList.contains('foldable')
@editor.foldBufferRow(bufferRow)
diff --git a/src/lines-component.coffee b/src/lines-component.coffee
index b5af56885..88645589a 100644
--- a/src/lines-component.coffee
+++ b/src/lines-component.coffee
@@ -43,7 +43,7 @@ class LinesComponent extends TiledComponent
@domNode
shouldRecreateAllTilesOnUpdate: ->
- @oldState.indentGuidesVisible isnt @newState.indentGuidesVisible or @newState.continuousReflow
+ @newState.continuousReflow
beforeUpdateSync: (state) ->
if @newState.maxHeight isnt @oldState.maxHeight
@@ -70,8 +70,6 @@ class LinesComponent extends TiledComponent
@cursorsComponent.updateSync(state)
- @oldState.indentGuidesVisible = @newState.indentGuidesVisible
-
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars})
buildEmptyState: ->
@@ -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)
diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee
index f4a7313ca..6844f21de 100644
--- a/src/lines-tile-component.coffee
+++ b/src/lines-tile-component.coffee
@@ -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()
diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee
index 2373463af..cfc954cf2 100644
--- a/src/lines-yardstick.coffee
+++ b/src/lines-yardstick.coffee
@@ -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()
diff --git a/src/marker-observation-window.coffee b/src/marker-observation-window.coffee
index aa7b71f69..ffb92c0ab 100644
--- a/src/marker-observation-window.coffee
+++ b/src/marker-observation-window.coffee
@@ -1,9 +1,9 @@
module.exports =
class MarkerObservationWindow
- constructor: (@displayBuffer, @bufferWindow) ->
+ constructor: (@decorationManager, @bufferWindow) ->
setScreenRange: (range) ->
- @bufferWindow.setRange(@displayBuffer.bufferRangeForScreenRange(range))
+ @bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range))
setBufferRange: (range) ->
@bufferWindow.setRange(range)
diff --git a/src/selection.coffee b/src/selection.coffee
index 2937baaee..7ecbb3fbc 100644
--- a/src/selection.coffee
+++ b/src/selection.coffee
@@ -87,7 +87,7 @@ class Selection extends Model
setBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
options.reversed ?= @isReversed()
- @editor.destroyFoldsContainingBufferRange(bufferRange) unless options.preserveFolds
+ @editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@modifySelection =>
needsFlash = options.flash
delete options.flash if options.flash?
@@ -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)
diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee
index 488b74d09..8c20055ed 100644
--- a/src/text-editor-component.coffee
+++ b/src/text-editor-component.coffee
@@ -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
diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee
deleted file mode 100644
index e99ad7323..000000000
--- a/src/text-editor-marker-layer.coffee
+++ /dev/null
@@ -1,192 +0,0 @@
-TextEditorMarker = require './text-editor-marker'
-
-# Public: *Experimental:* A container for a related set of markers at the
-# {TextEditor} level. Wraps an underlying {MarkerLayer} on the editor's
-# {TextBuffer}.
-#
-# This API is experimental and subject to change on any release.
-module.exports =
-class TextEditorMarkerLayer
- constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) ->
- @id = @bufferMarkerLayer.id
- @markersById = {}
-
- ###
- Section: Lifecycle
- ###
-
- # Essential: Destroy this layer.
- destroy: ->
- if @isDefaultLayer
- marker.destroy() for id, marker of @markersById
- else
- @bufferMarkerLayer.destroy()
-
- ###
- Section: Querying
- ###
-
- # Essential: Get an existing marker by its id.
- #
- # Returns a {TextEditorMarker}.
- getMarker: (id) ->
- if editorMarker = @markersById[id]
- editorMarker
- else if bufferMarker = @bufferMarkerLayer.getMarker(id)
- @markersById[id] = new TextEditorMarker(this, bufferMarker)
-
- # Essential: Get all markers in the layer.
- #
- # Returns an {Array} of {TextEditorMarker}s.
- getMarkers: ->
- @bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id)
-
- # Public: Get the number of markers in the marker layer.
- #
- # Returns a {Number}.
- getMarkerCount: ->
- @bufferMarkerLayer.getMarkerCount()
-
- # Public: Find markers in the layer conforming to the given parameters.
- #
- # See the documentation for {TextEditor::findMarkers}.
- findMarkers: (params) ->
- params = @translateToBufferMarkerParams(params)
- @bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id)
-
- ###
- Section: Marker creation
- ###
-
- # Essential: Create a marker on this layer with the given range in buffer
- # coordinates.
- #
- # See the documentation for {TextEditor::markBufferRange}
- markBufferRange: (bufferRange, options) ->
- @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id)
-
- # Essential: Create a marker on this layer with the given range in screen
- # coordinates.
- #
- # See the documentation for {TextEditor::markScreenRange}
- markScreenRange: (screenRange, options) ->
- bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange)
- @markBufferRange(bufferRange, options)
-
- # Public: Create a marker on this layer with the given buffer position and no
- # tail.
- #
- # See the documentation for {TextEditor::markBufferPosition}
- markBufferPosition: (bufferPosition, options) ->
- @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id)
-
- # Public: Create a marker on this layer with the given screen position and no
- # tail.
- #
- # See the documentation for {TextEditor::markScreenPosition}
- markScreenPosition: (screenPosition, options) ->
- bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition)
- @markBufferPosition(bufferPosition, options)
-
- ###
- Section: Event Subscription
- ###
-
- # Public: Subscribe to be notified asynchronously whenever markers are
- # created, updated, or destroyed on this layer. *Prefer this method for
- # optimal performance when interacting with layers that could contain large
- # numbers of markers.*
- #
- # * `callback` A {Function} that will be called with no arguments when changes
- # occur on this layer.
- #
- # Subscribers are notified once, asynchronously when any number of changes
- # occur in a given tick of the event loop. You should re-query the layer
- # to determine the state of markers in which you're interested in. It may
- # be counter-intuitive, but this is much more efficient than subscribing to
- # events on individual markers, which are expensive to deliver.
- #
- # Returns a {Disposable}.
- onDidUpdate: (callback) ->
- @bufferMarkerLayer.onDidUpdate(callback)
-
- # Public: Subscribe to be notified synchronously whenever markers are created
- # on this layer. *Avoid this method for optimal performance when interacting
- # with layers that could contain large numbers of markers.*
- #
- # * `callback` A {Function} that will be called with a {TextEditorMarker}
- # whenever a new marker is created.
- #
- # You should prefer {onDidUpdate} when synchronous notifications aren't
- # absolutely necessary.
- #
- # Returns a {Disposable}.
- onDidCreateMarker: (callback) ->
- @bufferMarkerLayer.onDidCreateMarker (bufferMarker) =>
- callback(@getMarker(bufferMarker.id))
-
- # Public: Subscribe to be notified synchronously when this layer is destroyed.
- #
- # Returns a {Disposable}.
- onDidDestroy: (callback) ->
- @bufferMarkerLayer.onDidDestroy(callback)
-
- ###
- Section: Private
- ###
-
- refreshMarkerScreenPositions: ->
- for marker in @getMarkers()
- marker.notifyObservers(textChanged: false)
- return
-
- didDestroyMarker: (marker) ->
- delete @markersById[marker.id]
-
- translateToBufferMarkerParams: (params) ->
- bufferMarkerParams = {}
- for key, value of params
- switch key
- when 'startBufferPosition'
- key = 'startPosition'
- when 'endBufferPosition'
- key = 'endPosition'
- when 'startScreenPosition'
- key = 'startPosition'
- value = @displayBuffer.bufferPositionForScreenPosition(value)
- when 'endScreenPosition'
- key = 'endPosition'
- value = @displayBuffer.bufferPositionForScreenPosition(value)
- when 'startBufferRow'
- key = 'startRow'
- when 'endBufferRow'
- key = 'endRow'
- when 'startScreenRow'
- key = 'startRow'
- value = @displayBuffer.bufferRowForScreenRow(value)
- when 'endScreenRow'
- key = 'endRow'
- value = @displayBuffer.bufferRowForScreenRow(value)
- when 'intersectsBufferRowRange'
- key = 'intersectsRowRange'
- when 'intersectsScreenRowRange'
- key = 'intersectsRowRange'
- [startRow, endRow] = value
- value = [@displayBuffer.bufferRowForScreenRow(startRow), @displayBuffer.bufferRowForScreenRow(endRow)]
- when 'containsBufferRange'
- key = 'containsRange'
- when 'containsBufferPosition'
- key = 'containsPosition'
- when 'containedInBufferRange'
- key = 'containedInRange'
- when 'containedInScreenRange'
- key = 'containedInRange'
- value = @displayBuffer.bufferRangeForScreenRange(value)
- when 'intersectsBufferRange'
- key = 'intersectsRange'
- when 'intersectsScreenRange'
- key = 'intersectsRange'
- value = @displayBuffer.bufferRangeForScreenRange(value)
- bufferMarkerParams[key] = value
-
- bufferMarkerParams
diff --git a/src/text-editor-marker.coffee b/src/text-editor-marker.coffee
deleted file mode 100644
index df84700ee..000000000
--- a/src/text-editor-marker.coffee
+++ /dev/null
@@ -1,371 +0,0 @@
-_ = require 'underscore-plus'
-{CompositeDisposable, Emitter} = require 'event-kit'
-
-# Essential: Represents a buffer annotation that remains logically stationary
-# even as the buffer changes. This is used to represent cursors, folds, snippet
-# targets, misspelled words, and anything else that needs to track a logical
-# location in the buffer over time.
-#
-# ### TextEditorMarker Creation
-#
-# Use {TextEditor::markBufferRange} rather than creating Markers directly.
-#
-# ### Head and Tail
-#
-# Markers always have a *head* and sometimes have a *tail*. If you think of a
-# marker as an editor selection, the tail is the part that's stationary and the
-# head is the part that moves when the mouse is moved. A marker without a tail
-# always reports an empty range at the head position. A marker with a head position
-# greater than the tail is in a "normal" orientation. If the head precedes the
-# tail the marker is in a "reversed" orientation.
-#
-# ### Validity
-#
-# Markers are considered *valid* when they are first created. Depending on the
-# invalidation strategy you choose, certain changes to the buffer can cause a
-# marker to become invalid, for example if the text surrounding the marker is
-# deleted. The strategies, in order of descending fragility:
-#
-# * __never__: The marker is never marked as invalid. This is a good choice for
-# markers representing selections in an editor.
-# * __surround__: The marker is invalidated by changes that completely surround it.
-# * __overlap__: The marker is invalidated by changes that surround the
-# start or end of the marker. This is the default.
-# * __inside__: The marker is invalidated by changes that extend into the
-# inside of the marker. Changes that end at the marker's start or
-# start at the marker's end do not invalidate the marker.
-# * __touch__: The marker is invalidated by a change that touches the marked
-# region in any way, including changes that end at the marker's
-# start or start at the marker's end. This is the most fragile strategy.
-#
-# See {TextEditor::markBufferRange} for usage.
-module.exports =
-class TextEditorMarker
- bufferMarkerSubscription: null
- oldHeadBufferPosition: null
- oldHeadScreenPosition: null
- oldTailBufferPosition: null
- oldTailScreenPosition: null
- wasValid: true
- hasChangeObservers: false
-
- ###
- Section: Construction and Destruction
- ###
-
- constructor: (@layer, @bufferMarker) ->
- {@displayBuffer} = @layer
- @emitter = new Emitter
- @disposables = new CompositeDisposable
- @id = @bufferMarker.id
-
- @disposables.add @bufferMarker.onDidDestroy => @destroyed()
-
- # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once
- # destroyed, a marker cannot be restored by undo/redo operations.
- destroy: ->
- @bufferMarker.destroy()
- @disposables.dispose()
-
- # Essential: Creates and returns a new {TextEditorMarker} with the same properties as
- # this marker.
- #
- # {Selection} markers (markers with a custom property `type: "selection"`)
- # should be copied with a different `type` value, for example with
- # `marker.copy({type: null})`. Otherwise, the new marker's selection will
- # be merged with this marker's selection, and a `null` value will be
- # returned.
- #
- # * `properties` (optional) {Object} properties to associate with the new
- # marker. The new marker's properties are computed by extending this marker's
- # properties with `properties`.
- #
- # Returns a {TextEditorMarker}.
- copy: (properties) ->
- @layer.getMarker(@bufferMarker.copy(properties).id)
-
- ###
- Section: Event Subscription
- ###
-
- # Essential: Invoke the given callback when the state of the marker changes.
- #
- # * `callback` {Function} to be called when the marker changes.
- # * `event` {Object} with the following keys:
- # * `oldHeadBufferPosition` {Point} representing the former head buffer position
- # * `newHeadBufferPosition` {Point} representing the new head buffer position
- # * `oldTailBufferPosition` {Point} representing the former tail buffer position
- # * `newTailBufferPosition` {Point} representing the new tail buffer position
- # * `oldHeadScreenPosition` {Point} representing the former head screen position
- # * `newHeadScreenPosition` {Point} representing the new head screen position
- # * `oldTailScreenPosition` {Point} representing the former tail screen position
- # * `newTailScreenPosition` {Point} representing the new tail screen position
- # * `wasValid` {Boolean} indicating whether the marker was valid before the change
- # * `isValid` {Boolean} indicating whether the marker is now valid
- # * `hadTail` {Boolean} indicating whether the marker had a tail before the change
- # * `hasTail` {Boolean} indicating whether the marker now has a tail
- # * `oldProperties` {Object} containing the marker's custom properties before the change.
- # * `newProperties` {Object} containing the marker's custom properties after the change.
- # * `textChanged` {Boolean} indicating whether this change was caused by a textual change
- # to the buffer or whether the marker was manipulated directly via its public API.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChange: (callback) ->
- unless @hasChangeObservers
- @oldHeadBufferPosition = @getHeadBufferPosition()
- @oldHeadScreenPosition = @getHeadScreenPosition()
- @oldTailBufferPosition = @getTailBufferPosition()
- @oldTailScreenPosition = @getTailScreenPosition()
- @wasValid = @isValid()
- @disposables.add @bufferMarker.onDidChange (event) => @notifyObservers(event)
- @hasChangeObservers = true
- @emitter.on 'did-change', callback
-
- # Essential: Invoke the given callback when the marker is destroyed.
- #
- # * `callback` {Function} to be called when the marker is destroyed.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.on 'did-destroy', callback
-
- ###
- Section: TextEditorMarker Details
- ###
-
- # Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be
- # invalidated when a region surrounding them in the buffer is changed.
- isValid: ->
- @bufferMarker.isValid()
-
- # Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker
- # can be invalid without being destroyed, in which case undoing the invalidating
- # operation would restore the marker. Once a marker is destroyed by calling
- # {TextEditorMarker::destroy}, no undo/redo operation can ever bring it back.
- isDestroyed: ->
- @bufferMarker.isDestroyed()
-
- # Essential: Returns a {Boolean} indicating whether the head precedes the tail.
- isReversed: ->
- @bufferMarker.isReversed()
-
- # Essential: Get the invalidation strategy for this marker.
- #
- # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`.
- #
- # Returns a {String}.
- getInvalidationStrategy: ->
- @bufferMarker.getInvalidationStrategy()
-
- # Essential: Returns an {Object} containing any custom properties associated with
- # the marker.
- getProperties: ->
- @bufferMarker.getProperties()
-
- # Essential: Merges an {Object} containing new properties into the marker's
- # existing properties.
- #
- # * `properties` {Object}
- setProperties: (properties) ->
- @bufferMarker.setProperties(properties)
-
- matchesProperties: (attributes) ->
- attributes = @layer.translateToBufferMarkerParams(attributes)
- @bufferMarker.matchesParams(attributes)
-
- ###
- Section: Comparing to other markers
- ###
-
- # Essential: Returns a {Boolean} indicating whether this marker is equivalent to
- # another marker, meaning they have the same range and options.
- #
- # * `other` {TextEditorMarker} other marker
- isEqual: (other) ->
- return false unless other instanceof @constructor
- @bufferMarker.isEqual(other.bufferMarker)
-
- # Essential: Compares this marker to another based on their ranges.
- #
- # * `other` {TextEditorMarker}
- #
- # Returns a {Number}
- compare: (other) ->
- @bufferMarker.compare(other.bufferMarker)
-
- ###
- Section: Managing the marker's range
- ###
-
- # Essential: Gets the buffer range of the display marker.
- #
- # Returns a {Range}.
- getBufferRange: ->
- @bufferMarker.getRange()
-
- # Essential: Modifies the buffer range of the display marker.
- #
- # * `bufferRange` The new {Range} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
- setBufferRange: (bufferRange, properties) ->
- @bufferMarker.setRange(bufferRange, properties)
-
- # Essential: Gets the screen range of the display marker.
- #
- # Returns a {Range}.
- getScreenRange: ->
- @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true)
-
- # Essential: Modifies the screen range of the display marker.
- #
- # * `screenRange` The new {Range} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
- setScreenRange: (screenRange, options) ->
- @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options)
-
- # Essential: Retrieves the buffer position of the marker's start. This will always be
- # less than or equal to the result of {TextEditorMarker::getEndBufferPosition}.
- #
- # Returns a {Point}.
- getStartBufferPosition: ->
- @bufferMarker.getStartPosition()
-
- # Essential: Retrieves the screen position of the marker's start. This will always be
- # less than or equal to the result of {TextEditorMarker::getEndScreenPosition}.
- #
- # Returns a {Point}.
- getStartScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true)
-
- # Essential: Retrieves the buffer position of the marker's end. This will always be
- # greater than or equal to the result of {TextEditorMarker::getStartBufferPosition}.
- #
- # Returns a {Point}.
- getEndBufferPosition: ->
- @bufferMarker.getEndPosition()
-
- # Essential: Retrieves the screen position of the marker's end. This will always be
- # greater than or equal to the result of {TextEditorMarker::getStartScreenPosition}.
- #
- # Returns a {Point}.
- getEndScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Retrieves the buffer position of the marker's head.
- #
- # Returns a {Point}.
- getHeadBufferPosition: ->
- @bufferMarker.getHeadPosition()
-
- # Extended: Sets the buffer position of the marker's head.
- #
- # * `bufferPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setHeadBufferPosition: (bufferPosition, properties) ->
- @bufferMarker.setHeadPosition(bufferPosition, properties)
-
- # Extended: Retrieves the screen position of the marker's head.
- #
- # Returns a {Point}.
- getHeadScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Sets the screen position of the marker's head.
- #
- # * `screenPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setHeadScreenPosition: (screenPosition, properties) ->
- @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties))
-
- # Extended: Retrieves the buffer position of the marker's tail.
- #
- # Returns a {Point}.
- getTailBufferPosition: ->
- @bufferMarker.getTailPosition()
-
- # Extended: Sets the buffer position of the marker's tail.
- #
- # * `bufferPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setTailBufferPosition: (bufferPosition) ->
- @bufferMarker.setTailPosition(bufferPosition)
-
- # Extended: Retrieves the screen position of the marker's tail.
- #
- # Returns a {Point}.
- getTailScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Sets the screen position of the marker's tail.
- #
- # * `screenPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setTailScreenPosition: (screenPosition, options) ->
- @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options))
-
- # Extended: Returns a {Boolean} indicating whether the marker has a tail.
- hasTail: ->
- @bufferMarker.hasTail()
-
- # Extended: Plants the marker's tail at the current head position. After calling
- # the marker's tail position will be its head position at the time of the
- # call, regardless of where the marker's head is moved.
- #
- # * `properties` (optional) {Object} properties to associate with the marker.
- plantTail: ->
- @bufferMarker.plantTail()
-
- # Extended: Removes the marker's tail. After calling the marker's head position
- # will be reported as its current tail position until the tail is planted
- # again.
- #
- # * `properties` (optional) {Object} properties to associate with the marker.
- clearTail: (properties) ->
- @bufferMarker.clearTail(properties)
-
- ###
- Section: Private utility methods
- ###
-
- # Returns a {String} representation of the marker
- inspect: ->
- "TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})"
-
- destroyed: ->
- @layer.didDestroyMarker(this)
- @emitter.emit 'did-destroy'
- @emitter.dispose()
-
- notifyObservers: ({textChanged}) ->
- textChanged ?= false
-
- newHeadBufferPosition = @getHeadBufferPosition()
- newHeadScreenPosition = @getHeadScreenPosition()
- newTailBufferPosition = @getTailBufferPosition()
- newTailScreenPosition = @getTailScreenPosition()
- isValid = @isValid()
-
- return if isValid is @wasValid and
- newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and
- newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and
- newTailBufferPosition.isEqual(@oldTailBufferPosition) and
- newTailScreenPosition.isEqual(@oldTailScreenPosition)
-
- changeEvent = {
- @oldHeadScreenPosition, newHeadScreenPosition,
- @oldTailScreenPosition, newTailScreenPosition,
- @oldHeadBufferPosition, newHeadBufferPosition,
- @oldTailBufferPosition, newTailBufferPosition,
- textChanged,
- isValid
- }
-
- @oldHeadBufferPosition = newHeadBufferPosition
- @oldHeadScreenPosition = newHeadScreenPosition
- @oldTailBufferPosition = newTailBufferPosition
- @oldTailScreenPosition = newTailScreenPosition
- @wasValid = isValid
-
- @emitter.emit 'did-change', changeEvent
diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee
index e5a9cd589..01a0293f6 100644
--- a/src/text-editor-presenter.coffee
+++ b/src/text-editor-presenter.coffee
@@ -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)
diff --git a/src/text-editor.coffee b/src/text-editor.coffee
index bdc283d8f..9607f3437 100644
--- a/src/text-editor.coffee
+++ b/src/text-editor.coffee
@@ -4,13 +4,17 @@ Grim = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
{Point, Range} = TextBuffer = require 'text-buffer'
LanguageMode = require './language-mode'
-DisplayBuffer = require './display-buffer'
+DecorationManager = require './decoration-manager'
+TokenizedBuffer = require './tokenized-buffer'
Cursor = require './cursor'
Model = require './model'
Selection = require './selection'
TextMateScopeSelector = require('first-mate').ScopeSelector
GutterContainer = require './gutter-container'
TextEditorElement = require './text-editor-element'
+{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
+
+ZERO_WIDTH_NBSP = '\ufeff'
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
@@ -54,6 +58,8 @@ TextEditorElement = require './text-editor-element'
# soft wraps and folds to ensure your code interacts with them correctly.
module.exports =
class TextEditor extends Model
+ serializationVersion: 1
+
buffer: null
languageMode: null
cursors: null
@@ -62,22 +68,45 @@ class TextEditor extends Model
selectionFlashDuration: 500
gutterContainer: null
editorElement: null
+ verticalScrollMargin: 2
+ horizontalScrollMargin: 6
+ softWrapped: null
+ editorWidthInChars: null
+ lineHeightInPixels: null
+ defaultCharWidth: null
+ height: null
+ width: null
registered: false
Object.defineProperty @prototype, "element",
get: -> @getElement()
+ Object.defineProperty(@prototype, 'displayBuffer', get: ->
+ Grim.deprecate("""
+ `TextEditor.prototype.displayBuffer` has always been private, but now
+ it is gone. Reading the `displayBuffer` property now returns a reference
+ to the containing `TextEditor`, which now provides *some* of the API of
+ the defunct `DisplayBuffer` class.
+ """)
+ this
+ )
+
@deserialize: (state, atomEnvironment) ->
+ # TODO: Return null on version mismatch when 1.8.0 has been out for a while
+ if state.version isnt @prototype.serializationVersion and state.displayBuffer?
+ state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer
+
try
- displayBuffer = DisplayBuffer.deserialize(state.displayBuffer, atomEnvironment)
+ state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
catch error
if error.syscall is 'read'
return # Error reading the file, don't deserialize an editor for it
else
throw error
- state.displayBuffer = displayBuffer
- state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId)
+ state.buffer = state.tokenizedBuffer.buffer
+ state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId) ? state.buffer.addDisplayLayer()
+ state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId)
state.config = atomEnvironment.config
state.clipboard = atomEnvironment.clipboard
state.grammarRegistry = atomEnvironment.grammars
@@ -92,10 +121,11 @@ class TextEditor extends Model
super
{
- @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength,
- softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
- @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @clipboard, @grammarRegistry,
- @assert, grammar, showInvisibles, @autoHeight, @scrollPastEnd
+ @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, @tabLength,
+ @softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation,
+ @mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @config, @clipboard, @grammarRegistry,
+ @assert, @applicationDelegate, grammar, @showInvisibles, @autoHeight, @scrollPastEnd, @editorWidthInChars,
+ @tokenizedBuffer, @ignoreInvisibles, @displayLayer
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
@@ -114,23 +144,29 @@ class TextEditor extends Model
@scrollPastEnd ?= true
@hasTerminatedPendingState = false
- showInvisibles ?= true
+ @showInvisibles ?= true
- buffer ?= new TextBuffer
- @displayBuffer ?= new DisplayBuffer({
- buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode,
- @config, @assert, @grammarRegistry
+ @buffer ?= new TextBuffer
+ @tokenizedBuffer ?= new TokenizedBuffer({
+ @tabLength, @buffer, @largeFileMode, @config, @grammarRegistry, @assert
})
- @buffer = @displayBuffer.buffer
- @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true)
+ @displayLayer ?= @buffer.addDisplayLayer()
+ @displayLayer.setTextDecorationLayer(@tokenizedBuffer)
+ @defaultMarkerLayer = @displayLayer.addMarkerLayer()
+ @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true)
+
+ @decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer)
+
+ @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
+
+ @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
for marker in @selectionsMarkerLayer.getMarkers()
- marker.setProperties(preserveFolds: true)
@addSelection(marker)
@subscribeToTabTypeConfig()
@subscribeToBuffer()
- @subscribeToDisplayBuffer()
+ @subscribeToDisplayLayer()
if @cursors.length is 0 and not suppressCursorCreation
initialLine = Math.max(parseInt(initialLine) or 0, 0)
@@ -151,13 +187,22 @@ class TextEditor extends Model
@setGrammar(grammar)
serialize: ->
+ tokenizedBufferState = @tokenizedBuffer.serialize()
+
deserializer: 'TextEditor'
+ version: @serializationVersion
id: @id
softTabs: @softTabs
firstVisibleScreenRow: @getFirstVisibleScreenRow()
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
- displayBuffer: @displayBuffer.serialize()
selectionsMarkerLayerId: @selectionsMarkerLayer.id
+ softWrapped: @isSoftWrapped()
+ editorWidthInChars: @editorWidthInChars
+ # TODO: Remove this forward-compatible fallback once 1.8 reaches stable.
+ displayBuffer: {tokenizedBuffer: tokenizedBufferState}
+ tokenizedBuffer: tokenizedBufferState
+ largeFileMode: @largeFileMode
+ displayLayerId: @displayLayer.id
registered: @registered
subscribeToBuffer: ->
@@ -180,11 +225,27 @@ class TextEditor extends Model
onDidTerminatePendingState: (callback) ->
@emitter.on 'did-terminate-pending-state', callback
- subscribeToDisplayBuffer: ->
+ subscribeToScopedConfigSettings: =>
+ @scopedConfigSubscriptions?.dispose()
+ @scopedConfigSubscriptions = subscriptions = new CompositeDisposable
+
+ scopeDescriptor = @getRootScopeDescriptor()
+ subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+
+ @resetDisplayLayer()
+
+ subscribeToDisplayLayer: ->
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
- @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
- @disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this)
- @disposables.add @displayBuffer.onDidChange (e) =>
+ @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
+ @disposables.add @tokenizedBuffer.onDidTokenize @handleTokenization.bind(this)
+ @disposables.add @displayLayer.onDidChangeSync (e) =>
@mergeIntersectingSelections()
@emitter.emit 'did-change', e
@@ -193,13 +254,28 @@ class TextEditor extends Model
@tabTypeSubscription = @config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), =>
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
+ resetDisplayLayer: ->
+ @displayLayer.reset({
+ invisibles: @getInvisibles(),
+ softWrapColumn: @getSoftWrapColumn(),
+ showIndentGuides: not @isMini() and @config.get('editor.showIndentGuide', scope: @getRootScopeDescriptor()),
+ atomicSoftTabs: @config.get('editor.atomicSoftTabs', scope: @getRootScopeDescriptor()),
+ tabLength: @getTabLength(),
+ ratioForCharacter: @ratioForCharacter.bind(this),
+ isWrapBoundary: isWrapBoundary,
+ foldCharacter: ZERO_WIDTH_NBSP
+ })
+
destroyed: ->
@disposables.dispose()
+ @displayLayer.destroy()
+ @scopedConfigSubscriptions.dispose()
+ @disposables.dispose()
+ @tokenizedBuffer.destroy()
@tabTypeSubscription.dispose()
selection.destroy() for selection in @selections.slice()
@selectionsMarkerLayer.destroy()
@buffer.release()
- @displayBuffer.destroy()
@languageMode.destroy()
@gutterContainer.destroy()
@emitter.emit 'did-destroy'
@@ -283,7 +359,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeSoftWrapped: (callback) ->
- @displayBuffer.onDidChangeSoftWrapped(callback)
+ @emitter.on 'did-change-soft-wrapped', callback
# Extended: Calls your `callback` when the buffer's encoding has changed.
#
@@ -437,7 +513,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeDecorations: (callback) ->
- @displayBuffer.observeDecorations(callback)
+ @decorationManager.observeDecorations(callback)
# Extended: Calls your `callback` when a {Decoration} is added to the editor.
#
@@ -446,7 +522,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddDecoration: (callback) ->
- @displayBuffer.onDidAddDecoration(callback)
+ @decorationManager.onDidAddDecoration(callback)
# Extended: Calls your `callback` when a {Decoration} is removed from the editor.
#
@@ -455,7 +531,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidRemoveDecoration: (callback) ->
- @displayBuffer.onDidRemoveDecoration(callback)
+ @decorationManager.onDidRemoveDecoration(callback)
# Extended: Calls your `callback` when the placeholder text is changed.
#
@@ -466,9 +542,6 @@ class TextEditor extends Model
onDidChangePlaceholderText: (callback) ->
@emitter.on 'did-change-placeholder-text', callback
- onDidChangeCharacterWidths: (callback) ->
- @displayBuffer.onDidChangeCharacterWidths(callback)
-
onDidChangeFirstVisibleScreenRow: (callback, fromView) ->
@emitter.on 'did-change-first-visible-screen-row', callback
@@ -483,17 +556,14 @@ class TextEditor extends Model
@getElement().onDidChangeScrollLeft(callback)
onDidRequestAutoscroll: (callback) ->
- @displayBuffer.onDidRequestAutoscroll(callback)
+ @emitter.on 'did-request-autoscroll', callback
# TODO Remove once the tabs package no longer uses .on subscriptions
onDidChangeIcon: (callback) ->
@emitter.on 'did-change-icon', callback
- onDidUpdateMarkers: (callback) ->
- @displayBuffer.onDidUpdateMarkers(callback)
-
onDidUpdateDecorations: (callback) ->
- @displayBuffer.onDidUpdateDecorations(callback)
+ @decorationManager.onDidUpdateDecorations(callback)
# Essential: Retrieves the current {TextBuffer}.
getBuffer: -> @buffer
@@ -503,31 +573,32 @@ class TextEditor extends Model
# Create an {TextEditor} with its initial state based on this object
copy: ->
- displayBuffer = @displayBuffer.copy()
- selectionsMarkerLayer = displayBuffer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
+ displayLayer = @displayLayer.copy()
+ selectionsMarkerLayer = displayLayer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
softTabs = @getSoftTabs()
newEditor = new TextEditor({
- @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs,
+ @buffer, selectionsMarkerLayer, @tabLength, softTabs,
suppressCursorCreation: true, @config,
@firstVisibleScreenRow, @firstVisibleScreenColumn,
- @clipboard, @grammarRegistry, @assert
+ @clipboard, @grammarRegistry, @assert, displayLayer
})
newEditor
# Controls visibility based on the given {Boolean}.
- setVisible: (visible) -> @displayBuffer.setVisible(visible)
+ setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
setMini: (mini) ->
if mini isnt @mini
@mini = mini
- @displayBuffer.setIgnoreInvisibles(@mini)
+ @ignoreInvisibles = @mini
+ @resetDisplayLayer()
@emitter.emit 'did-change-mini', @mini
@mini
isMini: -> @mini
setUpdatedSynchronously: (updatedSynchronously) ->
- @displayBuffer.setUpdatedSynchronously(updatedSynchronously)
+ @decorationManager.setUpdatedSynchronously(updatedSynchronously)
onDidChangeMini: (callback) ->
@emitter.on 'did-change-mini', callback
@@ -580,12 +651,18 @@ class TextEditor extends Model
# * `editorWidthInChars` A {Number} representing the width of the
# {TextEditorElement} in characters.
setEditorWidthInChars: (editorWidthInChars) ->
- @displayBuffer.setEditorWidthInChars(editorWidthInChars)
+ if editorWidthInChars > 0
+ previousWidthInChars = @editorWidthInChars
+ @editorWidthInChars = editorWidthInChars
+ if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped()
+ @resetDisplayLayer()
# Returns the editor width in characters.
getEditorWidthInChars: ->
- @displayBuffer.getEditorWidthInChars()
-
+ if @width? and @defaultCharWidth > 0
+ Math.max(0, Math.floor(@width / @defaultCharWidth))
+ else
+ @editorWidthInChars
###
Section: File Details
@@ -718,7 +795,7 @@ class TextEditor extends Model
# Essential: Returns a {Number} representing the number of screen lines in the
# editor. This accounts for folds.
- getScreenLineCount: -> @displayBuffer.getLineCount()
+ getScreenLineCount: -> @displayLayer.getScreenLineCount()
# Essential: Returns a {Number} representing the last zero-indexed buffer row
# number of the editor.
@@ -726,7 +803,7 @@ class TextEditor extends Model
# Essential: Returns a {Number} representing the last zero-indexed screen row
# number of the editor.
- getLastScreenRow: -> @displayBuffer.getLastRow()
+ getLastScreenRow: -> @getScreenLineCount() - 1
# Essential: Returns a {String} representing the contents of the line at the
# given buffer row.
@@ -738,29 +815,43 @@ class TextEditor extends Model
# given screen row.
#
# * `screenRow` A {Number} representing a zero-indexed screen row.
- lineTextForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow)?.text
+ lineTextForScreenRow: (screenRow) ->
+ @screenLineForScreenRow(screenRow)?.lineText
- # Gets the screen line for the given screen row.
- #
- # * `screenRow` - A {Number} indicating the screen row.
- #
- # Returns {TokenizedLine}
- tokenizedLineForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow)
+ logScreenLines: (start=0, end=@getLastScreenRow()) ->
+ for row in [start..end]
+ line = @lineTextForScreenRow(row)
+ console.log row, @bufferRowForScreenRow(row), line, line.length
+ return
- # {Delegates to: DisplayBuffer.tokenizedLinesForScreenRows}
- tokenizedLinesForScreenRows: (start, end) -> @displayBuffer.tokenizedLinesForScreenRows(start, end)
+ tokensForScreenRow: (screenRow) ->
+ for tagCode in @screenLineForScreenRow(screenRow).tagCodes when @displayLayer.isOpenTagCode(tagCode)
+ @displayLayer.tagForCode(tagCode)
- bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row)
+ screenLineForScreenRow: (screenRow) ->
+ return if screenRow < 0 or screenRow > @getLastScreenRow()
+ @displayLayer.getScreenLines(screenRow, screenRow + 1)[0]
- # {Delegates to: DisplayBuffer.bufferRowsForScreenRows}
- bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
+ bufferRowForScreenRow: (screenRow) ->
+ @displayLayer.translateScreenPosition(Point(screenRow, 0)).row
- screenRowForBufferRow: (row) -> @displayBuffer.screenRowForBufferRow(row)
+ bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
+ for screenRow in [startScreenRow..endScreenRow]
+ @bufferRowForScreenRow(screenRow)
- # {Delegates to: DisplayBuffer.getMaxLineLength}
- getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength()
+ screenRowForBufferRow: (row) ->
+ if @largeFileMode
+ row
+ else
+ @displayLayer.translateBufferPosition(Point(row, 0)).row
- getLongestScreenRow: -> @displayBuffer.getLongestScreenRow()
+ getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition()
+
+ getMaxScreenLineLength: -> @getRightmostScreenPosition().column
+
+ getLongestScreenRow: -> @getRightmostScreenPosition().row
+
+ lineLengthForScreenRow: (screenRow) -> @displayLayer.lineLengthForScreenRow(screenRow)
# Returns the range for the given buffer row.
#
@@ -868,8 +959,7 @@ class TextEditor extends Model
# Move lines intersecting the most recent selection or multiple selections
# up by one row in screen coordinates.
moveLineUp: ->
- selections = @getSelectedBufferRanges()
- selections.sort (a, b) -> a.compare(b)
+ selections = @getSelectedBufferRanges().sort((a, b) -> a.compare(b))
if selections[0].start.row is 0
return
@@ -890,58 +980,38 @@ class TextEditor extends Model
selection.end.row = selections[0].end.row
selections.shift()
- # Compute the range spanned by all these selections...
- linesRangeStart = [selection.start.row, 0]
+ # Compute the buffer range spanned by all these selections, expanding it
+ # so that it includes any folded region that intersects them.
+ startRow = selection.start.row
+ endRow = selection.end.row
if selection.end.row > selection.start.row and selection.end.column is 0
# Don't move the last line of a multi-line selection if the selection ends at column 0
- linesRange = new Range(linesRangeStart, selection.end)
- else
- linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0])
+ endRow--
- # If there's a fold containing either the starting row or the end row
- # of the selection then the whole fold needs to be moved and restored.
- # The initial fold range is stored and will be translated once the
- # insert delta is know.
- selectionFoldRanges = []
- foldAtSelectionStart =
- @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
- foldAtSelectionEnd =
- @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
- if fold = foldAtSelectionStart ? foldAtSelectionEnd
- selectionFoldRanges.push range = fold.getBufferRange()
- newEndRow = range.end.row + 1
- linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
- fold.destroy()
+ {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow)
+ {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
+ linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
# If selected line range is preceded by a fold, one line above on screen
# could be multiple lines in the buffer.
- precedingScreenRow = @screenRowForBufferRow(linesRange.start.row) - 1
- precedingBufferRow = @bufferRowForScreenRow(precedingScreenRow)
- insertDelta = linesRange.start.row - precedingBufferRow
+ {bufferRow: precedingRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow - 1)
+ insertDelta = linesRange.start.row - precedingRow
# Any folds in the text that is moved will need to be re-created.
# It includes the folds that were intersecting with the selection.
- rangesToRefold = selectionFoldRanges.concat(
- @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
- range = fold.getBufferRange()
- fold.destroy()
- range
- ).map (range) -> range.translate([-insertDelta, 0])
-
- # Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow)
- rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0]))
- fold.destroy()
+ rangesToRefold = @displayLayer
+ .destroyFoldsIntersectingBufferRange(linesRange)
+ .map((range) -> range.translate([-insertDelta, 0]))
# Delete lines spanned by selection and insert them on the preceding buffer row
lines = @buffer.getTextInRange(linesRange)
lines += @buffer.lineEndingForRow(linesRange.end.row - 1) unless lines[lines.length - 1] is '\n'
@buffer.delete(linesRange)
- @buffer.insert([precedingBufferRow, 0], lines)
+ @buffer.insert([precedingRow, 0], lines)
# Restore folds that existed before the lines were moved
for rangeToRefold in rangesToRefold
- @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row)
+ @displayLayer.foldBufferRange(rangeToRefold)
for selection in selectionsToMove
newSelectionRanges.push(selection.translate([-insertDelta, 0]))
@@ -972,63 +1042,42 @@ class TextEditor extends Model
selection.start.row = selections[0].start.row
selections.shift()
- # Compute the range spanned by all these selections...
- linesRangeStart = [selection.start.row, 0]
+ # Compute the buffer range spanned by all these selections, expanding it
+ # so that it includes any folded region that intersects them.
+ startRow = selection.start.row
+ endRow = selection.end.row
if selection.end.row > selection.start.row and selection.end.column is 0
# Don't move the last line of a multi-line selection if the selection ends at column 0
- linesRange = new Range(linesRangeStart, selection.end)
- else
- linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0])
+ endRow--
- # If there's a fold containing either the starting row or the end row
- # of the selection then the whole fold needs to be moved and restored.
- # The initial fold range is stored and will be translated once the
- # insert delta is know.
- selectionFoldRanges = []
- foldAtSelectionStart =
- @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
- foldAtSelectionEnd =
- @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
- if fold = foldAtSelectionStart ? foldAtSelectionEnd
- selectionFoldRanges.push range = fold.getBufferRange()
- newEndRow = range.end.row + 1
- linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
- fold.destroy()
+ {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow)
+ {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
+ linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
# If selected line range is followed by a fold, one line below on screen
# could be multiple lines in the buffer. But at the same time, if the
# next buffer row is wrapped, one line in the buffer can represent many
# screen rows.
- followingScreenRow = @displayBuffer.lastScreenRowForBufferRow(linesRange.end.row) + 1
- followingBufferRow = @bufferRowForScreenRow(followingScreenRow)
- insertDelta = followingBufferRow - linesRange.end.row
+ {bufferRow: followingRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
+ insertDelta = followingRow - linesRange.end.row
# Any folds in the text that is moved will need to be re-created.
# It includes the folds that were intersecting with the selection.
- rangesToRefold = selectionFoldRanges.concat(
- @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
- range = fold.getBufferRange()
- fold.destroy()
- range
- ).map (range) -> range.translate([insertDelta, 0])
-
- # Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow)
- rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0]))
- fold.destroy()
+ rangesToRefold = @displayLayer
+ .destroyFoldsIntersectingBufferRange(linesRange)
+ .map((range) -> range.translate([insertDelta, 0]))
# Delete lines spanned by selection and insert them on the following correct buffer row
- insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0)
lines = @buffer.getTextInRange(linesRange)
- if linesRange.end.row is @buffer.getLastRow()
+ if followingRow - 1 is @buffer.getLastRow()
lines = "\n#{lines}"
+ @buffer.insert([followingRow, 0], lines)
@buffer.delete(linesRange)
- @buffer.insert(insertPosition, lines)
# Restore folds that existed before the lines were moved
for rangeToRefold in rangesToRefold
- @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row)
+ @displayLayer.foldBufferRange(rangeToRefold)
for selection in selectionsToMove
newSelectionRanges.push(selection.translate([insertDelta, 0]))
@@ -1088,15 +1137,12 @@ class TextEditor extends Model
selectedBufferRange = selection.getBufferRange()
if selection.isEmpty()
{start} = selection.getScreenRange()
- selection.selectToScreenPosition([start.row + 1, 0])
+ selection.setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true)
[startRow, endRow] = selection.getBufferRowRange()
endRow++
- foldedRowRanges =
- @outermostFoldsInBufferRowRange(startRow, endRow)
- .map (fold) -> fold.getBufferRowRange()
-
+ intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]])
rangeToDuplicate = [[startRow, 0], [endRow, 0]]
textToDuplicate = @getTextInBufferRange(rangeToDuplicate)
textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow()
@@ -1104,8 +1150,9 @@ class TextEditor extends Model
delta = endRow - startRow
selection.setBufferRange(selectedBufferRange.translate([delta, 0]))
- for [foldStartRow, foldEndRow] in foldedRowRanges
- @createFold(foldStartRow + delta, foldEndRow + delta)
+ for fold in intersectingFolds
+ foldRange = @displayLayer.bufferRangeForFold(fold)
+ @displayLayer.foldBufferRange(foldRange.translate([delta, 0]))
return
replaceSelectedText: (options={}, fn) ->
@@ -1344,7 +1391,18 @@ class TextEditor extends Model
# * `options` (optional) An options hash for {::clipScreenPosition}.
#
# Returns a {Point}.
- screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options)
+ screenPositionForBufferPosition: (bufferPosition, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
+ @displayLayer.translateBufferPosition(bufferPosition, options)
# Essential: Convert a position in screen-coordinates to buffer-coordinates.
#
@@ -1354,21 +1412,40 @@ class TextEditor extends Model
# * `options` (optional) An options hash for {::clipScreenPosition}.
#
# Returns a {Point}.
- bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options)
+ bufferPositionForScreenPosition: (screenPosition, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
+ @displayLayer.translateScreenPosition(screenPosition, options)
# Essential: Convert a range in buffer-coordinates to screen-coordinates.
#
# * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates.
#
# Returns a {Range}.
- screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange)
+ screenRangeForBufferRange: (bufferRange, options) ->
+ bufferRange = Range.fromObject(bufferRange)
+ start = @screenPositionForBufferPosition(bufferRange.start, options)
+ end = @screenPositionForBufferPosition(bufferRange.end, options)
+ new Range(start, end)
# Essential: Convert a range in screen-coordinates to buffer-coordinates.
#
# * `screenRange` {Range} in screen coordinates to translate into buffer coordinates.
#
# Returns a {Range}.
- bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange)
+ bufferRangeForScreenRange: (screenRange) ->
+ screenRange = Range.fromObject(screenRange)
+ start = @bufferPositionForScreenPosition(screenRange.start)
+ end = @bufferPositionForScreenPosition(screenRange.end)
+ new Range(start, end)
# Extended: Clip the given {Point} to a valid position in the buffer.
#
@@ -1417,26 +1494,44 @@ class TextEditor extends Model
#
# * `screenPosition` The {Point} representing the position to clip.
# * `options` (optional) {Object}
- # * `wrapBeyondNewlines` {Boolean} if `true`, continues wrapping past newlines
- # * `wrapAtSoftNewlines` {Boolean} if `true`, continues wrapping past soft newlines
- # * `screenLine` {Boolean} if `true`, indicates that you're using a line number, not a row number
+ # * `clipDirection` {String} If `'backward'`, returns the first valid
+ # position preceding an invalid position. If `'forward'`, returns the
+ # first valid position following an invalid position. If `'closest'`,
+ # returns the first valid position closest to an invalid position.
+ # Defaults to `'closest'`.
#
# Returns a {Point}.
- clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options)
+ clipScreenPosition: (screenPosition, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
+ @displayLayer.clipScreenPosition(screenPosition, options)
# Extended: Clip the start and end of the given range to valid positions on screen.
# See {::clipScreenPosition} for more information.
#
# * `range` The {Range} to clip.
# * `options` (optional) See {::clipScreenPosition} `options`.
+ #
# Returns a {Range}.
- clipScreenRange: (range, options) -> @displayBuffer.clipScreenRange(range, options)
+ clipScreenRange: (screenRange, options) ->
+ screenRange = Range.fromObject(screenRange)
+ start = @displayLayer.clipScreenPosition(screenRange.start, options)
+ end = @displayLayer.clipScreenPosition(screenRange.end, options)
+ Range(start, end)
###
Section: Decorations
###
- # Essential: Add a decoration that tracks a {TextEditorMarker}. When the
+ # Essential: Add a decoration that tracks a {DisplayMarker}. When the
# marker moves, is invalidated, or is destroyed, the decoration will be
# updated to reflect the marker's state.
#
@@ -1457,8 +1552,8 @@ class TextEditor extends Model
#
# ```
# * __overlay__: Positions the view associated with the given item at the head
- # or tail of the given `TextEditorMarker`.
- # * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter
+ # or tail of the given `DisplayMarker`.
+ # * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter
# decorations are created by calling {Gutter::decorateMarker} on the
# desired `Gutter` instance.
# * __block__: Positions the view associated with the given item before or
@@ -1466,21 +1561,21 @@ class TextEditor extends Model
#
# ## Arguments
#
- # * `marker` A {TextEditorMarker} you want this decoration to follow.
+ # * `marker` A {DisplayMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration e.g.
# `{type: 'line-number', class: 'linter-error'}`
# * `type` There are several supported decoration types. The behavior of the
# types are as follows:
# * `line` Adds the given `class` to the lines overlapping the rows
- # spanned by the `TextEditorMarker`.
+ # spanned by the `DisplayMarker`.
# * `line-number` Adds the given `class` to the line numbers overlapping
- # the rows spanned by the `TextEditorMarker`.
+ # the rows spanned by the `DisplayMarker`.
# * `highlight` Creates a `.highlight` div with the nested class with up
- # to 3 nested regions that fill the area spanned by the `TextEditorMarker`.
+ # to 3 nested regions that fill the area spanned by the `DisplayMarker`.
# * `overlay` Positions the view associated with the given item at the
- # head or tail of the given `TextEditorMarker`, depending on the `position`
+ # head or tail of the given `DisplayMarker`, depending on the `position`
# property.
- # * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling
+ # * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling
# {Gutter::decorateMarker} on the desired `Gutter` instance.
# * `block` Positions the view associated with the given item before or
# after the row of the given `TextEditorMarker`, depending on the `position`
@@ -1491,13 +1586,13 @@ class TextEditor extends Model
# corresponding view registered. Only applicable to the `gutter`,
# `overlay` and `block` types.
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
- # the head of the `TextEditorMarker`. Only applicable to the `line` and
+ # the head of the `DisplayMarker`. Only applicable to the `line` and
# `line-number` types.
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
- # the associated `TextEditorMarker` is empty. Only applicable to the `gutter`,
+ # the associated `DisplayMarker` is empty. Only applicable to the `gutter`,
# `line`, and `line-number` types.
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
- # if the associated `TextEditorMarker` is non-empty. Only applicable to the
+ # if the associated `DisplayMarker` is non-empty. Only applicable to the
# `gutter`, `line`, and `line-number` types.
# * `position` (optional) Only applicable to decorations of type `overlay` and `block`,
# controls where the view is positioned relative to the `TextEditorMarker`.
@@ -1506,21 +1601,19 @@ class TextEditor extends Model
#
# Returns a {Decoration} object
decorateMarker: (marker, decorationParams) ->
- @displayBuffer.decorateMarker(marker, decorationParams)
+ @decorationManager.decorateMarker(marker, decorationParams)
- # Essential: *Experimental:* Add a decoration to every marker in the given
- # marker layer. Can be used to decorate a large number of markers without
- # having to create and manage many individual decorations.
+ # Essential: Add a decoration to every marker in the given marker layer. Can
+ # be used to decorate a large number of markers without having to create and
+ # manage many individual decorations.
#
- # * `markerLayer` A {TextEditorMarkerLayer} or {MarkerLayer} to decorate.
+ # * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate.
# * `decorationParams` The same parameters that are passed to
# {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`.
#
- # This API is experimental and subject to change on any release.
- #
# Returns a {LayerDecoration}.
decorateMarkerLayer: (markerLayer, decorationParams) ->
- @displayBuffer.decorateMarkerLayer(markerLayer, decorationParams)
+ @decorationManager.decorateMarkerLayer(markerLayer, decorationParams)
# Deprecated: Get all the decorations within a screen row range on the default
# layer.
@@ -1530,14 +1623,14 @@ class TextEditor extends Model
#
# Returns an {Object} of decorations in the form
# `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}`
- # where the keys are {TextEditorMarker} IDs, and the values are an array of decoration
+ # where the keys are {DisplayMarker} IDs, and the values are an array of decoration
# params objects attached to the marker.
# Returns an empty object when no decorations are found
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
- @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow)
+ @decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow)
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
- @displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
+ @decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
# Extended: Get all decorations.
#
@@ -1546,7 +1639,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getDecorations: (propertyFilter) ->
- @displayBuffer.getDecorations(propertyFilter)
+ @decorationManager.getDecorations(propertyFilter)
# Extended: Get all decorations of type 'line'.
#
@@ -1555,7 +1648,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getLineDecorations: (propertyFilter) ->
- @displayBuffer.getLineDecorations(propertyFilter)
+ @decorationManager.getLineDecorations(propertyFilter)
# Extended: Get all decorations of type 'line-number'.
#
@@ -1564,7 +1657,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getLineNumberDecorations: (propertyFilter) ->
- @displayBuffer.getLineNumberDecorations(propertyFilter)
+ @decorationManager.getLineNumberDecorations(propertyFilter)
# Extended: Get all decorations of type 'highlight'.
#
@@ -1573,7 +1666,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getHighlightDecorations: (propertyFilter) ->
- @displayBuffer.getHighlightDecorations(propertyFilter)
+ @decorationManager.getHighlightDecorations(propertyFilter)
# Extended: Get all decorations of type 'overlay'.
#
@@ -1582,13 +1675,13 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getOverlayDecorations: (propertyFilter) ->
- @displayBuffer.getOverlayDecorations(propertyFilter)
+ @decorationManager.getOverlayDecorations(propertyFilter)
decorationForId: (id) ->
- @displayBuffer.decorationForId(id)
+ @decorationManager.decorationForId(id)
decorationsForMarkerId: (id) ->
- @displayBuffer.decorationsForMarkerId(id)
+ @decorationManager.decorationsForMarkerId(id)
###
Section: Markers
@@ -1625,9 +1718,9 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {TextEditorMarker}.
- markBufferRange: (args...) ->
- @displayBuffer.markBufferRange(args...)
+ # Returns a {DisplayMarker}.
+ markBufferRange: (bufferRange, options) ->
+ @defaultMarkerLayer.markBufferRange(bufferRange, options)
# Essential: Create a marker on the default marker layer with the given range
# in screen coordinates. This marker will maintain its logical location as the
@@ -1660,31 +1753,66 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {TextEditorMarker}.
- markScreenRange: (args...) ->
- @displayBuffer.markScreenRange(args...)
+ # Returns a {DisplayMarker}.
+ markScreenRange: (screenRange, options) ->
+ @defaultMarkerLayer.markScreenRange(screenRange, options)
- # Essential: Mark the given position in buffer coordinates on the default
- # marker layer.
+ # Essential: Create a marker on the default marker layer with the given buffer
+ # position and no tail. To group multiple markers together in their own
+ # private layer, see {::addMarkerLayer}.
#
- # * `position` A {Point} or {Array} of `[row, column]`.
- # * `options` (optional) See {TextBuffer::markRange}.
+ # * `bufferPosition` A {Point} or point-compatible {Array}
+ # * `options` (optional) An {Object} with the following keys:
+ # * `invalidate` (optional) {String} Determines the rules by which changes
+ # to the buffer *invalidate* the marker. (default: 'overlap') It can be
+ # any of the following strategies, in order of fragility:
+ # * __never__: The marker is never marked as invalid. This is a good choice for
+ # markers representing selections in an editor.
+ # * __surround__: The marker is invalidated by changes that completely surround it.
+ # * __overlap__: The marker is invalidated by changes that surround the
+ # start or end of the marker. This is the default.
+ # * __inside__: The marker is invalidated by changes that extend into the
+ # inside of the marker. Changes that end at the marker's start or
+ # start at the marker's end do not invalidate the marker.
+ # * __touch__: The marker is invalidated by a change that touches the marked
+ # region in any way, including changes that end at the marker's
+ # start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {TextEditorMarker}.
- markBufferPosition: (args...) ->
- @displayBuffer.markBufferPosition(args...)
+ # Returns a {DisplayMarker}.
+ markBufferPosition: (bufferPosition, options) ->
+ @defaultMarkerLayer.markBufferPosition(bufferPosition, options)
- # Essential: Mark the given position in screen coordinates on the default
- # marker layer.
+ # Essential: Create a marker on the default marker layer with the given screen
+ # position and no tail. To group multiple markers together in their own
+ # private layer, see {::addMarkerLayer}.
#
- # * `position` A {Point} or {Array} of `[row, column]`.
- # * `options` (optional) See {TextBuffer::markRange}.
+ # * `screenPosition` A {Point} or point-compatible {Array}
+ # * `options` (optional) An {Object} with the following keys:
+ # * `invalidate` (optional) {String} Determines the rules by which changes
+ # to the buffer *invalidate* the marker. (default: 'overlap') It can be
+ # any of the following strategies, in order of fragility:
+ # * __never__: The marker is never marked as invalid. This is a good choice for
+ # markers representing selections in an editor.
+ # * __surround__: The marker is invalidated by changes that completely surround it.
+ # * __overlap__: The marker is invalidated by changes that surround the
+ # start or end of the marker. This is the default.
+ # * __inside__: The marker is invalidated by changes that extend into the
+ # inside of the marker. Changes that end at the marker's start or
+ # start at the marker's end do not invalidate the marker.
+ # * __touch__: The marker is invalidated by a change that touches the marked
+ # region in any way, including changes that end at the marker's
+ # start or start at the marker's end. This is the most fragile strategy.
+ # * `clipDirection` {String} If `'backward'`, returns the first valid
+ # position preceding an invalid position. If `'forward'`, returns the
+ # first valid position following an invalid position. If `'closest'`,
+ # returns the first valid position closest to an invalid position.
+ # Defaults to `'closest'`.
#
- # Returns a {TextEditorMarker}.
- markScreenPosition: (args...) ->
- @displayBuffer.markScreenPosition(args...)
+ # Returns a {DisplayMarker}.
+ markScreenPosition: (screenPosition, options) ->
+ @defaultMarkerLayer.markScreenPosition(screenPosition, options)
- # Essential: Find all {TextEditorMarker}s on the default marker layer that
+ # Essential: Find all {DisplayMarker}s on the default marker layer that
# match the given properties.
#
# This method finds markers based on the given properties. Markers can be
@@ -1704,20 +1832,22 @@ class TextEditor extends Model
# in range-compatible {Array} in buffer coordinates.
# * `containsBufferPosition` Only include markers containing this {Point}
# or {Array} of `[row, column]` in buffer coordinates.
- findMarkers: (properties) ->
- @displayBuffer.findMarkers(properties)
+ #
+ # Returns an {Array} of {DisplayMarker}s
+ findMarkers: (params) ->
+ @defaultMarkerLayer.findMarkers(params)
- # Extended: Get the {TextEditorMarker} on the default layer for the given
+ # Extended: Get the {DisplayMarker} on the default layer for the given
# marker id.
#
# * `id` {Number} id of the marker
getMarker: (id) ->
- @displayBuffer.getMarker(id)
+ @defaultMarkerLayer.getMarker(id)
- # Extended: Get all {TextEditorMarker}s on the default marker layer. Consider
+ # Extended: Get all {DisplayMarker}s on the default marker layer. Consider
# using {::findMarkers}
getMarkers: ->
- @displayBuffer.getMarkers()
+ @defaultMarkerLayer.getMarkers()
# Extended: Get the number of markers in the default marker layer.
#
@@ -1728,39 +1858,38 @@ class TextEditor extends Model
destroyMarker: (id) ->
@getMarker(id)?.destroy()
- # Extended: *Experimental:* Create a marker layer to group related markers.
+ # Essential: Create a marker layer to group related markers.
#
# * `options` An {Object} containing the following keys:
# * `maintainHistory` A {Boolean} indicating whether marker state should be
# restored on undo/redo. Defaults to `false`.
+ # * `persistent` A {Boolean} indicating whether or not this marker layer
+ # should be serialized and deserialized along with the rest of the
+ # buffer. Defaults to `false`. If `true`, the marker layer's id will be
+ # maintained across the serialization boundary, allowing you to retrieve
+ # it via {::getMarkerLayer}.
#
- # This API is experimental and subject to change on any release.
- #
- # Returns a {TextEditorMarkerLayer}.
+ # Returns a {DisplayMarkerLayer}.
addMarkerLayer: (options) ->
- @displayBuffer.addMarkerLayer(options)
+ @displayLayer.addMarkerLayer(options)
- # Public: *Experimental:* Get a {TextEditorMarkerLayer} by id.
+ # Essential: Get a {DisplayMarkerLayer} by id.
#
# * `id` The id of the marker layer to retrieve.
#
- # This API is experimental and subject to change on any release.
- #
- # Returns a {MarkerLayer} or `undefined` if no layer exists with the given
- # id.
+ # Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the
+ # given id.
getMarkerLayer: (id) ->
- @displayBuffer.getMarkerLayer(id)
+ @displayLayer.getMarkerLayer(id)
- # Public: *Experimental:* Get the default {TextEditorMarkerLayer}.
+ # Essential: Get the default {DisplayMarkerLayer}.
#
# All marker APIs not tied to an explicit layer interact with this default
# layer.
#
- # This API is experimental and subject to change on any release.
- #
- # Returns a {TextEditorMarkerLayer}.
+ # Returns a {DisplayMarkerLayer}.
getDefaultMarkerLayer: ->
- @displayBuffer.getDefaultMarkerLayer()
+ @defaultMarkerLayer
###
Section: Cursors
@@ -1784,7 +1913,7 @@ class TextEditor extends Model
# If there are multiple cursors, they will be consolidated to a single cursor.
#
# * `position` A {Point} or {Array} of `[row, column]`
- # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with:
+ # * `options` (optional) An {Object} containing the following keys:
# * `autoscroll` Determines whether the editor scrolls to the new cursor's
# position. Defaults to true.
setCursorBufferPosition: (position, options) ->
@@ -1822,6 +1951,16 @@ class TextEditor extends Model
# * `autoscroll` Determines whether the editor scrolls to the new cursor's
# position. Defaults to true.
setCursorScreenPosition: (position, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
@moveCursors (cursor) -> cursor.setScreenPosition(position, options)
# Essential: Add a cursor at the given position in buffer coordinates.
@@ -1830,7 +1969,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtBufferPosition: (bufferPosition, options) ->
- @selectionsMarkerLayer.markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
+ @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'})
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1840,7 +1979,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtScreenPosition: (screenPosition, options) ->
- @selectionsMarkerLayer.markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
+ @selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'})
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1972,7 +2111,7 @@ class TextEditor extends Model
cursors.push(cursor)
cursors
- # Add a cursor based on the given {TextEditorMarker}.
+ # Add a cursor based on the given {DisplayMarker}.
addCursor: (marker) ->
cursor = new Cursor(editor: this, marker: marker, config: @config)
@cursors.push(cursor)
@@ -2123,10 +2262,14 @@ class TextEditor extends Model
# * `options` (optional) An options {Object}:
# * `reversed` A {Boolean} indicating whether to create the selection in a
# reversed orientation.
+ # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
+ # selection is set.
#
# Returns the added {Selection}.
addSelectionForBufferRange: (bufferRange, options={}) ->
- @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
+ unless options.preserveFolds
+ @destroyFoldsIntersectingBufferRange(bufferRange)
+ @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false})
@getLastSelection().autoscroll() unless options.autoscroll is false
@getLastSelection()
@@ -2136,12 +2279,11 @@ class TextEditor extends Model
# * `options` (optional) An options {Object}:
# * `reversed` A {Boolean} indicating whether to create the selection in a
# reversed orientation.
- #
+ # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
+ # selection is set.
# Returns the added {Selection}.
addSelectionForScreenRange: (screenRange, options={}) ->
- @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options))
- @getLastSelection().autoscroll() unless options.autoscroll is false
- @getLastSelection()
+ @addSelectionForBufferRange(@bufferRangeForScreenRange(screenRange), options)
# Essential: Select from the current cursor position to the given position in
# buffer coordinates.
@@ -2322,7 +2464,7 @@ class TextEditor extends Model
# Extended: Select the range of the given marker if it is valid.
#
- # * `marker` A {TextEditorMarker}
+ # * `marker` A {DisplayMarker}
#
# Returns the selected {Range} or `undefined` if the marker is invalid.
selectMarker: (marker) ->
@@ -2448,20 +2590,18 @@ class TextEditor extends Model
_.reduce(tail, reducer, [head])
return result if fn?
- # Add a {Selection} based on the given {TextEditorMarker}.
+ # Add a {Selection} based on the given {DisplayMarker}.
#
- # * `marker` The {TextEditorMarker} to highlight
+ # * `marker` The {DisplayMarker} to highlight
# * `options` (optional) An {Object} that pertains to the {Selection} constructor.
#
# Returns the new {Selection}.
addSelection: (marker, options={}) ->
- unless marker.getProperties().preserveFolds
- @destroyFoldsContainingBufferRange(marker.getBufferRange())
cursor = @addCursor(marker)
selection = new Selection(_.extend({editor: this, marker, cursor, @clipboard}, options))
@selections.push(selection)
selectionBufferRange = selection.getBufferRange()
- @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds)
+ @mergeIntersectingSelections(preserveFolds: options.preserveFolds)
if selection.destroyed
for selection in @getSelections()
@@ -2572,14 +2712,36 @@ class TextEditor extends Model
# Essential: Get the on-screen length of tab characters.
#
# Returns a {Number}.
- getTabLength: -> @displayBuffer.getTabLength()
+ getTabLength: ->
+ if @tabLength?
+ @tabLength
+ else
+ @config.get('editor.tabLength', scope: @getRootScopeDescriptor())
# Essential: Set the on-screen length of tab characters. Setting this to a
# {Number} This will override the `editor.tabLength` setting.
#
# * `tabLength` {Number} length of a single tab. Setting to `null` will
# fallback to using the `editor.tabLength` config setting
- setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)
+ setTabLength: (tabLength) ->
+ return if tabLength is @tabLength
+
+ @tabLength = tabLength
+ @tokenizedBuffer.setTabLength(@tabLength)
+ @resetDisplayLayer()
+
+ setIgnoreInvisibles: (ignoreInvisibles) ->
+ return if ignoreInvisibles is @ignoreInvisibles
+
+ @ignoreInvisibles = ignoreInvisibles
+ @resetDisplayLayer()
+
+ getInvisibles: ->
+ scopeDescriptor = @getRootScopeDescriptor()
+ if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles and @showInvisibles
+ @config.get('editor.invisibles', scope: scopeDescriptor)
+ else
+ {}
# Extended: Determine if the buffer uses hard or soft tabs.
#
@@ -2590,7 +2752,7 @@ class TextEditor extends Model
# whitespace.
usesSoftTabs: ->
for bufferRow in [0..@buffer.getLastRow()]
- continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
+ continue if @tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
line = @buffer.lineForRow(bufferRow)
return true if line[0] is ' '
@@ -2633,14 +2795,27 @@ class TextEditor extends Model
# Essential: Determine whether lines in this editor are soft-wrapped.
#
# Returns a {Boolean}.
- isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped()
+ isSoftWrapped: ->
+ if @largeFileMode
+ false
+ else
+ scopeDescriptor = @getRootScopeDescriptor()
+ @softWrapped ? @config.get('editor.softWrap', scope: scopeDescriptor) ? false
# Essential: Enable or disable soft wrapping for this editor.
#
# * `softWrapped` A {Boolean}
#
# Returns a {Boolean}.
- setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped)
+ setSoftWrapped: (softWrapped) ->
+ if softWrapped isnt @softWrapped
+ @softWrapped = softWrapped
+ @resetDisplayLayer()
+ softWrapped = @isSoftWrapped()
+ @emitter.emit 'did-change-soft-wrapped', softWrapped
+ softWrapped
+ else
+ @isSoftWrapped()
# Essential: Toggle soft wrapping for this editor
#
@@ -2648,7 +2823,15 @@ class TextEditor extends Model
toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped())
# Essential: Gets the column at which column will soft wrap
- getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn()
+ getSoftWrapColumn: ->
+ scopeDescriptor = @getRootScopeDescriptor()
+ if @isSoftWrapped()
+ if @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
+ @config.get('editor.preferredLineLength', scope: scopeDescriptor)
+ else
+ @getEditorWidthInChars()
+ else
+ Infinity
###
Section: Indentation
@@ -2706,7 +2889,7 @@ class TextEditor extends Model
#
# Returns a {Number}.
indentLevelForLine: (line) ->
- @displayBuffer.indentLevelForLine(line)
+ @tokenizedBuffer.indentLevelForLine(line)
# Extended: Indent rows intersecting selections based on the grammar's suggested
# indent level.
@@ -2734,7 +2917,7 @@ class TextEditor extends Model
# Essential: Get the current {Grammar} of this editor.
getGrammar: ->
- @displayBuffer.getGrammar()
+ @tokenizedBuffer.grammar
# Essential: Set the current {Grammar} of this editor.
#
@@ -2743,11 +2926,15 @@ class TextEditor extends Model
#
# * `grammar` {Grammar}
setGrammar: (grammar) ->
- @displayBuffer.setGrammar(grammar)
+ @tokenizedBuffer.setGrammar(grammar)
# Reload the grammar based on the file name.
reloadGrammar: ->
- @displayBuffer.reloadGrammar()
+ @tokenizedBuffer.reloadGrammar()
+
+ # Experimental: Get a notification when async tokenization is completed.
+ onDidTokenize: (callback) ->
+ @tokenizedBuffer.onDidTokenize(callback)
###
Section: Managing Syntax Scopes
@@ -2757,7 +2944,7 @@ class TextEditor extends Model
# e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
# {Config::get} to get language specific config values.
getRootScopeDescriptor: ->
- @displayBuffer.getRootScopeDescriptor()
+ @tokenizedBuffer.rootScopeDescriptor
# Essential: Get the syntactic scopeDescriptor for the given position in buffer
# coordinates. Useful with {Config::get}.
@@ -2770,7 +2957,7 @@ class TextEditor extends Model
#
# Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition: (bufferPosition) ->
- @displayBuffer.scopeDescriptorForBufferPosition(bufferPosition)
+ @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)
# Extended: Get the range in buffer coordinates of all tokens surrounding the
# cursor that match the given scope selector.
@@ -2782,7 +2969,10 @@ class TextEditor extends Model
#
# Returns a {Range}.
bufferRangeForScopeAtCursor: (scopeSelector) ->
- @displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
+ @bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
+
+ bufferRangeForScopeAtPosition: (scopeSelector, position) ->
+ @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position)
# Extended: Determine if the given row is entirely a comment
isBufferRowCommented: (bufferRow) ->
@@ -2794,8 +2984,8 @@ class TextEditor extends Model
getCursorScope: ->
@getLastCursor().getScopeDescriptor()
- # {Delegates to: DisplayBuffer.tokenForBufferPosition}
- tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)
+ tokenForBufferPosition: (bufferPosition) ->
+ @tokenizedBuffer.tokenForPosition(bufferPosition)
###
Section: Clipboard Operations
@@ -2927,7 +3117,7 @@ class TextEditor extends Model
#
# * `bufferRow` A {Number}
unfoldBufferRow: (bufferRow) ->
- @displayBuffer.unfoldBufferRow(bufferRow)
+ @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity)))
# Extended: For each selection, fold the rows it intersects.
foldSelectedLines: ->
@@ -2957,7 +3147,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldableAtBufferRow: (bufferRow) ->
- @displayBuffer.isFoldableAtBufferRow(bufferRow)
+ @tokenizedBuffer.isFoldableAtRow(bufferRow)
# Extended: Determine whether the given row in screen coordinates is foldable.
#
@@ -2967,8 +3157,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldableAtScreenRow: (screenRow) ->
- bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow)
- @isFoldableAtBufferRow(bufferRow)
+ @isFoldableAtBufferRow(@bufferRowForScreenRow(screenRow))
# Extended: Fold the given buffer row if it isn't currently folded, and unfold
# it otherwise.
@@ -2990,7 +3179,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldedAtBufferRow: (bufferRow) ->
- @displayBuffer.isFoldedAtBufferRow(bufferRow)
+ @displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
# Extended: Determine whether the given row in screen coordinates is folded.
#
@@ -2998,41 +3187,23 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldedAtScreenRow: (screenRow) ->
- @displayBuffer.isFoldedAtScreenRow(screenRow)
+ @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))
- # TODO: Rename to foldRowRange?
- createFold: (startRow, endRow) ->
- @displayBuffer.createFold(startRow, endRow)
+ # Creates a new fold between two row numbers.
+ #
+ # startRow - The row {Number} to start folding at
+ # endRow - The row {Number} to end the fold
+ #
+ # Returns the new {Fold}.
+ foldBufferRowRange: (startRow, endRow) ->
+ @foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity)))
- # {Delegates to: DisplayBuffer.destroyFoldWithId}
- destroyFoldWithId: (id) ->
- @displayBuffer.destroyFoldWithId(id)
+ foldBufferRange: (range) ->
+ @displayLayer.foldBufferRange(range)
# Remove any {Fold}s found that intersect the given buffer range.
destroyFoldsIntersectingBufferRange: (bufferRange) ->
- @destroyFoldsContainingBufferRange(bufferRange)
-
- for row in [bufferRange.end.row..bufferRange.start.row]
- fold.destroy() for fold in @displayBuffer.foldsStartingAtBufferRow(row)
-
- return
-
- # Remove any {Fold}s found that contain the given buffer range.
- destroyFoldsContainingBufferRange: (bufferRange) ->
- @unfoldBufferRow(bufferRange.start.row)
- @unfoldBufferRow(bufferRange.end.row)
-
- # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow}
- largestFoldContainingBufferRow: (bufferRow) ->
- @displayBuffer.largestFoldContainingBufferRow(bufferRow)
-
- # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow}
- largestFoldStartingAtScreenRow: (screenRow) ->
- @displayBuffer.largestFoldStartingAtScreenRow(screenRow)
-
- # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange}
- outermostFoldsInBufferRowRange: (startRow, endRow) ->
- @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow)
+ @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange)
###
Section: Gutters
@@ -3083,7 +3254,7 @@ class TextEditor extends Model
# * `options` (optional) {Object}
# * `center` Center the editor around the position if possible. (default: false)
scrollToBufferPosition: (bufferPosition, options) ->
- @displayBuffer.scrollToBufferPosition(bufferPosition, options)
+ @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options)
# Essential: Scrolls the editor to the given screen position.
#
@@ -3092,7 +3263,7 @@ class TextEditor extends Model
# * `options` (optional) {Object}
# * `center` Center the editor around the position if possible. (default: false)
scrollToScreenPosition: (screenPosition, options) ->
- @displayBuffer.scrollToScreenPosition(screenPosition, options)
+ @scrollToScreenRange(new Range(screenPosition, screenPosition), options)
scrollToTop: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.")
@@ -3104,7 +3275,9 @@ class TextEditor extends Model
@getElement().scrollToBottom()
- scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options)
+ scrollToScreenRange: (screenRange, options = {}) ->
+ scrollEvent = {screenRange, options}
+ @emitter.emit "did-request-autoscroll", scrollEvent
getHorizontalScrollbarHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.")
@@ -3187,51 +3360,69 @@ class TextEditor extends Model
Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead")
@getElement().pixelPositionForScreenPosition(screenPosition)
- getSelectionMarkerAttributes: ->
- {type: 'selection', invalidate: 'never'}
+ getVerticalScrollMargin: ->
+ maxScrollMargin = Math.floor(((@height / @getLineHeightInPixels()) - 1) / 2)
+ Math.min(@verticalScrollMargin, maxScrollMargin)
- getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
- setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)
+ setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
- getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin()
- setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin)
+ getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@width / @getDefaultCharWidth()) - 1) / 2))
+ setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
- getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels()
- setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels)
+ getLineHeightInPixels: -> @lineHeightInPixels
+ setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels
- getKoreanCharWidth: -> @displayBuffer.getKoreanCharWidth()
+ getKoreanCharWidth: -> @koreanCharWidth
+ getHalfWidthCharWidth: -> @halfWidthCharWidth
+ getDoubleWidthCharWidth: -> @doubleWidthCharWidth
+ getDefaultCharWidth: -> @defaultCharWidth
- getHalfWidthCharWidth: -> @displayBuffer.getHalfWidthCharWidth()
+ ratioForCharacter: (character) ->
+ if isKoreanCharacter(character)
+ @getKoreanCharWidth() / @getDefaultCharWidth()
+ else if isHalfWidthCharacter(character)
+ @getHalfWidthCharWidth() / @getDefaultCharWidth()
+ else if isDoubleWidthCharacter(character)
+ @getDoubleWidthCharWidth() / @getDefaultCharWidth()
+ else
+ 1
- getDoubleWidthCharWidth: -> @displayBuffer.getDoubleWidthCharWidth()
-
- getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
- @displayBuffer.setDefaultCharWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
+ doubleWidthCharWidth ?= defaultCharWidth
+ halfWidthCharWidth ?= defaultCharWidth
+ koreanCharWidth ?= defaultCharWidth
+ if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth
+ @defaultCharWidth = defaultCharWidth
+ @doubleWidthCharWidth = doubleWidthCharWidth
+ @halfWidthCharWidth = halfWidthCharWidth
+ @koreanCharWidth = koreanCharWidth
+ @resetDisplayLayer() if @isSoftWrapped() and @getEditorWidthInChars()?
+ defaultCharWidth
setHeight: (height, reentrant=false) ->
if reentrant
- @displayBuffer.setHeight(height)
+ @height = height
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
@getElement().setHeight(height)
getHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.")
- @displayBuffer.getHeight()
-
- getClientHeight: -> @displayBuffer.getClientHeight()
+ @height
setWidth: (width, reentrant=false) ->
if reentrant
- @displayBuffer.setWidth(width)
+ oldWidth = @width
+ @width = width
+ @resetDisplayLayer() if width isnt oldWidth and @isSoftWrapped()
+ @width
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
@getElement().setWidth(width)
getWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.")
- @displayBuffer.getWidth()
+ @width
# Experimental: Scroll the editor such that the given screen row is at the
# top of the visible area.
@@ -3239,10 +3430,8 @@ class TextEditor extends Model
unless fromView
maxScreenRow = @getScreenLineCount() - 1
unless @config.get('editor.scrollPastEnd') and @scrollPastEnd
- height = @displayBuffer.getHeight()
- lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
- if height? and lineHeightInPixels?
- maxScreenRow -= Math.floor(height / lineHeightInPixels)
+ if @height? and @lineHeightInPixels?
+ maxScreenRow -= Math.floor(@height / @lineHeightInPixels)
screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0)
unless screenRow is @firstVisibleScreenRow
@@ -3252,10 +3441,8 @@ class TextEditor extends Model
getFirstVisibleScreenRow: -> @firstVisibleScreenRow
getLastVisibleScreenRow: ->
- height = @displayBuffer.getHeight()
- lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
- if height? and lineHeightInPixels?
- Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getScreenLineCount() - 1)
+ if @height? and @lineHeightInPixels?
+ Math.min(@firstVisibleScreenRow + Math.floor(@height / @lineHeightInPixels), @getScreenLineCount() - 1)
else
null
@@ -3350,8 +3537,6 @@ class TextEditor extends Model
inspect: ->
""
- logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
-
emitWillInsertTextEvent: (text) ->
result = true
cancel = -> result = false
diff --git a/src/text-utils.coffee b/src/text-utils.coffee
index af17335aa..f4d62772e 100644
--- a/src/text-utils.coffee
+++ b/src/text-utils.coffee
@@ -94,6 +94,13 @@ isCJKCharacter = (character) ->
isHalfWidthCharacter(character) or
isKoreanCharacter(character)
+isWordStart = (previousCharacter, character) ->
+ (previousCharacter is ' ' or previousCharacter is '\t') and
+ (character isnt ' ' and character isnt '\t')
+
+isWrapBoundary = (previousCharacter, character) ->
+ isWordStart(previousCharacter, character) or isCJKCharacter(character)
+
# Does the given string contain at least surrogate pair, variation sequence,
# or combined character?
#
@@ -107,4 +114,8 @@ hasPairedCharacter = (string) ->
index++
false
-module.exports = {isPairedCharacter, hasPairedCharacter, isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isCJKCharacter}
+module.exports = {
+ isPairedCharacter, hasPairedCharacter,
+ isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter,
+ isWrapBoundary
+}
diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee
index 8f0fe202f..f9af1e4ca 100644
--- a/src/token-iterator.coffee
+++ b/src/token-iterator.coffee
@@ -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
diff --git a/src/token.coffee b/src/token.coffee
index 60e8194f8..d531ba04a 100644
--- a/src/token.coffee
+++ b/src/token.coffee
@@ -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
diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee
new file mode 100644
index 000000000..780156e42
--- /dev/null
+++ b/src/tokenized-buffer-iterator.coffee
@@ -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
diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee
index 065715806..ea7082a6d 100644
--- a/src/tokenized-buffer.coffee
+++ b/src/tokenized-buffer.coffee
@@ -7,6 +7,7 @@ TokenizedLine = require './tokenized-line'
TokenIterator = require './token-iterator'
Token = require './token'
ScopeDescriptor = require './scope-descriptor'
+TokenizedBufferIterator = require './tokenized-buffer-iterator'
module.exports =
class TokenizedBuffer extends Model
@@ -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) ->
diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee
index c1ac4caff..f8faad865 100644
--- a/src/tokenized-line.coffee
+++ b/src/tokenized-line.coffee
@@ -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]