diff --git a/apm/package.json b/apm/package.json index 4ddb4dabe..d4fcc851a 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.9.3" + "atom-package-manager": "1.10.0" } } 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 d50f77eb7..694a100f8 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.23", "line-top-index": "0.2.0", - "marked": "^0.3.4", + "marked": "^0.3.5", "normalize-package-data": "^2.0.0", "nslog": "^3", "ohnogit": "0.0.11", @@ -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" @@ -77,7 +77,7 @@ "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.1", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.30.0", + "autocomplete-plus": "2.31.0", "autocomplete-snippets": "1.10.0", "autoflow": "0.27.0", "autosave": "0.23.1", @@ -87,7 +87,7 @@ "command-palette": "0.38.0", "deprecation-cop": "0.54.1", "dev-live-reload": "0.47.0", - "encoding-selector": "0.21.0", + "encoding-selector": "0.22.0", "exception-reporting": "0.38.1", "fuzzy-finder": "1.0.5", "git-diff": "1.0.1", @@ -97,20 +97,20 @@ "image-view": "0.57.0", "incompatible-packages": "0.26.1", "keybinding-resolver": "0.35.0", - "line-ending-selector": "0.4.1", + "line-ending-selector": "0.5.0", "link": "0.31.1", "markdown-preview": "0.158.0", "metrics": "0.53.1", "notifications": "0.63.2", "open-on-github": "1.1.0", "package-generator": "1.0.0", - "settings-view": "0.235.1", + "settings-view": "0.236.0", "snippets": "1.0.2", "spell-check": "0.67.1", "status-bar": "1.2.6", "styleguide": "0.45.2", - "symbols-view": "0.112.0", - "tabs": "0.93.1", + "symbols-view": "0.113.0", + "tabs": "0.93.2", "timecop": "0.33.1", "tree-view": "0.206.2", "update-package-dependencies": "0.10.0", diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index 643a2d411..84d8b0440 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -1,5 +1,6 @@ ChildProcess = require 'child_process' path = require 'path' +fs = require 'fs-plus' BufferedProcess = require '../src/buffered-process' describe "BufferedProcess", -> @@ -15,20 +16,20 @@ describe "BufferedProcess", -> describe "when there is an error handler specified", -> describe "when an error event is emitted by the process", -> it "calls the error handler and does not throw an exception", -> - process = new BufferedProcess - command: 'bad-command-nope' + bufferedProcess = new BufferedProcess + command: 'bad-command-nope1' args: ['nothing'] - options: {} + options: {shell: false} errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() - process.onWillThrowError(errorSpy) + bufferedProcess.onWillThrowError(errorSpy) waitsFor -> errorSpy.callCount > 0 runs -> expect(window.onerror).not.toHaveBeenCalled() expect(errorSpy).toHaveBeenCalled() - expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT' + expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope1 ENOENT' describe "when an error is thrown spawning the process", -> it "calls the error handler and does not throw an exception", -> @@ -37,13 +38,13 @@ describe "BufferedProcess", -> error.code = 'EAGAIN' throw error - process = new BufferedProcess + bufferedProcess = new BufferedProcess command: 'ls' args: [] options: {} errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() - process.onWillThrowError(errorSpy) + bufferedProcess.onWillThrowError(errorSpy) waitsFor -> errorSpy.callCount > 0 @@ -53,56 +54,24 @@ describe "BufferedProcess", -> expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'Something is really wrong' describe "when there is not an error handler specified", -> - it "calls the error handler and does not throw an exception", -> - process = new BufferedProcess - command: 'bad-command-nope' + it "does throw an exception", -> + new BufferedProcess + command: 'bad-command-nope2' args: ['nothing'] - options: {} + options: {shell: false} waitsFor -> window.onerror.callCount > 0 runs -> expect(window.onerror).toHaveBeenCalled() - expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope`' + expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope2`' expect(window.onerror.mostRecentCall.args[4].name).toBe 'BufferedProcessError' - describe "on Windows", -> - originalPlatform = null - - beforeEach -> - # Prevent any commands from actually running and affecting the host - originalSpawn = ChildProcess.spawn - spyOn(ChildProcess, 'spawn').andCallFake -> - # Just spawn something that won't actually modify the host - if originalPlatform is 'win32' - originalSpawn('dir') - else - originalSpawn('ls') - - originalPlatform = process.platform - Object.defineProperty process, 'platform', value: 'win32' - - afterEach -> - Object.defineProperty process, 'platform', value: originalPlatform - - describe "when the explorer command is spawned on Windows", -> - it "doesn't quote arguments of the form /root,C...", -> - new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']}) - expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"' - - it "spawns the command using a cmd.exe wrapper", -> - new BufferedProcess({command: 'dir'}) - expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe' - expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s' - expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d' - expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c' - expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"' - it "calls the specified stdout, stderr, and exit callbacks", -> stdout = '' stderr = '' exitCallback = jasmine.createSpy('exit callback') - process = new BufferedProcess + new BufferedProcess command: atom.packages.getApmPath() args: ['-h'] options: {} @@ -116,29 +85,51 @@ describe "BufferedProcess", -> expect(stderr).toContain 'apm - Atom Package Manager' expect(stdout).toEqual '' - it "calls the specified stdout callback only with whole lines", -> + it "calls the specified stdout callback with whole lines", -> exitCallback = jasmine.createSpy('exit callback') - baseContent = "There are dozens of us! Dozens! It's as Ann as the nose on Plain's face. Can you believe that the only reason the club is going under is because it's in a terrifying neighborhood? She calls it a Mayonegg. Waiting for the Emmys. BTW did you know won 6 Emmys and was still canceled early by Fox? COME ON. I'll buy you a hundred George Michaels that you can teach to drive! Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'" - content = (baseContent for _ in [1..200]).join('\n') + loremPath = require.resolve("./fixtures/lorem.txt") + content = fs.readFileSync(loremPath).toString() + baseContent = content.split('\n') stdout = '' - endLength = 10 - outputAlwaysEndsWithStew = true - process = new BufferedProcess - command: '/bin/echo' - args: [content] + allLinesEndWithNewline = true + new BufferedProcess + command: if process.platform is 'win32' then 'type' else 'cat' + args: [loremPath] options: {} stdout: (lines) -> + endsWithNewline = (lines.charAt lines.length - 1) is '\n' + if not endsWithNewline then allLinesEndWithNewline = false stdout += lines - - end = baseContent.substr(baseContent.length - endLength, endLength) - lineEndsWithStew = lines.substr(lines.length - endLength, endLength) is end - expect(lineEndsWithStew).toBeTrue - - outputAlwaysEndsWithStew = outputAlwaysEndsWithStew and lineEndsWithStew exit: exitCallback waitsFor -> exitCallback.callCount is 1 runs -> - expect(outputAlwaysEndsWithStew).toBeTrue - expect(stdout).toBe content += '\n' + expect(allLinesEndWithNewline).toBeTrue + expect(stdout).toBe content + + describe "on Windows", -> + originalPlatform = null + + beforeEach -> + # Prevent any commands from actually running and affecting the host + originalSpawn = ChildProcess.spawn + spyOn(ChildProcess, 'spawn') + originalPlatform = process.platform + Object.defineProperty process, 'platform', value: 'win32' + + afterEach -> + Object.defineProperty process, 'platform', value: originalPlatform + + describe "when the explorer command is spawned on Windows", -> + it "doesn't quote arguments of the form /root,C...", -> + new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']}) + expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"' + + it "spawns the command using a cmd.exe wrapper when options.shell is undefined", -> + new BufferedProcess({command: 'dir'}) + expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe' + expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s' + expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d' + expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c' + expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"' diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index ecdd42fd6..aaf044b1d 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -74,6 +74,13 @@ describe "CommandRegistry", -> grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) expect(calls).toEqual ['.foo.bar', '.bar', '.foo'] + it "orders inline listeners by reverse registration order", -> + calls = [] + registry.add child, 'command', -> calls.push('child1') + registry.add child, 'command', -> calls.push('child2') + child.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['child2', 'child1'] + it "stops bubbling through ancestors when .stopPropagation() is called on the event", -> calls = [] diff --git a/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/fixtures/lorem.txt b/spec/fixtures/lorem.txt new file mode 100644 index 000000000..be8db8ab8 --- /dev/null +++ b/spec/fixtures/lorem.txt @@ -0,0 +1,3 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ultricies nulla id nibh aliquam, vitae euismod ipsum scelerisque. Vestibulum vulputate facilisis nisi, eu rhoncus turpis pretium ut. Curabitur facilisis urna in diam efficitur, vel maximus tellus consectetur. Suspendisse pulvinar felis sed metus tristique, a posuere dui suscipit. Ut vehicula, tellus ac blandit consequat, libero dui hendrerit elit, non pretium metus odio sed dolor. Vivamus quis volutpat ipsum. In convallis magna nec nunc tristique malesuada. Sed sed hendrerit lacus. Etiam arcu dui, consequat vel neque vitae, iaculis egestas justo. Donec lacinia odio nulla, condimentum porta erat accumsan at. Nunc vulputate nulla vel nunc fermentum egestas. +Duis ultricies libero elit, nec facilisis mi rhoncus ornare. Aliquam aliquet libero vitae arcu porttitor mattis. Vestibulum ultricies consectetur arcu, non gravida magna eleifend vel. Phasellus varius mattis ultricies. Vestibulum placerat lacus non consectetur fringilla. Duis congue, arcu iaculis vehicula hendrerit, purus odio faucibus ipsum, et fermentum massa tellus euismod nulla. Vivamus pellentesque blandit massa, sit amet hendrerit turpis congue eu. Suspendisse diam dui, vestibulum nec semper varius, maximus eu nunc. Vivamus facilisis pulvinar viverra. Praesent luctus lectus id est porttitor volutpat. Suspendisse est augue, mattis a tincidunt id, condimentum in turpis. Curabitur at erat commodo orci interdum tincidunt. Sed sodales elit odio, a placerat ipsum luctus nec. Sed maximus, justo ut pharetra pellentesque, orci mi faucibus enim, quis viverra arcu dui sed nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent quis velit libero. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus a rutrum tortor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce bibendum odio et neque vestibulum rutrum. Vestibulum commodo, nibh non sodales lobortis, dui ex consectetur leo, a finibus libero lectus ac diam. Etiam dui nunc, bibendum a tempor vel, vestibulum lacinia neque. Mauris consectetur odio sit amet maximus pretium. Sed rutrum nunc at ante ullamcorper fermentum. Proin at quam a mauris pellentesque viverra. Nunc pretium pulvinar ipsum. Vestibulum eu nibh ut ex gravida tempus. Praesent ut elit ut ligula tristique dapibus ut sit amet leo. Proin non molestie erat. diff --git a/spec/fixtures/shebang b/spec/fixtures/shebang index f15429b13..f343f6833 100644 --- a/spec/fixtures/shebang +++ b/spec/fixtures/shebang @@ -1,3 +1,3 @@ #!/usr/bin/ruby -puts "America – fuck yeah!" \ No newline at end of file +puts "Atom fixture test" 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/pane-spec.coffee b/spec/pane-spec.coffee index 9969b1dcc..8abbb0ece 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -917,6 +917,82 @@ describe "Pane", -> expect(item1.save).not.toHaveBeenCalled() expect(pane.isDestroyed()).toBe false + describe "when item fails to save", -> + [pane, item1, item2] = [] + + beforeEach -> + pane = new Pane({items: [new Item("A"), new Item("B")], applicationDelegate: atom.applicationDelegate, config: atom.config}) + [item1, item2] = pane.getItems() + + item1.shouldPromptToSave = -> true + item1.getURI = -> "/test/path" + + item1.save = jasmine.createSpy("save").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + it "does not destroy the pane if save fails and user clicks cancel", -> + confirmations = 0 + confirm.andCallFake -> + confirmations++ + if confirmations is 1 + return 0 # click save + else + return 1 # click cancel + + pane.close() + + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(item1.save).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe false + + it "does destroy the pane if the user saves the file under a new name", -> + item1.saveAs = jasmine.createSpy("saveAs").andReturn(true) + + confirmations = 0 + confirm.andCallFake -> + confirmations++ + return 0 # save and then save as + + showSaveDialog.andReturn("new/path") + + pane.close() + + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true + + it "asks again if the saveAs also fails", -> + item1.saveAs = jasmine.createSpy("saveAs").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + confirmations = 0 + confirm.andCallFake -> + confirmations++ + if confirmations < 3 + return 0 # save, save as, save as + return 2 # don't save + + showSaveDialog.andReturn("new/path") + + pane.close() + + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(3) + expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true + describe "::destroy()", -> [container, pane1, pane2] = [] diff --git a/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 96122e073..0c7ebd0e8 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-registry-spec.coffee b/spec/text-editor-registry-spec.coffee index 04665bef2..80f29f897 100644 --- a/spec/text-editor-registry-spec.coffee +++ b/spec/text-editor-registry-spec.coffee @@ -10,6 +10,7 @@ describe "TextEditorRegistry", -> it "gets added to the list of registered editors", -> editor = {} registry.add(editor) + expect(editor.registered).toBe true expect(registry.editors.size).toBe 1 expect(registry.editors.has(editor)).toBe(true) @@ -19,6 +20,16 @@ describe "TextEditorRegistry", -> expect(registry.editors.size).toBe 1 disposable.dispose() expect(registry.editors.size).toBe 0 + expect(editor.registered).toBe false + + it "can be removed", -> + editor = {} + registry.add(editor) + expect(registry.editors.size).toBe 1 + success = registry.remove(editor) + expect(success).toBe true + expect(registry.editors.size).toBe 0 + expect(editor.registered).toBe false describe "when the registry is observed", -> it "calls the callback for current and future editors until unsubscribed", -> diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index e5e58a5cc..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 -> @@ -3174,7 +3178,7 @@ describe "TextEditor", -> expect(editor.indentationForBufferRow(0)).toBe 1 expect(editor.indentationForBufferRow(1)).toBe 1 - it "indents the new line to the correct level when editor.autoIndent is true and using a off-side rule language", -> + it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", -> waitsForPromise -> atom.packages.activatePackage('language-coffee-script') @@ -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)", -> @@ -5153,7 +5163,7 @@ describe "TextEditor", -> expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 describe "when the line preceding the newline does't add a level of indentation", -> - it "indents the new line to the same level a as the preceding line", -> + it "indents the new line to the same level as the preceding line", -> editor.setCursorBufferPosition([5, 14]) editor.insertText('\n') expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5) @@ -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/buffered-process.coffee b/src/buffered-process.coffee index 59f2a7e9a..07fcfb664 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -50,7 +50,7 @@ class BufferedProcess options ?= {} @command = command # Related to joyent/node#2318 - if process.platform is 'win32' + if process.platform is 'win32' and not options.shell? # Quote all arguments and escapes inner quotes if args? cmdArgs = args.filter (arg) -> arg? diff --git a/src/command-registry.coffee b/src/command-registry.coffee index db2cf498d..955a1b540 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -244,11 +244,14 @@ class CommandRegistry (@selectorBasedListenersByCommandName[event.type] ? []) .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) .sort (a, b) -> a.compare(b) - listeners = listeners.concat(selectorBasedListeners) + listeners = selectorBasedListeners.concat(listeners) matched = true if listeners.length > 0 - for listener in listeners + # Call inline listeners first in reverse registration order, + # and selector-based listeners by specificity and reverse + # registration order. + for listener in listeners by -1 break if immediatePropagationStopped listener.callback.call(currentTarget, dispatchedEvent) @@ -271,8 +274,8 @@ class SelectorBasedListener @sequenceNumber = SequenceCount++ compare: (other) -> - other.specificity - @specificity or - other.sequenceNumber - @sequenceNumber + @specificity - other.specificity or + @sequenceNumber - other.sequenceNumber class InlineListener constructor: (@callback) -> 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/pane.coffee b/src/pane.coffee index e8858a2b9..add6a365b 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -577,15 +577,23 @@ class Pane extends Model else return true - chosen = @applicationDelegate.confirm - message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?" - detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: ["Save", "Cancel", "Don't Save"] + saveDialog = (saveButtonText, saveFn, message) => + chosen = @applicationDelegate.confirm + message: message + detailedMessage: "Your changes will be lost if you close this item without saving." + buttons: [saveButtonText, "Cancel", "Don't save"] + switch chosen + when 0 then saveFn(item, saveError) + when 1 then false + when 2 then true - switch chosen - when 0 then @saveItem(item, -> true) - when 1 then false - when 2 then true + saveError = (error) => + if error + saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}") + else + true + + saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?") # Public: Save the active item. saveActiveItem: (nextAction) -> @@ -602,9 +610,11 @@ class Pane extends Model # Public: Save the given item. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is - # successfully saved. - saveItem: (item, nextAction) -> + # * `nextAction` (optional) {Function} which will be called with no argument + # after the item is successfully saved, or with the error if it failed. + # The return value will be that of `nextAction` or `undefined` if it was not + # provided + saveItem: (item, nextAction) => if typeof item?.getURI is 'function' itemURI = item.getURI() else if typeof item?.getUri is 'function' @@ -613,9 +623,12 @@ class Pane extends Model if itemURI? try item.save?() + nextAction?() catch error - @handleSaveError(error, item) - nextAction?() + if nextAction + nextAction(error) + else + @handleSaveError(error, item) else @saveItemAs(item, nextAction) @@ -623,9 +636,11 @@ class Pane extends Model # path they select. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is - # successfully saved. - saveItemAs: (item, nextAction) -> + # * `nextAction` (optional) {Function} which will be called with no argument + # after the item is successfully saved, or with the error if it failed. + # The return value will be that of `nextAction` or `undefined` if it was not + # provided + saveItemAs: (item, nextAction) => return unless item?.saveAs? saveOptions = item.getSaveDialogOptions?() ? {} @@ -634,9 +649,12 @@ class Pane extends Model if newItemPath try item.saveAs(newItemPath) + nextAction?() catch error - @handleSaveError(error, item) - nextAction?() + if nextAction + nextAction(error) + else + @handleSaveError(error, item) # Public: Save all items. saveItems: -> 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-element.coffee b/src/text-editor-element.coffee index a0ec1b7fa..0c9fa6123 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -96,7 +96,7 @@ class TextEditorElement extends HTMLElement throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config? throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes? throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace? - throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert? + throw new Error("Must pass an assert parameter when initializing TextEditorElements") unless @assert? throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles? throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars? 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-registry.coffee b/src/text-editor-registry.coffee index 8a17335d4..e31630fee 100644 --- a/src/text-editor-registry.coffee +++ b/src/text-editor-registry.coffee @@ -26,8 +26,20 @@ class TextEditorRegistry # editor is destroyed. add: (editor) -> @editors.add(editor) + editor.registered = true + @emitter.emit 'did-add-editor', editor - new Disposable => @editors.delete(editor) + new Disposable => @remove(editor) + + # Remove a `TextEditor`. + # + # * `editor` The editor to remove. + # + # Returns a {Boolean} indicating whether the editor was successfully removed. + remove: (editor) -> + removed = @editors.delete(editor) + editor.registered = false + removed # Invoke the given callback with all the current and future registered # `TextEditors`. diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 3eb15c63c..3d52f4e68 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,21 +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 @@ -91,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 +145,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,14 +188,23 @@ 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 - registered: atom.textEditors.editors.has this + 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: -> @buffer.retain() @@ -180,11 +226,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 +255,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 +360,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 +514,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 +523,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 +532,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 +543,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 +557,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 +574,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 +652,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 +796,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 +804,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 +816,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 +960,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 +981,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 +1043,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 +1138,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 +1151,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 +1392,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 +1413,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 +1495,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 +1553,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 +1562,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 +1587,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 +1602,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 +1624,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 +1640,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 +1649,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 +1658,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 +1667,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 +1676,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 +1719,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 +1754,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 +1833,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 +1859,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 +1914,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 +1952,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 +1970,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 +1980,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 +2112,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 +2263,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 +2280,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 +2465,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 +2591,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 +2713,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 +2753,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 +2796,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 +2824,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 +2890,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 +2918,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 +2927,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 +2945,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 +2958,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 +2970,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 +2985,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 +3118,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 +3148,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 +3158,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 +3180,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 +3188,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 +3255,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 +3264,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 +3276,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 +3361,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 +3431,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 +3442,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 +3538,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] diff --git a/src/workspace.coffee b/src/workspace.coffee index f75f00bc6..5e9de93dd 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -1092,7 +1092,7 @@ class Workspace extends Model if editor.getPath() checkoutHead = => @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) - .then (repository) => + .then (repository) -> repository?.async.checkoutHeadForEditor(editor) if @config.get('editor.confirmCheckoutHeadRevision')