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]