diff --git a/benchmarks/text-editor-large-file-construction.bench.js b/benchmarks/text-editor-large-file-construction.bench.js index 30b640733..ec037e9e4 100644 --- a/benchmarks/text-editor-large-file-construction.bench.js +++ b/benchmarks/text-editor-large-file-construction.bench.js @@ -26,7 +26,7 @@ export default async function ({test}) { let t0 = window.performance.now() const buffer = new TextBuffer({text}) - const editor = new TextEditor({buffer, largeFileMode: true}) + const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true}) atom.workspace.getActivePane().activateItem(editor) let t1 = window.performance.now() diff --git a/benchmarks/text-editor-long-lines.bench.js b/benchmarks/text-editor-long-lines.bench.js index c162db420..ac90e4a71 100644 --- a/benchmarks/text-editor-long-lines.bench.js +++ b/benchmarks/text-editor-long-lines.bench.js @@ -33,7 +33,7 @@ export default async function ({test}) { let t0 = window.performance.now() const buffer = new TextBuffer({text}) - const editor = new TextEditor({buffer, largeFileMode: true}) + const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true}) editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) atom.workspace.getActivePane().activateItem(editor) let t1 = window.performance.now() diff --git a/package.json b/package.json index 603cc8fd5..f82dc7d89 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.3.15", + "electronVersion": "1.6.9", "dependencies": { "async": "0.2.6", "atom-keymap": "8.1.2", @@ -27,9 +27,10 @@ "color": "^0.7.3", "dedent": "^0.6.0", "devtron": "1.3.0", + "etch": "^0.12.4", "event-kit": "^2.3.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.4", + "first-mate": "7.0.6", "fs-plus": "^3.0.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", @@ -40,7 +41,7 @@ "jasmine-tagged": "^1.1.4", "key-path-helpers": "^0.4.0", "less-cache": "1.1.0", - "line-top-index": "0.2.0", + "line-top-index": "0.3.1", "marked": "^0.3.6", "minimatch": "^3.0.3", "mocha": "2.5.1", @@ -64,7 +65,7 @@ "sinon": "1.17.4", "@atom/source-map-support": "^0.3.4", "temp": "^0.8.3", - "text-buffer": "11.4.1", + "text-buffer": "12.1.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -94,7 +95,7 @@ "autosave": "0.24.3", "background-tips": "0.27.0", "bookmarks": "0.44.4", - "bracket-matcher": "0.85.5", + "bracket-matcher": "0.85.6", "command-palette": "0.40.4", "dalek": "0.2.1", "deprecation-cop": "0.56.7", @@ -102,7 +103,7 @@ "encoding-selector": "0.23.3", "exception-reporting": "0.41.4", "find-and-replace": "0.208.1", - "fuzzy-finder": "1.5.6", + "fuzzy-finder": "1.5.7", "github": "0.1.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", @@ -113,7 +114,7 @@ "line-ending-selector": "0.6.3", "link": "0.31.3", "markdown-preview": "0.159.12", - "metrics": "1.2.3", + "metrics": "1.2.4", "notifications": "0.67.1", "open-on-github": "1.2.1", "package-generator": "1.1.1", @@ -125,7 +126,7 @@ "symbols-view": "0.116.0", "tabs": "0.106.0", "timecop": "0.36.0", - "tree-view": "0.217.0-7", + "tree-view": "0.217.0-8", "update-package-dependencies": "0.12.0", "welcome": "0.36.3", "whitespace": "0.36.2", @@ -185,7 +186,12 @@ "spyOn", "waitsFor", "waitsForPromise", - "indexedDB" + "indexedDB", + "IntersectionObserver", + "FocusEvent", + "requestAnimationFrame", + "HTMLElement", + "snapshotResult" ] } } diff --git a/script/package.json b/script/package.json index 852f71746..2ad431985 100644 --- a/script/package.json +++ b/script/package.json @@ -7,10 +7,10 @@ "coffeelint": "1.15.7", "colors": "1.1.2", "csslint": "1.0.2", - "donna": "1.0.13", - "electron-chromedriver": "~1.3", + "donna": "1.0.16", + "electron-chromedriver": "~1.6", "electron-link": "0.0.24", - "electron-mksnapshot": "~1.3", + "electron-mksnapshot": "~1.6", "electron-packager": "7.3.0", "electron-winstaller": "2.5.2", "fs-extra": "0.30.0", diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index e856c1e10..7bdc93038 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -29,10 +29,12 @@ describe "AtomEnvironment", -> atom.setSize(originalSize.width, originalSize.height) it 'sets the size of the window, and can retrieve the size just set', -> - newWidth = originalSize.width + 12 - newHeight = originalSize.height + 23 - atom.setSize(newWidth, newHeight) - expect(atom.getSize()).toEqual width: newWidth, height: newHeight + newWidth = originalSize.width - 12 + newHeight = originalSize.height - 23 + waitsForPromise -> + atom.setSize(newWidth, newHeight) + runs -> + expect(atom.getSize()).toEqual width: newWidth, height: newHeight describe ".isReleasedVersion()", -> it "returns false if the version is a SHA and true otherwise", -> @@ -221,44 +223,70 @@ describe "AtomEnvironment", -> atom.loadState().then (state) -> expect(state).toEqual(serializedState) it "saves state when the CPU is idle after a keydown or mousedown event", -> - spyOn(atom, 'saveState') + atomEnv = new AtomEnvironment({ + applicationDelegate: global.atom.applicationDelegate, + }) idleCallbacks = [] - spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback) + atomEnv.initialize({ + window: { + requestIdleCallback: (callback) -> idleCallbacks.push(callback), + addEventListener: -> + removeEventListener: -> + }, + document: document.implementation.createHTMLDocument() + }) + + spyOn(atomEnv, 'saveState') keydown = new KeyboardEvent('keydown') - atom.document.dispatchEvent(keydown) - advanceClock atom.saveStateDebounceInterval + atomEnv.document.dispatchEvent(keydown) + advanceClock atomEnv.saveStateDebounceInterval idleCallbacks.shift()() - expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false}) - expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true}) + expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false}) + expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true}) - atom.saveState.reset() + atomEnv.saveState.reset() mousedown = new MouseEvent('mousedown') - atom.document.dispatchEvent(mousedown) - advanceClock atom.saveStateDebounceInterval + atomEnv.document.dispatchEvent(mousedown) + advanceClock atomEnv.saveStateDebounceInterval idleCallbacks.shift()() - expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false}) - expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true}) + expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false}) + expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true}) + + atomEnv.destroy() it "ignores mousedown/keydown events happening after calling unloadEditorWindow", -> - spyOn(atom, 'saveState') + atomEnv = new AtomEnvironment({ + applicationDelegate: global.atom.applicationDelegate, + }) idleCallbacks = [] - spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback) + atomEnv.initialize({ + window: { + requestIdleCallback: (callback) -> idleCallbacks.push(callback), + addEventListener: -> + removeEventListener: -> + }, + document: document.implementation.createHTMLDocument() + }) + + spyOn(atomEnv, 'saveState') mousedown = new MouseEvent('mousedown') - atom.document.dispatchEvent(mousedown) - atom.unloadEditorWindow() - expect(atom.saveState).not.toHaveBeenCalled() + atomEnv.document.dispatchEvent(mousedown) + atomEnv.unloadEditorWindow() + expect(atomEnv.saveState).not.toHaveBeenCalled() - advanceClock atom.saveStateDebounceInterval + advanceClock atomEnv.saveStateDebounceInterval idleCallbacks.shift()() - expect(atom.saveState).not.toHaveBeenCalled() + expect(atomEnv.saveState).not.toHaveBeenCalled() mousedown = new MouseEvent('mousedown') - atom.document.dispatchEvent(mousedown) - advanceClock atom.saveStateDebounceInterval + atomEnv.document.dispatchEvent(mousedown) + advanceClock atomEnv.saveStateDebounceInterval idleCallbacks.shift()() - expect(atom.saveState).not.toHaveBeenCalled() + expect(atomEnv.saveState).not.toHaveBeenCalled() + + atomEnv.destroy() it "serializes the project state with all the options supplied in saveState", -> spyOn(atom.project, 'serialize').andReturn({foo: 42}) @@ -288,10 +316,13 @@ describe "AtomEnvironment", -> } ) }) + atom2.initialize({document, window}) atom2.deserialize(atom.serialize()) expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain') + atom2.destroy() + describe "openInitialEmptyEditorIfNecessary", -> describe "when there are no paths set", -> beforeEach -> @@ -452,11 +483,14 @@ describe "AtomEnvironment", -> } atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) atomEnvironment.initialize({window, document: fakeDocument}) - spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn [] - spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve() - atomEnvironment.startEditorWindow() - atomEnvironment.unloadEditorWindow() - atomEnvironment.destroy() + spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve()) + spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve()) + spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve()) + waitsForPromise -> + atomEnvironment.startEditorWindow() + runs -> + atomEnvironment.unloadEditorWindow() + atomEnvironment.destroy() describe "::whenShellEnvironmentLoaded()", -> [atomEnvironment, envLoaded, spy] = [] @@ -471,21 +505,19 @@ describe "AtomEnvironment", -> applicationDelegate: atom.applicationDelegate updateProcessEnv: -> promise atomEnvironment.initialize({window, document}) - spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn [] - spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve() spy = jasmine.createSpy() - atomEnvironment.startEditorWindow() afterEach -> - atomEnvironment.unloadEditorWindow() atomEnvironment.destroy() it "is triggered once the shell environment is loaded", -> atomEnvironment.whenShellEnvironmentLoaded spy + atomEnvironment.updateProcessEnvAndTriggerHooks() envLoaded() runs -> expect(spy).toHaveBeenCalled() it "triggers the callback immediately if the shell environment is already loaded", -> + atomEnvironment.updateProcessEnvAndTriggerHooks() envLoaded() runs -> atomEnvironment.whenShellEnvironmentLoaded spy diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee index a91059efe..455afcb27 100644 --- a/spec/atom-reporter.coffee +++ b/spec/atom-reporter.coffee @@ -21,8 +21,8 @@ formatStackTrace = (spec, message='', stackTrace) -> lines.shift() if message.trim() is errorMatch?[1]?.trim() for line, index in lines - # Remove prefix of lines matching: at . (path:1:2) - prefixMatch = line.match(/at \. \(([^)]+)\)/) + # Remove prefix of lines matching: at jasmine.Spec. (path:1:2) + prefixMatch = line.match(/at jasmine\.Spec\. \(([^)]+)\)/) line = "at #{prefixMatch[1]}" if prefixMatch # Relativize locations to spec directory diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index 4e7b2b395..070ad7a0b 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -43,6 +43,7 @@ describe "Babel transpiler support", -> describe "when a .js file does not start with 'use babel';", -> it "does not transpile it using babel", -> + spyOn(console, 'error') expect(-> require('./fixtures/babel/invalid.js')).toThrow() it "does not try to log to stdout or stderr while parsing the file", -> diff --git a/spec/custom-gutter-component-spec.coffee b/spec/custom-gutter-component-spec.coffee deleted file mode 100644 index 731e7bfeb..000000000 --- a/spec/custom-gutter-component-spec.coffee +++ /dev/null @@ -1,129 +0,0 @@ -CustomGutterComponent = require '../src/custom-gutter-component' -Gutter = require '../src/gutter' - -describe "CustomGutterComponent", -> - [gutterComponent, gutter] = [] - - beforeEach -> - mockGutterContainer = {} - gutter = new Gutter(mockGutterContainer, {name: 'test-gutter'}) - gutterComponent = new CustomGutterComponent({gutter, views: atom.views}) - - it "creates a gutter DOM node with only an empty 'custom-decorations' child node when it is initialized", -> - expect(gutterComponent.getDomNode().classList.contains('gutter')).toBe true - expect(gutterComponent.getDomNode().getAttribute('gutter-name')).toBe 'test-gutter' - expect(gutterComponent.getDomNode().children.length).toBe 1 - decorationsWrapperNode = gutterComponent.getDomNode().children.item(0) - expect(decorationsWrapperNode.classList.contains('custom-decorations')).toBe true - - it "makes its view accessible from the view registry", -> - expect(gutterComponent.getDomNode()).toBe gutter.getElement() - - it "hides its DOM node when ::hideNode is called, and shows its DOM node when ::showNode is called", -> - gutterComponent.hideNode() - expect(gutterComponent.getDomNode().style.display).toBe 'none' - gutterComponent.showNode() - expect(gutterComponent.getDomNode().style.display).toBe '' - - describe "::updateSync", -> - decorationItem1 = document.createElement('div') - - buildTestState = (customDecorations) -> - mockTestState = - content: if customDecorations then customDecorations else {} - styles: - scrollHeight: 100 - scrollTop: 10 - backgroundColor: 'black' - - mockTestState - - it "sets the custom-decoration wrapper's scrollHeight, scrollTop, and background color", -> - decorationsWrapperNode = gutterComponent.getDomNode().children.item(0) - expect(decorationsWrapperNode.style.height).toBe '' - expect(decorationsWrapperNode.style['-webkit-transform']).toBe '' - expect(decorationsWrapperNode.style.backgroundColor).toBe '' - - gutterComponent.updateSync(buildTestState({})) - expect(decorationsWrapperNode.style.height).not.toBe '' - expect(decorationsWrapperNode.style['-webkit-transform']).not.toBe '' - expect(decorationsWrapperNode.style.backgroundColor).not.toBe '' - - it "creates a new DOM node for a new decoration and adds it to the gutter at the right place", -> - customDecorations = - 'decoration-id-1': - top: 0 - height: 10 - item: decorationItem1 - class: 'test-class-1' - - gutterComponent.updateSync(buildTestState(customDecorations)) - decorationsWrapperNode = gutterComponent.getDomNode().children.item(0) - expect(decorationsWrapperNode.children.length).toBe 1 - - decorationNode = decorationsWrapperNode.children.item(0) - expect(decorationNode.style.top).toBe '0px' - expect(decorationNode.style.height).toBe '10px' - expect(decorationNode.classList.contains('test-class-1')).toBe true - expect(decorationNode.classList.contains('decoration')).toBe true - expect(decorationNode.children.length).toBe 1 - - decorationItem = decorationNode.children.item(0) - expect(decorationItem).toBe decorationItem1 - - it "updates the existing DOM node for a decoration that existed but has new properties", -> - initialCustomDecorations = - 'decoration-id-1': - top: 0 - height: 10 - item: decorationItem1 - class: 'test-class-1' - gutterComponent.updateSync(buildTestState(initialCustomDecorations)) - initialDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0) - - # Change the dimensions and item, remove the class. - decorationItem2 = document.createElement('div') - changedCustomDecorations = - 'decoration-id-1': - top: 10 - height: 20 - item: decorationItem2 - gutterComponent.updateSync(buildTestState(changedCustomDecorations)) - changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0) - expect(changedDecorationNode).toBe initialDecorationNode - expect(changedDecorationNode.style.top).toBe '10px' - expect(changedDecorationNode.style.height).toBe '20px' - expect(changedDecorationNode.classList.contains('test-class-1')).toBe false - expect(changedDecorationNode.classList.contains('decoration')).toBe true - expect(changedDecorationNode.children.length).toBe 1 - decorationItem = changedDecorationNode.children.item(0) - expect(decorationItem).toBe decorationItem2 - - # Remove the item, add a class. - changedCustomDecorations = - 'decoration-id-1': - top: 10 - height: 20 - class: 'test-class-2' - gutterComponent.updateSync(buildTestState(changedCustomDecorations)) - changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0) - expect(changedDecorationNode).toBe initialDecorationNode - expect(changedDecorationNode.style.top).toBe '10px' - expect(changedDecorationNode.style.height).toBe '20px' - expect(changedDecorationNode.classList.contains('test-class-2')).toBe true - expect(changedDecorationNode.classList.contains('decoration')).toBe true - expect(changedDecorationNode.children.length).toBe 0 - - it "removes any decorations that existed previously but aren't in the latest update", -> - customDecorations = - 'decoration-id-1': - top: 0 - height: 10 - class: 'test-class-1' - gutterComponent.updateSync(buildTestState(customDecorations)) - decorationsWrapperNode = gutterComponent.getDomNode().children.item(0) - expect(decorationsWrapperNode.children.length).toBe 1 - - emptyCustomDecorations = {} - gutterComponent.updateSync(buildTestState(emptyCustomDecorations)) - expect(decorationsWrapperNode.children.length).toBe 0 diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee index e57660a57..76bc37b75 100644 --- a/spec/decoration-manager-spec.coffee +++ b/spec/decoration-manager-spec.coffee @@ -1,21 +1,21 @@ DecorationManager = require '../src/decoration-manager' +TextEditor = require '../src/text-editor' describe "DecorationManager", -> - [decorationManager, buffer, displayLayer, markerLayer1, markerLayer2] = [] + [decorationManager, buffer, editor, markerLayer1, markerLayer2] = [] beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - displayLayer = buffer.addDisplayLayer() - markerLayer1 = displayLayer.addMarkerLayer() - markerLayer2 = displayLayer.addMarkerLayer() - decorationManager = new DecorationManager(displayLayer) + editor = new TextEditor({buffer}) + markerLayer1 = editor.addMarkerLayer() + markerLayer2 = editor.addMarkerLayer() + decorationManager = new DecorationManager(editor) waitsForPromise -> atom.packages.activatePackage('language-javascript') afterEach -> - decorationManager.destroy() - buffer.release() + buffer.destroy() describe "decorations", -> [layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties] = [] @@ -29,7 +29,6 @@ describe "DecorationManager", -> it "can add decorations associated with markers and remove them", -> expect(layer1MarkerDecoration).toBeDefined() expect(layer1MarkerDecoration.getProperties()).toBe decorationProperties - expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).toBe layer1MarkerDecoration expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual { "#{layer1Marker.id}": [layer1MarkerDecoration], "#{layer2Marker.id}": [layer2MarkerDecoration] @@ -37,15 +36,12 @@ describe "DecorationManager", -> layer1MarkerDecoration.destroy() expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id]).not.toBeDefined() - expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined() layer2MarkerDecoration.destroy() expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id]).not.toBeDefined() - expect(decorationManager.decorationForId(layer2MarkerDecoration.id)).not.toBeDefined() it "will not fail if the decoration is removed twice", -> layer1MarkerDecoration.destroy() layer1MarkerDecoration.destroy() - expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined() it "does not allow destroyed markers to be decorated", -> layer1Marker.destroy() @@ -55,7 +51,7 @@ describe "DecorationManager", -> expect(decorationManager.getOverlayDecorations()).toEqual [] it "does not allow destroyed marker layers to be decorated", -> - layer = displayLayer.addMarkerLayer() + layer = editor.addMarkerLayer() layer.destroy() expect(-> decorationManager.decorateMarkerLayer(layer, {type: 'highlight'}) diff --git a/spec/dom-element-pool-spec.js b/spec/dom-element-pool-spec.js deleted file mode 100644 index 91120ee48..000000000 --- a/spec/dom-element-pool-spec.js +++ /dev/null @@ -1,115 +0,0 @@ -const DOMElementPool = require ('../src/dom-element-pool') - -describe('DOMElementPool', function () { - let domElementPool - - beforeEach(() => { - domElementPool = new DOMElementPool() - spyOn(atom, 'isReleasedVersion').andReturn(true) - }) - - it('builds DOM nodes, recycling them when they are freed', function () { - let elements - const [div, span1, span2, span3, span4, span5, textNode] = Array.from(elements = [ - domElementPool.buildElement('div', 'foo'), - domElementPool.buildElement('span'), - domElementPool.buildElement('span'), - domElementPool.buildElement('span'), - domElementPool.buildElement('span'), - domElementPool.buildElement('span'), - domElementPool.buildText('Hello world!') - ]) - - expect(div.className).toBe('foo') - div.textContent = 'testing' - div.style.backgroundColor = 'red' - div.dataset.foo = 'bar' - - expect(textNode.textContent).toBe('Hello world!') - - div.appendChild(span1) - span1.appendChild(span2) - div.appendChild(span3) - span3.appendChild(span4) - span4.appendChild(textNode) - - domElementPool.freeElementAndDescendants(div) - domElementPool.freeElementAndDescendants(span5) - - expect(elements.includes(domElementPool.buildElement('div'))).toBe(true) - expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) - expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) - expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) - expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) - expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) - expect(elements.includes(domElementPool.buildText('another text'))).toBe(true) - - expect(elements.includes(domElementPool.buildElement('div'))).toBe(false) - expect(elements.includes(domElementPool.buildElement('span'))).toBe(false) - expect(elements.includes(domElementPool.buildText('unexisting'))).toBe(false) - - expect(div.className).toBe('') - expect(div.textContent).toBe('') - expect(div.style.backgroundColor).toBe('') - expect(div.dataset.foo).toBeUndefined() - - expect(textNode.textContent).toBe('another text') - }) - - it('forgets free nodes after being cleared', function () { - const span = domElementPool.buildElement('span') - const div = domElementPool.buildElement('div') - domElementPool.freeElementAndDescendants(span) - domElementPool.freeElementAndDescendants(div) - - domElementPool.clear() - - expect(domElementPool.buildElement('span')).not.toBe(span) - expect(domElementPool.buildElement('div')).not.toBe(div) - }) - - it('does not attempt to free nodes that were not created by the pool', () => { - let assertionFailure - atom.onDidFailAssertion((error) => assertionFailure = error) - - const foreignDiv = document.createElement('div') - const div = domElementPool.buildElement('div') - div.appendChild(foreignDiv) - domElementPool.freeElementAndDescendants(div) - const span = domElementPool.buildElement('span') - span.appendChild(foreignDiv) - domElementPool.freeElementAndDescendants(span) - - expect(assertionFailure).toBeUndefined() - }) - - it('fails an assertion when freeing the same element twice', function () { - let assertionFailure - atom.onDidFailAssertion((error) => assertionFailure = error) - - const div = domElementPool.buildElement('div') - div.textContent = 'testing' - domElementPool.freeElementAndDescendants(div) - expect(assertionFailure).toBeUndefined() - domElementPool.freeElementAndDescendants(div) - expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!') - expect(assertionFailure.metadata.content).toBe('
testing
') - }) - - it('fails an assertion when freeing the same text node twice', function () { - let assertionFailure - atom.onDidFailAssertion((error) => assertionFailure = error) - - const node = domElementPool.buildText('testing') - domElementPool.freeElementAndDescendants(node) - expect(assertionFailure).toBeUndefined() - domElementPool.freeElementAndDescendants(node) - expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!') - expect(assertionFailure.metadata.content).toBe('testing') - }) - - it('throws an error when trying to free an invalid element', function () { - expect(() => domElementPool.freeElementAndDescendants(null)).toThrow() - expect(() => domElementPool.freeElementAndDescendants(undefined)).toThrow() - }) -}) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee deleted file mode 100644 index c3396ff9f..000000000 --- a/spec/fake-lines-yardstick.coffee +++ /dev/null @@ -1,63 +0,0 @@ -{Point} = require 'text-buffer' -{isPairedCharacter} = require '../src/text-utils' - -module.exports = -class FakeLinesYardstick - constructor: (@model, @lineTopIndex) -> - {@displayLayer} = @model - @characterWidthsByScope = {} - - getScopedCharacterWidth: (scopeNames, char) -> - @getScopedCharacterWidths(scopeNames)[char] - - getScopedCharacterWidths: (scopeNames) -> - scope = @characterWidthsByScope - for scopeName in scopeNames - scope[scopeName] ?= {} - scope = scope[scopeName] - scope.characterWidths ?= {} - scope.characterWidths - - setScopedCharacterWidth: (scopeNames, character, width) -> - @getScopedCharacterWidths(scopeNames)[character] = width - - pixelPositionForScreenPosition: (screenPosition) -> - screenPosition = Point.fromObject(screenPosition) - - targetRow = screenPosition.row - targetColumn = screenPosition.column - - top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow) - left = 0 - column = 0 - - scopes = [] - startIndex = 0 - {tagCodes, lineText} = @model.screenLineForScreenRow(targetRow) - for tagCode in tagCodes - if @displayLayer.isOpenTagCode(tagCode) - scopes.push(@displayLayer.tagForCode(tagCode)) - else if @displayLayer.isCloseTagCode(tagCode) - scopes.splice(scopes.lastIndexOf(@displayLayer.tagForCode(tagCode)), 1) - else - text = lineText.substr(startIndex, tagCode) - startIndex += tagCode - characterWidths = @getScopedCharacterWidths(scopes) - - valueIndex = 0 - while valueIndex < text.length - if isPairedCharacter(text, valueIndex) - char = text[valueIndex...valueIndex + 2] - charLength = 2 - valueIndex += 2 - else - char = text[valueIndex] - charLength = 1 - valueIndex++ - - break if column is targetColumn - - left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0' - column += charLength - - {top, left} diff --git a/spec/fixtures/babel/invalid.js b/spec/fixtures/babel/invalid.js index f02fd2fd6..585a4365b 100644 --- a/spec/fixtures/babel/invalid.js +++ b/spec/fixtures/babel/invalid.js @@ -1,3 +1,3 @@ 'use 6to6'; -module.exports = async function hello() {} +module.exports = async function* hello() {} diff --git a/spec/gutter-container-component-spec.coffee b/spec/gutter-container-component-spec.coffee deleted file mode 100644 index b62485cad..000000000 --- a/spec/gutter-container-component-spec.coffee +++ /dev/null @@ -1,160 +0,0 @@ -Gutter = require '../src/gutter' -GutterContainerComponent = require '../src/gutter-container-component' -DOMElementPool = require '../src/dom-element-pool' - -describe "GutterContainerComponent", -> - [gutterContainerComponent] = [] - mockGutterContainer = {} - - buildTestState = (gutters) -> - styles = - scrollHeight: 100 - scrollTop: 10 - backgroundColor: 'black' - - mockTestState = {gutters: []} - for gutter in gutters - if gutter.name is 'line-number' - content = {maxLineNumberDigits: 10, lineNumbers: {}} - else - content = {} - mockTestState.gutters.push({gutter, styles, content, visible: gutter.visible}) - - mockTestState - - beforeEach -> - domElementPool = new DOMElementPool - mockEditor = {} - mockMouseDown = -> - gutterContainerComponent = new GutterContainerComponent({editor: mockEditor, onMouseDown: mockMouseDown, domElementPool, views: atom.views}) - - it "creates a DOM node with no child gutter nodes when it is initialized", -> - expect(gutterContainerComponent.getDomNode() instanceof HTMLElement).toBe true - expect(gutterContainerComponent.getDomNode().children.length).toBe 0 - - describe "when updated with state that contains a new line-number gutter", -> - it "adds a LineNumberGutterComponent to its children", -> - lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) - testState = buildTestState([lineNumberGutter]) - - expect(gutterContainerComponent.getDomNode().children.length).toBe 0 - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - expectedGutterNode = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedGutterNode.classList.contains('gutter')).toBe true - expectedLineNumbersNode = expectedGutterNode.children.item(0) - expect(expectedLineNumbersNode.classList.contains('line-numbers')).toBe true - - expect(gutterContainerComponent.getLineNumberGutterComponent().getDomNode()).toBe expectedGutterNode - - describe "when updated with state that contains a new custom gutter", -> - it "adds a CustomGutterComponent to its children", -> - customGutter = new Gutter(mockGutterContainer, {name: 'custom'}) - testState = buildTestState([customGutter]) - - expect(gutterContainerComponent.getDomNode().children.length).toBe 0 - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - expectedGutterNode = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedGutterNode.classList.contains('gutter')).toBe true - expectedCustomDecorationsNode = expectedGutterNode.children.item(0) - expect(expectedCustomDecorationsNode.classList.contains('custom-decorations')).toBe true - - describe "when updated with state that contains a new gutter that is not visible", -> - it "creates the gutter view but hides it, and unhides it when it is later updated to be visible", -> - customGutter = new Gutter(mockGutterContainer, {name: 'custom', visible: false}) - testState = buildTestState([customGutter]) - - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedCustomGutterNode.style.display).toBe 'none' - - customGutter.show() - testState = buildTestState([customGutter]) - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedCustomGutterNode.style.display).toBe '' - - describe "when updated with a gutter that already exists", -> - it "reuses the existing gutter view, instead of recreating it", -> - customGutter = new Gutter(mockGutterContainer, {name: 'custom'}) - testState = buildTestState([customGutter]) - - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) - - testState = buildTestState([customGutter]) - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - expect(gutterContainerComponent.getDomNode().children.item(0)).toBe expectedCustomGutterNode - - it "removes a gutter from the DOM if it does not appear in the latest state update", -> - lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) - testState = buildTestState([lineNumberGutter]) - gutterContainerComponent.updateSync(testState) - - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - testState = buildTestState([]) - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 0 - - describe "when updated with multiple gutters", -> - it "positions (and repositions) the gutters to match the order they appear in each state update", -> - lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) - customGutter1 = new Gutter(mockGutterContainer, {name: 'custom', priority: -100}) - testState = buildTestState([customGutter1, lineNumberGutter]) - - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 2 - expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedCustomGutterNode).toBe customGutter1.getElement() - expectedLineNumbersNode = gutterContainerComponent.getDomNode().children.item(1) - expect(expectedLineNumbersNode).toBe lineNumberGutter.getElement() - - # Add a gutter. - customGutter2 = new Gutter(mockGutterContainer, {name: 'custom2', priority: -10}) - testState = buildTestState([customGutter1, customGutter2, lineNumberGutter]) - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 3 - expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedCustomGutterNode1).toBe customGutter1.getElement() - expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(1) - expect(expectedCustomGutterNode2).toBe customGutter2.getElement() - expectedLineNumbersNode = gutterContainerComponent.getDomNode().children.item(2) - expect(expectedLineNumbersNode).toBe lineNumberGutter.getElement() - - # Hide one gutter, reposition one gutter, remove one gutter; and add a new gutter. - customGutter2.hide() - customGutter3 = new Gutter(mockGutterContainer, {name: 'custom3', priority: 100}) - testState = buildTestState([customGutter2, customGutter1, customGutter3]) - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 3 - expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedCustomGutterNode2).toBe customGutter2.getElement() - expect(expectedCustomGutterNode2.style.display).toBe 'none' - expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(1) - expect(expectedCustomGutterNode1).toBe customGutter1.getElement() - expectedCustomGutterNode3 = gutterContainerComponent.getDomNode().children.item(2) - expect(expectedCustomGutterNode3).toBe customGutter3.getElement() - - it "reorders correctly when prepending multiple gutters at once", -> - lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) - testState = buildTestState([lineNumberGutter]) - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 1 - expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedCustomGutterNode).toBe lineNumberGutter.getElement() - - # Prepend two gutters at once - customGutter1 = new Gutter(mockGutterContainer, {name: 'first', priority: -200}) - customGutter2 = new Gutter(mockGutterContainer, {name: 'second', priority: -100}) - testState = buildTestState([customGutter1, customGutter2, lineNumberGutter]) - gutterContainerComponent.updateSync(testState) - expect(gutterContainerComponent.getDomNode().children.length).toBe 3 - expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0) - expect(expectedCustomGutterNode1).toBe customGutter1.getElement() - expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(1) - expect(expectedCustomGutterNode2).toBe customGutter2.getElement() diff --git a/spec/gutter-container-spec.coffee b/spec/gutter-container-spec.coffee index e38367835..dc4af0b8c 100644 --- a/spec/gutter-container-spec.coffee +++ b/spec/gutter-container-spec.coffee @@ -3,7 +3,9 @@ GutterContainer = require '../src/gutter-container' describe 'GutterContainer', -> gutterContainer = null - fakeTextEditor = {} + fakeTextEditor = { + scheduleComponentUpdate: -> + } beforeEach -> gutterContainer = new GutterContainer fakeTextEditor diff --git a/spec/gutter-spec.coffee b/spec/gutter-spec.coffee index 30748b787..47c5983f6 100644 --- a/spec/gutter-spec.coffee +++ b/spec/gutter-spec.coffee @@ -1,7 +1,9 @@ Gutter = require '../src/gutter' describe 'Gutter', -> - fakeGutterContainer = {} + fakeGutterContainer = { + scheduleComponentUpdate: -> + } name = 'name' describe '::hide', -> diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 7e62a69f4..7a06fce9b 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -31,7 +31,6 @@ describe("HistoryManager", () => { }) historyManager = new HistoryManager({stateStore, project, commands: commandRegistry}) - historyManager.initialize(window.localStorage) await historyManager.loadState() }) @@ -76,7 +75,7 @@ describe("HistoryManager", () => { it("saves the state", async () => { await historyManager.clearProjects() - const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) + const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) await historyManager2.loadState() expect(historyManager.getProjects().length).toBe(0) }) @@ -187,7 +186,7 @@ describe("HistoryManager", () => { it("saves the state", async () => { await historyManager.addProject(["/save/state"]) await historyManager.saveState() - const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) + const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) await historyManager2.loadState() expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) diff --git a/spec/integration/smoke-spec.coffee b/spec/integration/smoke-spec.coffee index 527ed1f8f..e147cf5c0 100644 --- a/spec/integration/smoke-spec.coffee +++ b/spec/integration/smoke-spec.coffee @@ -6,7 +6,7 @@ runAtom = require './helpers/start-atom' describe "Smoke Test", -> return unless process.platform is 'darwin' # Fails on win32 - + atomHome = temp.mkdirSync('atom-home') beforeEach -> @@ -14,7 +14,10 @@ describe "Smoke Test", -> season.writeFileSync(path.join(atomHome, 'config.cson'), { '*': { welcome: {showOnStartup: false}, - core: {telemetryConsent: 'no'} + core: { + telemetryConsent: 'no', + disabledPackages: ['github'] + } } }) @@ -28,6 +31,7 @@ describe "Smoke Test", -> .then (exists) -> expect(exists).toBe true .waitForPaneItemCount(1, 1000) .click("atom-text-editor") + .waitUntil((-> @execute(-> document.activeElement.closest('atom-text-editor'))), 5000) .keys("Hello!") .execute -> atom.workspace.getActiveTextEditor().getText() .then ({value}) -> expect(value).toBe "Hello!" diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee deleted file mode 100644 index 68fd74804..000000000 --- a/spec/lines-yardstick-spec.coffee +++ /dev/null @@ -1,248 +0,0 @@ -LinesYardstick = require '../src/lines-yardstick' -LineTopIndex = require 'line-top-index' -{Point} = require 'text-buffer' - -describe "LinesYardstick", -> - [editor, mockLineNodesProvider, createdLineNodes, linesYardstick, buildLineNode] = [] - - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - createdLineNodes = [] - - buildLineNode = (screenRow) -> - startIndex = 0 - scopes = [] - screenLine = editor.screenLineForScreenRow(screenRow) - lineNode = document.createElement("div") - lineNode.style.whiteSpace = "pre" - 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 = - lineNodesById: {} - - lineIdForScreenRow: (screenRow) -> - editor.screenLineForScreenRow(screenRow)?.id - - lineNodeForScreenRow: (screenRow) -> - if id = @lineIdForScreenRow(screenRow) - @lineNodesById[id] ?= buildLineNode(screenRow) - - textNodesForScreenRow: (screenRow) -> - lineNode = @lineNodeForScreenRow(screenRow) - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) - textNodes = [] - textNodes.push(textNode) while textNode = iterator.nextNode() - textNodes - - editor.setLineHeightInPixels(14) - lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()}) - linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) - - afterEach -> - lineNode.remove() for lineNode in createdLineNodes - atom.themes.removeStylesheet('test') - - describe "::pixelPositionForScreenPosition(screenPosition)", -> - it "converts screen positions to pixel positions", -> - atom.styles.addStyleSheet """ - * { - font-size: 12px; - font-family: monospace; - } - .syntax--function { - font-size: 16px - } - """ - - 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: 38, top: 0}) - - switch process.platform - when 'darwin' - 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.875, top: 28}) - when 'win32' - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 42, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 71, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 280, top: 28}) - - it "reuses already computed pixel positions unless it is invalidated", -> - atom.styles.addStyleSheet """ - * { - font-size: 16px; - font-family: monospace; - } - """ - - 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: 96, top: 70}) - - atom.styles.addStyleSheet """ - * { - font-size: 20px; - } - """ - - 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: 96, top: 70}) - - linesYardstick.invalidateCache() - - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 24, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70}) - - 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. - atom.styles.addStyleSheet """ - * { - font-size: 11px; - font-family: monospace; - } - """ - - text = " \\vec{w}_j^r(\\text{new}) &= \\vec{w}_j^r(\\text{old}) + \\Delta\\vec{w}_j^r, \\\\" - buildLineNode = (screenRow) -> - lineNode = document.createElement("div") - lineNode.style.whiteSpace = "pre" - # We couldn't reproduce the problem with a simple string, so we're - # attaching the full one that comes from a bug report. - lineNode.innerHTML = ' \\vec{w}_j^r(\\text{new}) &= \\vec{w}_j^r(\\text{old}) + \\Delta\\vec{w}_j^r, \\\\' - jasmine.attachToDOM(lineNode) - createdLineNodes.push(lineNode) - lineNode - - editor.setText(text) - - switch process.platform - when 'darwin' - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 230.90625 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 237.5 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 244.09375 - when 'win32' - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 245 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 252 - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 259 - - it "handles lines containing a mix of left-to-right and right-to-left characters", -> - editor.setText('Persian, locally known as Parsi or Farsi (زبان فارسی), the predominant modern descendant of Old Persian.\n') - - atom.styles.addStyleSheet """ - * { - font-size: 14px; - font-family: monospace; - } - """ - - lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()}) - linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) - - switch process.platform - when 'darwin' - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 126, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 521, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 487, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 873.625, top: 0}) - when 'win32' - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 120, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 496, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 464, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 832, top: 0}) - - describe "::screenPositionForPixelPosition(pixelPosition)", -> - it "converts pixel positions to screen positions", -> - atom.styles.addStyleSheet """ - * { - font-size: 12px; - font-family: monospace; - } - .syntax--function { - font-size: 16px - } - """ - - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 12.5})).toEqual([0, 2]) - expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 18.8})).toEqual([1, 3]) - 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: 70, left: 99.9})).toEqual([5, 14]) - expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30]) - - switch process.platform - when 'darwin' - expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29]) - expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33]) - when 'win32' - expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 30]) - expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 34]) - - it "overshoots to the nearest character when text nodes are not spatially contiguous", -> - atom.styles.addStyleSheet """ - * { - font-size: 12px; - font-family: monospace; - } - """ - - buildLineNode = (screenRow) -> - lineNode = document.createElement("div") - lineNode.style.whiteSpace = "pre" - lineNode.innerHTML = 'foobar' - jasmine.attachToDOM(lineNode) - createdLineNodes.push(lineNode) - lineNode - editor.setText("foobar") - - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6]) - - it "clips pixel positions above buffer start", -> - expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] - expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] - expect(linesYardstick.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] - expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] - - it "clips pixel positions below buffer end", -> - expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] - 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/spec-helper.coffee b/spec/spec-helper.coffee index 44d8b4460..2379cc650 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -64,6 +64,7 @@ beforeEach -> atom.project.setPaths([specProjectPath]) window.resetTimeouts() + spyOn(Date, 'now').andCallFake -> window.now spyOn(_._, "now").andCallFake -> window.now spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout @@ -179,6 +180,7 @@ jasmine.useRealClock = -> jasmine.unspy(window, 'setTimeout') jasmine.unspy(window, 'clearTimeout') jasmine.unspy(_._, 'now') + jasmine.unspy(Date, 'now') # The clock is halfway mocked now in a sad and terrible way... only setTimeout # and clearTimeout are included. This method will also include setInterval. We @@ -186,6 +188,8 @@ jasmine.useRealClock = -> jasmine.useMockClock = -> spyOn(window, 'setInterval').andCallFake(fakeSetInterval) spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) + spyOn(Date, 'now').andCallFake(-> window.now) + addCustomMatchers = (spec) -> spec.addMatchers diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 35d2af832..0a7b28c74 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1,5163 +1,3544 @@ -/** @babel */ +const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') -import {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} from './async-spec-helpers' -import Grim from 'grim' -import TextEditor from '../src/text-editor' -import TextEditorElement from '../src/text-editor-element' -import _, {extend, flatten, last, toArray} from 'underscore-plus' +const TextEditorComponent = require('../src/text-editor-component') +const TextEditorElement = require('../src/text-editor-element') +const TextEditor = require('../src/text-editor') +const TextBuffer = require('text-buffer') +const {Point} = TextBuffer +const fs = require('fs') +const path = require('path') +const Grim = require('grim') +const electron = require('electron') +const clipboard = require('../src/safe-clipboard') -const NBSP = String.fromCharCode(160) -const TILE_SIZE = 3 +const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8') +const NBSP_CHARACTER = '\u00a0' -describe('TextEditorComponent', function () { - let charWidth, component, componentNode, contentNode, editor, - horizontalScrollbarNode, lineHeightInPixels, tileHeightInPixels, - verticalScrollbarNode, wrapperNode, animationFrameRequests - - function runAnimationFrames (runFollowupFrames) { - if (runFollowupFrames) { - let fn - while (fn = animationFrameRequests.shift()) fn() - } else { - const requests = animationFrameRequests.slice() - animationFrameRequests = [] - for (let fn of requests) fn() - } - } - - beforeEach(async function () { - animationFrameRequests = [] - spyOn(window, 'requestAnimationFrame').andCallFake(function (fn) { animationFrameRequests.push(fn) }) - jasmine.useMockClock() - - await atom.packages.activatePackage('language-javascript') - editor = await atom.workspace.open('sample.js') - editor.update({autoHeight: true}) - - contentNode = document.querySelector('#jasmine-content') - contentNode.style.width = '1000px' - - wrapperNode = new TextEditorElement() - wrapperNode.tileSize = TILE_SIZE - wrapperNode.initialize(editor, atom) - wrapperNode.setUpdatedSynchronously(false) - jasmine.attachToDOM(wrapperNode) - - component = wrapperNode.component - component.setFontFamily('monospace') - component.setLineHeight(1.3) - component.setFontSize(20) - - lineHeightInPixels = editor.getLineHeightInPixels() - tileHeightInPixels = TILE_SIZE * lineHeightInPixels - charWidth = editor.getDefaultCharWidth() - - componentNode = component.getDomNode() - verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') - horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') - - component.measureDimensions() - advanceClock(atom.views.minimumPollInterval) - runAnimationFrames(true) - }) - - afterEach(function () { - contentNode.style.width = '' - }) - - describe('async updates', function () { - it('handles corrupted state gracefully', function () { - editor.insertNewline() - component.presenter.startRow = -1 - component.presenter.endRow = 9999 - runAnimationFrames() // assert an update does occur - }) - - it('does not update when an animation frame was requested but the component got destroyed before its delivery', function () { - editor.setText('You should not see this update.') - component.destroy() - - runAnimationFrames() - - expect(component.lineNodeForScreenRow(0).textContent).not.toBe('You should not see this update.') - }) - }) - - describe('line rendering', function () { - function expectTileContainsRow (tileNode, screenRow, {top}) { - let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]') - let text = editor.lineTextForScreenRow(screenRow) - expect(lineNode.offsetTop).toBe(top) - if (text === '') { - expect(lineNode.textContent).toBe(' ') - } else { - expect(lineNode.textContent).toBe(text) +document.registerElement('text-editor-component-test-element', { + prototype: Object.create(HTMLElement.prototype, { + attachedCallback: { + value: function () { + this.didAttach() } } - - it('gives the lines container the same height as the wrapper node', function () { - let linesNode = componentNode.querySelector('.lines') - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) - wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - - runAnimationFrames() - - expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) - }) - - it('renders higher tiles in front of lower ones', function () { - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - - runAnimationFrames() - - let tilesNodes = component.tileNodesForLines() - expect(tilesNodes[0].style.zIndex).toBe('2') - expect(tilesNodes[1].style.zIndex).toBe('1') - expect(tilesNodes[2].style.zIndex).toBe('0') - verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - tilesNodes = component.tileNodesForLines() - expect(tilesNodes[0].style.zIndex).toBe('3') - expect(tilesNodes[1].style.zIndex).toBe('2') - expect(tilesNodes[2].style.zIndex).toBe('1') - expect(tilesNodes[3].style.zIndex).toBe('0') - }) - - it('renders the currently-visible lines in a tiled fashion', function () { - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - - runAnimationFrames() - - let tilesNodes = component.tileNodesForLines() - expect(tilesNodes.length).toBe(3) - - expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') - expect(tilesNodes[0].querySelectorAll('.line').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[0], 0, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 1, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 2, { - top: 2 * lineHeightInPixels - }) - - expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') - expect(tilesNodes[1].querySelectorAll('.line').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[1], 3, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 4, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 5, { - top: 2 * lineHeightInPixels - }) - - expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') - expect(tilesNodes[2].querySelectorAll('.line').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[2], 6, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[2], 7, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[2], 8, { - top: 2 * lineHeightInPixels - }) - - expect(component.lineNodeForScreenRow(9)).toBeUndefined() - - verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - tilesNodes = component.tileNodesForLines() - expect(component.lineNodeForScreenRow(2)).toBeUndefined() - expect(tilesNodes.length).toBe(3) - - expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, ' + (0 * tileHeightInPixels - 5) + 'px, 0px)') - expect(tilesNodes[0].querySelectorAll('.line').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[0], 3, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 4, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 5, { - top: 2 * lineHeightInPixels - }) - - expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels - 5) + 'px, 0px)') - expect(tilesNodes[1].querySelectorAll('.line').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[1], 6, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 7, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 8, { - top: 2 * lineHeightInPixels - }) - - expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels - 5) + 'px, 0px)') - expect(tilesNodes[2].querySelectorAll('.line').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[2], 9, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[2], 10, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[2], 11, { - top: 2 * lineHeightInPixels - }) - }) - - it('updates the top position of subsequent tiles when lines are inserted or removed', function () { - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - editor.getBuffer().deleteRows(0, 1) - - runAnimationFrames() - - let tilesNodes = component.tileNodesForLines() - expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') - expectTileContainsRow(tilesNodes[0], 0, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 1, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 2, { - top: 2 * lineHeightInPixels - }) - - expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') - expectTileContainsRow(tilesNodes[1], 3, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 4, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 5, { - top: 2 * lineHeightInPixels - }) - - editor.getBuffer().insert([0, 0], '\n\n') - - runAnimationFrames() - - tilesNodes = component.tileNodesForLines() - expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') - expectTileContainsRow(tilesNodes[0], 0, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 1, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[0], 2, { - top: 2 * lineHeightInPixels - }) - - expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') - expectTileContainsRow(tilesNodes[1], 3, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 4, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[1], 5, { - top: 2 * lineHeightInPixels - }) - - expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') - expectTileContainsRow(tilesNodes[2], 6, { - top: 0 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[2], 7, { - top: 1 * lineHeightInPixels - }) - expectTileContainsRow(tilesNodes[2], 8, { - top: 2 * lineHeightInPixels - }) - }) - - it('updates the lines when lines are inserted or removed above the rendered row range', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - let buffer = editor.getBuffer() - buffer.insert([0, 0], '\n\n') - - runAnimationFrames() - - expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3)) - buffer.delete([[0, 0], [3, 0]]) - - runAnimationFrames() - - expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3)) - }) - - it('updates the top position of lines when the line height changes', function () { - let initialLineHeightInPixels = editor.getLineHeightInPixels() - - component.setLineHeight(2) - - runAnimationFrames() - - let newLineHeightInPixels = editor.getLineHeightInPixels() - expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) - expect(component.lineNodeForScreenRow(1).offsetTop).toBe(1 * newLineHeightInPixels) - }) - - it('updates the top position of lines when the font size changes', function () { - let initialLineHeightInPixels = editor.getLineHeightInPixels() - component.setFontSize(10) - - runAnimationFrames() - - let newLineHeightInPixels = editor.getLineHeightInPixels() - expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) - expect(component.lineNodeForScreenRow(1).offsetTop).toBe(1 * newLineHeightInPixels) - }) - - it('renders the .lines div at the full height of the editor if there are not enough lines to scroll vertically', function () { - editor.setText('') - wrapperNode.style.height = '300px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - let linesNode = componentNode.querySelector('.lines') - expect(linesNode.offsetHeight).toBe(300) - }) - - it('assigns the width of each line so it extends across the full width of the editor', function () { - let gutterWidth = componentNode.querySelector('.gutter').offsetWidth - let scrollViewNode = componentNode.querySelector('.scroll-view') - let lineNodes = Array.from(componentNode.querySelectorAll('.line')) - - componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' - component.measureDimensions() - - runAnimationFrames() - - expect(wrapperNode.getScrollWidth()).toBeGreaterThan(scrollViewNode.offsetWidth) - let editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() - for (let lineNode of lineNodes) { - expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth) - } - - componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' - component.measureDimensions() - - runAnimationFrames() - - let scrollViewWidth = scrollViewNode.offsetWidth - for (let lineNode of lineNodes) { - expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth) - } - }) - - it('renders a placeholder space on empty lines when no line-ending character is defined', function () { - editor.update({showInvisibles: false}) - 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 () { - let linesNode = componentNode.querySelector('.lines') - let backgroundColor = getComputedStyle(wrapperNode).backgroundColor - - expect(linesNode.style.backgroundColor).toBe(backgroundColor) - for (let tileNode of component.tileNodesForLines()) { - expect(tileNode.style.backgroundColor).toBe(backgroundColor) - } - - wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' - // Polling should happen automatically in a mutation observer but its async - // and everything is mocked to be sync - atom.views.performDocumentPoll() - runAnimationFrames(true) - - expect(linesNode.style.backgroundColor).toBe('rgb(255, 0, 0)') - for (let tileNode of component.tileNodesForLines()) { - expect(tileNode.style.backgroundColor).toBe('rgb(255, 0, 0)') - } - }) - - it('applies .leading-whitespace for lines with leading spaces and/or tabs', function () { - editor.setText(' a') - - runAnimationFrames() - - let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) - - editor.setText('\ta') - runAnimationFrames() - - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) - }) - - it('applies .trailing-whitespace for lines with trailing spaces and/or tabs', function () { - editor.setText(' ') - runAnimationFrames() - - let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) - - editor.setText('\t') - runAnimationFrames() - - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) - editor.setText('a ') - runAnimationFrames() - - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) - editor.setText('a\t') - runAnimationFrames() - - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) - }) - - it('keeps rebuilding lines when continuous reflow is on', function () { - wrapperNode.setContinuousReflow(true) - let oldLineNode = componentNode.querySelectorAll('.line')[1] - - while (true) { - runAnimationFrames() - if (componentNode.querySelectorAll('.line')[1] !== oldLineNode) break - } - }) - - describe('when showInvisibles is enabled', function () { - const invisibles = { - eol: 'E', - space: 'S', - tab: 'T', - cr: 'C' - } - - beforeEach(function () { - editor.update({ - showInvisibles: true, - invisibles: invisibles - }) - runAnimationFrames() - }) - - it('re-renders the lines when the showInvisibles config option changes', function () { - editor.setText(' a line with tabs\tand spaces \n') - runAnimationFrames() - - expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) - - editor.update({showInvisibles: false}) - runAnimationFrames() - - expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ') - - editor.update({showInvisibles: true}) - runAnimationFrames() - - expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) - }) - - it('displays leading/trailing spaces, tabs, and newlines as visible characters', function () { - editor.setText(' a line with tabs\tand spaces \n') - - runAnimationFrames() - - expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) - - let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('invisible-character')).toBe(true) - expect(leafNodes[leafNodes.length - 1].classList.contains('invisible-character')).toBe(true) - }) - - it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', function () { - editor.setText('let\n') - runAnimationFrames() - expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') - }) - - it('displays trailing carriage returns using a visible, non-empty value', function () { - editor.setText('a line that ends with a carriage return\r\n') - runAnimationFrames() - expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ends with a carriage return' + invisibles.cr + invisibles.eol) - }) - - it('renders invisible line-ending characters on empty lines', function () { - expect(component.lineNodeForScreenRow(10).textContent).toBe(invisibles.eol) - }) - - it('renders a placeholder space on empty lines when the line-ending character is an empty string', function () { - editor.update({invisibles: {eol: ''}}) - runAnimationFrames() - expect(component.lineNodeForScreenRow(10).textContent).toBe(' ') - }) - - it('renders an placeholder space on empty lines when the line-ending character is false', function () { - editor.update({invisibles: {eol: false}}) - runAnimationFrames() - expect(component.lineNodeForScreenRow(10).textContent).toBe(' ') - }) - - it('interleaves invisible line-ending characters with indent guides on empty lines', function () { - editor.update({showIndentGuide: true}) - - runAnimationFrames() - - editor.setTabLength(2) - editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', { - normalizeLineEndings: false - }) - runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') - - editor.setTabLength(3) - runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') - - editor.setTabLength(1) - runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') - - editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') - editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') - runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') - }) - - describe('when soft wrapping is enabled', function () { - beforeEach(function () { - editor.setText('a line that wraps \n') - editor.setSoftWrapped(true) - runAnimationFrames() - - componentNode.style.width = 17 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - runAnimationFrames() - }) - - it('does not show end of line invisibles at the end of wrapped lines', function () { - expect(component.lineNodeForScreenRow(0).textContent).toBe('a line ') - expect(component.lineNodeForScreenRow(1).textContent).toBe('that wraps' + invisibles.space + invisibles.eol) - }) - }) - }) - - describe('when indent guides are enabled', function () { - beforeEach(function () { - editor.update({showIndentGuide: true}) - runAnimationFrames() - }) - - it('adds an "indent-guide" class to spans comprising the leading whitespace', function () { - let line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe(' ') - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) - - let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes[0].textContent).toBe(' ') - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false) - }) - - it('renders leading whitespace spans with the "indent-guide" class for empty lines', function () { - editor.getBuffer().insert([1, Infinity], '\n') - runAnimationFrames() - - let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe(2) - expect(line2LeafNodes[0].textContent).toBe(' ') - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) - }) - - it('renders indent guides correctly on lines containing only whitespace', function () { - editor.getBuffer().insert([1, Infinity], '\n ') - runAnimationFrames() - - let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe(3) - expect(line2LeafNodes[0].textContent).toBe(' ') - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[2].textContent).toBe(' ') - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) - }) - - it('renders indent guides correctly on lines containing only whitespace when invisibles are enabled', function () { - editor.update({ - showInvisibles: true, - invisibles: { - space: '-', - eol: 'x' - } - }) - editor.getBuffer().insert([1, Infinity], '\n ') - - runAnimationFrames() - - let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe(4) - expect(line2LeafNodes[0].textContent).toBe('--') - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[1].textContent).toBe('--') - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[2].textContent).toBe('--') - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) - expect(line2LeafNodes[3].textContent).toBe('x') - }) - - it('does not render indent guides in trailing whitespace for lines containing non whitespace characters', function () { - editor.getBuffer().setText(' hi ') - - runAnimationFrames() - - let line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(line0LeafNodes[0].textContent).toBe(' ') - expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line0LeafNodes[1].textContent).toBe(' ') - expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe(false) - }) - - it('updates the indent guides on empty lines preceding an indentation change', function () { - editor.getBuffer().insert([12, 0], '\n') - runAnimationFrames() - - editor.getBuffer().insert([13, 0], ' ') - runAnimationFrames() - - let line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) - expect(line12LeafNodes[0].textContent).toBe(' ') - expect(line12LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line12LeafNodes[1].textContent).toBe(' ') - expect(line12LeafNodes[1].classList.contains('indent-guide')).toBe(true) - }) - - it('updates the indent guides on empty lines following an indentation change', function () { - editor.getBuffer().insert([12, 2], '\n') - - runAnimationFrames() - - editor.getBuffer().insert([12, 0], ' ') - runAnimationFrames() - - let line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) - expect(line13LeafNodes[0].textContent).toBe(' ') - expect(line13LeafNodes[0].classList.contains('indent-guide')).toBe(true) - expect(line13LeafNodes[1].textContent).toBe(' ') - expect(line13LeafNodes[1].classList.contains('indent-guide')).toBe(true) - }) - }) - - describe('when indent guides are disabled', function () { - beforeEach(function () { - expect(atom.config.get('editor.showIndentGuide')).toBe(false) - }) - - it('does not render indent guides on lines containing only whitespace', function () { - editor.getBuffer().insert([1, Infinity], '\n ') - - runAnimationFrames() - - let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe(1) - expect(line2LeafNodes[0].textContent).toBe(' ') - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(false) - }) - }) - - describe('when the buffer contains null bytes', function () { - it('excludes the null byte from character measurement', function () { - editor.setText('a\0b') - runAnimationFrames() - expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual(2 * charWidth) - }) - }) - - describe('when there is a fold', function () { - it('renders a fold marker on the folded line', function () { - let foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() - editor.foldBufferRow(4) - - runAnimationFrames() - - foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() - editor.unfoldBufferRow(4) - - runAnimationFrames() - - foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() - }) - }) }) - - describe('gutter rendering', function () { - function expectTileContainsRow (tileNode, screenRow, {top, text}) { - let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]') - expect(lineNode.offsetTop).toBe(top) - expect(lineNode.textContent).toBe(text) - } - - it('renders higher tiles in front of lower ones', function () { - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames(true) - - let tilesNodes = component.tileNodesForLineNumbers() - expect(tilesNodes[0].style.zIndex).toBe('2') - expect(tilesNodes[1].style.zIndex).toBe('1') - expect(tilesNodes[2].style.zIndex).toBe('0') - verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - tilesNodes = component.tileNodesForLineNumbers() - expect(tilesNodes[0].style.zIndex).toBe('3') - expect(tilesNodes[1].style.zIndex).toBe('2') - expect(tilesNodes[2].style.zIndex).toBe('1') - expect(tilesNodes[3].style.zIndex).toBe('0') - }) - - it('gives the line numbers container the same height as the wrapper node', function () { - let linesNode = componentNode.querySelector('.line-numbers') - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - - runAnimationFrames() - - expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) - wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - - runAnimationFrames() - - expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) - }) - - it('renders the currently-visible line numbers in a tiled fashion', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let tilesNodes = component.tileNodesForLineNumbers() - expect(tilesNodes.length).toBe(3) - - expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') - expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe(3) - expectTileContainsRow(tilesNodes[0], 0, { - top: lineHeightInPixels * 0, - text: '' + NBSP + '1' - }) - expectTileContainsRow(tilesNodes[0], 1, { - top: lineHeightInPixels * 1, - text: '' + NBSP + '2' - }) - expectTileContainsRow(tilesNodes[0], 2, { - top: lineHeightInPixels * 2, - text: '' + NBSP + '3' - }) - - expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') - expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe(3) - expectTileContainsRow(tilesNodes[1], 3, { - top: lineHeightInPixels * 0, - text: '' + NBSP + '4' - }) - expectTileContainsRow(tilesNodes[1], 4, { - top: lineHeightInPixels * 1, - text: '' + NBSP + '5' - }) - expectTileContainsRow(tilesNodes[1], 5, { - top: lineHeightInPixels * 2, - text: '' + NBSP + '6' - }) - - expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') - expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe(3) - expectTileContainsRow(tilesNodes[2], 6, { - top: lineHeightInPixels * 0, - text: '' + NBSP + '7' - }) - expectTileContainsRow(tilesNodes[2], 7, { - top: lineHeightInPixels * 1, - text: '' + NBSP + '8' - }) - expectTileContainsRow(tilesNodes[2], 8, { - top: lineHeightInPixels * 2, - text: '' + NBSP + '9' - }) - verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - tilesNodes = component.tileNodesForLineNumbers() - expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined() - expect(tilesNodes.length).toBe(3) - - expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, ' + (0 * tileHeightInPixels - 5) + 'px, 0px)') - expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[0], 3, { - top: lineHeightInPixels * 0, - text: '' + NBSP + '4' - }) - expectTileContainsRow(tilesNodes[0], 4, { - top: lineHeightInPixels * 1, - text: '' + NBSP + '5' - }) - expectTileContainsRow(tilesNodes[0], 5, { - top: lineHeightInPixels * 2, - text: '' + NBSP + '6' - }) - - expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels - 5) + 'px, 0px)') - expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[1], 6, { - top: 0 * lineHeightInPixels, - text: '' + NBSP + '7' - }) - expectTileContainsRow(tilesNodes[1], 7, { - top: 1 * lineHeightInPixels, - text: '' + NBSP + '8' - }) - expectTileContainsRow(tilesNodes[1], 8, { - top: 2 * lineHeightInPixels, - text: '' + NBSP + '9' - }) - - expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels - 5) + 'px, 0px)') - expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe(TILE_SIZE) - expectTileContainsRow(tilesNodes[2], 9, { - top: 0 * lineHeightInPixels, - text: '10' - }) - expectTileContainsRow(tilesNodes[2], 10, { - top: 1 * lineHeightInPixels, - text: '11' - }) - expectTileContainsRow(tilesNodes[2], 11, { - top: 2 * lineHeightInPixels, - text: '12' - }) - }) - - it('updates the translation of subsequent line numbers when lines are inserted or removed', function () { - editor.getBuffer().insert([0, 0], '\n\n') - runAnimationFrames() - - let lineNumberNodes = componentNode.querySelectorAll('.line-number') - expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe(1 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe(2 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe(0 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe(1 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe(2 * lineHeightInPixels) - editor.getBuffer().insert([0, 0], '\n\n') - - runAnimationFrames() - - expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe(1 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe(2 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe(0 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe(1 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe(2 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe(0 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe(1 * lineHeightInPixels) - expect(component.lineNumberNodeForScreenRow(8).offsetTop).toBe(2 * lineHeightInPixels) - }) - - it('renders • characters for soft-wrapped lines', function () { - editor.setSoftWrapped(true) - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 30 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - - runAnimationFrames() - - expect(componentNode.querySelectorAll('.line-number').length).toBe(9 + 1) - expect(component.lineNumberNodeForScreenRow(0).textContent).toBe('' + NBSP + '1') - expect(component.lineNumberNodeForScreenRow(1).textContent).toBe('' + NBSP + '•') - expect(component.lineNumberNodeForScreenRow(2).textContent).toBe('' + NBSP + '2') - expect(component.lineNumberNodeForScreenRow(3).textContent).toBe('' + NBSP + '•') - expect(component.lineNumberNodeForScreenRow(4).textContent).toBe('' + NBSP + '3') - expect(component.lineNumberNodeForScreenRow(5).textContent).toBe('' + NBSP + '•') - expect(component.lineNumberNodeForScreenRow(6).textContent).toBe('' + NBSP + '4') - expect(component.lineNumberNodeForScreenRow(7).textContent).toBe('' + NBSP + '•') - expect(component.lineNumberNodeForScreenRow(8).textContent).toBe('' + NBSP + '•') - }) - - it('pads line numbers to be right-justified based on the maximum number of line number digits', function () { - const input = []; - for (let i = 1; i <= 100; ++i) { - input.push(i); - } - editor.getBuffer().setText(input.join('\n')) - runAnimationFrames() - - for (let screenRow = 0; screenRow <= 8; ++screenRow) { - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + NBSP + (screenRow + 1)) - } - expect(component.lineNumberNodeForScreenRow(99).textContent).toBe('100') - let gutterNode = componentNode.querySelector('.gutter') - let initialGutterWidth = gutterNode.offsetWidth - editor.getBuffer().delete([[1, 0], [2, 0]]) - - runAnimationFrames() - - for (let screenRow = 0; screenRow <= 8; ++screenRow) { - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) - } - expect(gutterNode.offsetWidth).toBeLessThan(initialGutterWidth) - editor.getBuffer().insert([0, 0], '\n\n') - - runAnimationFrames() - - for (let screenRow = 0; screenRow <= 8; ++screenRow) { - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + NBSP + (screenRow + 1)) - } - expect(component.lineNumberNodeForScreenRow(99).textContent).toBe('100') - expect(gutterNode.offsetWidth).toBe(initialGutterWidth) - }) - - it('renders the .line-numbers div at the full height of the editor even if it\'s taller than its content', function () { - wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe(componentNode.offsetHeight) - }) - - it('applies the background color of the gutter or the editor to the line numbers to improve GPU performance', function () { - let gutterNode = componentNode.querySelector('.gutter') - let lineNumbersNode = gutterNode.querySelector('.line-numbers') - let backgroundColor = getComputedStyle(wrapperNode).backgroundColor - expect(lineNumbersNode.style.backgroundColor).toBe(backgroundColor) - for (let tileNode of component.tileNodesForLineNumbers()) { - expect(tileNode.style.backgroundColor).toBe(backgroundColor) - } - - gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' - atom.views.performDocumentPoll() - runAnimationFrames() - - expect(lineNumbersNode.style.backgroundColor).toBe('rgb(255, 0, 0)') - for (let tileNode of component.tileNodesForLineNumbers()) { - expect(tileNode.style.backgroundColor).toBe('rgb(255, 0, 0)') - } - }) - - it('hides or shows the gutter based on the "::isLineNumberGutterVisible" property on the model and the global "editor.showLineNumbers" config setting', function () { - expect(component.gutterContainerComponent.getLineNumberGutterComponent() != null).toBe(true) - editor.setLineNumberGutterVisible(false) - runAnimationFrames() - - expect(componentNode.querySelector('.gutter').style.display).toBe('none') - editor.update({showLineNumbers: false}) - runAnimationFrames() - - expect(componentNode.querySelector('.gutter').style.display).toBe('none') - editor.setLineNumberGutterVisible(true) - runAnimationFrames() - - expect(componentNode.querySelector('.gutter').style.display).toBe('none') - editor.update({showLineNumbers: true}) - runAnimationFrames() - - expect(componentNode.querySelector('.gutter').style.display).toBe('') - expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true) - }) - - it('keeps rebuilding line numbers when continuous reflow is on', function () { - wrapperNode.setContinuousReflow(true) - let oldLineNode = componentNode.querySelectorAll('.line-number')[1] - - while (true) { - runAnimationFrames() - if (componentNode.querySelectorAll('.line-number')[1] !== oldLineNode) break - } - }) - - describe('fold decorations', function () { - describe('rendering fold decorations', function () { - it('adds the foldable class to line numbers when the line is foldable', function () { - expect(lineNumberHasClass(0, 'foldable')).toBe(true) - expect(lineNumberHasClass(1, 'foldable')).toBe(true) - expect(lineNumberHasClass(2, 'foldable')).toBe(false) - expect(lineNumberHasClass(3, 'foldable')).toBe(false) - expect(lineNumberHasClass(4, 'foldable')).toBe(true) - expect(lineNumberHasClass(5, 'foldable')).toBe(false) - }) - - it('updates the foldable class on the correct line numbers when the foldable positions change', function () { - editor.getBuffer().insert([0, 0], '\n') - runAnimationFrames() - - expect(lineNumberHasClass(0, 'foldable')).toBe(false) - expect(lineNumberHasClass(1, 'foldable')).toBe(true) - expect(lineNumberHasClass(2, 'foldable')).toBe(true) - expect(lineNumberHasClass(3, 'foldable')).toBe(false) - expect(lineNumberHasClass(4, 'foldable')).toBe(false) - expect(lineNumberHasClass(5, 'foldable')).toBe(true) - expect(lineNumberHasClass(6, 'foldable')).toBe(false) - }) - - it('updates the foldable class on a line number that becomes foldable', function () { - expect(lineNumberHasClass(11, 'foldable')).toBe(false) - editor.getBuffer().insert([11, 44], '\n fold me') - runAnimationFrames() - expect(lineNumberHasClass(11, 'foldable')).toBe(true) - editor.undo() - runAnimationFrames() - expect(lineNumberHasClass(11, 'foldable')).toBe(false) - }) - - it('adds, updates and removes the folded class on the correct line number componentNodes', function () { - editor.foldBufferRow(4) - runAnimationFrames() - - expect(lineNumberHasClass(4, 'folded')).toBe(true) - - editor.getBuffer().insert([0, 0], '\n') - runAnimationFrames() - - expect(lineNumberHasClass(4, 'folded')).toBe(false) - expect(lineNumberHasClass(5, 'folded')).toBe(true) - - editor.unfoldBufferRow(5) - runAnimationFrames() - - expect(lineNumberHasClass(5, 'folded')).toBe(false) - }) - - describe('when soft wrapping is enabled', function () { - beforeEach(function () { - editor.setSoftWrapped(true) - runAnimationFrames() - componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - runAnimationFrames() - }) - - it('does not add the foldable class for soft-wrapped lines', 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', function () { - editor.foldBufferRange([[3, 19], [3, 21]]) - runAnimationFrames() - - expect(lineNumberHasClass(11, 'folded')).toBe(true) - expect(lineNumberHasClass(12, 'folded')).toBe(false) - }) - }) - }) - - describe('mouse interactions with fold indicators', function () { - let gutterNode - - function buildClickEvent (target) { - return buildMouseEvent('click', { - target: target - }) - } - - beforeEach(function () { - gutterNode = componentNode.querySelector('.gutter') - }) - - describe('when the component is destroyed', function () { - it('stops listening for folding events', function () { - let lineNumber, target - component.destroy() - lineNumber = component.lineNumberNodeForScreenRow(1) - target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) - }) - }) - - it('folds and unfolds the block represented by the fold indicator when clicked', function () { - expect(lineNumberHasClass(1, 'folded')).toBe(false) - - let lineNumber = component.lineNumberNodeForScreenRow(1) - let target = lineNumber.querySelector('.icon-right') - - target.dispatchEvent(buildClickEvent(target)) - - runAnimationFrames() - - expect(lineNumberHasClass(1, 'folded')).toBe(true) - lineNumber = component.lineNumberNodeForScreenRow(1) - target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) - - runAnimationFrames() - - expect(lineNumberHasClass(1, 'folded')).toBe(false) - }) - - it('unfolds all the free-form folds intersecting the buffer row when clicked', function () { - expect(lineNumberHasClass(3, 'foldable')).toBe(false) - - editor.foldBufferRange([[3, 4], [5, 4]]) - editor.foldBufferRange([[5, 5], [8, 10]]) - runAnimationFrames() - 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)) - runAnimationFrames() - 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() - runAnimationFrames() - editor.foldBufferRange([[3, 19], [3, 21]]) // fold starting on a soft-wrapped portion of the line - runAnimationFrames() - expect(lineNumberHasClass(11, 'folded')).toBe(true) - - lineNumber = component.lineNumberNodeForScreenRow(11) - target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) - runAnimationFrames() - 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)) - waits(100) - runs(function () { - expect(lineNumberHasClass(1, 'folded')).toBe(false) - }) - }) - }) - }) - }) - - describe('cursor rendering', function () { - it('renders the currently visible cursors', function () { - let cursor1 = editor.getLastCursor() - cursor1.setScreenPosition([0, 5], { - autoscroll: false - }) - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe(1) - expect(cursorNodes[0].offsetHeight).toBe(lineHeightInPixels) - expect(cursorNodes[0].offsetWidth).toBeCloseTo(charWidth, 0) - expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(5 * charWidth)) + 'px, ' + (0 * lineHeightInPixels) + 'px)') - let cursor2 = editor.addCursorAtScreenPosition([8, 11], { - autoscroll: false - }) - let cursor3 = editor.addCursorAtScreenPosition([4, 10], { - autoscroll: false - }) - runAnimationFrames() - - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe(2) - expect(cursorNodes[0].offsetTop).toBe(0) - expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(5 * charWidth)) + 'px, ' + (0 * lineHeightInPixels) + 'px)') - expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(10 * charWidth)) + 'px, ' + (4 * lineHeightInPixels) + 'px)') - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - horizontalScrollbarNode.scrollLeft = 3.5 * charWidth - horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe(2) - expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(10 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (4 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') - expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (8 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') - editor.onDidChangeCursorPosition(cursorMovedListener = jasmine.createSpy('cursorMovedListener')) - cursor3.setScreenPosition([4, 11], { - autoscroll: false - }) - runAnimationFrames() - - expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (4 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') - expect(cursorMovedListener).toHaveBeenCalled() - cursor3.destroy() - runAnimationFrames() - - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe(1) - expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (8 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') - }) - - it('accounts for character widths when positioning cursors', function () { - component.setFontFamily('sans-serif') - editor.setCursorScreenPosition([0, 16]) - runAnimationFrames() - - let cursor = componentNode.querySelector('.cursor') - let cursorRect = cursor.getBoundingClientRect() - let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.syntax--storage.syntax--type.syntax--function.syntax--js').firstChild - let range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - let rangeRect = range.getBoundingClientRect() - expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) - expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) - }) - - it('accounts for the width of paired characters when positioning cursors', function () { - component.setFontFamily('sans-serif') - editor.setText('he\u0301y') - editor.setCursorBufferPosition([0, 3]) - runAnimationFrames() - - let cursor = componentNode.querySelector('.cursor') - let cursorRect = cursor.getBoundingClientRect() - let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.syntax--source.syntax--js').childNodes[0] - let range = document.createRange(cursorLocationTextNode) - range.setStart(cursorLocationTextNode, 3) - range.setEnd(cursorLocationTextNode, 4) - let rangeRect = range.getBoundingClientRect() - expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) - expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) - }) - - it('positions cursors after the fold-marker when a fold ends the line', function () { - editor.foldBufferRow(0) - runAnimationFrames() - editor.setCursorScreenPosition([0, 30]) - runAnimationFrames() - - 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', function () { - component.setFontFamily('sans-serif') - editor.setCursorScreenPosition([0, 16]) - runAnimationFrames(true) - - atom.styles.addStyleSheet('.syntax--function.syntax--js {\n font-weight: bold;\n}', { - context: 'atom-text-editor' - }) - runAnimationFrames(true) - - let cursor = componentNode.querySelector('.cursor') - let cursorRect = cursor.getBoundingClientRect() - let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.syntax--storage.syntax--type.syntax--function.syntax--js').firstChild - let range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - let rangeRect = range.getBoundingClientRect() - expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) - expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) - atom.themes.removeStylesheet('test') - }) - - it('sets the cursor to the default character width at the end of a line', function () { - editor.setCursorScreenPosition([0, Infinity]) - runAnimationFrames() - let cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) - }) - - it('gives the cursor a non-zero width even if it\'s inside atomic tokens', function () { - editor.setCursorScreenPosition([1, 0]) - runAnimationFrames() - let cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) - }) - - it('blinks cursors when they are not moving', async function () { - let cursorsNode = componentNode.querySelector('.cursors') - wrapperNode.focus() - runAnimationFrames() - expect(cursorsNode.classList.contains('blink-off')).toBe(false) - advanceClock(component.cursorBlinkPeriod / 2) - runAnimationFrames() - expect(cursorsNode.classList.contains('blink-off')).toBe(true) - advanceClock(component.cursorBlinkPeriod / 2) - runAnimationFrames() - expect(cursorsNode.classList.contains('blink-off')).toBe(false) - editor.moveRight() - runAnimationFrames() - expect(cursorsNode.classList.contains('blink-off')).toBe(false) - advanceClock(component.cursorBlinkResumeDelay) - runAnimationFrames(true) - expect(cursorsNode.classList.contains('blink-off')).toBe(false) - advanceClock(component.cursorBlinkPeriod / 2) - runAnimationFrames() - expect(cursorsNode.classList.contains('blink-off')).toBe(true) - }) - - it('renders cursors that are associated with empty selections', function () { - editor.update({showCursorOnSelection: true}) - editor.setSelectedScreenRange([[0, 4], [4, 6]]) - editor.addCursorAtScreenPosition([6, 8]) - runAnimationFrames() - let cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe(2) - expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(6 * charWidth)) + 'px, ' + (4 * lineHeightInPixels) + 'px)') - expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(8 * charWidth)) + 'px, ' + (6 * lineHeightInPixels) + 'px)') - }) - - it('does not render cursors that are associated with non-empty selections when showCursorOnSelection is false', function () { - editor.update({showCursorOnSelection: false}) - editor.setSelectedScreenRange([[0, 4], [4, 6]]) - editor.addCursorAtScreenPosition([6, 8]) - runAnimationFrames() - let cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe(1) - expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(8 * charWidth)) + 'px, ' + (6 * lineHeightInPixels) + 'px)') - }) - - it('updates cursor positions when the line height changes', function () { - editor.setCursorBufferPosition([1, 10]) - component.setLineHeight(2) - runAnimationFrames() - let cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') - }) - - it('updates cursor positions when the font size changes', function () { - editor.setCursorBufferPosition([1, 10]) - component.setFontSize(10) - runAnimationFrames() - let cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') - }) - - it('updates cursor positions when the font family changes', function () { - editor.setCursorBufferPosition([1, 10]) - component.setFontFamily('sans-serif') - runAnimationFrames() - let cursorNode = componentNode.querySelector('.cursor') - let left = wrapperNode.pixelPositionForScreenPosition([1, 10]).left - expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(left)) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') - }) - }) - - describe('selection rendering', function () { - let scrollViewClientLeft, scrollViewNode - - beforeEach(function () { - scrollViewNode = componentNode.querySelector('.scroll-view') - scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left - }) - - it('renders 1 region for 1-line selections', function () { - editor.setSelectedScreenRange([[1, 6], [1, 10]]) - runAnimationFrames() - - let regions = componentNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(1) - - let regionRect = regions[0].getBoundingClientRect() - expect(regionRect.top).toBe(1 * lineHeightInPixels) - expect(regionRect.height).toBe(1 * lineHeightInPixels) - expect(regionRect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) - expect(regionRect.width).toBeCloseTo(4 * charWidth, 0) - }) - - it('renders 2 regions for 2-line selections', function () { - editor.setSelectedScreenRange([[1, 6], [2, 10]]) - runAnimationFrames() - - let tileNode = component.tileNodesForLines()[0] - let regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(2) - - let region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe(1 * lineHeightInPixels) - expect(region1Rect.height).toBe(1 * lineHeightInPixels) - expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) - expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) - - let region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe(2 * lineHeightInPixels) - expect(region2Rect.height).toBe(1 * lineHeightInPixels) - expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) - expect(region2Rect.width).toBeCloseTo(10 * charWidth, 0) - }) - - it('renders 3 regions per tile for selections with more than 2 lines', function () { - editor.setSelectedScreenRange([[0, 6], [5, 10]]) - runAnimationFrames() - - let region1Rect, region2Rect, region3Rect, regions, tileNode - tileNode = component.tileNodesForLines()[0] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(3) - - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe(0) - expect(region1Rect.height).toBe(1 * lineHeightInPixels) - expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) - expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) - - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe(1 * lineHeightInPixels) - expect(region2Rect.height).toBe(1 * lineHeightInPixels) - expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) - expect(region2Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) - - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe(2 * lineHeightInPixels) - expect(region3Rect.height).toBe(1 * lineHeightInPixels) - expect(region3Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) - expect(region3Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) - - tileNode = component.tileNodesForLines()[1] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(3) - - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe(3 * lineHeightInPixels) - expect(region1Rect.height).toBe(1 * lineHeightInPixels) - expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) - expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) - - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe(4 * lineHeightInPixels) - expect(region2Rect.height).toBe(1 * lineHeightInPixels) - expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) - expect(region2Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) - - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe(5 * lineHeightInPixels) - expect(region3Rect.height).toBe(1 * lineHeightInPixels) - expect(region3Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) - expect(region3Rect.width).toBeCloseTo(10 * charWidth, 0) - }) - - it('does not render empty selections', function () { - editor.addSelectionForBufferRange([[2, 2], [2, 2]]) - runAnimationFrames() - expect(editor.getSelections()[0].isEmpty()).toBe(true) - expect(editor.getSelections()[1].isEmpty()).toBe(true) - expect(componentNode.querySelectorAll('.selection').length).toBe(0) - }) - - it('updates selections when the line height changes', function () { - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - component.setLineHeight(2) - runAnimationFrames() - let selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) - }) - - it('updates selections when the font size changes', function () { - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - component.setFontSize(10) - - runAnimationFrames() - - let selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) - expect(selectionNode.offsetLeft).toBeCloseTo(6 * editor.getDefaultCharWidth(), 0) - }) - - it('updates selections when the font family changes', function () { - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - component.setFontFamily('sans-serif') - - runAnimationFrames() - - let selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) - expect(selectionNode.offsetLeft).toBeCloseTo(wrapperNode.pixelPositionForScreenPosition([1, 6]).left, 0) - }) - - it('will flash the selection when flash:true is passed to editor::setSelectedBufferRange', async function () { - editor.setSelectedBufferRange([[1, 6], [1, 10]], { - flash: true - }) - runAnimationFrames() - - let selectionNode = componentNode.querySelector('.selection') - expect(selectionNode.classList.contains('flash')).toBe(true) - - advanceClock(editor.selectionFlashDuration) - - editor.setSelectedBufferRange([[1, 5], [1, 7]], { - flash: true - }) - runAnimationFrames() - - expect(selectionNode.classList.contains('flash')).toBe(true) - }) - }) - - describe('line decoration rendering', async function () { - let decoration, marker - - beforeEach(async function () { - marker = editor.addMarkerLayer({ - maintainHistory: true - }).markBufferRange([[2, 13], [3, 15]], { - invalidate: 'inside' - }) - decoration = editor.decorateMarker(marker, { - type: ['line-number', 'line'], - 'class': 'a' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - }) - - it('applies line decoration classes to lines and line numbers', async function () { - expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let marker2 = editor.markBufferRange([[9, 0], [9, 0]]) - editor.decorateMarker(marker2, { - type: ['line-number', 'line'], - 'class': 'b' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - expect(lineAndLineNumberHaveClass(9, 'b')).toBe(true) - - editor.foldBufferRow(5) - runAnimationFrames() - - expect(lineAndLineNumberHaveClass(9, 'b')).toBe(false) - expect(lineAndLineNumberHaveClass(6, 'b')).toBe(true) - }) - - it('only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped', async function () { - editor.setText('a line that wraps, ok') - editor.setSoftWrapped(true) - componentNode.style.width = 16 * charWidth + 'px' - component.measureDimensions() - - runAnimationFrames() - marker.destroy() - marker = editor.markBufferRange([[0, 0], [0, 2]]) - editor.decorateMarker(marker, { - type: ['line-number', 'line'], - 'class': 'b' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineNumberHasClass(0, 'b')).toBe(true) - expect(lineNumberHasClass(1, 'b')).toBe(false) - marker.setBufferRange([[0, 0], [0, Infinity]]) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineNumberHasClass(0, 'b')).toBe(true) - expect(lineNumberHasClass(1, 'b')).toBe(true) - }) - - it('updates decorations when markers move', async function () { - expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) - - editor.getBuffer().insert([0, 0], '\n') - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(4, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(5, 'a')).toBe(false) - - marker.setBufferRange([[4, 4], [6, 4]]) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(3, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(4, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(5, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(6, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(7, 'a')).toBe(false) - }) - - it('remove decoration classes when decorations are removed', async function () { - decoration.destroy() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(lineNumberHasClass(1, 'a')).toBe(false) - expect(lineNumberHasClass(2, 'a')).toBe(false) - expect(lineNumberHasClass(3, 'a')).toBe(false) - expect(lineNumberHasClass(4, 'a')).toBe(false) - }) - - it('removes decorations when their marker is invalidated', async function () { - editor.getBuffer().insert([3, 2], 'n') - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(marker.isValid()).toBe(false) - expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(3, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) - editor.undo() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(marker.isValid()).toBe(true) - expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) - expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) - expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) - }) - - it('removes decorations when their marker is destroyed', async function () { - marker.destroy() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(lineNumberHasClass(1, 'a')).toBe(false) - expect(lineNumberHasClass(2, 'a')).toBe(false) - expect(lineNumberHasClass(3, 'a')).toBe(false) - expect(lineNumberHasClass(4, 'a')).toBe(false) - }) - - describe('when the decoration\'s "onlyHead" property is true', async function () { - it('only applies the decoration\'s class to lines containing the marker\'s head', async function () { - editor.decorateMarker(marker, { - type: ['line-number', 'line'], - 'class': 'only-head', - onlyHead: true - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe(false) - expect(lineAndLineNumberHaveClass(2, 'only-head')).toBe(false) - expect(lineAndLineNumberHaveClass(3, 'only-head')).toBe(true) - expect(lineAndLineNumberHaveClass(4, 'only-head')).toBe(false) - }) - }) - - describe('when the decoration\'s "onlyEmpty" property is true', function () { - it('only applies the decoration when its marker is empty', async function () { - editor.decorateMarker(marker, { - type: ['line-number', 'line'], - 'class': 'only-empty', - onlyEmpty: true - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) - expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(false) - - marker.clearTail() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) - expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(true) - }) - }) - - describe('when the decoration\'s "onlyNonEmpty" property is true', function () { - it('only applies the decoration when its marker is non-empty', async function () { - editor.decorateMarker(marker, { - type: ['line-number', 'line'], - 'class': 'only-non-empty', - onlyNonEmpty: true - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(true) - expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(true) - - marker.clearTail() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(false) - expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(false) - }) - }) - }) - - describe('block decorations rendering', function () { - let markerLayer - - function createBlockDecorationBeforeScreenRow(screenRow, {className}) { - let item = document.createElement("div") - item.className = className || "" - let blockDecoration = editor.decorateMarker( - markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}), - {type: "block", item: item, position: "before"} - ) - return [item, blockDecoration] - } - - function createBlockDecorationAfterScreenRow(screenRow, {className}) { - let item = document.createElement("div") - item.className = className || "" - let blockDecoration = editor.decorateMarker( - markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}), - {type: "block", item: item, position: "after"} - ) - return [item, blockDecoration] - } - - beforeEach(function () { - markerLayer = editor.addMarkerLayer() - wrapperNode.style.height = 5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - }) - - afterEach(function () { - atom.themes.removeStylesheet('test') - }) - - it("renders visible and yet-to-be-measured block decorations, inserting them between the appropriate lines and refreshing them as needed", async function () { - let [item1, blockDecoration1] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"}) - let [item2, blockDecoration2] = createBlockDecorationBeforeScreenRow(2, {className: "decoration-2"}) - let [item3, blockDecoration3] = createBlockDecorationBeforeScreenRow(4, {className: "decoration-3"}) - let [item4, blockDecoration4] = createBlockDecorationBeforeScreenRow(7, {className: "decoration-4"}) - let [item5, blockDecoration5] = createBlockDecorationAfterScreenRow(7, {className: "decoration-5"}) - let [item6, blockDecoration6] = createBlockDecorationAfterScreenRow(12, {className: "decoration-6"}) - - atom.styles.addStyleSheet( - `atom-text-editor .decoration-1 { width: 30px; height: 80px; } - atom-text-editor .decoration-2 { width: 30px; height: 40px; } - atom-text-editor .decoration-3 { width: 30px; height: 100px; } - atom-text-editor .decoration-4 { width: 30px; height: 120px; } - atom-text-editor .decoration-5 { width: 30px; height: 42px; } - atom-text-editor .decoration-6 { width: 30px; height: 22px; }`, - {context: 'atom-text-editor'} - ) - runAnimationFrames() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) - expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 80 + 40 + 100 + 120 + 42 + 22) - expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 80 + 40 + "px") - expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") - expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + "px") - expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") - expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBe(item1) - expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) - expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() - expect(item1.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 0) - expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 2 + 80) - expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 4 + 80 + 40) - - editor.setCursorScreenPosition([0, 0]) - editor.insertNewline() - blockDecoration1.destroy() - runAnimationFrames() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) - expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 40 + 100 + 120 + 42 + 22) - expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") - expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") - expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 40 + "px") - expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") - expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) - expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() - expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) - expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) - - atom.styles.addStyleSheet( - 'atom-text-editor .decoration-2 { height: 60px; }', - {context: 'atom-text-editor'} - ) - - runAnimationFrames() // causes the DOM to update and to retrieve new styles - runAnimationFrames() // applies the changes - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) - expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 60 + 100 + 120 + 42 + 22) - expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") - expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") - expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 60 + "px") - expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") - expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) - expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() - expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) - expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) - - item2.style.height = "20px" - wrapperNode.invalidateBlockDecorationDimensions(blockDecoration2) - runAnimationFrames() // causes the DOM to update and to retrieve new styles - runAnimationFrames() // applies the changes - expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) - expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 20 + 100 + 120 + 42 + 22) - expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") - expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") - expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 20 + "px") - expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") - expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) - expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) - expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBe(item5) - expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() - expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) - expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 20) - expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100) - expect(item5.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100 + 120 + lineHeightInPixels) - - item6.style.height = "33px" - wrapperNode.invalidateBlockDecorationDimensions(blockDecoration6) - runAnimationFrames() // causes the DOM to update and to retrieve new styles - runAnimationFrames() // applies the changes - expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) - expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 20 + 100 + 120 + 42 + 33) - expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") - expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") - expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 20 + "px") - expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") - expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() - expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) - expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) - expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) - expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBe(item5) - expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() - expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) - expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 20) - expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100) - expect(item5.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100 + 120 + lineHeightInPixels) - }) - - it("correctly sets screen rows on block decoration and ruler nodes, both initially and when decorations move", function () { - let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"}) - atom.styles.addStyleSheet( - 'atom-text-editor .decoration-1 { width: 30px; height: 80px; }', - {context: 'atom-text-editor'} - ) - - runAnimationFrames() - const line0 = component.lineNodeForScreenRow(0) - expect(item.previousSibling.dataset.screenRow).toBe("0") - expect(item.dataset.screenRow).toBe("0") - expect(item.nextSibling.dataset.screenRow).toBe("0") - expect(line0.previousSibling).toBe(item.nextSibling) - - editor.setCursorBufferPosition([0, 0]) - editor.insertNewline() - runAnimationFrames() - const line1 = component.lineNodeForScreenRow(1) - expect(item.previousSibling.dataset.screenRow).toBe("1") - expect(item.dataset.screenRow).toBe("1") - expect(item.nextSibling.dataset.screenRow).toBe("1") - expect(line1.previousSibling).toBe(item.nextSibling) - - editor.setCursorBufferPosition([0, 0]) - editor.insertNewline() - runAnimationFrames() - const line2 = component.lineNodeForScreenRow(2) - expect(item.previousSibling.dataset.screenRow).toBe("2") - expect(item.dataset.screenRow).toBe("2") - expect(item.nextSibling.dataset.screenRow).toBe("2") - expect(line2.previousSibling).toBe(item.nextSibling) - - blockDecoration.getMarker().setHeadBufferPosition([4, 0]) - runAnimationFrames() - const line4 = component.lineNodeForScreenRow(4) - expect(item.previousSibling.dataset.screenRow).toBe("4") - expect(item.dataset.screenRow).toBe("4") - expect(item.nextSibling.dataset.screenRow).toBe("4") - expect(line4.previousSibling).toBe(item.nextSibling) - }) - - it('measures block decorations taking into account both top and bottom margins of the element and its children', function () { - let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"}) - let child = document.createElement("div") - child.style.height = "7px" - child.style.width = "30px" - child.style.marginBottom = "20px" - item.appendChild(child) - atom.styles.addStyleSheet( - 'atom-text-editor .decoration-1 { width: 30px; margin-top: 10px; }', - {context: 'atom-text-editor'} - ) - - runAnimationFrames() // causes the DOM to update and to retrieve new styles - runAnimationFrames() // applies the changes - - expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 10 + 7 + 20 + "px") - expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") - expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") - expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) - expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") - expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - }) - - it('allows the same block decoration item to be moved from one tile to another in the same animation frame', function () { - let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(5, {className: "decoration-1"}) - runAnimationFrames() - expect(component.tileNodesForLines()[0].querySelector('.decoration-1')).toBeNull() - expect(component.tileNodesForLines()[1].querySelector('.decoration-1')).toBe(item) - - blockDecoration.getMarker().setHeadBufferPosition([0, 0]) - runAnimationFrames() - expect(component.tileNodesForLines()[0].querySelector('.decoration-1')).toBe(item) - expect(component.tileNodesForLines()[1].querySelector('.decoration-1')).toBeNull() - }) - }) - - describe('highlight decoration rendering', function () { - let decoration, marker, scrollViewClientLeft - - beforeEach(async function () { - scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left - marker = editor.addMarkerLayer({ - maintainHistory: true - }).markBufferRange([[2, 13], [3, 15]], { - invalidate: 'inside' - }) - decoration = editor.decorateMarker(marker, { - type: 'highlight', - 'class': 'test-highlight' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - }) - - it('does not render highlights for off-screen lines until they come on-screen', async function () { - wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - marker = editor.markBufferRange([[9, 2], [9, 4]], { - invalidate: 'inside' - }) - editor.decorateMarker(marker, { - type: 'highlight', - 'class': 'some-highlight' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(component.presenter.endRow).toBeLessThan(9) - let regions = componentNode.querySelectorAll('.some-highlight .region') - expect(regions.length).toBe(0) - verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - - expect(component.presenter.endRow).toBeGreaterThan(8) - regions = componentNode.querySelectorAll('.some-highlight .region') - expect(regions.length).toBe(1) - let regionRect = regions[0].style - expect(regionRect.top).toBe(0 + 'px') - expect(regionRect.height).toBe(1 * lineHeightInPixels + 'px') - expect(regionRect.left).toBe(Math.round(2 * charWidth) + 'px') - expect(regionRect.width).toBe(Math.round(2 * charWidth) + 'px') - }) - - it('renders highlights decoration\'s marker is added', function () { - let regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe(2) - }) - - it('removes highlights when a decoration is removed', async function () { - decoration.destroy() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - let regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe(0) - }) - - it('does not render a highlight that is within a fold', async function () { - editor.foldBufferRow(1) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(componentNode.querySelectorAll('.test-highlight').length).toBe(0) - }) - - it('removes highlights when a decoration\'s marker is destroyed', async function () { - marker.destroy() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - let regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe(0) - }) - - it('only renders highlights when a decoration\'s marker is valid', async function () { - editor.getBuffer().insert([3, 2], 'n') - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(marker.isValid()).toBe(false) - let regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe(0) - editor.getBuffer().undo() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(marker.isValid()).toBe(true) - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe(2) - }) - - it('allows multiple space-delimited decoration classes', async function () { - decoration.setProperties({ - type: 'highlight', - 'class': 'foo bar' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(componentNode.querySelectorAll('.foo.bar').length).toBe(2) - decoration.setProperties({ - type: 'highlight', - 'class': 'bar baz' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(componentNode.querySelectorAll('.bar.baz').length).toBe(2) - }) - - it('renders classes on the regions directly if "deprecatedRegionClass" option is defined', async function () { - decoration = editor.decorateMarker(marker, { - type: 'highlight', - 'class': 'test-highlight', - deprecatedRegionClass: 'test-highlight-region' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - let regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') - expect(regions.length).toBe(2) - }) - - describe('when flashing a decoration via Decoration::flash()', function () { - let highlightNode - - beforeEach(function () { - highlightNode = componentNode.querySelectorAll('.test-highlight')[1] - }) - - it('adds and removes the flash class specified in ::flash', async function () { - expect(highlightNode.classList.contains('flash-class')).toBe(false) - decoration.flash('flash-class', 10) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(highlightNode.classList.contains('flash-class')).toBe(true) - advanceClock(10) - expect(highlightNode.classList.contains('flash-class')).toBe(false) - }) - - describe('when ::flash is called again before the first has finished', function () { - it('removes the class from the decoration highlight before adding it for the second ::flash call', async function () { - decoration.flash('flash-class', 500) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(highlightNode.classList.contains('flash-class')).toBe(true) - - decoration.flash('flash-class', 500) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(highlightNode.classList.contains('flash-class')).toBe(false) - runAnimationFrames() - expect(highlightNode.classList.contains('flash-class')).toBe(true) - advanceClock(500) - expect(highlightNode.classList.contains('flash-class')).toBe(false) - }) - }) - }) - - describe('when a decoration\'s marker moves', function () { - it('moves rendered highlights when the buffer is changed', async function () { - let regionStyle = componentNode.querySelector('.test-highlight .region').style - let originalTop = parseInt(regionStyle.top) - expect(originalTop).toBe(2 * lineHeightInPixels) - - editor.getBuffer().insert([0, 0], '\n') - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - regionStyle = componentNode.querySelector('.test-highlight .region').style - let newTop = parseInt(regionStyle.top) - expect(newTop).toBe(0) - }) - - it('moves rendered highlights when the marker is manually moved', async function () { - let regionStyle = componentNode.querySelector('.test-highlight .region').style - expect(parseInt(regionStyle.top)).toBe(2 * lineHeightInPixels) - - marker.setBufferRange([[5, 8], [5, 13]]) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - regionStyle = componentNode.querySelector('.test-highlight .region').style - expect(parseInt(regionStyle.top)).toBe(2 * lineHeightInPixels) - }) - }) - - describe('when a decoration is updated via Decoration::update', function () { - it('renders the decoration\'s new params', async function () { - expect(componentNode.querySelector('.test-highlight')).toBeTruthy() - decoration.setProperties({ - type: 'highlight', - 'class': 'new-test-highlight' - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - expect(componentNode.querySelector('.test-highlight')).toBeFalsy() - expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() - }) - }) - }) - - describe('overlay decoration rendering', function () { - let gutterWidth, item - - beforeEach(function () { - item = document.createElement('div') - item.classList.add('overlay-test') - item.style.background = 'red' - gutterWidth = componentNode.querySelector('.gutter').offsetWidth - }) - - 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.markBufferRange([[2, 13], [2, 13]], { - invalidate: 'never' - }) - let decoration = editor.decorateMarker(marker, { - type: 'overlay', - item: item - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - let overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') - expect(overlay).toBe(item) - - decoration.destroy() - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') - expect(overlay).toBe(null) - }) - - it('renders the overlay element with the CSS class specified by the decoration', async function () { - let marker = editor.markBufferRange([[2, 13], [2, 13]], { - invalidate: 'never' - }) - let decoration = editor.decorateMarker(marker, { - type: 'overlay', - 'class': 'my-overlay', - item: item - }) - - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - let overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') - expect(overlay).not.toBe(null) - let child = overlay.querySelector('.overlay-test') - expect(child).toBe(item) - }) - }) - - describe('when the marker is not empty', function () { - it('renders at the head of the marker by default', async function () { - let marker = editor.markBufferRange([[2, 5], [2, 10]], { - invalidate: 'never' - }) - let decoration = editor.decorateMarker(marker, { - type: 'overlay', - item: item - }) - - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - let position = wrapperNode.pixelPositionForBufferPosition([2, 10]) - let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') - expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') - }) - }) - - describe('positioning the overlay when near the edge of the editor', function () { - let itemHeight, itemWidth, windowHeight, windowWidth - - beforeEach(async function () { - atom.storeWindowDimensions() - itemWidth = Math.round(4 * editor.getDefaultCharWidth()) - itemHeight = 4 * editor.getLineHeightInPixels() - windowWidth = Math.round(gutterWidth + 30 * editor.getDefaultCharWidth()) - windowHeight = 10 * editor.getLineHeightInPixels() - item.style.width = itemWidth + 'px' - item.style.height = itemHeight + 'px' - wrapperNode.style.width = windowWidth + 'px' - wrapperNode.style.height = windowHeight + 'px' - editor.update({autoHeight: false}) - await atom.setWindowDimensions({ - width: windowWidth, - height: windowHeight - }) - - component.measureDimensions() - component.measureWindowSize() - runAnimationFrames() - }) - - afterEach(function () { - atom.restoreWindowDimensions() - }) - - it('slides horizontally left when near the right edge on #win32 and #darwin', async function () { - let marker = editor.markBufferRange([[0, 26], [0, 26]], { - invalidate: 'never' - }) - let decoration = editor.decorateMarker(marker, { - type: 'overlay', - item: item - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - let position = wrapperNode.pixelPositionForBufferPosition([0, 26]) - let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - if (process.platform == 'darwin') { // Result is 359px on win32, expects 375px - expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') - } - expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') - - editor.insertText('a') - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(overlay.style.left).toBe(window.innerWidth - itemWidth + 'px') - expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') - - editor.insertText('b') - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(overlay.style.left).toBe(window.innerWidth - itemWidth + 'px') - expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') - - // window size change - const innerWidthBefore = window.innerWidth - await atom.setWindowDimensions({ - width: Math.round(gutterWidth + 20 * editor.getDefaultCharWidth()), - height: windowHeight, - }) - // wait for window to resize :( - await conditionPromise(() => { - return window.innerWidth !== innerWidthBefore - }) - - atom.views.performDocumentPoll() - runAnimationFrames() - - expect(overlay.style.left).toBe(window.innerWidth - itemWidth + 'px') - expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') - }) - }) - }) - - describe('hidden input field', function () { - it('renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused', async function () { - editor.setVerticalScrollMargin(0) - editor.setHorizontalScrollMargin(0) - let inputNode = componentNode.querySelector('.hidden-input') - wrapperNode.style.height = 5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(editor.getCursorScreenPosition()).toEqual([0, 0]) - - wrapperNode.setScrollTop(3 * lineHeightInPixels) - wrapperNode.setScrollLeft(3 * charWidth) - runAnimationFrames() - - expect(inputNode.offsetTop).toBe(0) - expect(inputNode.offsetLeft).toBe(0) - - editor.setCursorBufferPosition([5, 4], { - autoscroll: false - }) - await decorationsUpdatedPromise(editor) - runAnimationFrames() - - expect(inputNode.offsetTop).toBe(0) - expect(inputNode.offsetLeft).toBe(0) - - wrapperNode.focus() - runAnimationFrames() - - expect(inputNode.offsetTop).toBe((5 * lineHeightInPixels) - wrapperNode.getScrollTop()) - expect(inputNode.offsetLeft).toBeCloseTo((4 * charWidth) - wrapperNode.getScrollLeft(), 0) - - inputNode.blur() - runAnimationFrames() - - expect(inputNode.offsetTop).toBe(0) - expect(inputNode.offsetLeft).toBe(0) - - editor.setCursorBufferPosition([1, 2], { - autoscroll: false - }) - runAnimationFrames() - - expect(inputNode.offsetTop).toBe(0) - expect(inputNode.offsetLeft).toBe(0) - - inputNode.focus() - runAnimationFrames() - - expect(inputNode.offsetTop).toBe(0) - expect(inputNode.offsetLeft).toBe(0) - }) - }) - - describe('mouse interactions on the lines', function () { - let linesNode - - beforeEach(function () { - linesNode = componentNode.querySelector('.lines') - }) - - describe('when the mouse is single-clicked above the first line', function () { - it('moves the cursor to the start of file buffer position', function () { - let height - editor.setText('foo') - editor.setCursorBufferPosition([0, 3]) - height = 4.5 * lineHeightInPixels - wrapperNode.style.height = height + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let coordinates = clientCoordinatesForScreenPosition([0, 2]) - coordinates.clientY = -1 - linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - - runAnimationFrames() - expect(editor.getCursorScreenPosition()).toEqual([0, 0]) - }) - }) - - describe('when the mouse is single-clicked below the last line', function () { - it('moves the cursor to the end of file buffer position', function () { - editor.setText('foo') - editor.setCursorBufferPosition([0, 0]) - let height = 4.5 * lineHeightInPixels - wrapperNode.style.height = height + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let coordinates = clientCoordinatesForScreenPosition([0, 2]) - coordinates.clientY = height * 2 - - linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - runAnimationFrames() - - expect(editor.getCursorScreenPosition()).toEqual([0, 3]) - }) - }) - - describe('when a non-folded line is single-clicked', function () { - describe('when no modifier keys are held down', function () { - it('moves the cursor to the nearest screen position', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - wrapperNode.setScrollTop(3.5 * lineHeightInPixels) - wrapperNode.setScrollLeft(2 * charWidth) - runAnimationFrames() - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) - runAnimationFrames() - expect(editor.getCursorScreenPosition()).toEqual([4, 8]) - }) - }) - - describe('when the shift key is held down', function () { - it('selects to the nearest screen position', function () { - editor.setCursorScreenPosition([3, 4]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { - shiftKey: true - })) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [5, 6]]) - }) - }) - - describe('when the command key is held down', function () { - describe('the current cursor position and screen position do not match', function () { - it('adds a cursor at the nearest screen position', function () { - editor.setCursorScreenPosition([3, 4]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { - metaKey: true - })) - runAnimationFrames() - expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]], [[5, 6], [5, 6]]]) - }) - }) - - describe('when there are multiple cursors, and one of the cursor\'s screen position is the same as the mouse click screen position', function () { - it('removes a cursor at the mouse screen position', function () { - editor.setCursorScreenPosition([3, 4]) - editor.addCursorAtScreenPosition([5, 2]) - editor.addCursorAtScreenPosition([7, 5]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { - metaKey: true - })) - runAnimationFrames() - expect(editor.getSelectedScreenRanges()).toEqual([[[5, 2], [5, 2]], [[7, 5], [7, 5]]]) - }) - }) - - describe('when there is a single cursor and the click occurs at the cursor\'s screen position', function () { - it('neither adds a new cursor nor removes the current cursor', function () { - editor.setCursorScreenPosition([3, 4]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { - metaKey: true - })) - runAnimationFrames() - expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]]]) - }) - }) - }) - }) - - describe('when a non-folded line is double-clicked', function () { - describe('when no modifier keys are held down', function () { - it('selects the word containing the nearest screen position', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 2 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [5, 13]]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), { - detail: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual([[6, 6], [6, 6]]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), { - detail: 1, - shiftKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual([[6, 6], [8, 8]]) - }) - }) - - describe('when the command key is held down', function () { - it('selects the word containing the newly-added cursor', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 1, - metaKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 2, - metaKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [0, 0]], [[5, 6], [5, 13]]]) - }) - }) - }) - - describe('when a non-folded line is triple-clicked', function () { - describe('when no modifier keys are held down', function () { - it('selects the line containing the nearest screen position', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 2 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 3 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), { - detail: 1, - shiftKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [7, 0]]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([7, 5]), { - detail: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), { - detail: 1, - shiftKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual([[7, 5], [8, 8]]) - }) - }) - - describe('when the command key is held down', function () { - it('selects the line containing the newly-added cursor', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 1, - metaKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 2, - metaKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 3, - metaKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [0, 0]], [[5, 0], [6, 0]]]) - }) - }) - }) - - describe('when the mouse is clicked and dragged', function () { - it('selects to the nearest screen position until the mouse button is released', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { - which: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { - which: 1 - })) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), { - which: 1 - })) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]]) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), { - which: 1 - })) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]]) - }) - - it('autoscrolls when the cursor approaches the boundaries of the editor', function () { - wrapperNode.style.height = '100px' - wrapperNode.style.width = '100px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBe(0) - - linesNode.dispatchEvent(buildMouseEvent('mousedown', { - clientX: 0, - clientY: 0 - }, { - which: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mousemove', { - clientX: 100, - clientY: 50 - }, { - which: 1 - })) - - for (let i = 0; i <= 5; ++i) { - runAnimationFrames() - } - - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) - linesNode.dispatchEvent(buildMouseEvent('mousemove', { - clientX: 100, - clientY: 100 - }, { - which: 1 - })) - - for (let i = 0; i <= 5; ++i) { - runAnimationFrames() - } - - expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) - let previousScrollTop = wrapperNode.getScrollTop() - let previousScrollLeft = wrapperNode.getScrollLeft() - - linesNode.dispatchEvent(buildMouseEvent('mousemove', { - clientX: 10, - clientY: 50 - }, { - which: 1 - })) - - for (let i = 0; i <= 5; ++i) { - runAnimationFrames() - } - - expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) - expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) - linesNode.dispatchEvent(buildMouseEvent('mousemove', { - clientX: 10, - clientY: 10 - }, { - which: 1 - })) - - for (let i = 0; i <= 5; ++i) { - runAnimationFrames() - } - - expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) - }) - - it('stops selecting if the mouse is dragged into the dev tools', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { - which: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { - which: 1 - })) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), { - which: 0 - })) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { - which: 1 - })) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) - }) - - it('stops selecting before the buffer is modified during the drag', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { - which: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) - - editor.insertText('x') - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[2, 5], [2, 5]]) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { - which: 1 - })) - expect(editor.getSelectedScreenRange()).toEqual([[2, 5], [2, 5]]) - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { - which: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [5, 4]]) - - editor.delete() - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [2, 4]]) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { - which: 1 - })) - expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [2, 4]]) - }) - - describe('when the command key is held down', function () { - it('adds a new selection and selects to the nearest screen position, then merges intersecting selections when the mouse button is released', function () { - editor.setSelectedScreenRange([[4, 4], [4, 9]]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { - which: 1, - metaKey: true - })) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRanges()).toEqual([[[4, 4], [4, 9]], [[2, 4], [6, 8]]]) - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRanges()).toEqual([[[4, 4], [4, 9]], [[2, 4], [4, 6]]]) - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([4, 6]), { - which: 1 - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[2, 4], [4, 9]]]) - }) - }) - - describe('when the editor is destroyed while dragging', function () { - it('cleans up the handlers for window.mouseup and window.mousemove', function () { - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { - which: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { - which: 1 - })) - runAnimationFrames() - - spyOn(window, 'removeEventListener').andCallThrough() - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), { - which: 1 - })) - - editor.destroy() - runAnimationFrames() - - for (let call of window.removeEventListener.calls) { - call.args.pop() - } - expect(window.removeEventListener).toHaveBeenCalledWith('mouseup') - expect(window.removeEventListener).toHaveBeenCalledWith('mousemove') - }) - }) - }) - - describe('when the mouse is double-clicked and dragged', function () { - it('expands the selection over the nearest word as the cursor moves', function () { - jasmine.attachToDOM(wrapperNode) - wrapperNode.style.height = 6 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 2 - })) - expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [5, 13]]) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [12, 2]]) - let maximalScrollTop = wrapperNode.getScrollTop() - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [9, 4]]) - expect(wrapperNode.getScrollTop()).toBe(maximalScrollTop) - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), { - which: 1 - })) - }) - }) - - describe('when the mouse is triple-clicked and dragged', function () { - it('expands the selection over the nearest line as the cursor moves', function () { - jasmine.attachToDOM(wrapperNode) - wrapperNode.style.height = 6 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 1 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 2 - })) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { - detail: 3 - })) - expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [12, 2]]) - let maximalScrollTop = wrapperNode.getScrollTop() - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), { - which: 1 - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [8, 0]]) - expect(wrapperNode.getScrollTop()).toBe(maximalScrollTop) - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), { - which: 1 - })) - }) - }) - - 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', function () { - editor.foldBufferRange([[4, 6], [4, 10]]) - editor.foldBufferRange([[4, 15], [4, 20]]) - runAnimationFrames() - - let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') - expect(foldMarkers.length).toBe(2) - expect(editor.isFoldedAtBufferRow(4)).toBe(true) - - clickElementAtPosition(foldMarkers[0], [4, 6]) - runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') - expect(foldMarkers.length).toBe(1) - expect(editor.isFoldedAtBufferRow(4)).toBe(true) - - clickElementAtPosition(foldMarkers[0], [4, 15]) - runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') - expect(foldMarkers.length).toBe(0) - expect(editor.isFoldedAtBufferRow(4)).toBe(false) - }) - - it('unfolds only the selected fold when other folds are inside it', function () { - editor.foldBufferRange([[4, 10], [4, 15]]) - editor.foldBufferRange([[4, 4], [4, 5]]) - editor.foldBufferRange([[4, 4], [4, 20]]) - runAnimationFrames() - let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') - expect(foldMarkers.length).toBe(1) - expect(editor.isFoldedAtBufferRow(4)).toBe(true) - - clickElementAtPosition(foldMarkers[0], [4, 4]) - runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') - expect(foldMarkers.length).toBe(1) - expect(editor.isFoldedAtBufferRow(4)).toBe(true) - - clickElementAtPosition(foldMarkers[0], [4, 4]) - runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') - expect(foldMarkers.length).toBe(1) - expect(editor.isFoldedAtBufferRow(4)).toBe(true) - - clickElementAtPosition(foldMarkers[0], [4, 10]) - runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') - expect(foldMarkers.length).toBe(0) - expect(editor.isFoldedAtBufferRow(4)).toBe(false) - }) - }) - - describe('when the horizontal scrollbar is interacted with', function () { - it('clicking on the scrollbar does not move the cursor', function () { - let target = horizontalScrollbarNode - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), { - target: target - })) - expect(editor.getCursorScreenPosition()).toEqual([0, 0]) - }) - }) - }) - - describe('mouse interactions on the gutter', function () { - let gutterNode - - beforeEach(function () { - gutterNode = componentNode.querySelector('.gutter') - }) - - describe('when the component is destroyed', function () { - it('stops listening for selection events', function () { - component.destroy() - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) - expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [0, 0]]) - }) - }) - - describe('when the gutter is clicked', function () { - it('selects the clicked row', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) - expect(editor.getSelectedScreenRange()).toEqual([[4, 0], [5, 0]]) - }) - }) - - describe('when the gutter is meta-clicked', function () { - it('creates a new selection for the clicked row', function () { - editor.setSelectedScreenRange([[3, 0], [3, 2]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [5, 0]]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [5, 0]], [[6, 0], [7, 0]]]) - }) - }) - - describe('when the gutter is shift-clicked', function () { - beforeEach(function () { - editor.setSelectedScreenRange([[3, 4], [4, 5]]) - }) - - describe('when the clicked row is before the current selection\'s tail', function () { - it('selects to the beginning of the clicked row', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { - shiftKey: true - })) - expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 4]]) - }) - }) - - describe('when the clicked row is after the current selection\'s tail', function () { - it('selects to the beginning of the row following the clicked row', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { - shiftKey: true - })) - expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [7, 0]]) - }) - }) - }) - - describe('when the gutter is clicked and dragged', function () { - describe('when dragging downward', function () { - it('selects the rows between the start and end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) - expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) - }) - }) - - describe('when dragging upward', function () { - it('selects the rows between the start and end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) - expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) - }) - }) - - it('orients the selection appropriately when the mouse moves above or below the initially-clicked row', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - runAnimationFrames() - expect(editor.getLastSelection().isReversed()).toBe(true) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - runAnimationFrames() - expect(editor.getLastSelection().isReversed()).toBe(false) - }) - - it('autoscrolls when the cursor approaches the top or bottom of the editor', function () { - wrapperNode.style.height = 6 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) - let maxScrollTop = wrapperNode.getScrollTop() - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(maxScrollTop) - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBeLessThan(maxScrollTop) - }) - - it('stops selecting if a textInput event occurs during the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) - - let inputEvent = new Event('textInput') - inputEvent.data = 'x' - Object.defineProperty(inputEvent, 'target', { - get: function () { - return componentNode.querySelector('.hidden-input') - } - }) - componentNode.dispatchEvent(inputEvent) - runAnimationFrames() - - expect(editor.getSelectedScreenRange()).toEqual([[2, 1], [2, 1]]) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12))) - expect(editor.getSelectedScreenRange()).toEqual([[2, 1], [2, 1]]) - }) - }) - - describe('when the gutter is meta-clicked and dragged', function () { - beforeEach(function () { - editor.setSelectedScreenRange([[3, 0], [3, 2]]) - }) - - describe('when dragging downward', function () { - it('selects the rows between the start and end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), { - metaKey: true - })) - runAnimationFrames() - - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [7, 0]]]) - }) - - it('merges overlapping selections when the mouse button is released', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), { - metaKey: true - })) - runAnimationFrames() - - expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[2, 0], [7, 0]]]) - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [7, 0]]]) - }) - }) - - describe('when dragging upward', function () { - it('selects the rows between the start and end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), { - metaKey: true - })) - runAnimationFrames() - - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [7, 0]]]) - }) - - it('merges overlapping selections', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), { - metaKey: true - })) - runAnimationFrames() - - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [7, 0]]]) - }) - }) - }) - - describe('when the gutter is shift-clicked and dragged', function () { - describe('when the shift-click is below the existing selection\'s tail', function () { - describe('when dragging downward', function () { - it('selects the rows between the existing selection\'s tail and the end of the drag', function () { - editor.setSelectedScreenRange([[3, 4], [4, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [9, 0]]) - }) - }) - - describe('when dragging upward', function () { - it('selects the rows between the end of the drag and the tail of the existing selection', function () { - editor.setSelectedScreenRange([[4, 4], [5, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[4, 4], [6, 0]]) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [4, 4]]) - }) - }) - }) - - describe('when the shift-click is above the existing selection\'s tail', function () { - describe('when dragging upward', function () { - it('selects the rows between the end of the drag and the tail of the existing selection', function () { - editor.setSelectedScreenRange([[4, 4], [5, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [4, 4]]) - }) - }) - - describe('when dragging downward', function () { - it('selects the rows between the existing selection\'s tail and the end of the drag', function () { - editor.setSelectedScreenRange([[3, 4], [4, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [3, 4]]) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [9, 0]]) - }) - }) - }) - }) - - describe('when soft wrap is enabled', function () { - beforeEach(function () { - gutterNode = componentNode.querySelector('.gutter') - editor.setSoftWrapped(true) - runAnimationFrames() - componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - runAnimationFrames() - }) - - describe('when the gutter is clicked', function () { - it('selects the clicked buffer row', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) - expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [2, 0]]) - }) - }) - - describe('when the gutter is meta-clicked', function () { - it('creates a new selection for the clicked buffer row', function () { - editor.setSelectedScreenRange([[1, 0], [1, 2]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [1, 2]], [[2, 0], [5, 0]]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [1, 2]], [[2, 0], [5, 0]], [[5, 0], [10, 0]]]) - }) - }) - - describe('when the gutter is shift-clicked', function () { - beforeEach(function () { - return editor.setSelectedScreenRange([[7, 4], [7, 6]]) - }) - - describe('when the clicked row is before the current selection\'s tail', function () { - it('selects to the beginning of the clicked buffer row', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { - shiftKey: true - })) - expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [7, 4]]) - }) - }) - - describe('when the clicked row is after the current selection\'s tail', function () { - it('selects to the beginning of the screen row following the clicked buffer row', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), { - shiftKey: true - })) - expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [17, 0]]) - }) - }) - }) - - describe('when the gutter is clicked and dragged', function () { - describe('when dragging downward', function () { - it('selects the buffer row containing the click, then screen rows until the end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) - expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [6, 14]]) - }) - }) - - describe('when dragging upward', function () { - it('selects the buffer row containing the click, then screen rows until the end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1))) - expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [10, 0]]) - }) - }) - }) - - describe('when the gutter is meta-clicked and dragged', function () { - beforeEach(function () { - editor.setSelectedScreenRange([[7, 4], [7, 6]]) - }) - - describe('when dragging downward', function () { - it('adds a selection from the buffer row containing the click to the screen row containing the end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), { - metaKey: true - })) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(3), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[0, 0], [3, 14]]]) - }) - - it('merges overlapping selections on mouseup', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), { - metaKey: true - })) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [7, 12]]]) - }) - }) - - describe('when dragging upward', function () { - it('adds a selection from the buffer row containing the click to the screen row containing the end of the drag', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), { - metaKey: true - })) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [20, 0]]]) - }) - - it('merges overlapping selections on mouseup', function () { - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), { - metaKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), { - metaKey: true - })) - runAnimationFrames() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), { - metaKey: true - })) - expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [20, 0]]]) - }) - }) - }) - - describe('when the gutter is shift-clicked and dragged', function () { - describe('when the shift-click is below the existing selection\'s tail', function () { - describe('when dragging downward', function () { - it('selects the screen rows between the existing selection\'s tail and the end of the drag', function () { - editor.setSelectedScreenRange([[1, 4], [1, 7]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 5]]) - }) - }) - - describe('when dragging upward', function () { - it('selects the screen rows between the end of the drag and the tail of the existing selection', function () { - editor.setSelectedScreenRange([[1, 4], [1, 7]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [7, 12]]) - }) - }) - }) - - describe('when the shift-click is above the existing selection\'s tail', function () { - describe('when dragging upward', function () { - it('selects the screen rows between the end of the drag and the tail of the existing selection', function () { - editor.setSelectedScreenRange([[7, 4], [7, 6]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [7, 4]]) - }) - }) - - describe('when dragging downward', function () { - it('selects the screen rows between the existing selection\'s tail and the end of the drag', function () { - editor.setSelectedScreenRange([[7, 4], [7, 6]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { - shiftKey: true - })) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3))) - runAnimationFrames() - expect(editor.getSelectedScreenRange()).toEqual([[3, 2], [7, 4]]) - }) - }) - }) - }) - }) - }) - - describe('focus handling', function () { - let inputNode - beforeEach(function () { - inputNode = componentNode.querySelector('.hidden-input') - }) - - it('transfers focus to the hidden input', function () { - expect(document.activeElement).toBe(document.body) - wrapperNode.focus() - expect(document.activeElement).toBe(inputNode) - }) - - it('adds the "is-focused" class to the editor when the hidden input is focused', function () { - expect(document.activeElement).toBe(document.body) - inputNode.focus() - runAnimationFrames() - - expect(componentNode.classList.contains('is-focused')).toBe(true) - expect(wrapperNode.classList.contains('is-focused')).toBe(true) - inputNode.blur() - runAnimationFrames() - - expect(componentNode.classList.contains('is-focused')).toBe(false) - expect(wrapperNode.classList.contains('is-focused')).toBe(false) - }) - }) - - describe('selection handling', function () { - let cursor - - beforeEach(function () { - editor.setCursorScreenPosition([0, 0]) - runAnimationFrames() - }) - - it('adds the "has-selection" class to the editor when there is a selection', function () { - expect(componentNode.classList.contains('has-selection')).toBe(false) - editor.selectDown() - runAnimationFrames() - expect(componentNode.classList.contains('has-selection')).toBe(true) - editor.moveDown() - runAnimationFrames() - expect(componentNode.classList.contains('has-selection')).toBe(false) - }) - }) - - describe('scrolling', function () { - it('updates the vertical scrollbar when the scrollTop is changed in the model', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - expect(verticalScrollbarNode.scrollTop).toBe(0) - wrapperNode.setScrollTop(10) - runAnimationFrames() - expect(verticalScrollbarNode.scrollTop).toBe(10) - }) - - it('updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model', function () { - componentNode.style.width = 30 * charWidth + 'px' - component.measureDimensions() - runAnimationFrames() - - let top = 0 - let tilesNodes = component.tileNodesForLines() - for (let tileNode of tilesNodes) { - expect(tileNode.style['-webkit-transform']).toBe('translate3d(0px, ' + top + 'px, 0px)') - top += tileNode.offsetHeight - } - expect(horizontalScrollbarNode.scrollLeft).toBe(0) - wrapperNode.setScrollLeft(100) - - runAnimationFrames() - - top = 0 - for (let tileNode of tilesNodes) { - expect(tileNode.style['-webkit-transform']).toBe('translate3d(-100px, ' + top + 'px, 0px)') - top += tileNode.offsetHeight - } - expect(horizontalScrollbarNode.scrollLeft).toBe(100) - }) - - it('updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes', function () { - componentNode.style.width = 30 * charWidth + 'px' - component.measureDimensions() - runAnimationFrames() - expect(wrapperNode.getScrollLeft()).toBe(0) - horizontalScrollbarNode.scrollLeft = 100 - horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - runAnimationFrames(true) - expect(wrapperNode.getScrollLeft()).toBe(100) - }) - - it('does not obscure the last line with the horizontal scrollbar', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - runAnimationFrames() - - let lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) - let bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom - topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top - expect(bottomOfLastLine).toBe(topOfHorizontalScrollbar) - wrapperNode.style.width = 100 * charWidth + 'px' - component.measureDimensions() - runAnimationFrames() - - bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom - let bottomOfEditor = componentNode.getBoundingClientRect().bottom - expect(bottomOfLastLine).toBe(bottomOfEditor) - }) - - it('does not obscure the last character of the longest line with the vertical scrollbar', function () { - wrapperNode.style.height = 7 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - wrapperNode.setScrollLeft(Infinity) - - runAnimationFrames() - let rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right - let leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left - expect(Math.round(rightOfLongestLine)).toBeCloseTo(leftOfVerticalScrollbar - 1, 0) - }) - - it('only displays dummy scrollbars when scrollable in that direction', function () { - expect(verticalScrollbarNode.style.display).toBe('none') - expect(horizontalScrollbarNode.style.display).toBe('none') - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = '1000px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(verticalScrollbarNode.style.display).toBe('') - expect(horizontalScrollbarNode.style.display).toBe('none') - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - runAnimationFrames() - - expect(verticalScrollbarNode.style.display).toBe('') - expect(horizontalScrollbarNode.style.display).toBe('') - wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(verticalScrollbarNode.style.display).toBe('none') - expect(horizontalScrollbarNode.style.display).toBe('') - }) - - it('makes the dummy scrollbar divs only as tall/wide as the actual scrollbars', function () { - wrapperNode.style.height = 4 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - atom.styles.addStyleSheet('::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}', { - context: 'atom-text-editor' - }) - - runAnimationFrames() - runAnimationFrames() - - let scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') - expect(verticalScrollbarNode.offsetWidth).toBe(8) - expect(horizontalScrollbarNode.offsetHeight).toBe(8) - expect(scrollbarCornerNode.offsetWidth).toBe(8) - expect(scrollbarCornerNode.offsetHeight).toBe(8) - atom.themes.removeStylesheet('test') - }) - - it('assigns the bottom/right of the scrollbars to the width of the opposite scrollbar if it is visible', function () { - let scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') - expect(verticalScrollbarNode.style.bottom).toBe('0px') - expect(horizontalScrollbarNode.style.right).toBe('0px') - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = '1000px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(verticalScrollbarNode.style.bottom).toBe('0px') - expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') - expect(scrollbarCornerNode.style.display).toBe('none') - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - runAnimationFrames() - - expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') - expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') - expect(scrollbarCornerNode.style.display).toBe('') - wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') - expect(horizontalScrollbarNode.style.right).toBe('0px') - expect(scrollbarCornerNode.style.display).toBe('none') - }) - - it('accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar', function () { - let gutterNode = componentNode.querySelector('.gutter') - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - runAnimationFrames() - - expect(horizontalScrollbarNode.scrollWidth).toBe(wrapperNode.getScrollWidth()) - expect(horizontalScrollbarNode.style.left).toBe('0px') - }) - }) - - describe('mousewheel events', function () { - beforeEach(function () { - editor.update({scrollSensitivity: 100}) - }) - - describe('updating scrollTop and scrollLeft', function () { - beforeEach(function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - }) - - it('updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)', function () { - expect(verticalScrollbarNode.scrollTop).toBe(0) - expect(horizontalScrollbarNode.scrollLeft).toBe(0) - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: -5, - wheelDeltaY: -10 - })) - runAnimationFrames() - - expect(verticalScrollbarNode.scrollTop).toBe(10) - expect(horizontalScrollbarNode.scrollLeft).toBe(0) - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: -15, - wheelDeltaY: -5 - })) - runAnimationFrames() - - expect(verticalScrollbarNode.scrollTop).toBe(10) - expect(horizontalScrollbarNode.scrollLeft).toBe(15) - }) - - it('updates the scrollLeft or scrollTop according to the scroll sensitivity', function () { - editor.update({scrollSensitivity: 50}) - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: -5, - wheelDeltaY: -10 - })) - runAnimationFrames() - - expect(horizontalScrollbarNode.scrollLeft).toBe(0) - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: -15, - wheelDeltaY: -5 - })) - runAnimationFrames() - - expect(verticalScrollbarNode.scrollTop).toBe(5) - expect(horizontalScrollbarNode.scrollLeft).toBe(7) - }) - }) - - describe('when the mousewheel event\'s target is a line', function () { - it('keeps the line on the DOM if it is scrolled off-screen', function () { - component.presenter.stoppedScrollingDelay = 3000 // account for slower build machines - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let lineNode = componentNode.querySelector('.line') - let wheelEvent = new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: -500 - }) - Object.defineProperty(wheelEvent, 'target', { - get: function () { - return lineNode - } - }) - componentNode.dispatchEvent(wheelEvent) - runAnimationFrames() - - expect(componentNode.contains(lineNode)).toBe(true) - }) - - it('does not set the mouseWheelScreenRow if scrolling horizontally', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let lineNode = componentNode.querySelector('.line') - let wheelEvent = new WheelEvent('mousewheel', { - wheelDeltaX: 10, - wheelDeltaY: 0 - }) - Object.defineProperty(wheelEvent, 'target', { - get: function () { - return lineNode - } - }) - componentNode.dispatchEvent(wheelEvent) - runAnimationFrames() - - expect(component.presenter.mouseWheelScreenRow).toBe(null) - }) - - it('clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling', async function () { - expect(wrapperNode.getScrollTop()).toBe(0) - let lineNode = componentNode.querySelector('.line') - let wheelEvent = new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: 10 - }) - Object.defineProperty(wheelEvent, 'target', { - get: function () { - return lineNode - } - }) - componentNode.dispatchEvent(wheelEvent) - expect(wrapperNode.getScrollTop()).toBe(0) - expect(component.presenter.mouseWheelScreenRow).toBe(0) - - advanceClock(component.presenter.stoppedScrollingDelay) - expect(component.presenter.mouseWheelScreenRow).toBeNull() - }) - - it('does not preserve the line if it is on screen', function () { - let lineNode, lineNodes, wheelEvent - expect(componentNode.querySelectorAll('.line-number').length).toBe(14) - lineNodes = componentNode.querySelectorAll('.line') - expect(lineNodes.length).toBe(13) - lineNode = lineNodes[0] - wheelEvent = new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: 100 - }) - Object.defineProperty(wheelEvent, 'target', { - get: function () { - return lineNode - } - }) - componentNode.dispatchEvent(wheelEvent) - expect(component.presenter.mouseWheelScreenRow).toBe(0) - editor.insertText('hello') - expect(componentNode.querySelectorAll('.line-number').length).toBe(14) - expect(componentNode.querySelectorAll('.line').length).toBe(13) - }) - }) - - describe('when the mousewheel event\'s target is a line number', function () { - it('keeps the line number on the DOM if it is scrolled off-screen', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let lineNumberNode = componentNode.querySelectorAll('.line-number')[1] - let wheelEvent = new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: -500 - }) - Object.defineProperty(wheelEvent, 'target', { - get: function () { - return lineNumberNode - } - }) - componentNode.dispatchEvent(wheelEvent) - runAnimationFrames() - - expect(componentNode.contains(lineNumberNode)).toBe(true) - }) - }) - - describe('when the mousewheel event\'s target is a block decoration', function () { - it('keeps it on the DOM if it is scrolled off-screen', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - let item = document.createElement("div") - item.style.width = "30px" - item.style.height = "30px" - item.className = "decoration-1" - editor.decorateMarker( - editor.markScreenPosition([0, 0], {invalidate: "never"}), - {type: "block", item: item} - ) - - runAnimationFrames() - - let wheelEvent = new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: -500 - }) - Object.defineProperty(wheelEvent, 'target', { - get: function () { - return item - } - }) - componentNode.dispatchEvent(wheelEvent) - runAnimationFrames() - - expect(component.getTopmostDOMNode().contains(item)).toBe(true) - }) - }) - - describe('when the mousewheel event\'s target is an SVG element inside a block decoration', function () { - it('keeps the block decoration on the DOM if it is scrolled off-screen', function () { - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - const item = document.createElement('div') - const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg") - item.appendChild(svgElement) - editor.decorateMarker( - editor.markScreenPosition([0, 0], {invalidate: "never"}), - {type: "block", item: item} - ) - - runAnimationFrames() - - let wheelEvent = new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: -500 - }) - Object.defineProperty(wheelEvent, 'target', { - get: function () { - return svgElement - } - }) - componentNode.dispatchEvent(wheelEvent) - runAnimationFrames() - - expect(component.getTopmostDOMNode().contains(item)).toBe(true) - }) - }) - - it('only prevents the default action of the mousewheel event if it actually lead to scrolling', function () { - spyOn(WheelEvent.prototype, 'preventDefault').andCallThrough() - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: 50 - })) - expect(wrapperNode.getScrollTop()).toBe(0) - expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: -3000 - })) - runAnimationFrames() - - let maxScrollTop = wrapperNode.getScrollTop() - expect(WheelEvent.prototype.preventDefault).toHaveBeenCalled() - WheelEvent.prototype.preventDefault.reset() - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: 0, - wheelDeltaY: -30 - })) - expect(wrapperNode.getScrollTop()).toBe(maxScrollTop) - expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: 50, - wheelDeltaY: 0 - })) - expect(wrapperNode.getScrollLeft()).toBe(0) - expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: -3000, - wheelDeltaY: 0 - })) - runAnimationFrames() - - let maxScrollLeft = wrapperNode.getScrollLeft() - expect(WheelEvent.prototype.preventDefault).toHaveBeenCalled() - WheelEvent.prototype.preventDefault.reset() - componentNode.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaX: -30, - wheelDeltaY: 0 - })) - expect(wrapperNode.getScrollLeft()).toBe(maxScrollLeft) - expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() - }) - }) - - describe('input events', function () { - function buildTextInputEvent ({data, target}) { - let event = new Event('textInput') - event.data = data - Object.defineProperty(event, 'target', { - get: function () { - return target - } - }) - return event - } - - function buildKeydownEvent ({keyCode, target}) { - let event = new KeyboardEvent('keydown') - Object.defineProperty(event, 'keyCode', { - get: function () { - return keyCode - } - }) - Object.defineProperty(event, 'target', { - get: function () { - return target - } - }) - return event - } - - let inputNode - - beforeEach(function () { - inputNode = componentNode.querySelector('.hidden-input') - }) - - it('inserts the newest character in the input\'s value into the buffer', function () { - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'x', - target: inputNode - })) - runAnimationFrames() - - expect(editor.lineTextForBufferRow(0)).toBe('xvar quicksort = function () {') - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'y', - target: inputNode - })) - - expect(editor.lineTextForBufferRow(0)).toBe('xyvar quicksort = function () {') - }) - - it('replaces the last character if a keypress event is bracketed by keydown events with matching keyCodes, which occurs when the accented character menu is shown', function () { - componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode})) - componentNode.dispatchEvent(buildTextInputEvent({data: 'u', target: inputNode})) - componentNode.dispatchEvent(new KeyboardEvent('keypress')) - componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode})) - componentNode.dispatchEvent(new KeyboardEvent('keyup')) - runAnimationFrames() - - expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {') - inputNode.setSelectionRange(0, 1) - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'ü', - target: inputNode - })) - runAnimationFrames() - - expect(editor.lineTextForBufferRow(0)).toBe('üvar quicksort = function () {') - }) - - it('does not handle input events when input is disabled', function () { - component.setInputEnabled(false) - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'x', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') - runAnimationFrames() - expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') - }) - - it('groups events that occur close together in time into single undo entries', function () { - let currentTime = 0 - spyOn(Date, 'now').andCallFake(function () { - return currentTime - }) - editor.update({undoGroupingInterval: 100}) - editor.setText('') - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'x', - target: inputNode - })) - currentTime += 99 - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'y', - target: inputNode - })) - currentTime += 99 - componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', { - bubbles: true, - cancelable: true - })) - currentTime += 101 - componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', { - bubbles: true, - cancelable: true - })) - expect(editor.getText()).toBe('xy\nxy\nxy') - componentNode.dispatchEvent(new CustomEvent('core:undo', { - bubbles: true, - cancelable: true - })) - expect(editor.getText()).toBe('xy\nxy') - componentNode.dispatchEvent(new CustomEvent('core:undo', { - bubbles: true, - cancelable: true - })) - expect(editor.getText()).toBe('') - }) - - describe('when IME composition is used to insert international characters', function () { - function buildIMECompositionEvent (event, {data, target} = {}) { - event = new Event(event) - event.data = data - Object.defineProperty(event, 'target', { - get: function () { - return target - } - }) - return event - } - - let inputNode - - beforeEach(function () { - inputNode = componentNode.querySelector('.hidden-input') - }) - - describe('when nothing is selected', function () { - it('inserts the chosen completion', function () { - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { - target: inputNode - })) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 's', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('svar quicksort = function () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 'sd', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('sdvar quicksort = function () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { - target: inputNode - })) - componentNode.dispatchEvent(buildTextInputEvent({ - data: '速度', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('速度var quicksort = function () {') - }) - - it('reverts back to the original text when the completion helper is dismissed', function () { - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { - target: inputNode - })) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 's', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('svar quicksort = function () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 'sd', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('sdvar quicksort = function () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') - }) - - it('allows multiple accented character to be inserted with the \' on a US international layout', function () { - inputNode.value = '\'' - inputNode.setSelectionRange(0, 1) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { - target: inputNode - })) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: '\'', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('\'var quicksort = function () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { - target: inputNode - })) - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'á', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('ávar quicksort = function () {') - inputNode.value = '\'' - inputNode.setSelectionRange(0, 1) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { - target: inputNode - })) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: '\'', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('á\'var quicksort = function () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { - target: inputNode - })) - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'á', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('áávar quicksort = function () {') - }) - }) - - describe('when a string is selected', function () { - beforeEach(function () { - editor.setSelectedBufferRanges([[[0, 4], [0, 9]], [[0, 16], [0, 19]]]) - }) - - it('inserts the chosen completion', function () { - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { - target: inputNode - })) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 's', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var ssort = sction () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 'sd', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var sdsort = sdction () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { - target: inputNode - })) - componentNode.dispatchEvent(buildTextInputEvent({ - data: '速度', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var 速度sort = 速度ction () {') - }) - - it('reverts back to the original text when the completion helper is dismissed', function () { - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { - target: inputNode - })) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 's', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var ssort = sction () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { - data: 'sd', - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var sdsort = sdction () {') - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { - target: inputNode - })) - expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') - }) - }) - }) - }) - - describe('commands', function () { - describe('editor:consolidate-selections', function () { - it('consolidates selections on the editor model, aborting the key binding if there is only one selection', function () { - spyOn(editor, 'consolidateSelections').andCallThrough() - let event = new CustomEvent('editor:consolidate-selections', { - bubbles: true, - cancelable: true - }) - event.abortKeyBinding = jasmine.createSpy('event.abortKeyBinding') - componentNode.dispatchEvent(event) - expect(editor.consolidateSelections).toHaveBeenCalled() - expect(event.abortKeyBinding).toHaveBeenCalled() - }) - }) - }) - - describe('when decreasing the fontSize', function () { - it('decreases the widths of the korean char, the double width char and the half width char', function () { - originalDefaultCharWidth = editor.getDefaultCharWidth() - koreanDefaultCharWidth = editor.getKoreanCharWidth() - doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth() - halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth() - component.setFontSize(10) - runAnimationFrames() - expect(editor.getDefaultCharWidth()).toBeLessThan(originalDefaultCharWidth) - expect(editor.getKoreanCharWidth()).toBeLessThan(koreanDefaultCharWidth) - expect(editor.getDoubleWidthCharWidth()).toBeLessThan(doubleWidthDefaultCharWidth) - expect(editor.getHalfWidthCharWidth()).toBeLessThan(halfWidthDefaultCharWidth) - }) - }) - - describe('when increasing the fontSize', function() { - it('increases the widths of the korean char, the double width char and the half width char', function () { - originalDefaultCharWidth = editor.getDefaultCharWidth() - koreanDefaultCharWidth = editor.getKoreanCharWidth() - doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth() - halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth() - component.setFontSize(25) - runAnimationFrames() - expect(editor.getDefaultCharWidth()).toBeGreaterThan(originalDefaultCharWidth) - expect(editor.getKoreanCharWidth()).toBeGreaterThan(koreanDefaultCharWidth) - expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(doubleWidthDefaultCharWidth) - expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(halfWidthDefaultCharWidth) - }) - }) - - describe('hiding and showing the editor', function () { - describe('when the editor is hidden when it is mounted', function () { - it('defers measurement and rendering until the editor becomes visible', function () { - wrapperNode.remove() - let hiddenParent = document.createElement('div') - hiddenParent.style.display = 'none' - contentNode.appendChild(hiddenParent) - wrapperNode = new TextEditorElement() - wrapperNode.tileSize = TILE_SIZE - wrapperNode.initialize(editor, atom) - hiddenParent.appendChild(wrapperNode) - component = wrapperNode.component - componentNode = component.getDomNode() - expect(componentNode.querySelectorAll('.line').length).toBe(0) - hiddenParent.style.display = 'block' - atom.views.performDocumentPoll() - expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan(0) - }) - }) - - describe('when the lineHeight changes while the editor is hidden', function () { - it('does not attempt to measure the lineHeightInPixels until the editor becomes visible again', function () { - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - let initialLineHeightInPixels = editor.getLineHeightInPixels() - component.setLineHeight(2) - expect(editor.getLineHeightInPixels()).toBe(initialLineHeightInPixels) - wrapperNode.style.display = '' - component.checkForVisibilityChange() - expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeightInPixels) - }) - }) - - describe('when the fontSize changes while the editor is hidden', function () { - it('does not attempt to measure the lineHeightInPixels or defaultCharWidth until the editor becomes visible again', function () { - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - let initialLineHeightInPixels = editor.getLineHeightInPixels() - let initialCharWidth = editor.getDefaultCharWidth() - component.setFontSize(22) - expect(editor.getLineHeightInPixels()).toBe(initialLineHeightInPixels) - expect(editor.getDefaultCharWidth()).toBe(initialCharWidth) - wrapperNode.style.display = '' - component.checkForVisibilityChange() - expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeightInPixels) - expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) - }) - - it('does not re-measure character widths until the editor is shown again', function () { - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - component.setFontSize(22) - editor.getBuffer().insert([0, 0], 'a') - wrapperNode.style.display = '' - component.checkForVisibilityChange() - editor.setCursorBufferPosition([0, Infinity]) - runAnimationFrames() - let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo(line0Right, 0) - }) - }) - - describe('when the fontFamily changes while the editor is hidden', function () { - it('does not attempt to measure the defaultCharWidth until the editor becomes visible again', function () { - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - let initialLineHeightInPixels = editor.getLineHeightInPixels() - let initialCharWidth = editor.getDefaultCharWidth() - component.setFontFamily('serif') - expect(editor.getDefaultCharWidth()).toBe(initialCharWidth) - wrapperNode.style.display = '' - component.checkForVisibilityChange() - expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) - }) - - it('does not re-measure character widths until the editor is shown again', function () { - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - component.setFontFamily('serif') - wrapperNode.style.display = '' - component.checkForVisibilityChange() - editor.setCursorBufferPosition([0, Infinity]) - runAnimationFrames() - let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo(line0Right, 0) - }) - }) - - describe('when stylesheets change while the editor is hidden', function () { - afterEach(function () { - atom.themes.removeStylesheet('test') - }) - - it('does not re-measure character widths until the editor is shown again', function () { - atom.config.set('editor.fontFamily', 'sans-serif') - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - atom.themes.applyStylesheet('test', '.syntax--function.syntax--js {\n font-weight: bold;\n}') - wrapperNode.style.display = '' - component.checkForVisibilityChange() - editor.setCursorBufferPosition([0, Infinity]) - runAnimationFrames() - let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo(line0Right, 0) - }) - }) - }) - - describe('soft wrapping', function () { - beforeEach(function () { - editor.setSoftWrapped(true) - runAnimationFrames() - }) - - it('updates the wrap location when the editor is resized', function () { - let newHeight = 4 * editor.getLineHeightInPixels() + 'px' - expect(parseInt(newHeight)).toBeLessThan(wrapperNode.offsetHeight) - wrapperNode.style.height = newHeight - editor.update({autoHeight: false}) - atom.views.performDocumentPoll() - runAnimationFrames() - - expect(componentNode.querySelectorAll('.line')).toHaveLength(7) - let gutterWidth = componentNode.querySelector('.gutter').offsetWidth - componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - atom.views.performDocumentPoll() - runAnimationFrames() - expect(componentNode.querySelector('.line').textContent).toBe('var quicksort ') - }) - - it('accounts for the scroll view\'s padding when determining the wrap location', function () { - let scrollViewNode = componentNode.querySelector('.scroll-view') - scrollViewNode.style.paddingLeft = 20 + 'px' - componentNode.style.width = 30 * charWidth + 'px' - atom.views.performDocumentPoll() - runAnimationFrames() - expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = ') - }) - }) - - describe('default decorations', function () { - it('applies .cursor-line decorations for line numbers overlapping selections', function () { - editor.setCursorScreenPosition([4, 4]) - runAnimationFrames() - - expect(lineNumberHasClass(3, 'cursor-line')).toBe(false) - expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) - expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - runAnimationFrames() - - expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) - expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) - editor.setSelectedScreenRange([[3, 4], [4, 0]]) - runAnimationFrames() - - expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) - expect(lineNumberHasClass(4, 'cursor-line')).toBe(false) - }) - - it('does not apply .cursor-line to the last line of a selection if it\'s empty', function () { - editor.setSelectedScreenRange([[3, 4], [5, 0]]) - runAnimationFrames() - expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) - expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) - expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) - }) - - it('applies .cursor-line decorations for lines containing the cursor in non-empty selections', function () { - editor.setCursorScreenPosition([4, 4]) - runAnimationFrames() - - expect(lineHasClass(3, 'cursor-line')).toBe(false) - expect(lineHasClass(4, 'cursor-line')).toBe(true) - expect(lineHasClass(5, 'cursor-line')).toBe(false) - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - runAnimationFrames() - - expect(lineHasClass(2, 'cursor-line')).toBe(false) - expect(lineHasClass(3, 'cursor-line')).toBe(false) - expect(lineHasClass(4, 'cursor-line')).toBe(false) - expect(lineHasClass(5, 'cursor-line')).toBe(false) - }) - - it('applies .cursor-line-no-selection to line numbers for rows containing the cursor when the selection is empty', function () { - editor.setCursorScreenPosition([4, 4]) - runAnimationFrames() - - expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(true) - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - runAnimationFrames() - - expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(false) - }) - }) - - describe('height', function () { - describe('when autoHeight is true', function () { - it('assigns the editor\'s height to based on its contents', function () { - jasmine.attachToDOM(wrapperNode) - expect(editor.getAutoHeight()).toBe(true) - expect(wrapperNode.offsetHeight).toBe(editor.getLineHeightInPixels() * editor.getScreenLineCount()) - editor.insertText('\n\n\n') - runAnimationFrames() - expect(wrapperNode.offsetHeight).toBe(editor.getLineHeightInPixels() * editor.getScreenLineCount()) - }) - }) - - describe('when autoHeight is false', function () { - it('does not assign the height of the editor, instead allowing content to scroll', function () { - jasmine.attachToDOM(wrapperNode) - editor.update({autoHeight: false}) - wrapperNode.style.height = '200px' - expect(wrapperNode.offsetHeight).toBe(200) - editor.insertText('\n\n\n') - runAnimationFrames() - expect(wrapperNode.offsetHeight).toBe(200) - }) - }) - - describe('when autoHeight is not assigned on the editor', function () { - it('implicitly assigns autoHeight to true and emits a deprecation warning if the editor has its height assigned via an inline style', function () { - editor = new TextEditor() - element = editor.getElement() - element.setUpdatedSynchronously(false) - element.style.height = '200px' - - spyOn(Grim, 'deprecate') - jasmine.attachToDOM(element) - - expect(element.offsetHeight).toBe(200) - expect(element.querySelector('.editor-contents--private').offsetHeight).toBe(200) - expect(Grim.deprecate.callCount).toBe(1) - expect(Grim.deprecate.argsForCall[0][0]).toMatch(/inline style/) - }) - - it('implicitly assigns autoHeight to true and emits a deprecation warning if the editor has its height assigned via position absolute with an assigned top and bottom', function () { - editor = new TextEditor() - element = editor.getElement() - element.setUpdatedSynchronously(false) - parentElement = document.createElement('div') - parentElement.style.position = 'absolute' - parentElement.style.height = '200px' - element.style.position = 'absolute' - element.style.top = '0px' - element.style.bottom = '0px' - parentElement.appendChild(element) - - spyOn(Grim, 'deprecate') - - jasmine.attachToDOM(parentElement) - element.component.measureDimensions() - - expect(element.offsetHeight).toBe(200) - expect(element.querySelector('.editor-contents--private').offsetHeight).toBe(200) - expect(Grim.deprecate.callCount).toBe(1) - expect(Grim.deprecate.argsForCall[0][0]).toMatch(/absolute/) - }) - }) - - describe('when the wrapper view has an explicit height', function () { - it('does not assign a height on the component node', function () { - wrapperNode.style.height = '200px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - expect(componentNode.style.height).toBe('') - }) - }) - - describe('when the wrapper view does not have an explicit height', function () { - it('assigns a height on the component node based on the editor\'s content', function () { - expect(wrapperNode.style.height).toBe('') - expect(componentNode.style.height).toBe(editor.getScreenLineCount() * lineHeightInPixels + 'px') - }) - }) - }) - - describe('width', function () { - it('sizes the editor element according to the content width when auto width is true, or according to the container width otherwise', function () { - contentNode.style.width = '600px' - component.measureDimensions() - editor.setText("abcdefghi") - runAnimationFrames() - expect(wrapperNode.offsetWidth).toBe(contentNode.offsetWidth) - - editor.update({autoWidth: true}) - runAnimationFrames() - const editorWidth1 = wrapperNode.offsetWidth - expect(editorWidth1).toBeGreaterThan(0) - expect(editorWidth1).toBeLessThan(contentNode.offsetWidth) - - editor.setText("abcdefghijkl") - editor.update({autoWidth: true}) - runAnimationFrames() - const editorWidth2 = wrapperNode.offsetWidth - expect(editorWidth2).toBeGreaterThan(editorWidth1) - expect(editorWidth2).toBeLessThan(contentNode.offsetWidth) - - editor.update({autoWidth: false}) - runAnimationFrames() - expect(wrapperNode.offsetWidth).toBe(contentNode.offsetWidth) - }) - }) - - describe('when the "mini" property is true', function () { - beforeEach(function () { - editor.setMini(true) - runAnimationFrames() - }) - - it('does not render the gutter', function () { - expect(componentNode.querySelector('.gutter')).toBeNull() - }) - - it('adds the "mini" class to the wrapper view', function () { - expect(wrapperNode.classList.contains('mini')).toBe(true) - }) - - it('does not have an opaque background on lines', function () { - expect(component.linesComponent.getDomNode().getAttribute('style')).not.toContain('background-color') - }) - - it('does not render invisible characters', function () { - editor.update({ - showInvisibles: true, - invisibles: {eol: 'E'} - }) - expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = function () {') - }) - - it('does not assign an explicit line-height on the editor contents', function () { - expect(componentNode.style.lineHeight).toBe('') - }) - - it('does not apply cursor-line decorations', function () { - expect(component.lineNodeForScreenRow(0).classList.contains('cursor-line')).toBe(false) - }) - }) - - describe('when placholderText is specified', function () { - it('renders the placeholder text when the buffer is empty', function () { - editor.setPlaceholderText('Hello World') - expect(componentNode.querySelector('.placeholder-text')).toBeNull() - editor.setText('') - runAnimationFrames() - - expect(componentNode.querySelector('.placeholder-text').textContent).toBe('Hello World') - editor.setText('hey') - runAnimationFrames() - - expect(componentNode.querySelector('.placeholder-text')).toBeNull() - }) - }) - - describe('grammar data attributes', function () { - it('adds and updates the grammar data attribute based on the current grammar', function () { - expect(wrapperNode.dataset.grammar).toBe('source js') - editor.setGrammar(atom.grammars.nullGrammar) - expect(wrapperNode.dataset.grammar).toBe('text plain null-grammar') - }) - }) - - describe('encoding data attributes', function () { - it('adds and updates the encoding data attribute based on the current encoding', function () { - expect(wrapperNode.dataset.encoding).toBe('utf8') - editor.setEncoding('utf16le') - expect(wrapperNode.dataset.encoding).toBe('utf16le') - }) - }) - - describe('detaching and reattaching the editor (regression)', function () { - it('does not throw an exception', function () { - wrapperNode.remove() - jasmine.attachToDOM(wrapperNode) - atom.commands.dispatch(wrapperNode, 'core:move-right') - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - }) - }) - - describe('autoscroll', function () { - beforeEach(function () { - editor.setVerticalScrollMargin(2) - editor.setHorizontalScrollMargin(2) - component.setLineHeight('10px') - component.setFontSize(17) - component.measureDimensions() - runAnimationFrames() - - wrapperNode.style.width = 55 + component.getGutterWidth() + 'px' - wrapperNode.style.height = '55px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - component.presenter.setHorizontalScrollbarHeight(0) - component.presenter.setVerticalScrollbarWidth(0) - runAnimationFrames() - }) - - describe('when selecting buffer ranges', function () { - it('autoscrolls the selection if it is last unless the "autoscroll" option is false', function () { - expect(wrapperNode.getScrollTop()).toBe(0) - editor.setSelectedBufferRange([[5, 6], [6, 8]]) - runAnimationFrames() - - let right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left - expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) - expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBe(0) - editor.setSelectedBufferRange([[6, 6], [6, 8]]) - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) - expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) - }) - }) - - describe('when adding selections for buffer ranges', function () { - it('autoscrolls to the added selection if needed', function () { - editor.addSelectionForBufferRange([[8, 10], [8, 15]]) - runAnimationFrames() - - let right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left - expect(wrapperNode.getScrollBottom()).toBe((9 * 10) + (2 * 10)) - expect(wrapperNode.getScrollRight()).toBeCloseTo(right + 2 * 10, 0) - }) - }) - - describe('when selecting lines containing cursors', function () { - it('autoscrolls to the selection', function () { - editor.setCursorScreenPosition([5, 6]) - runAnimationFrames() - - wrapperNode.scrollToTop() - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.selectLinesContainingCursors() - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) - }) - }) - - describe('when inserting text', function () { - describe('when there are multiple empty selections on different lines', function () { - it('autoscrolls to the last cursor', function () { - editor.setCursorScreenPosition([1, 2], { - autoscroll: false - }) - runAnimationFrames() - - editor.addCursorAtScreenPosition([10, 4], { - autoscroll: false - }) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.insertText('a') - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(75) - }) - }) - }) - - describe('when scrolled to cursor position', function () { - it('scrolls the last cursor into view, centering around the cursor if possible and the "center" option is not false', function () { - editor.setCursorScreenPosition([8, 8], { - autoscroll: false - }) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBe(0) - editor.scrollToCursorPosition() - runAnimationFrames() - - let right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left - expect(wrapperNode.getScrollTop()).toBe((8.8 * 10) - 30) - expect(wrapperNode.getScrollBottom()).toBe((8.3 * 10) + 30) - expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) - wrapperNode.setScrollTop(0) - editor.scrollToCursorPosition({ - center: false - }) - expect(wrapperNode.getScrollTop()).toBe((7.8 - editor.getVerticalScrollMargin()) * 10) - expect(wrapperNode.getScrollBottom()).toBe((9.3 + editor.getVerticalScrollMargin()) * 10) - }) - }) - - describe('moving cursors', function () { - it('scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor', function () { - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) - editor.setCursorScreenPosition([2, 0]) - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) - editor.moveDown() - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe(6 * 10) - editor.moveDown() - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe(7 * 10) - }) - - it('scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor', function () { - editor.setCursorScreenPosition([11, 0]) - runAnimationFrames() - - wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - runAnimationFrames() - - editor.moveUp() - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe(wrapperNode.getScrollHeight()) - editor.moveUp() - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(7 * 10) - editor.moveUp() - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(6 * 10) - }) - - it('scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor', function () { - expect(wrapperNode.getScrollLeft()).toBe(0) - expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) - editor.setCursorScreenPosition([0, 2]) - runAnimationFrames() - - expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) - editor.moveRight() - runAnimationFrames() - - let margin = component.presenter.getHorizontalScrollMarginInPixels() - let right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin - expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) - editor.moveRight() - runAnimationFrames() - - right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin - expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) - }) - - it('scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor', function () { - wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) - runAnimationFrames() - - expect(wrapperNode.getScrollRight()).toBe(wrapperNode.getScrollWidth()) - editor.setCursorScreenPosition([6, 62], { - autoscroll: false - }) - runAnimationFrames() - - editor.moveLeft() - runAnimationFrames() - - let margin = component.presenter.getHorizontalScrollMarginInPixels() - let left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin - expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) - editor.moveLeft() - runAnimationFrames() - - left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin - expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) - }) - - it('scrolls down when inserting lines makes the document longer than the editor\'s height', function () { - editor.setCursorScreenPosition([13, Infinity]) - editor.insertNewline() - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe(14 * 10) - editor.insertNewline() - runAnimationFrames() - - expect(wrapperNode.getScrollBottom()).toBe(15 * 10) - }) - - it('autoscrolls to the cursor when it moves due to undo', function () { - editor.insertText('abc') - wrapperNode.setScrollTop(Infinity) - runAnimationFrames() - - editor.undo() - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - }) - - it('does not scroll when the cursor moves into the visible area', function () { - editor.setCursorBufferPosition([0, 0]) - runAnimationFrames() - - wrapperNode.setScrollTop(40) - runAnimationFrames() - - editor.setCursorBufferPosition([6, 0]) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(40) - }) - - it('honors the autoscroll option on cursor and selection manipulation methods', function () { - expect(wrapperNode.getScrollTop()).toBe(0) - editor.addCursorAtScreenPosition([11, 11], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.addCursorAtBufferPosition([11, 11], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.setCursorScreenPosition([11, 11], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.setCursorBufferPosition([11, 11], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.addSelectionForBufferRange([[11, 11], [11, 11]], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.addSelectionForScreenRange([[11, 11], [11, 12]], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.setSelectedBufferRange([[11, 0], [11, 1]], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.setSelectedScreenRange([[11, 0], [11, 6]], {autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.clearSelections({autoscroll: false}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.addSelectionForScreenRange([[0, 0], [0, 4]]) - runAnimationFrames() - - editor.getCursors()[0].setScreenPosition([11, 11], {autoscroll: true}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) - editor.getCursors()[0].setBufferPosition([0, 0], {autoscroll: true}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], {autoscroll: true}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) - editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], {autoscroll: true}) - runAnimationFrames() - - expect(wrapperNode.getScrollTop()).toBe(0) - }) - }) - }) - - describe('::getVisibleRowRange()', function () { - beforeEach(function () { - wrapperNode.style.height = lineHeightInPixels * 8 + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - }) - - it('returns the first and the last visible rows', function () { - component.setScrollTop(0) - runAnimationFrames() - expect(component.getVisibleRowRange()).toEqual([0, 9]) - }) - - it('ends at last buffer row even if there\'s more space available', function () { - wrapperNode.style.height = lineHeightInPixels * 13 + 'px' - editor.update({autoHeight: false}) - component.measureDimensions() - runAnimationFrames() - - component.setScrollTop(60) - runAnimationFrames() - - expect(component.getVisibleRowRange()).toEqual([0, 13]) - }) - }) - - describe('::pixelPositionForScreenPosition()', () => { - it('returns the correct horizontal position, even if it is on a row that has not yet been rendered (regression)', () => { - editor.setTextInBufferRange([[5, 0], [6, 0]], 'hello world\n') - expect(wrapperNode.pixelPositionForScreenPosition([5, Infinity]).left).toBeGreaterThan(0) - }) - }) - - describe('middle mouse paste on Linux', function () { - let originalPlatform - - beforeEach(function () { - originalPlatform = process.platform - Object.defineProperty(process, 'platform', { - value: 'linux' - }) - }) - - afterEach(function () { - Object.defineProperty(process, 'platform', { - value: originalPlatform - }) - }) - - it('pastes the previously selected text at the clicked location', async function () { - let clipboardWrittenTo = false - spyOn(require('electron').ipcRenderer, 'send').andCallFake(function (eventName, selectedText) { - if (eventName === 'write-text-to-selection-clipboard') { - require('../src/safe-clipboard').writeText(selectedText, 'selection') - clipboardWrittenTo = true - } - }) - atom.clipboard.write('') - component.trackSelectionClipboard() - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - advanceClock(0) - - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([10, 0]), { - button: 1 - })) - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([10, 0]), { - which: 2 - })) - expect(atom.clipboard.read()).toBe('sort') - expect(editor.lineTextForBufferRow(10)).toBe('sort') - }) - - it('pastes the previously selected text at the clicked location, left clicks do not interfere', async function () { - let clipboardWrittenTo = false - spyOn(require('electron').ipcRenderer, 'send').andCallFake(function (eventName, selectedText) { - if (eventName === 'write-text-to-selection-clipboard') { - require('../src/safe-clipboard').writeText(selectedText, 'selection') - clipboardWrittenTo = true - } - }) - atom.clipboard.write('') - component.trackSelectionClipboard() - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - advanceClock(0) - - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([10, 0]), { - button: 0 - })) - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([10, 0]), { - which: 1 - })) - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([10, 0]), { - button: 1 - })) - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([10, 0]), { - which: 2 - })) - expect(atom.clipboard.read()).toBe('sort') - expect(editor.lineTextForBufferRow(10)).toBe('sort') - }) - }) - - function buildMouseEvent (type, ...propertiesObjects) { - let properties = extend({ - bubbles: true, - cancelable: true - }, ...propertiesObjects) - - if (properties.detail == null) { - properties.detail = 1 - } - - let event = new MouseEvent(type, properties) - if (properties.which != null) { - Object.defineProperty(event, 'which', { - get: function () { - return properties.which - } - }) - } - if (properties.target != null) { - Object.defineProperty(event, 'target', { - get: function () { - return properties.target - } - }) - Object.defineProperty(event, 'srcObject', { - get: function () { - return properties.target - } - }) - } - return event - } - - function clientCoordinatesForScreenPosition (screenPosition) { - let clientX, clientY, positionOffset, scrollViewClientRect - positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition) - scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() - clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() - clientY = scrollViewClientRect.top + positionOffset.top - wrapperNode.getScrollTop() - return { - clientX: clientX, - clientY: clientY - } - } - - function clientCoordinatesForScreenRowInGutter (screenRow) { - let clientX, clientY, gutterClientRect, positionOffset - positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, Infinity]) - gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect() - clientX = gutterClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() - clientY = gutterClientRect.top + positionOffset.top - wrapperNode.getScrollTop() - return { - clientX: clientX, - clientY: clientY - } - } - - function lineAndLineNumberHaveClass (screenRow, klass) { - return lineHasClass(screenRow, klass) && lineNumberHasClass(screenRow, klass) - } - - function lineNumberHasClass (screenRow, klass) { - return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) - } - - function lineNumberForBufferRowHasClass (bufferRow, klass) { - let screenRow - screenRow = editor.screenRowForBufferRow(bufferRow) - return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) - } - - function lineHasClass (screenRow, klass) { - return component.lineNodeForScreenRow(screenRow).classList.contains(klass) - } - - function getLeafNodes (node) { - if (node.children.length > 0) { - return flatten(toArray(node.children).map(getLeafNodes)) - } else { - return [node] - } - } - - function conditionPromise (condition) { - let timeoutError = new Error("Timed out waiting on condition") - Error.captureStackTrace(timeoutError, conditionPromise) - - return new Promise(function (resolve, reject) { - let interval = window.setInterval.originalValue.apply(window, [function () { - if (condition()) { - window.clearInterval(interval) - window.clearTimeout(timeout) - resolve() - } - }, 100]) - let timeout = window.setTimeout.originalValue.apply(window, [function () { - window.clearInterval(interval) - reject(timeoutError) - }, 5000]) - }) - } - - function decorationsUpdatedPromise(editor) { - return new Promise(function (resolve) { - let disposable = editor.onDidUpdateDecorations(function () { - disposable.dispose() - resolve() - }) - }) - } }) + +describe('TextEditorComponent', () => { + beforeEach(() => { + jasmine.useRealClock() + }) + + describe('rendering', () => { + it('renders lines and line numbers for the visible region', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) + + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(13) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13) + + element.style.height = 4 * component.measurements.lineHeight + 'px' + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(9) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + + await setScrollTop(component, 5 * component.getLineHeight()) + + // After scrolling down beyond > 3 rows, the order of line numbers and lines + // in the DOM is a bit weird because the first tile is recycled to the bottom + // when it is scrolled out of view + expect(Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map(element => element.textContent.trim())).toEqual([ + '10', '11', '12', '4', '5', '6', '7', '8', '9' + ]) + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.dataset.screenRow)).toEqual([ + '9', '10', '11', '3', '4', '5', '6', '7', '8' + ]) + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.textContent)).toEqual([ + editor.lineTextForScreenRow(9), + ' ', // this line is blank in the model, but we render a space to prevent the line from collapsing vertically + editor.lineTextForScreenRow(11), + editor.lineTextForScreenRow(3), + editor.lineTextForScreenRow(4), + editor.lineTextForScreenRow(5), + editor.lineTextForScreenRow(6), + editor.lineTextForScreenRow(7), + editor.lineTextForScreenRow(8) + ]) + + await setScrollTop(component, 2.5 * component.getLineHeight()) + expect(Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map(element => element.textContent.trim())).toEqual([ + '1', '2', '3', '4', '5', '6', '7', '8', '9' + ]) + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.dataset.screenRow)).toEqual([ + '0', '1', '2', '3', '4', '5', '6', '7', '8' + ]) + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.textContent)).toEqual([ + editor.lineTextForScreenRow(0), + editor.lineTextForScreenRow(1), + editor.lineTextForScreenRow(2), + editor.lineTextForScreenRow(3), + editor.lineTextForScreenRow(4), + editor.lineTextForScreenRow(5), + editor.lineTextForScreenRow(6), + editor.lineTextForScreenRow(7), + editor.lineTextForScreenRow(8) + ]) + }) + + it('bases the width of the lines div on the width of the longest initially-visible screen line', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 2, height: 20, width: 100}) + + { + expect(editor.getApproximateLongestScreenRow()).toBe(3) + const expectedWidth = Math.round( + component.pixelPositionForScreenPosition(Point(3, Infinity)).left + + component.getBaseCharacterWidth() + ) + expect(element.querySelector('.lines').style.width).toBe(expectedWidth + 'px') + } + + { + // Get the next update promise synchronously here to ensure we don't + // miss the update while polling the condition. + const nextUpdatePromise = component.getNextUpdatePromise() + await conditionPromise(() => editor.getApproximateLongestScreenRow() === 6) + await nextUpdatePromise + + // Capture the width first, then update the DOM so we can measure the + // longest line. + const actualWidth = element.querySelector('.lines').style.width + const expectedWidth = Math.round( + component.pixelPositionForScreenPosition(Point(6, Infinity)).left + + component.getBaseCharacterWidth() + ) + expect(actualWidth).toBe(expectedWidth + 'px') + } + }) + + it('makes the content at least as tall as the scroll container client height', async () => { + const {component, element, editor} = buildComponent({text: 'a', height: 100}) + expect(component.refs.content.offsetHeight).toBe(100) + + editor.setText('a\n'.repeat(30)) + await component.getNextUpdatePromise() + expect(component.refs.content.offsetHeight).toBeGreaterThan(100) + expect(component.refs.content.offsetHeight).toBe(component.getContentHeight()) + }) + + it('honors the scrollPastEnd option by adding empty space equivalent to the clientHeight to the end of the content area', async () => { + const {component, element, editor} = buildComponent({autoHeight: false, autoWidth: false}) + const {scrollContainer} = component.refs + + await editor.update({scrollPastEnd: true}) + await setEditorHeightInLines(component, 6) + + // scroll to end + await setScrollTop(component, scrollContainer.scrollHeight - scrollContainer.clientHeight) + expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 3) + + editor.update({scrollPastEnd: false}) + await component.getNextUpdatePromise() // wait for scrollable content resize + expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 6) + + // Always allows at least 3 lines worth of overscroll if the editor is short + await setEditorHeightInLines(component, 2) + await editor.update({scrollPastEnd: true}) + await setScrollTop(component, scrollContainer.scrollHeight - scrollContainer.clientHeight) + expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() + 1) + }) + + it('gives the line number tiles an explicit width and height so their layout can be strictly contained', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3}) + + const lineNumberGutterElement = component.refs.gutterContainer.refs.lineNumberGutter.element + expect(lineNumberGutterElement.offsetHeight).toBe(component.getScrollHeight()) + + for (const child of lineNumberGutterElement.children) { + expect(child.offsetWidth).toBe(lineNumberGutterElement.offsetWidth) + if (!child.classList.contains('line-number')) { + for (const lineNumberElement of child.children) { + expect(lineNumberElement.offsetWidth).toBe(lineNumberGutterElement.offsetWidth) + } + } + } + + editor.setText('x\n'.repeat(99)) + await component.getNextUpdatePromise() + expect(lineNumberGutterElement.offsetHeight).toBe(component.getScrollHeight()) + for (const child of lineNumberGutterElement.children) { + expect(child.offsetWidth).toBe(lineNumberGutterElement.offsetWidth) + if (!child.classList.contains('line-number')) { + for (const lineNumberElement of child.children) { + expect(lineNumberElement.offsetWidth).toBe(lineNumberGutterElement.offsetWidth) + } + } + } + }) + + it('keeps the number of tiles stable when the visible line count changes during vertical scrolling', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) + await setEditorHeightInLines(component, 5.5) + expect(component.refs.lineTiles.children.length).toBe(3) + + await setScrollTop(component, 0.5 * component.getLineHeight()) + expect(component.refs.lineTiles.children.length).toBe(3) + + await setScrollTop(component, 1 * component.getLineHeight()) + expect(component.refs.lineTiles.children.length).toBe(3) + }) + + it('recycles tiles on resize', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 2, autoHeight: false}) + await setEditorHeightInLines(component, 7) + await setScrollTop(component, 3.5 * component.getLineHeight()) + const lineNode = lineNodeForScreenRow(component, 7) + await setEditorHeightInLines(component, 4) + expect(lineNodeForScreenRow(component, 7)).toBe(lineNode) + }) + + it('updates lines numbers when a row\'s foldability changes (regression)', async () => { + const {component, element, editor} = buildComponent({text: 'abc\n'}) + editor.setCursorBufferPosition([1, 0]) + await component.getNextUpdatePromise() + expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull() + + editor.insertText(' def') + await component.getNextUpdatePromise() + expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeDefined() + + editor.undo() + await component.getNextUpdatePromise() + expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull() + }) + + it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => { + const {component, element, editor} = buildComponent({text: 'abc\n de\nfghijklm\n no', softWrapped: true}) + await setEditorWidthInCharacters(component, 5) + expect(lineNumberNodeForScreenRow(component, 0).classList.contains('foldable')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('foldable')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 2).classList.contains('foldable')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 3).classList.contains('foldable')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 4).classList.contains('foldable')).toBe(false) + }) + + it('renders dummy vertical and horizontal scrollbars when content overflows', async () => { + const {component, element, editor} = buildComponent({height: 100, width: 100}) + const verticalScrollbar = component.refs.verticalScrollbar.element + const horizontalScrollbar = component.refs.horizontalScrollbar.element + expect(verticalScrollbar.scrollHeight).toBe(component.getContentHeight()) + expect(horizontalScrollbar.scrollWidth).toBe(component.getContentWidth()) + expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0) + expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0) + expect(verticalScrollbar.style.bottom).toBe(getVerticalScrollbarWidth(component) + 'px') + expect(horizontalScrollbar.style.right).toBe(getHorizontalScrollbarHeight(component) + 'px') + expect(component.refs.scrollbarCorner).toBeDefined() + + setScrollTop(component, 100) + await setScrollLeft(component, 100) + expect(verticalScrollbar.scrollTop).toBe(100) + expect(horizontalScrollbar.scrollLeft).toBe(100) + + verticalScrollbar.scrollTop = 120 + horizontalScrollbar.scrollLeft = 120 + await component.getNextUpdatePromise() + expect(component.getScrollTop()).toBe(120) + expect(component.getScrollLeft()).toBe(120) + + editor.setText('a\n'.repeat(15)) + await component.getNextUpdatePromise() + expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0) + expect(getHorizontalScrollbarHeight(component)).toBe(0) + expect(verticalScrollbar.style.bottom).toBe('0px') + expect(component.refs.scrollbarCorner).toBeUndefined() + + editor.setText('a'.repeat(100)) + await component.getNextUpdatePromise() + expect(getVerticalScrollbarWidth(component)).toBe(0) + expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0) + expect(horizontalScrollbar.style.right).toBe('0px') + expect(component.refs.scrollbarCorner).toBeUndefined() + + editor.setText('') + await component.getNextUpdatePromise() + expect(getVerticalScrollbarWidth(component)).toBe(0) + expect(getHorizontalScrollbarHeight(component)).toBe(0) + expect(component.refs.scrollbarCorner).toBeUndefined() + + editor.setText(SAMPLE_TEXT) + await component.getNextUpdatePromise() + + // Does not show scrollbars if the content perfectly fits + element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + 'px' + element.style.height = component.getContentHeight() + 'px' + await component.getNextUpdatePromise() + expect(getVerticalScrollbarWidth(component)).toBe(0) + expect(getHorizontalScrollbarHeight(component)).toBe(0) + + // Shows scrollbars if the only reason we overflow is the presence of the + // scrollbar for the opposite axis. + element.style.width = component.getGutterContainerWidth() + component.getContentWidth() - 1 + 'px' + element.style.height = component.getContentHeight() + component.getHorizontalScrollbarHeight() - 1 + 'px' + await component.getNextUpdatePromise() + expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0) + expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0) + + element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + component.getVerticalScrollbarWidth() - 1 + 'px' + element.style.height = component.getContentHeight() - 1 + 'px' + await component.getNextUpdatePromise() + expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0) + expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0) + + }) + + it('updates the bottom/right of dummy scrollbars and client height/width measurements without forgetting the previous scroll top/left when scrollbar styles change', async () => { + const {component, element, editor} = buildComponent({height: 100, width: 100}) + expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10) + expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10) + setScrollTop(component, 20) + setScrollLeft(component, 10) + await component.getNextUpdatePromise() + + const style = document.createElement('style') + style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }' + jasmine.attachToDOM(style) + + TextEditor.didUpdateScrollbarStyles() + await component.getNextUpdatePromise() + + expect(getHorizontalScrollbarHeight(component)).toBe(10) + expect(getVerticalScrollbarWidth(component)).toBe(10) + expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px') + expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px') + expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10) + expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20) + expect(component.getScrollContainerClientHeight()).toBe(100 - 10) + expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10) + + // Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors. + await editor.update({mini: true}) + TextEditor.didUpdateScrollbarStyles() + component.scheduleUpdate() + await component.getNextUpdatePromise() + }) + + it('renders cursors within the visible row range', async () => { + const {component, element, editor} = buildComponent({height: 40, rowsPerTile: 2}) + await setScrollTop(component, 100) + + expect(component.getRenderedStartRow()).toBe(4) + expect(component.getRenderedEndRow()).toBe(10) + + editor.setCursorScreenPosition([0, 0], {autoscroll: false}) // out of view + editor.addCursorAtScreenPosition([2, 2], {autoscroll: false}) // out of view + editor.addCursorAtScreenPosition([4, 0], {autoscroll: false}) // line start + editor.addCursorAtScreenPosition([4, 4], {autoscroll: false}) // at token boundary + editor.addCursorAtScreenPosition([4, 6], {autoscroll: false}) // within token + editor.addCursorAtScreenPosition([5, Infinity], {autoscroll: false}) // line end + editor.addCursorAtScreenPosition([10, 2], {autoscroll: false}) // out of view + await component.getNextUpdatePromise() + + let cursorNodes = Array.from(element.querySelectorAll('.cursor')) + expect(cursorNodes.length).toBe(4) + verifyCursorPosition(component, cursorNodes[0], 4, 0) + verifyCursorPosition(component, cursorNodes[1], 4, 4) + verifyCursorPosition(component, cursorNodes[2], 4, 6) + verifyCursorPosition(component, cursorNodes[3], 5, 30) + + editor.setCursorScreenPosition([8, 11], {autoscroll: false}) + await component.getNextUpdatePromise() + + cursorNodes = Array.from(element.querySelectorAll('.cursor')) + expect(cursorNodes.length).toBe(1) + verifyCursorPosition(component, cursorNodes[0], 8, 11) + + editor.setCursorScreenPosition([0, 0], {autoscroll: false}) + await component.getNextUpdatePromise() + + cursorNodes = Array.from(element.querySelectorAll('.cursor')) + expect(cursorNodes.length).toBe(0) + + editor.setSelectedScreenRange([[8, 0], [12, 0]], {autoscroll: false}) + await component.getNextUpdatePromise() + cursorNodes = Array.from(element.querySelectorAll('.cursor')) + expect(cursorNodes.length).toBe(0) + }) + + it('hides cursors with non-empty selections when showCursorOnSelection is false', async () => { + const {component, element, editor} = buildComponent() + editor.setSelectedScreenRanges([ + [[0, 0], [0, 3]], + [[1, 0], [1, 0]] + ]) + await component.getNextUpdatePromise() + { + const cursorNodes = Array.from(element.querySelectorAll('.cursor')) + expect(cursorNodes.length).toBe(2) + verifyCursorPosition(component, cursorNodes[0], 0, 3) + verifyCursorPosition(component, cursorNodes[1], 1, 0) + } + + editor.update({showCursorOnSelection: false}) + await component.getNextUpdatePromise() + { + const cursorNodes = Array.from(element.querySelectorAll('.cursor')) + expect(cursorNodes.length).toBe(1) + verifyCursorPosition(component, cursorNodes[0], 1, 0) + } + + editor.setSelectedScreenRanges([ + [[0, 0], [0, 3]], + [[1, 0], [1, 4]] + ]) + await component.getNextUpdatePromise() + { + const cursorNodes = Array.from(element.querySelectorAll('.cursor')) + expect(cursorNodes.length).toBe(0) + } + }) + + it('blinks cursors when the editor is focused and the cursors are not moving', async () => { + assertDocumentFocused() + const {component, element, editor} = buildComponent() + component.props.cursorBlinkPeriod = 40 + component.props.cursorBlinkResumeDelay = 40 + editor.addCursorAtScreenPosition([1, 0]) + + element.focus() + await component.getNextUpdatePromise() + const [cursor1, cursor2] = element.querySelectorAll('.cursor') + + expect(getComputedStyle(cursor1).opacity).toBe('1') + expect(getComputedStyle(cursor2).opacity).toBe('1') + + await conditionPromise(() => + getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' + ) + + await conditionPromise(() => + getComputedStyle(cursor1).opacity === '1' && getComputedStyle(cursor2).opacity === '1' + ) + + await conditionPromise(() => + getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' + ) + + editor.moveRight() + await component.getNextUpdatePromise() + + expect(getComputedStyle(cursor1).opacity).toBe('1') + expect(getComputedStyle(cursor2).opacity).toBe('1') + }) + + it('gives cursors at the end of lines the width of an "x" character', async () => { + const {component, element, editor} = buildComponent() + editor.setCursorScreenPosition([0, Infinity]) + await component.getNextUpdatePromise() + expect(element.querySelector('.cursor').offsetWidth).toBe(Math.round(component.getBaseCharacterWidth())) + }) + + it('positions and sizes cursors correctly when they are located next to a fold marker', async () => { + const {component, element, editor} = buildComponent() + editor.foldBufferRange([[0, 3], [0, 6]]) + + editor.setCursorScreenPosition([0, 3]) + await component.getNextUpdatePromise() + const cursor = element.querySelector('.cursor') + verifyCursorPosition(component, element.querySelector('.cursor'), 0, 3) + + editor.setCursorScreenPosition([0, 4]) + await component.getNextUpdatePromise() + verifyCursorPosition(component, element.querySelector('.cursor'), 0, 4) + }) + + it('places the hidden input element at the location of the last cursor if it is visible', async () => { + const {component, element, editor} = buildComponent({height: 60, width: 120, rowsPerTile: 2}) + const {hiddenInput} = component.refs.cursorsAndInput.refs + setScrollTop(component, 100) + await setScrollLeft(component, 40) + + expect(component.getRenderedStartRow()).toBe(4) + expect(component.getRenderedEndRow()).toBe(10) + + // When out of view, the hidden input is positioned at 0, 0 + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + expect(hiddenInput.offsetTop).toBe(0) + expect(hiddenInput.offsetLeft).toBe(0) + + // Otherwise it is positioned at the last cursor position + editor.addCursorAtScreenPosition([7, 4]) + await component.getNextUpdatePromise() + expect(hiddenInput.getBoundingClientRect().top).toBe(clientTopForLine(component, 7)) + expect(Math.round(hiddenInput.getBoundingClientRect().left)).toBe(clientLeftForCharacter(component, 7, 4)) + }) + + it('soft wraps lines based on the content width when soft wrap is enabled', async () => { + let baseCharacterWidth, gutterContainerWidth + { + const {component, editor} = buildComponent() + baseCharacterWidth = component.getBaseCharacterWidth() + gutterContainerWidth = component.getGutterContainerWidth() + editor.destroy() + } + + const {component, element, editor} = buildComponent({ + width: gutterContainerWidth + (baseCharacterWidth * 55), + attach: false + }) + editor.setSoftWrapped(true) + jasmine.attachToDOM(element) + + expect(getEditorWidthInBaseCharacters(component)).toBe(55) + expect(lineNodeForScreenRow(component, 3).textContent).toBe( + ' var pivot = items.shift(), current, left = [], ' + ) + expect(lineNodeForScreenRow(component, 4).textContent).toBe( + ' right = [];' + ) + + await setEditorWidthInCharacters(component, 45) + expect(lineNodeForScreenRow(component, 3).textContent).toBe( + ' var pivot = items.shift(), current, left ' + ) + expect(lineNodeForScreenRow(component, 4).textContent).toBe( + ' = [], right = [];' + ) + + const {scrollContainer} = component.refs + expect(scrollContainer.clientWidth).toBe(scrollContainer.scrollWidth) + }) + + it('accounts for the width of the vertical scrollbar when soft-wrapping lines', async () => { + const {component, element, editor} = buildComponent({ + height: 200, + text: 'a'.repeat(300), + softWrapped: true + }) + await setEditorWidthInCharacters(component, 23) + expect(Math.floor(component.getScrollContainerClientWidth() / component.getBaseCharacterWidth())).toBe(20) + expect(editor.lineLengthForScreenRow(0)).toBe(20) + }) + + it('correctly forces the display layer to index visible rows when resizing (regression)', async () => { + const text = 'a'.repeat(30) + '\n' + 'b'.repeat(1000) + const {component, element, editor} = buildComponent({height: 300, width: 800, attach: false, text}) + editor.setSoftWrapped(true) + jasmine.attachToDOM(element) + + element.style.width = 200 + 'px' + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(24) + }) + + it('decorates the line numbers of folded lines', async () => { + const {component, element, editor} = buildComponent() + editor.foldBufferRow(1) + await component.getNextUpdatePromise() + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('folded')).toBe(true) + }) + + it('makes lines at least as wide as the scrollContainer', async () => { + const {component, element, editor} = buildComponent() + const {scrollContainer, gutterContainer} = component.refs + editor.setText('a') + await component.getNextUpdatePromise() + + expect(element.querySelector('.line').offsetWidth).toBe(scrollContainer.offsetWidth) + }) + + it('resizes based on the content when the autoHeight and/or autoWidth options are true', async () => { + const {component, element, editor} = buildComponent({autoHeight: true, autoWidth: true}) + const editorPadding = 3 + element.style.padding = editorPadding + 'px' + const {gutterContainer, scrollContainer} = component.refs + const initialWidth = element.offsetWidth + const initialHeight = element.offsetHeight + expect(initialWidth).toBe(component.getGutterContainerWidth() + component.getContentWidth() + 2 * editorPadding) + expect(initialHeight).toBe(component.getContentHeight() + 2 * editorPadding) + + // When autoWidth is enabled, width adjusts to content + editor.setCursorScreenPosition([6, Infinity]) + editor.insertText('x'.repeat(50)) + await component.getNextUpdatePromise() + expect(element.offsetWidth).toBe(component.getGutterContainerWidth() + component.getContentWidth() + 2 * editorPadding) + expect(element.offsetWidth).toBeGreaterThan(initialWidth) + + // When autoHeight is enabled, height adjusts to content + editor.insertText('\n'.repeat(5)) + await component.getNextUpdatePromise() + expect(element.offsetHeight).toBe(component.getContentHeight() + 2 * editorPadding) + expect(element.offsetHeight).toBeGreaterThan(initialHeight) + + // When a horizontal scrollbar is visible, autoHeight accounts for it + editor.update({autoWidth: false}) + await component.getNextUpdatePromise() + element.style.width = component.getGutterContainerWidth() + component.getContentHeight() - 20 + 'px' + await component.getNextUpdatePromise() + expect(component.isHorizontalScrollbarVisible()).toBe(true) + expect(component.isVerticalScrollbarVisible()).toBe(false) + expect(element.offsetHeight).toBe(component.getContentHeight() + component.getHorizontalScrollbarHeight() + 2 * editorPadding) + + // When a vertical scrollbar is visible, autoWidth accounts for it + editor.update({autoWidth: true, autoHeight: false}) + await component.getNextUpdatePromise() + element.style.height = component.getContentHeight() - 20 + await component.getNextUpdatePromise() + expect(component.isHorizontalScrollbarVisible()).toBe(false) + expect(component.isVerticalScrollbarVisible()).toBe(true) + expect(element.offsetWidth).toBe( + component.getGutterContainerWidth() + + component.getContentWidth() + + component.getVerticalScrollbarWidth() + + 2 * editorPadding + ) + }) + + it('supports the isLineNumberGutterVisible parameter', () => { + const {component, element, editor} = buildComponent({lineNumberGutterVisible: false}) + expect(element.querySelector('.line-number')).toBe(null) + }) + + it('supports the placeholderText parameter', () => { + const placeholderText = 'Placeholder Test' + const {element} = buildComponent({placeholderText, text: ''}) + expect(element.textContent).toContain(placeholderText) + }) + + it('adds the data-grammar attribute and updates it when the grammar changes', async () => { + await atom.packages.activatePackage('language-javascript') + + const {editor, element, component} = buildComponent() + expect(element.dataset.grammar).toBe('text plain null-grammar') + + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + await component.getNextUpdatePromise() + expect(element.dataset.grammar).toBe('source js') + }) + + it('adds the data-encoding attribute and updates it when the encoding changes', async () => { + const {editor, element, component} = buildComponent() + expect(element.dataset.encoding).toBe('utf8') + + editor.setEncoding('ascii') + await component.getNextUpdatePromise() + expect(element.dataset.encoding).toBe('ascii') + }) + + it('adds the has-selection class when the editor has a non-empty selection', async () => { + const {editor, element, component} = buildComponent() + expect(element.classList.contains('has-selection')).toBe(false) + + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[1, 0], [1, 10]] + ]) + await component.getNextUpdatePromise() + expect(element.classList.contains('has-selection')).toBe(true) + + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[1, 0], [1, 0]] + ]) + await component.getNextUpdatePromise() + expect(element.classList.contains('has-selection')).toBe(false) + }) + + it('assigns a buffer-row to each line number as a data field', async () => { + const {editor, element, component} = buildComponent() + editor.setSoftWrapped(true) + await component.getNextUpdatePromise() + await setEditorWidthInCharacters(component, 40) + + expect( + Array.from(element.querySelectorAll('.line-number:not(.dummy)')) + .map((element) => element.dataset.bufferRow) + ).toEqual([ + '0', '1', '2', '3', '3', '4', '5', '6', '6', '6', + '7', '8', '8', '8', '9', '10', '11', '11', '12' + ]) + }) + + it('does not blow away class names added to the element by packages when changing the class name', async () => { + assertDocumentFocused() + const {component, element, editor} = buildComponent() + element.classList.add('a', 'b') + expect(element.className).toBe('editor a b') + element.focus() + await component.getNextUpdatePromise() + expect(element.className).toBe('editor a b is-focused') + document.body.focus() + await component.getNextUpdatePromise() + expect(element.className).toBe('editor a b') + }) + + it('ignores resize events when the editor is hidden', async () => { + const {component, element, editor} = buildComponent({autoHeight: false}) + element.style.height = 5 * component.getLineHeight() + 'px' + await component.getNextUpdatePromise() + const originalClientContainerHeight = component.getClientContainerHeight() + const originalGutterContainerWidth = component.getGutterContainerWidth() + const originalLineNumberGutterWidth = component.getLineNumberGutterWidth() + expect(originalClientContainerHeight).toBeGreaterThan(0) + expect(originalGutterContainerWidth).toBeGreaterThan(0) + expect(originalLineNumberGutterWidth).toBeGreaterThan(0) + + element.style.display = 'none' + // In production, resize events are triggered before the intersection + // observer detects the editor's visibility has changed. In tests, we are + // unable to reproduce this scenario and so we simulate them. + expect(component.visible).toBe(true) + component.didResize() + component.didResizeGutterContainer() + expect(component.getClientContainerHeight()).toBe(originalClientContainerHeight) + expect(component.getGutterContainerWidth()).toBe(originalGutterContainerWidth) + expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth) + + // Ensure measurements stay the same after receiving the intersection + // observer events. + await conditionPromise(() => !component.visible) + expect(component.getClientContainerHeight()).toBe(originalClientContainerHeight) + expect(component.getGutterContainerWidth()).toBe(originalGutterContainerWidth) + expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth) + }) + }) + + describe('mini editors', () => { + it('adds the mini attribute and class even when the element is not attached', () => { + { + const {element, editor} = buildComponent({mini: true}) + expect(element.hasAttribute('mini')).toBe(true) + expect(element.classList.contains('mini')).toBe(true) + } + + { + const {element, editor} = buildComponent({mini: true, attach: false}) + expect(element.hasAttribute('mini')).toBe(true) + expect(element.classList.contains('mini')).toBe(true) + } + }) + + it('does not render the gutter container', () => { + const {component, element, editor} = buildComponent({mini: true}) + expect(component.refs.gutterContainer).toBeUndefined() + expect(element.querySelector('gutter-container')).toBeNull() + }) + + it('does not render line decorations for the cursor line', async () => { + const {component, element, editor} = buildComponent({mini: true}) + expect(element.querySelector('.line').classList.contains('cursor-line')).toBe(false) + + editor.update({mini: false}) + await component.getNextUpdatePromise() + expect(element.querySelector('.line').classList.contains('cursor-line')).toBe(true) + + editor.update({mini: true}) + await component.getNextUpdatePromise() + expect(element.querySelector('.line').classList.contains('cursor-line')).toBe(false) + }) + + it('does not render scrollbars', async () => { + const {component, element, editor} = buildComponent({mini: true, autoHeight: false}) + await setEditorWidthInCharacters(component, 10) + await setEditorHeightInLines(component, 1) + + editor.setText('x'.repeat(20) + 'y'.repeat(20)) + await component.getNextUpdatePromise() + + expect(component.isHorizontalScrollbarVisible()).toBe(false) + expect(component.isVerticalScrollbarVisible()).toBe(false) + expect(component.refs.horizontalScrollbar).toBeUndefined() + expect(component.refs.verticalScrollbar).toBeUndefined() + }) + }) + + describe('focus', () => { + beforeEach(() => { + assertDocumentFocused() + }) + + it('focuses the hidden input element and adds the is-focused class when focused', async () => { + const {component, element, editor} = buildComponent() + const {hiddenInput} = component.refs.cursorsAndInput.refs + + expect(document.activeElement).not.toBe(hiddenInput) + element.focus() + expect(document.activeElement).toBe(hiddenInput) + await component.getNextUpdatePromise() + expect(element.classList.contains('is-focused')).toBe(true) + + element.focus() // focusing back to the element does not blur + expect(document.activeElement).toBe(hiddenInput) + expect(element.classList.contains('is-focused')).toBe(true) + + document.body.focus() + expect(document.activeElement).not.toBe(hiddenInput) + await component.getNextUpdatePromise() + expect(element.classList.contains('is-focused')).toBe(false) + }) + + it('updates the component when the hidden input is focused directly', async () => { + const {component, element, editor} = buildComponent() + const {hiddenInput} = component.refs.cursorsAndInput.refs + expect(element.classList.contains('is-focused')).toBe(false) + expect(document.activeElement).not.toBe(hiddenInput) + + hiddenInput.focus() + await component.getNextUpdatePromise() + expect(element.classList.contains('is-focused')).toBe(true) + }) + + it('gracefully handles a focus event that occurs prior to the attachedCallback of the element', () => { + const {component, element, editor} = buildComponent({attach: false}) + const parent = document.createElement('text-editor-component-test-element') + parent.appendChild(element) + parent.didAttach = () => element.focus() + jasmine.attachToDOM(parent) + expect(document.activeElement).toBe(component.refs.cursorsAndInput.refs.hiddenInput) + }) + + it('gracefully handles a focus event that occurs prior to detecting the element has become visible', async () => { + const {component, element, editor} = buildComponent({attach: false}) + element.style.display = 'none' + jasmine.attachToDOM(element) + element.style.display = 'block' + element.focus() + await component.getNextUpdatePromise() + + expect(document.activeElement).toBe(component.refs.cursorsAndInput.refs.hiddenInput) + }) + + it('emits blur events only when focus shifts to something other than the editor itself or its hidden input', () => { + const {element} = buildComponent() + + let blurEventCount = 0 + element.addEventListener('blur', () => blurEventCount++) + + element.focus() + expect(blurEventCount).toBe(0) + element.focus() + expect(blurEventCount).toBe(0) + document.body.focus() + expect(blurEventCount).toBe(1) + }) + }) + + describe('autoscroll', () => { + it('automatically scrolls vertically when the requested range is within the vertical scroll margin of the top or bottom', async () => { + const {component, editor} = buildComponent({height: 120}) + expect(component.getLastVisibleRow()).toBe(7) + + editor.scrollToScreenRange([[4, 0], [6, 0]]) + await component.getNextUpdatePromise() + expect(component.getScrollBottom()).toBe((6 + 1 + editor.verticalScrollMargin) * component.getLineHeight()) + + editor.scrollToScreenPosition([8, 0]) + await component.getNextUpdatePromise() + expect(component.getScrollBottom()).toBe((8 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight) + + editor.scrollToScreenPosition([3, 0]) + await component.getNextUpdatePromise() + expect(component.getScrollTop()).toBe((3 - editor.verticalScrollMargin) * component.measurements.lineHeight) + + editor.scrollToScreenPosition([2, 0]) + await component.getNextUpdatePromise() + expect(component.getScrollTop()).toBe(0) + }) + + it('does not vertically autoscroll by more than half of the visible lines if the editor is shorter than twice the scroll margin', async () => { + const {component, element, editor} = buildComponent({autoHeight: false}) + element.style.height = 5.5 * component.measurements.lineHeight + 'px' + await component.getNextUpdatePromise() + expect(component.getLastVisibleRow()).toBe(5) + const scrollMarginInLines = 2 + + editor.scrollToScreenPosition([6, 0]) + await component.getNextUpdatePromise() + expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) + + editor.scrollToScreenPosition([6, 4]) + await component.getNextUpdatePromise() + expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) + + editor.scrollToScreenRange([[4, 4], [6, 4]]) + await component.getNextUpdatePromise() + expect(component.getScrollTop()).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight) + + editor.scrollToScreenRange([[4, 4], [6, 4]], {reversed: false}) + await component.getNextUpdatePromise() + expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) + }) + + it('autoscrolls the given range to the center of the screen if the `center` option is true', async () => { + const {component, editor} = buildComponent({height: 50}) + expect(component.getLastVisibleRow()).toBe(2) + + editor.scrollToScreenRange([[4, 0], [6, 0]], {center: true}) + await component.getNextUpdatePromise() + + const actualScrollCenter = (component.getScrollTop() + component.getScrollBottom()) / 2 + const expectedScrollCenter = Math.round((4 + 7) / 2 * component.getLineHeight()) + expect(actualScrollCenter).toBe(expectedScrollCenter) + }) + + it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the scroll container', async () => { + const {component, element, editor} = buildComponent() + const {scrollContainer} = component.refs + element.style.width = + component.getGutterContainerWidth() + + 3 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px' + await component.getNextUpdatePromise() + + editor.scrollToScreenRange([[1, 12], [2, 28]]) + await component.getNextUpdatePromise() + let expectedScrollLeft = Math.round( + clientLeftForCharacter(component, 1, 12) - + lineNodeForScreenRow(component, 1).getBoundingClientRect().left - + (editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) + ) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + + editor.scrollToScreenRange([[1, 12], [2, 28]], {reversed: false}) + await component.getNextUpdatePromise() + expectedScrollLeft = Math.round( + component.getGutterContainerWidth() + + clientLeftForCharacter(component, 2, 28) - + lineNodeForScreenRow(component, 2).getBoundingClientRect().left + + (editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) - + component.getScrollContainerClientWidth() + ) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + }) + + it('does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin', async () => { + const {component, editor} = buildComponent({autoHeight: false}) + await setEditorWidthInCharacters(component, 1.5 * editor.horizontalScrollMargin) + const editorWidthInChars = component.getScrollContainerWidth() / component.getBaseCharacterWidth() + expect(Math.round(editorWidthInChars)).toBe(9) + + editor.scrollToScreenRange([[6, 10], [6, 15]]) + await component.getNextUpdatePromise() + let expectedScrollLeft = Math.floor( + clientLeftForCharacter(component, 6, 10) - + lineNodeForScreenRow(component, 1).getBoundingClientRect().left - + Math.floor((editorWidthInChars - 1) / 2) * component.getBaseCharacterWidth() + ) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + }) + + it('correctly autoscrolls after inserting a line that exceeds the current content width', async () => { + const {component, element, editor} = buildComponent() + element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + 'px' + await component.getNextUpdatePromise() + + editor.setCursorScreenPosition([0, Infinity]) + editor.insertText('x'.repeat(100)) + await component.getNextUpdatePromise() + + expect(component.getScrollLeft()).toBe(component.getScrollWidth() - component.getScrollContainerClientWidth()) + }) + + it('accounts for the presence of horizontal scrollbars that appear during the same frame as the autoscroll', async () => { + const {component, element, editor} = buildComponent({autoHeight: false}) + const {scrollContainer} = component.refs + element.style.height = component.getContentHeight() / 2 + 'px' + element.style.width = component.getScrollWidth() + 'px' + await component.getNextUpdatePromise() + + editor.setCursorScreenPosition([10, Infinity]) + editor.insertText('\n\n' + 'x'.repeat(100)) + await component.getNextUpdatePromise() + + expect(component.getScrollTop()).toBe(component.getScrollHeight() - component.getScrollContainerClientHeight()) + expect(component.getScrollLeft()).toBe(component.getScrollWidth() - component.getScrollContainerClientWidth()) + + // Scrolling to the top should not throw an error. This failed + // previously due to horizontalPositionsToMeasure not being empty after + // autoscrolling vertically to account for the horizontal scrollbar. + spyOn(window, 'onerror') + await setScrollTop(component, 0) + expect(window.onerror).not.toHaveBeenCalled() + }) + }) + + describe('logical scroll positions', () => { + it('allows the scrollTop to be changed and queried in terms of rows via setScrollTopRow and getScrollTopRow', () => { + const {component, element, editor} = buildComponent({attach: false, height: 80}) + + // Caches the scrollTopRow if we don't have measurements + component.setScrollTopRow(6) + expect(component.getScrollTopRow()).toBe(6) + + // Assigns the scrollTop based on the logical position when attached + jasmine.attachToDOM(element) + const expectedScrollTop = Math.round(6 * component.getLineHeight()) + expect(component.getScrollTopRow()).toBe(6) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + + // Allows the scrollTopRow to be updated while attached + component.setScrollTopRow(4) + expect(component.getScrollTopRow()).toBe(4) + expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight())) + + // Preserves the scrollTopRow when sdetached + element.remove() + expect(component.getScrollTopRow()).toBe(4) + expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight())) + + component.setScrollTopRow(6) + expect(component.getScrollTopRow()).toBe(6) + expect(component.getScrollTop()).toBe(Math.round(6 * component.getLineHeight())) + + jasmine.attachToDOM(element) + element.style.height = '60px' + expect(component.getScrollTopRow()).toBe(6) + expect(component.getScrollTop()).toBe(Math.round(6 * component.getLineHeight())) + }) + + it('allows the scrollLeft to be changed and queried in terms of base character columns via setScrollLeftColumn and getScrollLeftColumn', () => { + const {component, element} = buildComponent({attach: false, width: 80}) + + // Caches the scrollTopRow if we don't have measurements + component.setScrollLeftColumn(2) + expect(component.getScrollLeftColumn()).toBe(2) + + // Assigns the scrollTop based on the logical position when attached + jasmine.attachToDOM(element) + expect(component.getScrollLeft()).toBe(Math.round(2 * component.getBaseCharacterWidth())) + + // Allows the scrollTopRow to be updated while attached + component.setScrollLeftColumn(4) + expect(component.getScrollLeft()).toBe(Math.round(4 * component.getBaseCharacterWidth())) + + // Preserves the scrollTopRow when detached + element.remove() + expect(component.getScrollLeft()).toBe(Math.round(4 * component.getBaseCharacterWidth())) + + component.setScrollLeftColumn(6) + expect(component.getScrollLeft()).toBe(Math.round(6 * component.getBaseCharacterWidth())) + + jasmine.attachToDOM(element) + element.style.width = '60px' + expect(component.getScrollLeft()).toBe(Math.round(6 * component.getBaseCharacterWidth())) + }) + }) + + describe('scrolling via the mouse wheel', () => { + it('scrolls vertically when deltaY is not 0', () => { + const mouseWheelScrollSensitivity = 0.4 + const {component, editor} = buildComponent({height: 50, mouseWheelScrollSensitivity}) + + { + const expectedScrollTop = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 0, deltaY: 20}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + } + + { + const expectedScrollTop = component.getScrollTop() - (10 * mouseWheelScrollSensitivity) + component.didMouseWheel({deltaX: 0, deltaY: -10}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + } + }) + + it('scrolls horizontally when deltaX is not 0', () => { + const mouseWheelScrollSensitivity = 0.4 + const {component, editor} = buildComponent({width: 50, mouseWheelScrollSensitivity}) + + { + const expectedScrollLeft = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 20, deltaY: 0}) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + expect(component.refs.content.style.transform).toBe(`translate(-${expectedScrollLeft}px, 0px)`) + } + + { + const expectedScrollLeft = component.getScrollLeft() - (10 * mouseWheelScrollSensitivity) + component.didMouseWheel({deltaX: -10, deltaY: 0}) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + expect(component.refs.content.style.transform).toBe(`translate(-${expectedScrollLeft}px, 0px)`) + } + }) + + it('inverts deltaX and deltaY when holding shift on Windows and Linux', async () => { + const mouseWheelScrollSensitivity = 0.4 + const {component, editor} = buildComponent({height: 50, width: 50, mouseWheelScrollSensitivity}) + + component.props.platform = 'linux' + { + const expectedScrollTop = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 0, deltaY: 20}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + await setScrollTop(component, 0) + } + + { + const expectedScrollLeft = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 0, deltaY: 20, shiftKey: true}) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + expect(component.refs.content.style.transform).toBe(`translate(-${expectedScrollLeft}px, 0px)`) + await setScrollLeft(component, 0) + } + + { + const expectedScrollTop = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 20, deltaY: 0, shiftKey: true}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + await setScrollTop(component, 0) + } + + component.props.platform = 'win32' + { + const expectedScrollTop = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 0, deltaY: 20}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + await setScrollTop(component, 0) + } + + { + const expectedScrollLeft = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 0, deltaY: 20, shiftKey: true}) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + expect(component.refs.content.style.transform).toBe(`translate(-${expectedScrollLeft}px, 0px)`) + await setScrollLeft(component, 0) + } + + { + const expectedScrollTop = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 20, deltaY: 0, shiftKey: true}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + await setScrollTop(component, 0) + } + + component.props.platform = 'darwin' + { + const expectedScrollTop = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 0, deltaY: 20}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + await setScrollTop(component, 0) + } + + { + const expectedScrollTop = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 0, deltaY: 20, shiftKey: true}) + expect(component.getScrollTop()).toBe(expectedScrollTop) + expect(component.refs.content.style.transform).toBe(`translate(0px, -${expectedScrollTop}px)`) + await setScrollTop(component, 0) + } + + { + const expectedScrollLeft = 20 * mouseWheelScrollSensitivity + component.didMouseWheel({deltaX: 20, deltaY: 0, shiftKey: true}) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) + expect(component.refs.content.style.transform).toBe(`translate(-${expectedScrollLeft}px, 0px)`) + await setScrollLeft(component, 0) + } + }) + }) + + describe('line and line number decorations', () => { + it('adds decoration classes on screen lines spanned by decorated markers', async () => { + const {component, element, editor} = buildComponent({softWrapped: true}) + await setEditorWidthInCharacters(component, 55) + expect(lineNodeForScreenRow(component, 3).textContent).toBe( + ' var pivot = items.shift(), current, left = [], ' + ) + expect(lineNodeForScreenRow(component, 4).textContent).toBe( + ' right = [];' + ) + + const marker1 = editor.markScreenRange([[1, 10], [3, 10]]) + const layer = editor.addMarkerLayer() + const marker2 = layer.markScreenPosition([5, 0]) + const marker3 = layer.markScreenPosition([8, 0]) + const marker4 = layer.markScreenPosition([10, 0]) + const markerDecoration = editor.decorateMarker(marker1, {type: ['line', 'line-number'], class: 'a'}) + const layerDecoration = editor.decorateMarkerLayer(layer, {type: ['line', 'line-number'], class: 'b'}) + layerDecoration.setPropertiesForMarker(marker4, {type: 'line', class: 'c'}) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 10).classList.contains('b')).toBe(false) + expect(lineNodeForScreenRow(component, 10).classList.contains('c')).toBe(true) + + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 2).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 3).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 4).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 5).classList.contains('b')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 8).classList.contains('b')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 10).classList.contains('b')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 10).classList.contains('c')).toBe(false) + + marker1.setScreenRange([[5, 0], [8, 0]]) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 5).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 6).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 7).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 8).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe(true) + + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 2).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 3).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 4).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 5).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 5).classList.contains('b')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 6).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 7).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 8).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 8).classList.contains('b')).toBe(true) + }) + + it('honors the onlyEmpty and onlyNonEmpty decoration options', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenPosition([1, 0]) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'a', onlyEmpty: true}) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'b', onlyNonEmpty: true}) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'c'}) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(false) + expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('b')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + + marker.setScreenRange([[1, 0], [2, 4]]) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('c')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('b')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 2).classList.contains('b')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 2).classList.contains('c')).toBe(true) + }) + + it('honors the onlyHead option', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenRange([[1, 4], [3, 4]]) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'a', onlyHead: true}) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 3).classList.contains('a')).toBe(true) + }) + + it('only decorates the last row of non-empty ranges that end at column 0 if omitEmptyLastRow is false', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenRange([[1, 0], [3, 0]]) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'a'}) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'b', omitEmptyLastRow: false}) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(false) + + expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 3).classList.contains('b')).toBe(true) + }) + + it('does not decorate invalidated markers', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenRange([[1, 0], [3, 0]], {invalidate: 'touch'}) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'a'}) + await component.getNextUpdatePromise() + expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(true) + + editor.getBuffer().insert([2, 0], 'x') + expect(marker.isValid()).toBe(false) + await component.getNextUpdatePromise() + expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(false) + }) + }) + + describe('highlight decorations', () => { + it('renders single-line highlights', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenRange([[1, 2], [1, 10]]) + editor.decorateMarker(marker, {type: 'highlight', class: 'a'}) + await component.getNextUpdatePromise() + + { + const regions = element.querySelectorAll('.highlight.a .region.a') + expect(regions.length).toBe(1) + const regionRect = regions[0].getBoundingClientRect() + expect(regionRect.top).toBe(lineNodeForScreenRow(component, 1).getBoundingClientRect().top) + expect(Math.round(regionRect.left)).toBe(clientLeftForCharacter(component, 1, 2)) + expect(Math.round(regionRect.right)).toBe(clientLeftForCharacter(component, 1, 10)) + } + + marker.setScreenRange([[1, 4], [1, 8]]) + await component.getNextUpdatePromise() + + { + const regions = element.querySelectorAll('.highlight.a .region.a') + expect(regions.length).toBe(1) + const regionRect = regions[0].getBoundingClientRect() + expect(regionRect.top).toBe(lineNodeForScreenRow(component, 1).getBoundingClientRect().top) + expect(regionRect.bottom).toBe(lineNodeForScreenRow(component, 1).getBoundingClientRect().bottom) + expect(Math.round(regionRect.left)).toBe(clientLeftForCharacter(component, 1, 4)) + expect(Math.round(regionRect.right)).toBe(clientLeftForCharacter(component, 1, 8)) + } + }) + + it('renders multi-line highlights that span across tiles', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3}) + const marker = editor.markScreenRange([[2, 4], [3, 4]]) + editor.decorateMarker(marker, {type: 'highlight', class: 'a'}) + + await component.getNextUpdatePromise() + + { + // We have 2 top-level highlight divs due to the regions being split + // across 2 different tiles + expect(element.querySelectorAll('.highlight.a').length).toBe(2) + + const regions = element.querySelectorAll('.highlight.a .region.a') + expect(regions.length).toBe(2) + const region0Rect = regions[0].getBoundingClientRect() + expect(region0Rect.top).toBe(lineNodeForScreenRow(component, 2).getBoundingClientRect().top) + expect(region0Rect.bottom).toBe(lineNodeForScreenRow(component, 2).getBoundingClientRect().bottom) + expect(Math.round(region0Rect.left)).toBe(clientLeftForCharacter(component, 2, 4)) + expect(Math.round(region0Rect.right)).toBe(component.refs.content.getBoundingClientRect().right) + + const region1Rect = regions[1].getBoundingClientRect() + expect(region1Rect.top).toBe(lineNodeForScreenRow(component, 3).getBoundingClientRect().top) + expect(region1Rect.bottom).toBe(lineNodeForScreenRow(component, 3).getBoundingClientRect().bottom) + expect(Math.round(region1Rect.left)).toBe(clientLeftForCharacter(component, 3, 0)) + expect(Math.round(region1Rect.right)).toBe(clientLeftForCharacter(component, 3, 4)) + } + + marker.setScreenRange([[2, 4], [5, 4]]) + await component.getNextUpdatePromise() + + { + // Still split across 2 tiles + expect(element.querySelectorAll('.highlight.a').length).toBe(2) + + const regions = element.querySelectorAll('.highlight.a .region.a') + expect(regions.length).toBe(4) // Each tile renders its + + const region0Rect = regions[0].getBoundingClientRect() + expect(region0Rect.top).toBe(lineNodeForScreenRow(component, 2).getBoundingClientRect().top) + expect(region0Rect.bottom).toBe(lineNodeForScreenRow(component, 2).getBoundingClientRect().bottom) + expect(Math.round(region0Rect.left)).toBe(clientLeftForCharacter(component, 2, 4)) + expect(Math.round(region0Rect.right)).toBe(component.refs.content.getBoundingClientRect().right) + + const region1Rect = regions[1].getBoundingClientRect() + expect(region1Rect.top).toBe(lineNodeForScreenRow(component, 3).getBoundingClientRect().top) + expect(region1Rect.bottom).toBe(lineNodeForScreenRow(component, 4).getBoundingClientRect().top) + expect(Math.round(region1Rect.left)).toBe(component.refs.content.getBoundingClientRect().left) + expect(Math.round(region1Rect.right)).toBe(component.refs.content.getBoundingClientRect().right) + + const region2Rect = regions[2].getBoundingClientRect() + expect(region2Rect.top).toBe(lineNodeForScreenRow(component, 4).getBoundingClientRect().top) + expect(region2Rect.bottom).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().top) + expect(Math.round(region2Rect.left)).toBe(component.refs.content.getBoundingClientRect().left) + expect(Math.round(region2Rect.right)).toBe(component.refs.content.getBoundingClientRect().right) + + const region3Rect = regions[3].getBoundingClientRect() + expect(region3Rect.top).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().top) + expect(region3Rect.bottom).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().bottom) + expect(Math.round(region3Rect.left)).toBe(component.refs.content.getBoundingClientRect().left) + expect(Math.round(region3Rect.right)).toBe(clientLeftForCharacter(component, 5, 4)) + } + }) + + it('can flash highlight decorations', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, height: 200}) + const marker = editor.markScreenRange([[2, 4], [3, 4]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'a'}) + decoration.flash('b', 10) + + // Flash on initial appearence of highlight + await component.getNextUpdatePromise() + const highlights = element.querySelectorAll('.highlight.a') + expect(highlights.length).toBe(2) // split across 2 tiles + + expect(highlights[0].classList.contains('b')).toBe(true) + expect(highlights[1].classList.contains('b')).toBe(true) + + await conditionPromise(() => + !highlights[0].classList.contains('b') && + !highlights[1].classList.contains('b') + ) + + // Don't flash on next update if another flash wasn't requested + await setScrollTop(component, 100) + expect(highlights[0].classList.contains('b')).toBe(false) + expect(highlights[1].classList.contains('b')).toBe(false) + + // Flash existing highlight + decoration.flash('c', 100) + await component.getNextUpdatePromise() + expect(highlights[0].classList.contains('c')).toBe(true) + expect(highlights[1].classList.contains('c')).toBe(true) + + // Add second flash class + decoration.flash('d', 100) + await component.getNextUpdatePromise() + expect(highlights[0].classList.contains('c')).toBe(true) + expect(highlights[1].classList.contains('c')).toBe(true) + expect(highlights[0].classList.contains('d')).toBe(true) + expect(highlights[1].classList.contains('d')).toBe(true) + + await conditionPromise(() => + !highlights[0].classList.contains('c') && + !highlights[1].classList.contains('c') && + !highlights[0].classList.contains('d') && + !highlights[1].classList.contains('d') + ) + + // Flashing the same class again before the first flash completes + // removes the flash class and adds it back on the next frame to ensure + // CSS transitions apply to the second flash. + decoration.flash('e', 100) + await component.getNextUpdatePromise() + expect(highlights[0].classList.contains('e')).toBe(true) + expect(highlights[1].classList.contains('e')).toBe(true) + + decoration.flash('e', 100) + await component.getNextUpdatePromise() + expect(highlights[0].classList.contains('e')).toBe(false) + expect(highlights[1].classList.contains('e')).toBe(false) + + await conditionPromise(() => + highlights[0].classList.contains('e') && + highlights[1].classList.contains('e') + ) + + await conditionPromise(() => + !highlights[0].classList.contains('e') && + !highlights[1].classList.contains('e') + ) + }) + + it('supports layer decorations', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 12}) + const markerLayer = editor.addMarkerLayer() + const marker1 = markerLayer.markScreenRange([[2, 4], [3, 4]]) + const marker2 = markerLayer.markScreenRange([[5, 6], [7, 8]]) + const decoration = editor.decorateMarkerLayer(markerLayer, {type: 'highlight', class: 'a'}) + await component.getNextUpdatePromise() + + const highlights = element.querySelectorAll('.highlight') + expect(highlights[0].classList.contains('a')).toBe(true) + expect(highlights[1].classList.contains('a')).toBe(true) + + decoration.setPropertiesForMarker(marker1, {type: 'highlight', class: 'b'}) + await component.getNextUpdatePromise() + expect(highlights[0].classList.contains('b')).toBe(true) + expect(highlights[1].classList.contains('a')).toBe(true) + + decoration.setPropertiesForMarker(marker1, null) + decoration.setPropertiesForMarker(marker2, {type: 'highlight', class: 'c'}) + await component.getNextUpdatePromise() + expect(highlights[0].classList.contains('a')).toBe(true) + expect(highlights[1].classList.contains('c')).toBe(true) + }) + + it('clears highlights when recycling a tile that previously contained highlights and now does not', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 2, autoHeight: false}) + await setEditorHeightInLines(component, 2) + const marker = editor.markScreenRange([[1, 2], [1, 10]]) + editor.decorateMarker(marker, {type: 'highlight', class: 'a'}) + + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.highlight.a').length).toBe(1) + + await setScrollTop(component, component.getLineHeight() * 3) + expect(element.querySelectorAll('.highlight.a').length).toBe(0) + }) + }) + + describe('overlay decorations', () => { + function attachFakeWindow (component) { + const fakeWindow = document.createElement('div') + fakeWindow.style.position = 'absolute' + fakeWindow.style.padding = 20 + 'px' + fakeWindow.style.backgroundColor = 'blue' + fakeWindow.appendChild(component.element) + jasmine.attachToDOM(fakeWindow) + spyOn(component, 'getWindowInnerWidth').andCallFake(() => fakeWindow.getBoundingClientRect().width) + spyOn(component, 'getWindowInnerHeight').andCallFake(() => fakeWindow.getBoundingClientRect().height) + return fakeWindow + } + + it('renders overlay elements at the specified screen position unless it would overflow the window', async () => { + const {component, element, editor} = buildComponent({width: 200, height: 100, attach: false}) + const fakeWindow = attachFakeWindow(component) + + await setScrollTop(component, 50) + await setScrollLeft(component, 100) + + const marker = editor.markScreenPosition([4, 25]) + + const overlayElement = document.createElement('div') + overlayElement.style.width = '50px' + overlayElement.style.height = '50px' + overlayElement.style.margin = '3px' + overlayElement.style.backgroundColor = 'red' + + const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'}) + await component.getNextUpdatePromise() + + const overlayWrapper = overlayElement.parentElement + expect(overlayWrapper.classList.contains('a')).toBe(true) + expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) + expect(overlayWrapper.getBoundingClientRect().left).toBe(clientLeftForCharacter(component, 4, 25)) + + // Updates the horizontal position on scroll + await setScrollLeft(component, 150) + expect(overlayWrapper.getBoundingClientRect().left).toBe(clientLeftForCharacter(component, 4, 25)) + + // Shifts the overlay horizontally to ensure the overlay element does not + // overflow the window + await setScrollLeft(component, 30) + expect(overlayElement.getBoundingClientRect().right).toBe(fakeWindow.getBoundingClientRect().right) + await setScrollLeft(component, 280) + expect(overlayElement.getBoundingClientRect().left).toBe(fakeWindow.getBoundingClientRect().left) + + // Updates the vertical position on scroll + await setScrollTop(component, 60) + expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) + + // Flips the overlay vertically to ensure the overlay element does not + // overflow the bottom of the window + setScrollLeft(component, 100) + await setScrollTop(component, 0) + expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4)) + + // Flips the overlay vertically on overlay resize if necessary + await setScrollTop(component, 20) + expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) + overlayElement.style.height = 60 + 'px' + await component.getNextUpdatePromise() + expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4)) + + // Does not flip the overlay vertically if it would overflow the top of the window + overlayElement.style.height = 80 + 'px' + await component.getNextUpdatePromise() + expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) + + // Can update overlay wrapper class + decoration.setProperties({type: 'overlay', item: overlayElement, class: 'b'}) + await component.getNextUpdatePromise() + expect(overlayWrapper.classList.contains('a')).toBe(false) + expect(overlayWrapper.classList.contains('b')).toBe(true) + + decoration.setProperties({type: 'overlay', item: overlayElement}) + await component.getNextUpdatePromise() + expect(overlayWrapper.classList.contains('b')).toBe(false) + }) + + it('does not attempt to avoid overflowing the window if `avoidOverflow` is false on the decoration', async () => { + const {component, element, editor} = buildComponent({width: 200, height: 100, attach: false}) + const fakeWindow = attachFakeWindow(component) + const overlayElement = document.createElement('div') + overlayElement.style.width = '50px' + overlayElement.style.height = '50px' + overlayElement.style.margin = '3px' + overlayElement.style.backgroundColor = 'red' + const marker = editor.markScreenPosition([4, 25]) + const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, avoidOverflow: false}) + await component.getNextUpdatePromise() + + await setScrollLeft(component, 30) + expect(overlayElement.getBoundingClientRect().right).toBeGreaterThan(fakeWindow.getBoundingClientRect().right) + await setScrollLeft(component, 280) + expect(overlayElement.getBoundingClientRect().left).toBeLessThan(fakeWindow.getBoundingClientRect().left) + }) + }) + + describe('custom gutter decorations', () => { + it('arranges custom gutters based on their priority', async () => { + const {component, element, editor} = buildComponent() + editor.addGutter({name: 'e', priority: 2}) + editor.addGutter({name: 'a', priority: -2}) + editor.addGutter({name: 'd', priority: 1}) + editor.addGutter({name: 'b', priority: -1}) + editor.addGutter({name: 'c', priority: 0}) + + await component.getNextUpdatePromise() + const gutters = component.refs.gutterContainer.element.querySelectorAll('.gutter') + expect(Array.from(gutters).map((g) => g.getAttribute('gutter-name'))).toEqual([ + 'a', 'b', 'c', 'line-number', 'd', 'e' + ]) + }) + + it('adjusts the left edge of the scroll container based on changes to the gutter container width', async () => { + const {component, element, editor} = buildComponent() + const {scrollContainer, gutterContainer} = component.refs + + function checkScrollContainerLeft () { + expect(scrollContainer.getBoundingClientRect().left).toBe(Math.round(gutterContainer.element.getBoundingClientRect().right)) + } + + checkScrollContainerLeft() + const gutterA = editor.addGutter({name: 'a'}) + await component.getNextUpdatePromise() + checkScrollContainerLeft() + + const gutterB = editor.addGutter({name: 'b'}) + await component.getNextUpdatePromise() + checkScrollContainerLeft() + + gutterA.getElement().style.width = 100 + 'px' + await component.getNextUpdatePromise() + checkScrollContainerLeft() + + gutterA.hide() + await component.getNextUpdatePromise() + checkScrollContainerLeft() + + gutterA.show() + await component.getNextUpdatePromise() + checkScrollContainerLeft() + + gutterA.destroy() + await component.getNextUpdatePromise() + checkScrollContainerLeft() + + gutterB.destroy() + await component.getNextUpdatePromise() + checkScrollContainerLeft() + }) + + it('allows the element of custom gutters to be retrieved before being rendered in the editor component', async () => { + const {component, element, editor} = buildComponent() + const [lineNumberGutter] = editor.getGutters() + const gutterA = editor.addGutter({name: 'a', priority: -1}) + const gutterB = editor.addGutter({name: 'b', priority: 1}) + + const lineNumberGutterElement = lineNumberGutter.getElement() + const gutterAElement = gutterA.getElement() + const gutterBElement = gutterB.getElement() + + await component.getNextUpdatePromise() + + expect(element.contains(lineNumberGutterElement)).toBe(true) + expect(element.contains(gutterAElement)).toBe(true) + expect(element.contains(gutterBElement)).toBe(true) + }) + + it('can show and hide custom gutters', async () => { + const {component, element, editor} = buildComponent() + const gutterA = editor.addGutter({name: 'a', priority: -1}) + const gutterB = editor.addGutter({name: 'b', priority: 1}) + const gutterAElement = gutterA.getElement() + const gutterBElement = gutterB.getElement() + + await component.getNextUpdatePromise() + expect(gutterAElement.style.display).toBe('') + expect(gutterBElement.style.display).toBe('') + + gutterA.hide() + await component.getNextUpdatePromise() + expect(gutterAElement.style.display).toBe('none') + expect(gutterBElement.style.display).toBe('') + + gutterB.hide() + await component.getNextUpdatePromise() + expect(gutterAElement.style.display).toBe('none') + expect(gutterBElement.style.display).toBe('none') + + gutterA.show() + await component.getNextUpdatePromise() + expect(gutterAElement.style.display).toBe('') + expect(gutterBElement.style.display).toBe('none') + }) + + it('renders decorations in custom gutters', async () => { + const {component, element, editor} = buildComponent() + const gutterA = editor.addGutter({name: 'a', priority: -1}) + const gutterB = editor.addGutter({name: 'b', priority: 1}) + const marker1 = editor.markScreenRange([[2, 0], [4, 0]]) + const marker2 = editor.markScreenRange([[6, 0], [7, 0]]) + const marker3 = editor.markScreenRange([[9, 0], [12, 0]]) + const decorationElement1 = document.createElement('div') + const decorationElement2 = document.createElement('div') + + const decoration1 = gutterA.decorateMarker(marker1, {class: 'a'}) + const decoration2 = gutterA.decorateMarker(marker2, {class: 'b', item: decorationElement1}) + const decoration3 = gutterB.decorateMarker(marker3, {item: decorationElement2}) + await component.getNextUpdatePromise() + + let [decorationNode1, decorationNode2] = gutterA.getElement().firstChild.children + const [decorationNode3] = gutterB.getElement().firstChild.children + + expect(decorationNode1.className).toBe('a') + expect(decorationNode1.getBoundingClientRect().top).toBe(clientTopForLine(component, 2)) + expect(decorationNode1.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 5)) + expect(decorationNode1.firstChild).toBeNull() + + expect(decorationNode2.className).toBe('b') + expect(decorationNode2.getBoundingClientRect().top).toBe(clientTopForLine(component, 6)) + expect(decorationNode2.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 8)) + expect(decorationNode2.firstChild).toBe(decorationElement1) + + expect(decorationNode3.className).toBe('') + expect(decorationNode3.getBoundingClientRect().top).toBe(clientTopForLine(component, 9)) + expect(decorationNode3.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 12) + component.getLineHeight()) + expect(decorationNode3.firstChild).toBe(decorationElement2) + + decoration1.setProperties({type: 'gutter', gutterName: 'a', class: 'c', item: decorationElement1}) + decoration2.setProperties({type: 'gutter', gutterName: 'a', item: decorationElement2}) + decoration3.destroy() + await component.getNextUpdatePromise() + expect(decorationNode1.className).toBe('c') + expect(decorationNode1.firstChild).toBe(decorationElement1) + expect(decorationNode2.className).toBe('') + expect(decorationNode2.firstChild).toBe(decorationElement2) + expect(gutterB.getElement().firstChild.children.length).toBe(0) + }) + }) + + describe('block decorations', () => { + it('renders visible block decorations between the appropriate lines, refreshing and measuring them as needed', async () => { + const editor = buildEditor({autoHeight: false}) + const {item: item1, decoration: decoration1} = createBlockDecorationAtScreenRow(editor, 0, {height: 11, position: 'before'}) + const {item: item2, decoration: decoration2} = createBlockDecorationAtScreenRow(editor, 2, {height: 22, margin: 10, position: 'before'}) + + // render an editor that already contains some block decorations + const {component, element} = buildComponent({editor, rowsPerTile: 3}) + await setEditorHeightInLines(component, 4) + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item1) + getElementHeight(item2) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item1) + getElementHeight(item2)}, + {tileStartRow: 3, height: 3 * component.getLineHeight()} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(item1.previousSibling.className).toBe('highlights') + expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2)) + + // add block decorations + const {item: item3, decoration: decoration3} = createBlockDecorationAtScreenRow(editor, 4, {height: 33, position: 'before'}) + const {item: item4, decoration: decoration4} = createBlockDecorationAtScreenRow(editor, 7, {height: 44, position: 'before'}) + const {item: item5, decoration: decoration5} = createBlockDecorationAtScreenRow(editor, 7, {height: 55, position: 'after'}) + const {item: item6, decoration: decoration6} = createBlockDecorationAtScreenRow(editor, 12, {height: 66, position: 'after'}) + await component.getNextUpdatePromise() + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item1) + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item1) + getElementHeight(item2)}, + {tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item3)} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(item1.previousSibling.className).toBe('highlights') + expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2)) + expect(item3.previousSibling).toBe(lineNodeForScreenRow(component, 3)) + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 4)) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(element.contains(item6)).toBe(false) + + // destroy decoration1 + decoration1.destroy() + await component.getNextUpdatePromise() + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2)}, + {tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item3)} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2)) + expect(item3.previousSibling).toBe(lineNodeForScreenRow(component, 3)) + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 4)) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(element.contains(item6)).toBe(false) + + // move decoration2 and decoration3 + decoration2.getMarker().setHeadScreenPosition([1, 0]) + decoration3.getMarker().setHeadScreenPosition([0, 0]) + await component.getNextUpdatePromise() + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3)}, + {tileStartRow: 3, height: 3 * component.getLineHeight()} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item3.previousSibling.className).toBe('highlights') + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(element.contains(item6)).toBe(false) + + // change the text + editor.getBuffer().setTextInRange([[0, 5], [0, 5]], '\n\n') + await component.getNextUpdatePromise() + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item3)}, + {tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item2)} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling.className).toBe('highlights') + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3)) + expect(item3.previousSibling.className).toBe('highlights') + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(element.contains(item4)).toBe(false) + expect(element.contains(item5)).toBe(false) + expect(element.contains(item6)).toBe(false) + + // scroll past the first tile + await setScrollTop(component, 3 * component.getLineHeight() + getElementHeight(item3)) + expect(component.getRenderedStartRow()).toBe(3) + expect(component.getRenderedEndRow()).toBe(12) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item2)}, + {tileStartRow: 6, height: 3 * component.getLineHeight()} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling.className).toBe('highlights') + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3)) + expect(element.contains(item3)).toBe(false) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 9)) + expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 9)) + expect(element.contains(item6)).toBe(false) + await setScrollTop(component, 0) + + // undo the previous change + editor.undo() + await component.getNextUpdatePromise() + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3)}, + {tileStartRow: 3, height: 3 * component.getLineHeight()} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item3.previousSibling.className).toBe('highlights') + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(element.contains(item6)).toBe(false) + + // invalidate decorations. this also tests a case where two decorations in + // the same tile change their height without affecting the tile height nor + // the content height. + item3.style.height = '22px' + item3.style.margin = '10px' + item2.style.height = '33px' + item2.style.margin = '0px' + component.invalidateBlockDecorationDimensions(decoration2) + component.invalidateBlockDecorationDimensions(decoration3) + await component.getNextUpdatePromise() + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3)}, + {tileStartRow: 3, height: 3 * component.getLineHeight()} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item3.previousSibling.className).toBe('highlights') + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(element.contains(item6)).toBe(false) + + // make decoration before row 0 as wide as the editor, and insert some text into it so that it wraps. + item3.style.height = '' + item3.style.margin = '' + item3.style.width = '' + item3.style.wordWrap = 'break-word' + const contentWidthInCharacters = Math.floor(component.getScrollContainerClientWidth() / component.getBaseCharacterWidth()) + item3.textContent = 'x'.repeat(contentWidthInCharacters * 2) + component.invalidateBlockDecorationDimensions(decoration3) + await component.getNextUpdatePromise() + + // make the editor wider, so that the decoration doesn't wrap anymore. + component.element.style.width = ( + component.getGutterContainerWidth() + + component.getScrollContainerClientWidth() * 2 + + component.getVerticalScrollbarWidth() + ) + 'px' + await component.getNextUpdatePromise() + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(9) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3)}, + {tileStartRow: 3, height: 3 * component.getLineHeight()} + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item3.previousSibling.className).toBe('highlights') + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(element.contains(item6)).toBe(false) + + // make the editor taller and wider and the same time, ensuring the number + // of rendered lines is correct. + setEditorHeightInLines(component, 13) + await setEditorWidthInCharacters(component, 50) + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(13) + expect(component.getScrollHeight()).toBe( + editor.getScreenLineCount() * component.getLineHeight() + + getElementHeight(item2) + getElementHeight(item3) + + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) + ) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3)}, + {tileStartRow: 3, height: 3 * component.getLineHeight()}, + {tileStartRow: 6, height: 3 * component.getLineHeight() + getElementHeight(item4) + getElementHeight(item5)}, + ]) + assertLinesAreAlignedWithLineNumbers(component) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13) + expect(element.contains(item1)).toBe(false) + expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(item3.previousSibling.className).toBe('highlights') + expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)) + expect(item4.previousSibling).toBe(lineNodeForScreenRow(component, 6)) + expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)) + expect(item5.nextSibling).toBe(lineNodeForScreenRow(component, 8)) + expect(item6.previousSibling).toBe(lineNodeForScreenRow(component, 12)) + }) + + function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, position}) { + const marker = editor.markScreenPosition([screenRow, 0], {invalidate: 'never'}) + const item = document.createElement('div') + item.style.height = height + 'px' + if (margin != null) item.style.margin = margin + 'px' + item.style.width = 30 + 'px' + const decoration = editor.decorateMarker(marker, {type: 'block', item, position}) + return {item, decoration} + } + + function assertTilesAreSizedAndPositionedCorrectly (component, tiles) { + let top = 0 + for (let tile of tiles) { + const linesTileElement = lineNodeForScreenRow(component, tile.tileStartRow).parentElement + const linesTileBoundingRect = linesTileElement.getBoundingClientRect() + expect(linesTileBoundingRect.height).toBe(tile.height) + expect(linesTileBoundingRect.top).toBe(top) + + const lineNumbersTileElement = lineNumberNodeForScreenRow(component, tile.tileStartRow).parentElement + const lineNumbersTileBoundingRect = lineNumbersTileElement.getBoundingClientRect() + expect(lineNumbersTileBoundingRect.height).toBe(tile.height) + expect(lineNumbersTileBoundingRect.top).toBe(top) + + top += tile.height + } + } + + function assertLinesAreAlignedWithLineNumbers (component) { + const startRow = component.getRenderedStartRow() + const endRow = component.getRenderedEndRow() + for (let row = startRow; row < endRow; row++) { + const lineNode = lineNodeForScreenRow(component, row) + const lineNumberNode = lineNumberNodeForScreenRow(component, row) + expect(lineNumberNode.getBoundingClientRect().top).toBe(lineNode.getBoundingClientRect().top) + } + } + }) + + describe('cursor decorations', () => { + it('allows default cursors to be customized', async () => { + const {component, element, editor} = buildComponent() + + editor.addCursorAtScreenPosition([1, 0]) + const [cursorMarker1, cursorMarker2] = editor.getCursors().map(c => c.getMarker()) + + editor.decorateMarker(cursorMarker1, {type: 'cursor', class: 'a'}) + editor.decorateMarker(cursorMarker2, {type: 'cursor', class: 'b', style: {visibility: 'hidden'}}) + editor.decorateMarker(cursorMarker2, {type: 'cursor', style: {backgroundColor: 'red'}}) + await component.getNextUpdatePromise() + + const cursorNodes = element.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(2) + + + expect(cursorNodes[0].className).toBe('cursor a') + expect(cursorNodes[1].className).toBe('cursor b') + expect(cursorNodes[1].style.visibility).toBe('hidden') + expect(cursorNodes[1].style.backgroundColor).toBe('red') + }) + + it('allows markers that are not actually associated with cursors to be decorated as if they were cursors', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenPosition([1, 0]) + editor.decorateMarker(marker, {type: 'cursor', class: 'a'}) + await component.getNextUpdatePromise() + + const cursorNodes = element.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(2) + expect(cursorNodes[0].className).toBe('cursor') + expect(cursorNodes[1].className).toBe('cursor a') + }) + }) + + describe('text decorations', () => { + it('injects spans with custom class names and inline styles based on text decorations', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 2}) + + const markerLayer = editor.addMarkerLayer() + const marker1 = markerLayer.markBufferRange([[0, 2], [2, 7]]) + const marker2 = markerLayer.markBufferRange([[0, 2], [3, 8]]) + const marker3 = markerLayer.markBufferRange([[1, 13], [2, 7]]) + editor.decorateMarker(marker1, {type: 'text', class: 'a', style: {color: 'red'}}) + editor.decorateMarker(marker2, {type: 'text', class: 'b', style: {color: 'blue'}}) + editor.decorateMarker(marker3, {type: 'text', class: 'c', style: {color: 'green'}}) + await component.getNextUpdatePromise() + + expect(textContentOnRowMatchingSelector(component, 0, '.a')).toBe(editor.lineTextForScreenRow(0).slice(2)) + expect(textContentOnRowMatchingSelector(component, 1, '.a')).toBe(editor.lineTextForScreenRow(1)) + expect(textContentOnRowMatchingSelector(component, 2, '.a')).toBe(editor.lineTextForScreenRow(2).slice(0, 7)) + expect(textContentOnRowMatchingSelector(component, 3, '.a')).toBe('') + + expect(textContentOnRowMatchingSelector(component, 0, '.b')).toBe(editor.lineTextForScreenRow(0).slice(2)) + expect(textContentOnRowMatchingSelector(component, 1, '.b')).toBe(editor.lineTextForScreenRow(1)) + expect(textContentOnRowMatchingSelector(component, 2, '.b')).toBe(editor.lineTextForScreenRow(2)) + expect(textContentOnRowMatchingSelector(component, 3, '.b')).toBe(editor.lineTextForScreenRow(3).slice(0, 8)) + + expect(textContentOnRowMatchingSelector(component, 0, '.c')).toBe('') + expect(textContentOnRowMatchingSelector(component, 1, '.c')).toBe(editor.lineTextForScreenRow(1).slice(13)) + expect(textContentOnRowMatchingSelector(component, 2, '.c')).toBe(editor.lineTextForScreenRow(2).slice(0, 7)) + expect(textContentOnRowMatchingSelector(component, 3, '.c')).toBe('') + + for (const span of element.querySelectorAll('.a:not(.c)')) { + expect(span.style.color).toBe('red') + } + for (const span of element.querySelectorAll('.b:not(.c):not(.a)')) { + expect(span.style.color).toBe('blue') + } + for (const span of element.querySelectorAll('.c')) { + expect(span.style.color).toBe('green') + } + + marker2.setHeadScreenPosition([3, 10]) + await component.getNextUpdatePromise() + expect(textContentOnRowMatchingSelector(component, 3, '.b')).toBe(editor.lineTextForScreenRow(3).slice(0, 10)) + }) + + it('correctly handles text decorations starting before the first rendered row and/or ending after the last rendered row', async () => { + const {component, element, editor} = buildComponent({autoHeight: false, rowsPerTile: 1}) + element.style.height = 4 * component.getLineHeight() + 'px' + await component.getNextUpdatePromise() + await setScrollTop(component, 4 * component.getLineHeight()) + expect(component.getRenderedStartRow()).toBe(4) + expect(component.getRenderedEndRow()).toBe(9) + + const markerLayer = editor.addMarkerLayer() + const marker1 = markerLayer.markBufferRange([[0, 0], [4, 5]]) + const marker2 = markerLayer.markBufferRange([[7, 2], [10, 8]]) + editor.decorateMarker(marker1, {type: 'text', class: 'a'}) + editor.decorateMarker(marker2, {type: 'text', class: 'b'}) + await component.getNextUpdatePromise() + + expect(textContentOnRowMatchingSelector(component, 4, '.a')).toBe(editor.lineTextForScreenRow(4).slice(0, 5)) + expect(textContentOnRowMatchingSelector(component, 5, '.a')).toBe('') + expect(textContentOnRowMatchingSelector(component, 6, '.a')).toBe('') + expect(textContentOnRowMatchingSelector(component, 7, '.a')).toBe('') + expect(textContentOnRowMatchingSelector(component, 8, '.a')).toBe('') + + expect(textContentOnRowMatchingSelector(component, 4, '.b')).toBe('') + expect(textContentOnRowMatchingSelector(component, 5, '.b')).toBe('') + expect(textContentOnRowMatchingSelector(component, 6, '.b')).toBe('') + expect(textContentOnRowMatchingSelector(component, 7, '.b')).toBe(editor.lineTextForScreenRow(7).slice(2)) + expect(textContentOnRowMatchingSelector(component, 8, '.b')).toBe(editor.lineTextForScreenRow(8)) + }) + + it('does not create empty spans when a text decoration contains a row but another text decoration starts or ends at the beginning of it', async () => { + const {component, element, editor} = buildComponent() + const markerLayer = editor.addMarkerLayer() + const marker1 = markerLayer.markBufferRange([[0, 2], [4, 0]]) + const marker2 = markerLayer.markBufferRange([[2, 0], [5, 8]]) + editor.decorateMarker(marker1, {type: 'text', class: 'a'}) + editor.decorateMarker(marker2, {type: 'text', class: 'b'}) + await component.getNextUpdatePromise() + for (const decorationSpan of element.querySelectorAll('.a, .b')) { + expect(decorationSpan.textContent).not.toBe('') + } + }) + + it('does not create empty text nodes when a text decoration ends right after a text tag', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markBufferRange([[0, 8], [0, 29]]) + editor.decorateMarker(marker, {type: 'text', class: 'a'}) + await component.getNextUpdatePromise() + for (const textNode of textNodesForScreenRow(component, 0)) { + expect(textNode.textContent).not.toBe('') + } + }) + + function textContentOnRowMatchingSelector (component, row, selector) { + return Array.from(lineNodeForScreenRow(component, row).querySelectorAll(selector)) + .map((span) => span.textContent) + .join('') + } + }) + + describe('mouse input', () => { + describe('on the lines', () => { + it('positions the cursor on single-click', async () => { + const {component, element, editor} = buildComponent() + const {lineHeight} = component.measurements + + editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false}) + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: clientLeftForCharacter(component, 0, 0) - 1, + clientY: clientTopForLine(component, 0) - 1 + }) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + const maxRow = editor.getLastScreenRow() + editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false}) + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: clientLeftForCharacter(component, maxRow, editor.lineLengthForScreenRow(maxRow)) + 1, + clientY: clientTopForLine(component, maxRow) + 1 + }) + expect(editor.getCursorScreenPosition()).toEqual([maxRow, editor.lineLengthForScreenRow(maxRow)]) + + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: clientLeftForCharacter(component, 0, editor.lineLengthForScreenRow(0)) + 1, + clientY: clientTopForLine(component, 0) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.lineLengthForScreenRow(0)]) + + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: (clientLeftForCharacter(component, 3, 0) + clientLeftForCharacter(component, 3, 1)) / 2, + clientY: clientTopForLine(component, 1) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([1, 0]) + + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 14]) + + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2 + 1, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 15]) + + editor.getBuffer().setTextInRange([[3, 14], [3, 15]], '🐣') + await component.getNextUpdatePromise() + + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 14]) + + component.didMouseDownOnContent({ + detail: 1, + button: 0, + clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2 + 1, + clientY: clientTopForLine(component, 3) + lineHeight / 2 + }) + expect(editor.getCursorScreenPosition()).toEqual([3, 16]) + }) + + it('selects words on double-click', () => { + const {component, editor} = buildComponent() + const {clientX, clientY} = clientPositionForCharacter(component, 1, 16) + component.didMouseDownOnContent({detail: 1, button: 0, clientX, clientY}) + component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY}) + expect(editor.getSelectedScreenRange()).toEqual([[1, 13], [1, 21]]) + }) + + it('selects lines on triple-click', () => { + const {component, editor} = buildComponent() + const {clientX, clientY} = clientPositionForCharacter(component, 1, 16) + component.didMouseDownOnContent({detail: 1, button: 0, clientX, clientY}) + component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY}) + component.didMouseDownOnContent({detail: 3, button: 0, clientX, clientY}) + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [2, 0]]) + }) + + it('adds or removes cursors when holding cmd or ctrl when single-clicking', () => { + const {component, editor} = buildComponent({platform: 'darwin'}) + expect(editor.getCursorScreenPositions()).toEqual([[0, 0]]) + + // add cursor at 1, 16 + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 1, 16), { + detail: 1, + button: 0, + metaKey: true + }) + ) + expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]) + + // remove cursor at 0, 0 + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 0, 0), { + detail: 1, + button: 0, + metaKey: true + }) + ) + expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]) + + // cmd-click cursor at 1, 16 but don't remove it because it's the last one + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 1, 16), { + detail: 1, + button: 0, + metaKey: true + }) + ) + expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]) + + // cmd-clicking within a selection destroys it + editor.addSelectionForScreenRange([[2, 10], [2, 15]]) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[1, 16], [1, 16]], + [[2, 10], [2, 15]] + ]) + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 2, 13), { + detail: 1, + button: 0, + metaKey: true + }) + ) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[1, 16], [1, 16]] + ]) + + // ctrl-click does not add cursors on macOS + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 1, 4), { + detail: 1, + button: 0, + ctrlKey: true + }) + ) + expect(editor.getCursorScreenPositions()).toEqual([[1, 4]]) + + // ctrl-click adds cursors on platforms *other* than macOS + component.props.platform = 'win32' + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 1, 16), { + detail: 1, + button: 0, + ctrlKey: true + }) + ) + expect(editor.getCursorScreenPositions()).toEqual([[1, 4], [1, 16]]) + }) + + it('adds word selections when holding cmd or ctrl when double-clicking', () => { + const {component, editor} = buildComponent() + editor.addCursorAtScreenPosition([1, 16]) + expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]) + + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 1, 16), { + detail: 1, + button: 0, + metaKey: true + }) + ) + component.didMouseDownOnContent( + Object.assign(clientPositionForCharacter(component, 1, 16), { + detail: 2, + button: 0, + metaKey: true + }) + ) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[0, 0], [0, 0]], + [[1, 13], [1, 21]] + ]) + }) + + it('adds line selections when holding cmd or ctrl when triple-clicking', () => { + const {component, editor} = buildComponent() + editor.addCursorAtScreenPosition([1, 16]) + expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]) + + const {clientX, clientY} = clientPositionForCharacter(component, 1, 16) + component.didMouseDownOnContent({detail: 1, button: 0, metaKey: true, clientX, clientY}) + component.didMouseDownOnContent({detail: 2, button: 0, metaKey: true, clientX, clientY}) + component.didMouseDownOnContent({detail: 3, button: 0, metaKey: true, clientX, clientY}) + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[0, 0], [0, 0]], + [[1, 0], [2, 0]] + ]) + }) + + it('expands the last selection on shift-click', () => { + const {component, element, editor} = buildComponent() + + editor.setCursorScreenPosition([2, 18]) + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + shiftKey: true + }, clientPositionForCharacter(component, 1, 4))) + expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [2, 18]]) + + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + shiftKey: true + }, clientPositionForCharacter(component, 4, 4))) + expect(editor.getSelectedScreenRange()).toEqual([[2, 18], [4, 4]]) + + // reorients word-wise selections to keep the word selected regardless of + // where the subsequent shift-click occurs + editor.setCursorScreenPosition([2, 18]) + editor.getLastSelection().selectWord() + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + shiftKey: true + }, clientPositionForCharacter(component, 1, 4))) + expect(editor.getSelectedScreenRange()).toEqual([[1, 2], [2, 20]]) + + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + shiftKey: true + }, clientPositionForCharacter(component, 3, 11))) + expect(editor.getSelectedScreenRange()).toEqual([[2, 14], [3, 13]]) + + // reorients line-wise selections to keep the word selected regardless of + // where the subsequent shift-click occurs + editor.setCursorScreenPosition([2, 18]) + editor.getLastSelection().selectLine() + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + shiftKey: true + }, clientPositionForCharacter(component, 1, 4))) + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 0]]) + + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + shiftKey: true + }, clientPositionForCharacter(component, 3, 11))) + expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [4, 0]]) + }) + + it('expands the last selection on drag', () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + }, clientPositionForCharacter(component, 1, 4))) + + { + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + didDrag(clientPositionForCharacter(component, 8, 8)) + expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [8, 8]]) + didDrag(clientPositionForCharacter(component, 4, 8)) + expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [4, 8]]) + didStopDragging() + expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [4, 8]]) + } + + // Click-drag a second selection... selections are not merged until the + // drag stops. + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + metaKey: 1, + }, clientPositionForCharacter(component, 8, 8))) + { + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[1][0] + didDrag(clientPositionForCharacter(component, 2, 8)) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[1, 4], [4, 8]], + [[2, 8], [8, 8]] + ]) + didDrag(clientPositionForCharacter(component, 6, 8)) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[1, 4], [4, 8]], + [[6, 8], [8, 8]] + ]) + didDrag(clientPositionForCharacter(component, 2, 8)) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[1, 4], [4, 8]], + [[2, 8], [8, 8]] + ]) + didStopDragging() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[1, 4], [8, 8]] + ]) + } + }) + + it('expands the selection word-wise on double-click-drag', () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + + component.didMouseDownOnContent(Object.assign({ + detail: 1, + button: 0, + }, clientPositionForCharacter(component, 1, 4))) + component.didMouseDownOnContent(Object.assign({ + detail: 2, + button: 0, + }, clientPositionForCharacter(component, 1, 4))) + + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[1][0] + didDrag(clientPositionForCharacter(component, 0, 8)) + expect(editor.getSelectedScreenRange()).toEqual([[0, 4], [1, 5]]) + didDrag(clientPositionForCharacter(component, 2, 10)) + expect(editor.getSelectedScreenRange()).toEqual([[1, 2], [2, 13]]) + }) + + it('expands the selection line-wise on triple-click-drag', () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + + const tripleClickPosition = clientPositionForCharacter(component, 2, 8) + component.didMouseDownOnContent(Object.assign({detail: 1, button: 0}, tripleClickPosition)) + component.didMouseDownOnContent(Object.assign({detail: 2, button: 0}, tripleClickPosition)) + component.didMouseDownOnContent(Object.assign({detail: 3, button: 0}, tripleClickPosition)) + + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[2][0] + didDrag(clientPositionForCharacter(component, 1, 8)) + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 0]]) + didDrag(clientPositionForCharacter(component, 4, 10)) + expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [5, 0]]) + }) + + it('destroys folds when clicking on their fold markers', async () => { + const {component, element, editor} = buildComponent() + editor.foldBufferRow(1) + await component.getNextUpdatePromise() + + const target = element.querySelector('.fold-marker') + const {clientX, clientY} = clientPositionForCharacter(component, 1, editor.lineLengthForScreenRow(1)) + component.didMouseDownOnContent({detail: 1, button: 0, target, clientX, clientY}) + expect(editor.isFoldedAtBufferRow(1)).toBe(false) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + + it('autoscrolls the content when dragging near the edge of the scroll container', async () => { + const {component, element, editor} = buildComponent({width: 200, height: 200}) + spyOn(component, 'handleMouseDragUntilMouseUp') + + let previousScrollTop = 0 + let previousScrollLeft = 0 + function assertScrolledDownAndRight () { + expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBeGreaterThan(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() + } + + function assertScrolledUpAndLeft () { + expect(component.getScrollTop()).toBeLessThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBeLessThan(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() + } + + component.didMouseDownOnContent({detail: 1, button: 0, clientX: 100, clientY: 100}) + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + + didDrag({clientX: 199, clientY: 199}) + assertScrolledDownAndRight() + didDrag({clientX: 199, clientY: 199}) + assertScrolledDownAndRight() + didDrag({clientX: 199, clientY: 199}) + assertScrolledDownAndRight() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUpAndLeft() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUpAndLeft() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUpAndLeft() + + // Don't artificially update scroll position beyond possible values + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).toBe(0) + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).toBe(0) + + const maxScrollTop = component.getMaxScrollTop() + const maxScrollLeft = component.getMaxScrollLeft() + setScrollTop(component, maxScrollTop) + await setScrollLeft(component, maxScrollLeft) + + didDrag({clientX: 199, clientY: 199}) + didDrag({clientX: 199, clientY: 199}) + didDrag({clientX: 199, clientY: 199}) + expect(component.getScrollTop()).toBe(maxScrollTop) + expect(component.getScrollLeft()).toBe(maxScrollLeft) + }) + + it('pastes the previously selected text when clicking the middle mouse button on Linux', async () => { + spyOn(electron.ipcRenderer, 'send').andCallFake(function (eventName, selectedText) { + if (eventName === 'write-text-to-selection-clipboard') { + clipboard.writeText(selectedText, 'selection') + } + }) + + const {component, editor} = buildComponent({platform: 'linux'}) + + // Middle mouse pasting. + editor.setSelectedBufferRange([[1, 6], [1, 10]]) + await conditionPromise(() => TextEditor.clipboard.read() === 'sort') + component.didMouseDownOnContent({ + button: 1, + clientX: clientLeftForCharacter(component, 10, 0), + clientY: clientTopForLine(component, 10) + }) + expect(TextEditor.clipboard.read()).toBe('sort') + expect(editor.lineTextForBufferRow(10)).toBe('sort') + editor.undo() + + // Ensure left clicks don't interfere. + editor.setSelectedBufferRange([[1, 2], [1, 5]]) + await conditionPromise(() => TextEditor.clipboard.read() === 'var') + component.didMouseDownOnContent({ + button: 0, + detail: 1, + clientX: clientLeftForCharacter(component, 10, 0), + clientY: clientTopForLine(component, 10) + }) + component.didMouseDownOnContent({ + button: 1, + clientX: clientLeftForCharacter(component, 10, 0), + clientY: clientTopForLine(component, 10) + }) + expect(editor.lineTextForBufferRow(10)).toBe('var') + }) + }) + + describe('on the line number gutter', () => { + it('selects all buffer rows intersecting the clicked screen row when a line number is clicked', async () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + editor.setSoftWrapped(true) + await component.getNextUpdatePromise() + + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + // Selects entire buffer line when clicked screen line is soft-wrapped + component.didMouseDownOnLineNumberGutter({ + button: 0, + clientY: clientTopForLine(component, 3) + }) + expect(editor.getSelectedScreenRange()).toEqual([[3, 0], [5, 0]]) + expect(editor.getSelectedBufferRange()).toEqual([[3, 0], [4, 0]]) + + // Selects entire screen line, even if folds cause that selection to + // span multiple buffer lines + component.didMouseDownOnLineNumberGutter({ + button: 0, + clientY: clientTopForLine(component, 5) + }) + expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [8, 0]]) + }) + + it('adds new selections when a line number is meta-clicked', async () => { + const {component, editor} = buildComponent() + editor.setSoftWrapped(true) + await component.getNextUpdatePromise() + + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + // Selects entire buffer line when clicked screen line is soft-wrapped + component.didMouseDownOnLineNumberGutter({ + button: 0, + metaKey: true, + clientY: clientTopForLine(component, 3) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [5, 0]] + ]) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [4, 0]] + ]) + + // Selects entire screen line, even if folds cause that selection to + // span multiple buffer lines + component.didMouseDownOnLineNumberGutter({ + button: 0, + metaKey: true, + clientY: clientTopForLine(component, 5) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [5, 0]], + [[5, 0], [6, 0]] + ]) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 0], [0, 0]], + [[3, 0], [4, 0]], + [[4, 0], [8, 0]] + ]) + }) + + it('expands the last selection when a line number is shift-clicked', async () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + editor.setSoftWrapped(true) + await component.getNextUpdatePromise() + + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + editor.setSelectedScreenRange([[3, 4], [3, 8]]) + editor.addCursorAtScreenPosition([2, 10]) + component.didMouseDownOnLineNumberGutter({ + button: 0, + shiftKey: true, + clientY: clientTopForLine(component, 5) + }) + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 4], [3, 8]], + [[2, 10], [8, 0]] + ]) + + // Original selection is preserved when shift-click-dragging + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + didDrag({ + clientY: clientTopForLine(component, 1) + }) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 4], [3, 8]], + [[1, 0], [2, 10]] + ]) + + didDrag({ + clientY: clientTopForLine(component, 5) + }) + + didStopDragging() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 10], [8, 0]] + ]) + }) + + it('expands the selection when dragging', async () => { + const {component, editor} = buildComponent() + spyOn(component, 'handleMouseDragUntilMouseUp') + editor.setSoftWrapped(true) + await component.getNextUpdatePromise() + + await setEditorWidthInCharacters(component, 50) + editor.foldBufferRange([[4, Infinity], [7, Infinity]]) + await component.getNextUpdatePromise() + + editor.setSelectedScreenRange([[3, 4], [3, 6]]) + + component.didMouseDownOnLineNumberGutter({ + button: 0, + metaKey: true, + clientY: clientTopForLine(component, 2) + }) + + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + + didDrag({ + clientY: clientTopForLine(component, 1) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 4], [3, 6]], + [[1, 0], [3, 0]] + ]) + + didDrag({ + clientY: clientTopForLine(component, 5) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 4], [3, 6]], + [[2, 0], [6, 0]] + ]) + expect(editor.isFoldedAtBufferRow(4)).toBe(true) + + didDrag({ + clientY: clientTopForLine(component, 3) + }) + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 4], [3, 6]], + [[2, 0], [4, 4]] + ]) + + didStopDragging() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[2, 0], [4, 4]] + ]) + }) + + it('toggles folding when clicking on the right icon of a foldable line number', async () => { + const {component, element, editor} = buildComponent() + let target = element.querySelectorAll('.line-number')[1].querySelector('.icon-right') + expect(editor.isFoldedAtScreenRow(1)).toBe(false) + + component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 1)}) + expect(editor.isFoldedAtScreenRow(1)).toBe(true) + await component.getNextUpdatePromise() + + component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 1)}) + await component.getNextUpdatePromise() + expect(editor.isFoldedAtScreenRow(1)).toBe(false) + + editor.foldBufferRange([[5, 12], [5, 17]]) + await component.getNextUpdatePromise() + expect(editor.isFoldedAtScreenRow(5)).toBe(true) + + target = element.querySelectorAll('.line-number')[6].querySelector('.icon-right') + component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 5)}) + expect(editor.isFoldedAtScreenRow(5)).toBe(false) + }) + + it('autoscrolls when dragging near the top or bottom of the gutter', async () => { + const {component, editor} = buildComponent({width: 200, height: 200}) + const {scrollContainer} = component.refs + spyOn(component, 'handleMouseDragUntilMouseUp') + + let previousScrollTop = 0 + let previousScrollLeft = 0 + function assertScrolledDown () { + expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBe(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() + } + + function assertScrolledUp () { + expect(component.getScrollTop()).toBeLessThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBe(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() + } + + component.didMouseDownOnLineNumberGutter({detail: 1, button: 0, clientX: 0, clientY: 100}) + const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + didDrag({clientX: 199, clientY: 199}) + assertScrolledDown() + didDrag({clientX: 199, clientY: 199}) + assertScrolledDown() + didDrag({clientX: 199, clientY: 199}) + assertScrolledDown() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUp() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUp() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUp() + + // Don't artificially update scroll measurements beyond the minimum or + // maximum possible scroll positions + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).toBe(0) + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).toBe(0) + + const maxScrollTop = component.getMaxScrollTop() + const maxScrollLeft = component.getMaxScrollLeft() + setScrollTop(component, maxScrollTop) + await setScrollLeft(component, maxScrollLeft) + + didDrag({clientX: 199, clientY: 199}) + didDrag({clientX: 199, clientY: 199}) + didDrag({clientX: 199, clientY: 199}) + expect(component.getScrollTop()).toBe(maxScrollTop) + expect(component.getScrollLeft()).toBe(maxScrollLeft) + }) + }) + + describe('on the scrollbars', () => { + it('delegates the mousedown events to the parent component unless the mousedown was on the actual scrollbar', async () => { + const {component, element, editor} = buildComponent({height: 100}) + await setEditorWidthInCharacters(component, 8.5) + + const verticalScrollbar = component.refs.verticalScrollbar + const horizontalScrollbar = component.refs.horizontalScrollbar + const leftEdgeOfVerticalScrollbar = verticalScrollbar.element.getBoundingClientRect().right - getVerticalScrollbarWidth(component) + const topEdgeOfHorizontalScrollbar = horizontalScrollbar.element.getBoundingClientRect().bottom - getHorizontalScrollbarHeight(component) + + verticalScrollbar.didMousedown({ + button: 0, + detail: 1, + clientY: clientTopForLine(component, 4), + clientX: leftEdgeOfVerticalScrollbar + }) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + verticalScrollbar.didMousedown({ + button: 0, + detail: 1, + clientY: clientTopForLine(component, 4), + clientX: leftEdgeOfVerticalScrollbar - 1 + }) + expect(editor.getCursorScreenPosition()).toEqual([4, 6]) + + horizontalScrollbar.didMousedown({ + button: 0, + detail: 1, + clientY: topEdgeOfHorizontalScrollbar, + clientX: component.refs.content.getBoundingClientRect().left + }) + expect(editor.getCursorScreenPosition()).toEqual([4, 6]) + + horizontalScrollbar.didMousedown({ + button: 0, + detail: 1, + clientY: topEdgeOfHorizontalScrollbar - 1, + clientX: component.refs.content.getBoundingClientRect().left + }) + expect(editor.getCursorScreenPosition()).toEqual([4, 0]) + }) + }) + }) + + describe('keyboard input', () => { + it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => { + const {editor, component, element} = buildComponent({text: ''}) + editor.insertText('x') + editor.setCursorBufferPosition([0, 1]) + + // Simulate holding the A key to open the press-and-hold menu, + // then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Escape'}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by typing a number. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Digit2'}) + component.didKeyup({code: 'Digit2'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by clicking on it. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then selecting one of them with Enter. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Enter'}) + component.didCompositionUpdate({data: 'á'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Enter'}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyO'}) + component.didKeypress({code: 'KeyO'}) + component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyO'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xoà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xoá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xoa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it by changing focus. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didCompositionUpdate({data: 'á'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + }) + }) + + describe('styling changes', () => { + it('updates the rendered content based on new measurements when the font dimensions change', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 1, autoHeight: false}) + await setEditorHeightInLines(component, 3) + editor.setCursorScreenPosition([1, 29], {autoscroll: false}) + await component.getNextUpdatePromise() + + let cursorNode = element.querySelector('.cursor') + const initialBaseCharacterWidth = editor.getDefaultCharWidth() + const initialDoubleCharacterWidth = editor.getDoubleWidthCharWidth() + const initialHalfCharacterWidth = editor.getHalfWidthCharWidth() + const initialKoreanCharacterWidth = editor.getKoreanCharWidth() + const initialRenderedLineCount = element.querySelectorAll('.line:not(.dummy)').length + const initialFontSize = parseInt(getComputedStyle(element).fontSize) + + expect(initialKoreanCharacterWidth).toBeDefined() + expect(initialDoubleCharacterWidth).toBeDefined() + expect(initialHalfCharacterWidth).toBeDefined() + expect(initialBaseCharacterWidth).toBeDefined() + expect(initialDoubleCharacterWidth).not.toBe(initialBaseCharacterWidth) + expect(initialHalfCharacterWidth).not.toBe(initialBaseCharacterWidth) + expect(initialKoreanCharacterWidth).not.toBe(initialBaseCharacterWidth) + verifyCursorPosition(component, cursorNode, 1, 29) + + element.style.fontSize = initialFontSize - 5 + 'px' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(editor.getDefaultCharWidth()).toBeLessThan(initialBaseCharacterWidth) + expect(editor.getDoubleWidthCharWidth()).toBeLessThan(initialDoubleCharacterWidth) + expect(editor.getHalfWidthCharWidth()).toBeLessThan(initialHalfCharacterWidth) + expect(editor.getKoreanCharWidth()).toBeLessThan(initialKoreanCharacterWidth) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBeGreaterThan(initialRenderedLineCount) + verifyCursorPosition(component, cursorNode, 1, 29) + + element.style.fontSize = initialFontSize + 10 + 'px' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialBaseCharacterWidth) + expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(initialDoubleCharacterWidth) + expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(initialHalfCharacterWidth) + expect(editor.getKoreanCharWidth()).toBeGreaterThan(initialKoreanCharacterWidth) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBeLessThan(initialRenderedLineCount) + verifyCursorPosition(component, cursorNode, 1, 29) + }) + + it('maintains the scrollTopRow and scrollLeftColumn when the font size changes', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 1, autoHeight: false}) + await setEditorHeightInLines(component, 3) + await setEditorWidthInCharacters(component, 20) + component.setScrollTopRow(4) + component.setScrollLeftColumn(10) + await component.getNextUpdatePromise() + + const initialFontSize = parseInt(getComputedStyle(element).fontSize) + element.style.fontSize = initialFontSize - 5 + 'px' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(component.getScrollTopRow()).toBe(4) + + element.style.fontSize = initialFontSize + 5 + 'px' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(component.getScrollTopRow()).toBe(4) + }) + + it('gracefully handles the editor being hidden after a styling change', async () => { + const {component, element, editor} = buildComponent({autoHeight: false}) + element.style.fontSize = parseInt(getComputedStyle(element).fontSize) + 5 + 'px' + TextEditor.didUpdateStyles() + element.style.display = 'none' + await component.getNextUpdatePromise() + }) + }) + + describe('synchronous updates', () => { + let editorElementWasUpdatedSynchronously + + beforeEach(() => { + editorElementWasUpdatedSynchronously = TextEditorElement.prototype.updatedSynchronously + }) + + afterEach(() => { + TextEditorElement.prototype.setUpdatedSynchronously(editorElementWasUpdatedSynchronously) + }) + + it('updates synchronously when updatedSynchronously is true', () => { + const editor = buildEditor() + const {element} = new TextEditorComponent({model: editor, updatedSynchronously: true}) + jasmine.attachToDOM(element) + + editor.setText('Lorem ipsum dolor') + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(l => l.textContent)).toEqual([ + editor.lineTextForScreenRow(0) + ]) + }) + + it('does not throw an exception on attachment when setting the soft-wrap column', () => { + const {component, element, editor} = buildComponent({width: 435, attach: false, updatedSynchronously: true}) + editor.setSoftWrapped(true) + spyOn(window, 'onerror').andCallThrough() + jasmine.attachToDOM(element) // should not throw an exception + expect(window.onerror).not.toHaveBeenCalled() + }) + + it('updates synchronously when creating a component via TextEditor and TextEditorElement.prototype.updatedSynchronously is true', () => { + TextEditorElement.prototype.setUpdatedSynchronously(true) + const editor = buildEditor() + const element = editor.element + jasmine.attachToDOM(element) + + editor.setText('Lorem ipsum dolor') + expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(l => l.textContent)).toEqual([ + editor.lineTextForScreenRow(0) + ]) + }) + + it('measures dimensions synchronously when measureDimensions is called on the component', () => { + TextEditorElement.prototype.setUpdatedSynchronously(true) + const editor = buildEditor({autoHeight: false}) + const element = editor.element + jasmine.attachToDOM(element) + + element.style.height = '100px' + expect(element.component.getClientContainerHeight()).not.toBe(100) + element.component.measureDimensions() + expect(element.component.getClientContainerHeight()).toBe(100) + }) + }) + + describe('pixelPositionForScreenPosition(point)', () => { + it('returns the pixel position for the given point, regardless of whether or not it is currently on screen', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 2, autoHeight: false}) + await setEditorHeightInLines(component, 3) + await setScrollTop(component, 3 * component.getLineHeight()) + + const {component: referenceComponent} = buildComponent() + const referenceContentRect = referenceComponent.refs.content.getBoundingClientRect() + + { + const {top, left} = component.pixelPositionForScreenPosition({row: 0, column: 0}) + expect(top).toBe(clientTopForLine(referenceComponent, 0) - referenceContentRect.top) + expect(left).toBe(clientLeftForCharacter(referenceComponent, 0, 0) - referenceContentRect.left) + } + + { + const {top, left} = component.pixelPositionForScreenPosition({row: 0, column: 5}) + expect(top).toBe(clientTopForLine(referenceComponent, 0) - referenceContentRect.top) + expect(left).toBe(clientLeftForCharacter(referenceComponent, 0, 5) - referenceContentRect.left) + } + + { + const {top, left} = component.pixelPositionForScreenPosition({row: 12, column: 1}) + expect(top).toBe(clientTopForLine(referenceComponent, 12) - referenceContentRect.top) + expect(left).toBe(clientLeftForCharacter(referenceComponent, 12, 1) - referenceContentRect.left) + } + }) + }) + + describe('screenPositionForPixelPosition', () => { + it('returns the screen position for the given pixel position, regardless of whether or not it is currently on screen', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 2, autoHeight: false}) + await setEditorHeightInLines(component, 3) + await setScrollTop(component, 3 * component.getLineHeight()) + const {component: referenceComponent} = buildComponent() + + { + const pixelPosition = referenceComponent.pixelPositionForScreenPosition({row: 0, column: 0}) + pixelPosition.top += component.getLineHeight() / 3 + pixelPosition.left += component.getBaseCharacterWidth() / 3 + expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([0, 0]) + } + + { + const pixelPosition = referenceComponent.pixelPositionForScreenPosition({row: 0, column: 5}) + pixelPosition.top += component.getLineHeight() / 3 + pixelPosition.left += component.getBaseCharacterWidth() / 3 + expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([0, 5]) + } + + { + const pixelPosition = referenceComponent.pixelPositionForScreenPosition({row: 5, column: 7}) + pixelPosition.top += component.getLineHeight() / 3 + pixelPosition.left += component.getBaseCharacterWidth() / 3 + expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([5, 7]) + } + + { + const pixelPosition = referenceComponent.pixelPositionForScreenPosition({row: 12, column: 1}) + pixelPosition.top += component.getLineHeight() / 3 + pixelPosition.left += component.getBaseCharacterWidth() / 3 + expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([12, 1]) + } + }) + }) + + describe('model methods that delegate to the component / element', () => { + it('delegates setHeight and getHeight to the component', async () => { + const {component, element, editor} = buildComponent({autoHeight: false}) + spyOn(Grim, 'deprecate') + expect(editor.getHeight()).toBe(component.getScrollContainerHeight()) + expect(Grim.deprecate.callCount).toBe(1) + + editor.setHeight(100) + await component.getNextUpdatePromise() + expect(component.getScrollContainerHeight()).toBe(100) + expect(Grim.deprecate.callCount).toBe(2) + }) + + it('delegates setWidth and getWidth to the component', async () => { + const {component, element, editor} = buildComponent() + spyOn(Grim, 'deprecate') + expect(editor.getWidth()).toBe(component.getScrollContainerWidth()) + expect(Grim.deprecate.callCount).toBe(1) + + editor.setWidth(100) + await component.getNextUpdatePromise() + expect(component.getScrollContainerWidth()).toBe(100) + expect(Grim.deprecate.callCount).toBe(2) + }) + + it('delegates getFirstVisibleScreenRow, getLastVisibleScreenRow, and getVisibleRowRange to the component', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) + element.style.height = 4 * component.measurements.lineHeight + 'px' + await component.getNextUpdatePromise() + await setScrollTop(component, 5 * component.getLineHeight()) + + expect(editor.getFirstVisibleScreenRow()).toBe(component.getFirstVisibleRow()) + expect(editor.getLastVisibleScreenRow()).toBe(component.getLastVisibleRow()) + expect(editor.getVisibleRowRange()).toEqual([component.getFirstVisibleRow(), component.getLastVisibleRow()]) + }) + + it('assigns scrollTop on the component when calling setFirstVisibleScreenRow', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) + element.style.height = 4 * component.measurements.lineHeight + 'px' + await component.getNextUpdatePromise() + + expect(component.getMaxScrollTop() / component.getLineHeight()).toBe(9) + expect(component.refs.verticalScrollbar.element.scrollTop).toBe(0 * component.getLineHeight()) + + editor.setFirstVisibleScreenRow(1) + expect(component.getFirstVisibleRow()).toBe(1) + await component.getNextUpdatePromise() + expect(component.refs.verticalScrollbar.element.scrollTop).toBe(1 * component.getLineHeight()) + + editor.setFirstVisibleScreenRow(5) + expect(component.getFirstVisibleRow()).toBe(5) + await component.getNextUpdatePromise() + expect(component.refs.verticalScrollbar.element.scrollTop).toBe(5 * component.getLineHeight()) + + editor.setFirstVisibleScreenRow(11) + expect(component.getFirstVisibleRow()).toBe(9) + await component.getNextUpdatePromise() + expect(component.refs.verticalScrollbar.element.scrollTop).toBe(9 * component.getLineHeight()) + }) + + it('delegates setFirstVisibleScreenColumn and getFirstVisibleScreenColumn to the component', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) + element.style.width = 30 * component.getBaseCharacterWidth() + 'px' + await component.getNextUpdatePromise() + expect(editor.getFirstVisibleScreenColumn()).toBe(0) + expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(0 * component.getBaseCharacterWidth()) + + setScrollLeft(component, 5.5 * component.getBaseCharacterWidth()) + expect(editor.getFirstVisibleScreenColumn()).toBe(5) + await component.getNextUpdatePromise() + expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(Math.round(5.5 * component.getBaseCharacterWidth())) + + editor.setFirstVisibleScreenColumn(12) + expect(component.getScrollLeft()).toBe(Math.round(12 * component.getBaseCharacterWidth())) + await component.getNextUpdatePromise() + expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(Math.round(12 * component.getBaseCharacterWidth())) + }) + }) +}) + +function buildEditor (params = {}) { + const text = params.text != null ? params.text : SAMPLE_TEXT + const buffer = new TextBuffer({text}) + const editorParams = {buffer} + if (params.height != null) params.autoHeight = false + for (const paramName of ['mini', 'autoHeight', 'autoWidth', 'lineNumberGutterVisible', 'placeholderText', 'softWrapped']) { + if (params[paramName] != null) editorParams[paramName] = params[paramName] + } + return new TextEditor(editorParams) +} + +function buildComponent (params = {}) { + const editor = params.editor || buildEditor(params) + const component = new TextEditorComponent({ + model: editor, + rowsPerTile: params.rowsPerTile, + updatedSynchronously: params.updatedSynchronously || false, + platform: params.platform, + mouseWheelScrollSensitivity: params.mouseWheelScrollSensitivity + }) + const {element} = component + if (!editor.getAutoHeight()) { + element.style.height = params.height ? params.height + 'px' : '600px' + } + if (!editor.getAutoWidth()) { + element.style.width = params.width ? params.width + 'px' : '800px' + } + if (params.attach !== false) jasmine.attachToDOM(element) + return {component, element, editor} +} + +function getEditorWidthInBaseCharacters (component) { + return Math.round(component.getScrollContainerWidth() / component.getBaseCharacterWidth()) +} + +async function setEditorHeightInLines(component, heightInLines) { + component.element.style.height = component.getLineHeight() * heightInLines + 'px' + await component.getNextUpdatePromise() +} + +async function setEditorWidthInCharacters (component, widthInCharacters) { + component.element.style.width = + component.getGutterContainerWidth() + + widthInCharacters * component.measurements.baseCharacterWidth + + 'px' + await component.getNextUpdatePromise() +} + +function verifyCursorPosition (component, cursorNode, row, column) { + const rect = cursorNode.getBoundingClientRect() + expect(Math.round(rect.top)).toBe(clientTopForLine(component, row)) + expect(Math.round(rect.left)).toBe(Math.round(clientLeftForCharacter(component, row, column))) +} + +function clientTopForLine (component, row) { + return lineNodeForScreenRow(component, row).getBoundingClientRect().top +} + +function clientLeftForCharacter (component, row, column) { + const textNodes = textNodesForScreenRow(component, row) + let textNodeStartColumn = 0 + for (const textNode of textNodes) { + const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length + if (column < textNodeEndColumn) { + const range = document.createRange() + range.setStart(textNode, column - textNodeStartColumn) + range.setEnd(textNode, column - textNodeStartColumn) + return range.getBoundingClientRect().left + } + textNodeStartColumn = textNodeEndColumn + } + + const lastTextNode = textNodes[textNodes.length - 1] + const range = document.createRange() + range.setStart(lastTextNode, 0) + range.setEnd(lastTextNode, lastTextNode.textContent.length) + return range.getBoundingClientRect().right +} + +function clientPositionForCharacter (component, row, column) { + return { + clientX: clientLeftForCharacter(component, row, column), + clientY: clientTopForLine(component, row) + } +} + +function lineNumberNodeForScreenRow (component, row) { + const gutterElement = component.refs.gutterContainer.refs.lineNumberGutter.element + const tileStartRow = component.tileStartRowForRow(row) + const tileIndex = component.renderedTileStartRows.indexOf(tileStartRow) + return gutterElement.children[tileIndex + 1].children[row - tileStartRow] +} + +function lineNodeForScreenRow (component, row) { + const renderedScreenLine = component.renderedScreenLineForRow(row) + return component.lineNodesByScreenLineId.get(renderedScreenLine.id) +} + +function textNodesForScreenRow (component, row) { + const screenLine = component.renderedScreenLineForRow(row) + return component.textNodesByScreenLineId.get(screenLine.id) +} + +function setScrollTop (component, scrollTop) { + component.setScrollTop(scrollTop) + component.scheduleUpdate() + return component.getNextUpdatePromise() +} + +function setScrollLeft (component, scrollLeft) { + component.setScrollLeft(scrollLeft) + component.scheduleUpdate() + return component.getNextUpdatePromise() +} + +function getHorizontalScrollbarHeight (component) { + const element = component.refs.horizontalScrollbar.element + return element.offsetHeight - element.clientHeight +} + +function getVerticalScrollbarWidth (component) { + const element = component.refs.verticalScrollbar.element + return element.offsetWidth - element.clientWidth +} + +function assertDocumentFocused () { + if (!document.hasFocus()) { + throw new Error('The document needs to be focused to run this test') + } +} + +function getElementHeight (element) { + const topRuler = document.createElement('div') + const bottomRuler = document.createElement('div') + let height + if (document.body.contains(element)) { + element.parentElement.insertBefore(topRuler, element) + element.parentElement.insertBefore(bottomRuler, element.nextSibling) + height = bottomRuler.offsetTop - topRuler.offsetTop + } else { + jasmine.attachToDOM(topRuler) + jasmine.attachToDOM(element) + jasmine.attachToDOM(bottomRuler) + height = bottomRuler.offsetTop - topRuler.offsetTop + element.remove() + } + + topRuler.remove() + bottomRuler.remove() + return height +} diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee deleted file mode 100644 index 867599c55..000000000 --- a/spec/text-editor-element-spec.coffee +++ /dev/null @@ -1,320 +0,0 @@ -TextEditor = require '../src/text-editor' -TextEditorElement = require '../src/text-editor-element' -{Disposable} = require 'event-kit' - -describe "TextEditorElement", -> - jasmineContent = null - - beforeEach -> - jasmineContent = document.body.querySelector('#jasmine-content') - - describe "instantiation", -> - it "honors the 'mini' attribute", -> - jasmineContent.innerHTML = "" - element = jasmineContent.firstChild - expect(element.getModel().isMini()).toBe true - - it "honors the 'placeholder-text' attribute", -> - jasmineContent.innerHTML = "" - element = jasmineContent.firstChild - expect(element.getModel().getPlaceholderText()).toBe 'testing' - - it "honors the 'gutter-hidden' attribute", -> - jasmineContent.innerHTML = "" - element = jasmineContent.firstChild - expect(element.getModel().isLineNumberGutterVisible()).toBe false - - it "honors the text content", -> - jasmineContent.innerHTML = "testing" - element = jasmineContent.firstChild - expect(element.getModel().getText()).toBe 'testing' - - describe "when the model is assigned", -> - it "adds the 'mini' attribute if .isMini() returns true on the model", -> - element = new TextEditorElement - model = new TextEditor({mini: true}) - element.setModel(model) - expect(element.hasAttribute('mini')).toBe true - - describe "when the editor is attached to the DOM", -> - it "mounts the component and unmounts when removed from the dom", -> - element = new TextEditorElement - jasmine.attachToDOM(element) - - component = element.component - expect(component.mounted).toBe true - element.remove() - expect(component.mounted).toBe false - - jasmine.attachToDOM(element) - expect(element.component.mounted).toBe true - - describe "when the editor is detached from the DOM and then reattached", -> - it "does not render duplicate line numbers", -> - editor = new TextEditor - editor.setText('1\n2\n3') - element = editor.getElement() - - jasmine.attachToDOM(element) - - initialCount = element.querySelectorAll('.line-number').length - - element.remove() - jasmine.attachToDOM(element) - expect(element.querySelectorAll('.line-number').length).toBe initialCount - - it "does not render duplicate decorations in custom gutters", -> - editor = new TextEditor - editor.setText('1\n2\n3') - editor.addGutter({name: 'test-gutter'}) - marker = editor.markBufferRange([[0, 0], [2, 0]]) - editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'}) - element = editor.getElement() - - jasmine.attachToDOM(element) - initialDecorationCount = element.querySelectorAll('.decoration').length - - element.remove() - jasmine.attachToDOM(element) - expect(element.querySelectorAll('.decoration').length).toBe initialDecorationCount - - it "can be re-focused using the previous `document.activeElement`", -> - editorElement = document.createElement('atom-text-editor') - jasmine.attachToDOM(editorElement) - editorElement.focus() - - activeElement = document.activeElement - - editorElement.remove() - jasmine.attachToDOM(editorElement) - activeElement.focus() - - expect(editorElement.hasFocus()).toBe(true) - - describe "focus and blur handling", -> - it "proxies focus/blur events to/from the hidden input", -> - element = new TextEditorElement - jasmineContent.appendChild(element) - - blurCalled = false - element.addEventListener 'blur', -> blurCalled = true - - element.focus() - expect(blurCalled).toBe false - expect(element.hasFocus()).toBe true - expect(document.activeElement).toBe element.querySelector('input') - - document.body.focus() - expect(blurCalled).toBe true - - it "doesn't trigger a blur event on the editor element when focusing an already focused editor element", -> - blurCalled = false - element = new TextEditorElement - element.addEventListener 'blur', -> blurCalled = true - - jasmineContent.appendChild(element) - expect(document.activeElement).toBe(document.body) - expect(blurCalled).toBe(false) - - element.focus() - expect(document.activeElement).toBe(element.querySelector('input')) - expect(blurCalled).toBe(false) - - element.focus() - expect(document.activeElement).toBe(element.querySelector('input')) - expect(blurCalled).toBe(false) - - describe "when focused while a parent node is being attached to the DOM", -> - class ElementThatFocusesChild extends HTMLDivElement - attachedCallback: -> - @firstChild.focus() - - document.registerElement("element-that-focuses-child", - prototype: ElementThatFocusesChild.prototype - ) - - it "proxies the focus event to the hidden input", -> - element = new TextEditorElement - parentElement = document.createElement("element-that-focuses-child") - parentElement.appendChild(element) - jasmineContent.appendChild(parentElement) - expect(document.activeElement).toBe element.querySelector('input') - - describe "when the themes finish loading", -> - [themeReloadCallback, initialThemeLoadComplete, element] = [] - - beforeEach -> - themeReloadCallback = null - initialThemeLoadComplete = false - - spyOn(atom.themes, 'isInitialLoadComplete').andCallFake -> - initialThemeLoadComplete - spyOn(atom.themes, 'onDidChangeActiveThemes').andCallFake (fn) -> - themeReloadCallback = fn - new Disposable - - element = new TextEditorElement() - element.style.height = '200px' - element.getModel().update({autoHeight: false}) - element.getModel().setText [0..20].join("\n") - - it "re-renders the scrollbar", -> - jasmineContent.appendChild(element) - - atom.styles.addStyleSheet(""" - ::-webkit-scrollbar { - width: 8px; - } - """, context: 'atom-text-editor') - - initialThemeLoadComplete = true - themeReloadCallback() - - verticalScrollbarNode = element.querySelector(".vertical-scrollbar") - scrollbarWidth = verticalScrollbarNode.offsetWidth - verticalScrollbarNode.clientWidth - expect(scrollbarWidth).toEqual(8) - - describe "::onDidAttach and ::onDidDetach", -> - it "invokes callbacks when the element is attached and detached", -> - element = new TextEditorElement - - attachedCallback = jasmine.createSpy("attachedCallback") - detachedCallback = jasmine.createSpy("detachedCallback") - - element.onDidAttach(attachedCallback) - element.onDidDetach(detachedCallback) - - jasmine.attachToDOM(element) - - expect(attachedCallback).toHaveBeenCalled() - expect(detachedCallback).not.toHaveBeenCalled() - - attachedCallback.reset() - element.remove() - - expect(attachedCallback).not.toHaveBeenCalled() - expect(detachedCallback).toHaveBeenCalled() - - describe "::setUpdatedSynchronously", -> - it "controls whether the text editor is updated synchronously", -> - spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> fn() - - element = new TextEditorElement - jasmine.attachToDOM(element) - - element.setUpdatedSynchronously(false) - expect(element.isUpdatedSynchronously()).toBe false - - element.getModel().setText("hello") - expect(window.requestAnimationFrame).toHaveBeenCalled() - - expect(element.textContent).toContain "hello" - - window.requestAnimationFrame.reset() - element.setUpdatedSynchronously(true) - element.getModel().setText("goodbye") - expect(window.requestAnimationFrame).not.toHaveBeenCalled() - expect(element.textContent).toContain "goodbye" - - describe "::getDefaultCharacterWidth", -> - it "returns null before the element is attached", -> - element = new TextEditorElement - expect(element.getDefaultCharacterWidth()).toBeNull() - - it "returns the width of a character in the root scope", -> - element = new TextEditorElement - jasmine.attachToDOM(element) - expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0) - - describe "::getMaxScrollTop", -> - it "returns the maximum scroll top that can be applied to the element", -> - editor = new TextEditor - editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16') - element = editor.getElement() - element.style.lineHeight = "10px" - element.style.width = "200px" - jasmine.attachToDOM(element) - - expect(element.getMaxScrollTop()).toBe(0) - - element.style.height = '100px' - editor.update({autoHeight: false}) - element.component.measureDimensions() - expect(element.getMaxScrollTop()).toBe(60) - - element.style.height = '120px' - element.component.measureDimensions() - expect(element.getMaxScrollTop()).toBe(40) - - element.style.height = '200px' - element.component.measureDimensions() - expect(element.getMaxScrollTop()).toBe(0) - - describe "on TextEditor::setMini", -> - it "changes the element's 'mini' attribute", -> - element = new TextEditorElement - jasmine.attachToDOM(element) - expect(element.hasAttribute('mini')).toBe false - element.getModel().setMini(true) - expect(element.hasAttribute('mini')).toBe true - element.getModel().setMini(false) - expect(element.hasAttribute('mini')).toBe false - - describe "events", -> - element = null - - beforeEach -> - element = new TextEditorElement - element.getModel().setText("lorem\nipsum\ndolor\nsit\namet") - element.setUpdatedSynchronously(true) - element.setHeight(20) - element.setWidth(20) - element.getModel().update({autoHeight: false}) - - describe "::onDidChangeScrollTop(callback)", -> - it "triggers even when subscribing before attaching the element", -> - positions = [] - subscription1 = element.onDidChangeScrollTop (p) -> positions.push(p) - jasmine.attachToDOM(element) - subscription2 = element.onDidChangeScrollTop (p) -> positions.push(p) - - positions.length = 0 - element.setScrollTop(10) - expect(positions).toEqual([10, 10]) - - element.remove() - jasmine.attachToDOM(element) - - positions.length = 0 - element.setScrollTop(20) - expect(positions).toEqual([20, 20]) - - subscription1.dispose() - - positions.length = 0 - element.setScrollTop(30) - expect(positions).toEqual([30]) - - describe "::onDidChangeScrollLeft(callback)", -> - it "triggers even when subscribing before attaching the element", -> - positions = [] - subscription1 = element.onDidChangeScrollLeft (p) -> positions.push(p) - jasmine.attachToDOM(element) - subscription2 = element.onDidChangeScrollLeft (p) -> positions.push(p) - - positions.length = 0 - element.setScrollLeft(10) - expect(positions).toEqual([10, 10]) - - element.remove() - jasmine.attachToDOM(element) - - positions.length = 0 - element.setScrollLeft(20) - expect(positions).toEqual([20, 20]) - - subscription1.dispose() - - positions.length = 0 - element.setScrollLeft(30) - expect(positions).toEqual([30]) diff --git a/spec/text-editor-element-spec.js b/spec/text-editor-element-spec.js new file mode 100644 index 000000000..c92c6f144 --- /dev/null +++ b/spec/text-editor-element-spec.js @@ -0,0 +1,428 @@ +/* global HTMLDivElement */ + +const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') +const TextEditor = require('../src/text-editor') +const TextEditorElement = require('../src/text-editor-element') + +describe('TextEditorElement', () => { + let jasmineContent + + beforeEach(() => { + jasmineContent = document.body.querySelector('#jasmine-content') + }) + + function buildTextEditorElement (options = {}) { + const element = new TextEditorElement() + element.setUpdatedSynchronously(false) + if (options.attach !== false) jasmine.attachToDOM(element) + return element + } + + it("honors the 'mini' attribute", () => { + jasmineContent.innerHTML = '' + const element = jasmineContent.firstChild + expect(element.getModel().isMini()).toBe(true) + + element.removeAttribute('mini') + expect(element.getModel().isMini()).toBe(false) + expect(element.getComponent().getGutterContainerWidth()).toBe(0) + + element.setAttribute('mini', '') + expect(element.getModel().isMini()).toBe(true) + }) + + it('sets the editor to mini if the model is accessed prior to attaching the element', () => { + const parent = document.createElement('div') + parent.innerHTML = '' + const element = parent.firstChild + expect(element.getModel().isMini()).toBe(true) + }) + + it("honors the 'placeholder-text' attribute", () => { + jasmineContent.innerHTML = "" + const element = jasmineContent.firstChild + expect(element.getModel().getPlaceholderText()).toBe('testing') + + element.setAttribute('placeholder-text', 'placeholder') + expect(element.getModel().getPlaceholderText()).toBe('placeholder') + + element.removeAttribute('placeholder-text') + expect(element.getModel().getPlaceholderText()).toBeNull() + }) + + it("only assigns 'placeholder-text' on the model if the attribute is present", () => { + const editor = new TextEditor({placeholderText: 'placeholder'}) + editor.getElement() + expect(editor.getPlaceholderText()).toBe('placeholder') + }) + + it("honors the 'gutter-hidden' attribute", () => { + jasmineContent.innerHTML = '' + const element = jasmineContent.firstChild + expect(element.getModel().isLineNumberGutterVisible()).toBe(false) + + element.removeAttribute('gutter-hidden') + expect(element.getModel().isLineNumberGutterVisible()).toBe(true) + + element.setAttribute('gutter-hidden', '') + expect(element.getModel().isLineNumberGutterVisible()).toBe(false) + }) + + it('honors the text content', () => { + jasmineContent.innerHTML = 'testing' + const element = jasmineContent.firstChild + expect(element.getModel().getText()).toBe('testing') + }) + + describe('when the model is assigned', () => + it("adds the 'mini' attribute if .isMini() returns true on the model", function (done) { + const element = buildTextEditorElement() + element.getModel().update({mini: true}) + atom.views.getNextUpdatePromise().then(() => { + expect(element.hasAttribute('mini')).toBe(true) + done() + }) + }) + ) + + describe('when the editor is attached to the DOM', () => + it('mounts the component and unmounts when removed from the dom', () => { + const element = buildTextEditorElement() + + const { component } = element + expect(component.attached).toBe(true) + element.remove() + expect(component.attached).toBe(false) + + jasmine.attachToDOM(element) + expect(element.component.attached).toBe(true) + }) + ) + + describe('when the editor is detached from the DOM and then reattached', () => { + it('does not render duplicate line numbers', () => { + const editor = new TextEditor() + editor.setText('1\n2\n3') + const element = editor.getElement() + jasmine.attachToDOM(element) + + const initialCount = element.querySelectorAll('.line-number').length + + element.remove() + jasmine.attachToDOM(element) + expect(element.querySelectorAll('.line-number').length).toBe(initialCount) + }) + + it('does not render duplicate decorations in custom gutters', () => { + const editor = new TextEditor() + editor.setText('1\n2\n3') + editor.addGutter({name: 'test-gutter'}) + const marker = editor.markBufferRange([[0, 0], [2, 0]]) + editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'}) + const element = editor.getElement() + + jasmine.attachToDOM(element) + const initialDecorationCount = element.querySelectorAll('.decoration').length + + element.remove() + jasmine.attachToDOM(element) + expect(element.querySelectorAll('.decoration').length).toBe(initialDecorationCount) + }) + + it('can be re-focused using the previous `document.activeElement`', () => { + const editorElement = buildTextEditorElement() + editorElement.focus() + + const { activeElement } = document + + editorElement.remove() + jasmine.attachToDOM(editorElement) + activeElement.focus() + + expect(editorElement.hasFocus()).toBe(true) + }) + }) + + describe('focus and blur handling', () => { + it('proxies focus/blur events to/from the hidden input', () => { + const element = buildTextEditorElement() + jasmineContent.appendChild(element) + + let blurCalled = false + element.addEventListener('blur', () => { + blurCalled = true + }) + + element.focus() + expect(blurCalled).toBe(false) + expect(element.hasFocus()).toBe(true) + expect(document.activeElement).toBe(element.querySelector('input')) + + document.body.focus() + expect(blurCalled).toBe(true) + }) + + it("doesn't trigger a blur event on the editor element when focusing an already focused editor element", () => { + let blurCalled = false + const element = buildTextEditorElement() + element.addEventListener('blur', () => { blurCalled = true }) + + jasmineContent.appendChild(element) + expect(document.activeElement).toBe(document.body) + expect(blurCalled).toBe(false) + + element.focus() + expect(document.activeElement).toBe(element.querySelector('input')) + expect(blurCalled).toBe(false) + + element.focus() + expect(document.activeElement).toBe(element.querySelector('input')) + expect(blurCalled).toBe(false) + }) + + describe('when focused while a parent node is being attached to the DOM', () => { + class ElementThatFocusesChild extends HTMLDivElement { + attachedCallback () { + this.firstChild.focus() + } + } + + document.registerElement('element-that-focuses-child', + {prototype: ElementThatFocusesChild.prototype} + ) + + it('proxies the focus event to the hidden input', () => { + const element = buildTextEditorElement() + const parentElement = document.createElement('element-that-focuses-child') + parentElement.appendChild(element) + jasmineContent.appendChild(parentElement) + expect(document.activeElement).toBe(element.querySelector('input')) + }) + }) + }) + + describe('::onDidAttach and ::onDidDetach', () => + it('invokes callbacks when the element is attached and detached', () => { + const element = buildTextEditorElement({attach: false}) + + const attachedCallback = jasmine.createSpy('attachedCallback') + const detachedCallback = jasmine.createSpy('detachedCallback') + + element.onDidAttach(attachedCallback) + element.onDidDetach(detachedCallback) + + jasmine.attachToDOM(element) + expect(attachedCallback).toHaveBeenCalled() + expect(detachedCallback).not.toHaveBeenCalled() + + attachedCallback.reset() + element.remove() + + expect(attachedCallback).not.toHaveBeenCalled() + expect(detachedCallback).toHaveBeenCalled() + }) + ) + + describe('::setUpdatedSynchronously', () => + it('controls whether the text editor is updated synchronously', () => { + spyOn(window, 'requestAnimationFrame').andCallFake(fn => fn()) + + const element = buildTextEditorElement() + jasmine.attachToDOM(element) + + expect(element.isUpdatedSynchronously()).toBe(false) + + element.getModel().setText('hello') + expect(window.requestAnimationFrame).toHaveBeenCalled() + + expect(element.textContent).toContain('hello') + + window.requestAnimationFrame.reset() + element.setUpdatedSynchronously(true) + element.getModel().setText('goodbye') + expect(window.requestAnimationFrame).not.toHaveBeenCalled() + expect(element.textContent).toContain('goodbye') + }) + ) + + describe('::getDefaultCharacterWidth', () => { + it('returns 0 before the element is attached', () => { + const element = buildTextEditorElement({attach: false}) + expect(element.getDefaultCharacterWidth()).toBe(0) + }) + + it('returns the width of a character in the root scope', () => { + const element = buildTextEditorElement() + jasmine.attachToDOM(element) + expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0) + }) + }) + + describe('::getMaxScrollTop', () => + it('returns the maximum scroll top that can be applied to the element', async () => { + const editor = new TextEditor() + editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16') + const element = editor.getElement() + element.style.lineHeight = '10px' + element.style.width = '200px' + jasmine.attachToDOM(element) + + expect(element.getMaxScrollTop()).toBe(0) + await editor.update({autoHeight: false}) + + element.style.height = '100px' + await element.getNextUpdatePromise() + expect(element.getMaxScrollTop()).toBe(60) + + element.style.height = '120px' + await element.getNextUpdatePromise() + expect(element.getMaxScrollTop()).toBe(40) + + element.style.height = '200px' + await element.getNextUpdatePromise() + expect(element.getMaxScrollTop()).toBe(0) + }) + ) + + describe('::setScrollTop and ::setScrollLeft', () => { + it('changes the scroll position', async () => { + element = buildTextEditorElement() + element.getModel().update({autoHeight: false}) + element.getModel().setText('lorem\nipsum\ndolor\nsit\namet') + element.setHeight(20) + await element.getNextUpdatePromise() + element.setWidth(20) + await element.getNextUpdatePromise() + + element.setScrollTop(22) + await element.getNextUpdatePromise() + expect(element.getScrollTop()).toBe(22) + + element.setScrollLeft(32) + await element.getNextUpdatePromise() + expect(element.getScrollLeft()).toBe(32) + }) + }) + + describe('on TextEditor::setMini', () => + it("changes the element's 'mini' attribute", async () => { + const element = buildTextEditorElement() + expect(element.hasAttribute('mini')).toBe(false) + element.getModel().setMini(true) + await element.getNextUpdatePromise() + expect(element.hasAttribute('mini')).toBe(true) + element.getModel().setMini(false) + await element.getNextUpdatePromise() + expect(element.hasAttribute('mini')).toBe(false) + }) + ) + + describe('::intersectsVisibleRowRange(start, end)', () => { + it('returns true if the given row range intersects the visible row range', async () => { + const element = buildTextEditorElement() + const editor = element.getModel() + editor.update({autoHeight: false}) + element.getModel().setText('x\n'.repeat(20)) + element.style.height = '120px' + await element.getNextUpdatePromise() + element.setScrollTop(80) + await element.getNextUpdatePromise() + expect(element.getVisibleRowRange()).toEqual([4, 11]) + + expect(element.intersectsVisibleRowRange(0, 4)).toBe(false) + expect(element.intersectsVisibleRowRange(0, 5)).toBe(true) + expect(element.intersectsVisibleRowRange(5, 8)).toBe(true) + expect(element.intersectsVisibleRowRange(11, 12)).toBe(false) + expect(element.intersectsVisibleRowRange(12, 13)).toBe(false) + }) + }) + + describe('::pixelRectForScreenRange(range)', () => { + it('returns a {top/left/width/height} object describing the rectangle between two screen positions, even if they are not on screen', async () => { + const element = buildTextEditorElement() + const editor = element.getModel() + editor.update({autoHeight: false}) + element.getModel().setText('xxxxxxxxxxxxxxxxxxxxxx\n'.repeat(20)) + element.style.height = '120px' + await element.getNextUpdatePromise() + element.setScrollTop(80) + await element.getNextUpdatePromise() + expect(element.getVisibleRowRange()).toEqual([4, 11]) + + const top = 2 * editor.getLineHeightInPixels() + const bottom = 13 * editor.getLineHeightInPixels() + const left = Math.round(3 * editor.getDefaultCharWidth()) + const right = Math.round(11 * editor.getDefaultCharWidth()) + expect(element.pixelRectForScreenRange([[2, 3], [13, 11]])).toEqual({ + top, + left, + height: bottom + editor.getLineHeightInPixels() - top, + width: right - left + }) + }) + }) + + describe('events', () => { + let element = null + + beforeEach(async () => { + element = buildTextEditorElement() + element.getModel().update({autoHeight: false}) + element.getModel().setText('lorem\nipsum\ndolor\nsit\namet') + element.setHeight(20) + await element.getNextUpdatePromise() + element.setWidth(20) + await element.getNextUpdatePromise() + }) + + describe('::onDidChangeScrollTop(callback)', () => + it('triggers even when subscribing before attaching the element', () => { + const positions = [] + const subscription1 = element.onDidChangeScrollTop(p => positions.push(p)) + element.onDidChangeScrollTop(p => positions.push(p)) + + positions.length = 0 + element.setScrollTop(10) + expect(positions).toEqual([10, 10]) + + element.remove() + jasmine.attachToDOM(element) + + positions.length = 0 + element.setScrollTop(20) + expect(positions).toEqual([20, 20]) + + subscription1.dispose() + + positions.length = 0 + element.setScrollTop(30) + expect(positions).toEqual([30]) + }) + ) + + describe('::onDidChangeScrollLeft(callback)', () => + it('triggers even when subscribing before attaching the element', () => { + const positions = [] + const subscription1 = element.onDidChangeScrollLeft(p => positions.push(p)) + element.onDidChangeScrollLeft(p => positions.push(p)) + + positions.length = 0 + element.setScrollLeft(10) + expect(positions).toEqual([10, 10]) + + element.remove() + jasmine.attachToDOM(element) + + positions.length = 0 + element.setScrollLeft(20) + expect(positions).toEqual([20, 20]) + + subscription1.dispose() + + positions.length = 0 + element.setScrollLeft(30) + expect(positions).toEqual([30]) + }) + ) + }) +}) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee deleted file mode 100644 index 2c4b6dbab..000000000 --- a/spec/text-editor-presenter-spec.coffee +++ /dev/null @@ -1,3901 +0,0 @@ -_ = require 'underscore-plus' -randomWords = require 'random-words' -TextBuffer = require 'text-buffer' -{Point, Range} = TextBuffer -TextEditor = require '../src/text-editor' -TextEditorPresenter = require '../src/text-editor-presenter' -FakeLinesYardstick = require './fake-lines-yardstick' -LineTopIndex = require 'line-top-index' - -describe "TextEditorPresenter", -> - # These `describe` and `it` blocks mirror the structure of the ::state object. - # Please maintain this structure when adding specs for new state fields. - describe "::get(Pre|Post)MeasurementState()", -> - [buffer, editor] = [] - - beforeEach -> - # These *should* be mocked in the spec helper, but changing that now would break packages :-( - spyOn(window, "setInterval").andCallFake window.fakeSetInterval - spyOn(window, "clearInterval").andCallFake window.fakeClearInterval - - buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) - editor = new TextEditor({buffer}) - waitsForPromise -> buffer.load() - - afterEach -> - editor.destroy() - buffer.destroy() - - getState = (presenter) -> - presenter.getPreMeasurementState() - presenter.getPostMeasurementState() - - addBlockDecorationBeforeScreenRow = (screenRow, item) -> - editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], invalidate: "never"), - type: "block", item: item, position: "before" - ) - - addBlockDecorationAfterScreenRow = (screenRow, item) -> - editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], invalidate: "never"), - type: "block", item: item, position: "after" - ) - - buildPresenterWithoutMeasurements = (params={}) -> - lineTopIndex = new LineTopIndex({ - defaultLineHeight: editor.getLineHeightInPixels() - }) - _.defaults params, - model: editor - contentFrameWidth: 500 - lineTopIndex: lineTopIndex - presenter = new TextEditorPresenter(params) - presenter.setLinesYardstick(new FakeLinesYardstick(editor, lineTopIndex)) - presenter - - buildPresenter = (params={}) -> - presenter = buildPresenterWithoutMeasurements(params) - presenter.setScrollTop(params.scrollTop) if params.scrollTop? - presenter.setScrollLeft(params.scrollLeft) if params.scrollLeft? - presenter.setExplicitHeight(params.explicitHeight ? 130) - presenter.setWindowSize(params.windowWidth ? 500, params.windowHeight ? 130) - presenter.setBoundingClientRect(params.boundingClientRect ? { - left: 0 - top: 0 - width: 500 - height: 130 - }) - presenter.setGutterWidth(params.gutterWidth ? 0) - presenter.setLineHeight(params.lineHeight ? 10) - presenter.setBaseCharacterWidth(params.baseCharacterWidth ? 10) - presenter.setHorizontalScrollbarHeight(params.horizontalScrollbarHeight ? 10) - presenter.setVerticalScrollbarWidth(params.verticalScrollbarWidth ? 10) - presenter - - expectValues = (actual, expected) -> - for key, value of expected - expect(actual[key]).toEqual value - - expectStateUpdatedToBe = (value, presenter, fn) -> - updatedState = false - disposable = presenter.onDidUpdateState -> - updatedState = true - disposable.dispose() - fn() - expect(updatedState).toBe(value) - - expectStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(true, presenter, fn) - - expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) - - waitsForStateToUpdate = (presenter, fn) -> - line = new Error().stack.split('\n')[2].split(':')[1] - - waitsFor "presenter state to update at line #{line}", 1000, (done) -> - disposable = presenter.onDidUpdateState -> - disposable.dispose() - process.nextTick(done) - fn?() - - tiledContentContract = (stateFn) -> - it "contains states for tiles that are visible on screen", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - - expectValues stateFn(presenter).tiles[0], { - top: 0 - } - expectValues stateFn(presenter).tiles[2], { - top: 2 - } - expectValues stateFn(presenter).tiles[4], { - top: 4 - } - expectValues stateFn(presenter).tiles[6], { - top: 6 - } - - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setScrollTop(3) - - expect(stateFn(presenter).tiles[0]).toBeUndefined() - - expectValues stateFn(presenter).tiles[2], { - top: -1 - } - expectValues stateFn(presenter).tiles[4], { - top: 1 - } - expectValues stateFn(presenter).tiles[6], { - top: 3 - } - expectValues stateFn(presenter).tiles[8], { - top: 5 - } - expectValues stateFn(presenter).tiles[10], { - top: 7 - } - - expect(stateFn(presenter).tiles[12]).toBeUndefined() - - it "includes state for tiles containing screen rows to measure", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - presenter.setScreenRowsToMeasure([10, 12]) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() - expect(stateFn(presenter).tiles[10]).toBeDefined() - expect(stateFn(presenter).tiles[12]).toBeDefined() - - # clearing additional rows won't trigger a state update - expectNoStateUpdate presenter, -> presenter.clearScreenRowsToMeasure() - - # when another change triggers a state update we remove useless lines - expectStateUpdate presenter, -> presenter.setScrollTop(1) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeDefined() - expect(stateFn(presenter).tiles[10]).toBeUndefined() - expect(stateFn(presenter).tiles[12]).toBeUndefined() - - describe "when there are block decorations", -> - it "computes each tile's height and scrollTop based on block decorations' height", -> - presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) - - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - blockDecoration2 = addBlockDecorationBeforeScreenRow(3) - blockDecoration3 = addBlockDecorationBeforeScreenRow(5) - blockDecoration4 = addBlockDecorationAfterScreenRow(5) - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 1) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 30) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 40) - presenter.setBlockDecorationDimensions(blockDecoration4, 0, 50) - - expect(stateFn(presenter).tiles[0].height).toBe(2 * 10 + 1) - expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) - expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1) - expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 40 + 50) - expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30) - expect(stateFn(presenter).tiles[6].height).toBe(2 * 10) - expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 40 + 50) - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - presenter.setScrollTop(21) - - expect(stateFn(presenter).tiles[0]).toBeUndefined() - expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1 - 21) - expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 40 + 50) - expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30 - 21) - expect(stateFn(presenter).tiles[6].height).toBe(2 * 10) - expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 40 + 50 - 21) - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - blockDecoration3.getMarker().setHeadScreenPosition([6, 0]) - - expect(stateFn(presenter).tiles[0]).toBeUndefined() - expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) - expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1 - 21) - expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 50) - expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30 - 21) - expect(stateFn(presenter).tiles[6].height).toBe(2 * 10 + 40) - expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 50 - 21) - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - it "works correctly when soft wrapping is enabled", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - blockDecoration2 = addBlockDecorationBeforeScreenRow(4) - blockDecoration3 = addBlockDecorationBeforeScreenRow(8) - - presenter = buildPresenter(explicitHeight: 330, lineHeight: 10, tileSize: 2, baseCharacterWidth: 5) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 30) - - expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) - expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) - expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) - expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10 + 20 + 30) - expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20 + 30) - - editor.update({softWrapped: true}) - presenter.setContentFrameWidth(5 * 25) - - expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) - expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) - expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) - expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10) - expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10) - expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10) - expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[14].top).toBe(14 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[16].top).toBe(16 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[18].top).toBe(18 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[20].top).toBe(20 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[22].top).toBe(22 * 10 + 10 + 20 + 30) - expect(stateFn(presenter).tiles[24].top).toBe(24 * 10 + 10 + 20 + 30) - expect(stateFn(presenter).tiles[26].top).toBe(26 * 10 + 10 + 20 + 30) - expect(stateFn(presenter).tiles[28].top).toBe(28 * 10 + 10 + 20 + 30) - - editor.update({softWrapped: false}) - - expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) - expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) - expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) - expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10 + 20) - expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10 + 20 + 30) - expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20 + 30) - - it "includes state for all tiles if no external ::explicitHeight is assigned", -> - presenter = buildPresenter(explicitHeight: null, tileSize: 2) - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[12]).toBeDefined() - - it "is empty until all of the required measurements are assigned", -> - presenter = buildPresenterWithoutMeasurements() - expect(stateFn(presenter).tiles).toEqual({}) - - presenter.setExplicitHeight(25) - expect(stateFn(presenter).tiles).toEqual({}) - - # Sets scroll row from model's logical position - presenter.setLineHeight(10) - expect(stateFn(presenter).tiles).not.toEqual({}) - - it "updates when ::scrollTop changes", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setScrollTop(2) - - expect(stateFn(presenter).tiles[0]).toBeUndefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeDefined() - expect(stateFn(presenter).tiles[10]).toBeUndefined() - - it "updates when ::explicitHeight changes", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setExplicitHeight(8) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeDefined() - expect(stateFn(presenter).tiles[10]).toBeUndefined() - - it "updates when ::lineHeight changes", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setLineHeight(4) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeUndefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() - - presenter.setMouseWheelScreenRow(0) - expectStateUpdate presenter, -> presenter.setScrollTop(4) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeUndefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[12]).toBeUndefined() - - expectStateUpdate presenter, -> advanceClock(200) - - expect(stateFn(presenter).tiles[0]).toBeUndefined() - expect(stateFn(presenter).tiles[2]).toBeUndefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[12]).toBeUndefined() - - - # should clear ::mouseWheelScreenRow after stoppedScrollingDelay elapses even if we don't scroll first - presenter.setMouseWheelScreenRow(4) - advanceClock(200) - expectStateUpdate presenter, -> presenter.setScrollTop(6) - expect(stateFn(presenter).tiles[4]).toBeUndefined() - - it "does not preserve deleted on-screen tiles even if they correspond to ::mouseWheelScreenRow", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) - - presenter.setMouseWheelScreenRow(2) - - expectStateUpdate presenter, -> editor.setText("") - - expect(stateFn(presenter).tiles[2]).toBeUndefined() - expect(stateFn(presenter).tiles[0]).toBeDefined() - - describe "during state retrieval", -> - it "does not trigger onDidUpdateState events", -> - presenter = buildPresenter() - expectNoStateUpdate presenter, -> getState(presenter) - - describe ".horizontalScrollbar", -> - describe ".visible", -> - it "is true if the scrollWidth exceeds the computed client width", -> - presenter = buildPresenter - explicitHeight: editor.getLineCount() * 10 - contentFrameWidth: editor.getMaxScreenLineLength() * 10 + 1 - baseCharacterWidth: 10 - lineHeight: 10 - horizontalScrollbarHeight: 10 - verticalScrollbarWidth: 10 - - expect(getState(presenter).horizontalScrollbar.visible).toBe false - - # ::contentFrameWidth itself is smaller than scrollWidth - presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10) - expect(getState(presenter).horizontalScrollbar.visible).toBe true - - # restore... - presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10 + 1) - expect(getState(presenter).horizontalScrollbar.visible).toBe false - - # visible vertical scrollbar makes the clientWidth smaller than the scrollWidth - presenter.setExplicitHeight((editor.getLineCount() * 10) - 1) - expect(getState(presenter).horizontalScrollbar.visible).toBe true - - it "is false if the editor is mini", -> - presenter = buildPresenter - explicitHeight: editor.getLineCount() * 10 - contentFrameWidth: editor.getMaxScreenLineLength() * 10 - 10 - baseCharacterWidth: 10 - - expect(getState(presenter).horizontalScrollbar.visible).toBe true - editor.setMini(true) - expect(getState(presenter).horizontalScrollbar.visible).toBe false - editor.setMini(false) - expect(getState(presenter).horizontalScrollbar.visible).toBe true - - it "is false when `editor.autoWidth` is true", -> - editor.update({autoWidth: true}) - presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 30, verticalScrollbarWidth: 7, baseCharacterWidth: 10) - getState(presenter) # trigger a state update to store state in the presenter - editor.setText('abcdefghijklm') - expect(getState(presenter).horizontalScrollbar.visible).toBe(false) - - describe ".height", -> - it "tracks the value of ::horizontalScrollbarHeight", -> - presenter = buildPresenter(horizontalScrollbarHeight: 10) - expect(getState(presenter).horizontalScrollbar.height).toBe 10 - expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(20) - expect(getState(presenter).horizontalScrollbar.height).toBe 20 - - describe ".right", -> - it "is ::verticalScrollbarWidth if the vertical scrollbar is visible and 0 otherwise", -> - presenter = buildPresenter - explicitHeight: editor.getLineCount() * 10 + 50 - contentFrameWidth: editor.getMaxScreenLineLength() * 10 - baseCharacterWidth: 10 - lineHeight: 10 - horizontalScrollbarHeight: 10 - verticalScrollbarWidth: 10 - - expect(getState(presenter).horizontalScrollbar.right).toBe 0 - presenter.setExplicitHeight((editor.getLineCount() * 10) - 1) - expect(getState(presenter).horizontalScrollbar.right).toBe 10 - - describe ".scrollWidth", -> - it "is initialized as the max of the ::contentFrameWidth and the width of the longest line", -> - maxLineLength = editor.getMaxScreenLineLength() - - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - - presenter = buildPresenter(contentFrameWidth: 10 * maxLineLength + 20, baseCharacterWidth: 10) - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 - - it "updates when the ::contentFrameWidth changes", -> - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 - - it "updates when character widths change", -> - waitsForPromise -> atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--meta.syntax--method-call.syntax--js', 'syntax--support.syntax--function.syntax--js'], 'p', 20) - presenter.measurementsChanged() - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide - - it "updates when ::softWrapped changes on the editor", -> - presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - expectStateUpdate presenter, -> editor.update({softWrapped: true}) - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe presenter.clientWidth - expectStateUpdate presenter, -> editor.update({softWrapped: false}) - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - it "updates when the longest line changes", -> - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - expectStateUpdate presenter, -> editor.setCursorBufferPosition([editor.getLongestScreenRow(), 0]) - expectStateUpdate presenter, -> editor.insertText('xyz') - - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - describe ".scrollLeft", -> - it "tracks the value of ::scrollLeft", -> - presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollLeft(50) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe 50 - - it "never exceeds the computed scrollWidth minus the computed clientWidth", -> - presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, explicitHeight: 100, contentFrameWidth: 500) - expectStateUpdate presenter, -> presenter.setScrollLeft(300) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - - expectStateUpdate presenter, -> presenter.setContentFrameWidth(600) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - - expectStateUpdate presenter, -> presenter.setVerticalScrollbarWidth(5) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - - expectStateUpdate presenter, -> editor.getBuffer().delete([[6, 0], [6, Infinity]]) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - - # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollLeftBefore = getState(presenter).horizontalScrollbar.scrollLeft - expectStateUpdate presenter, -> editor.getBuffer().insert([6, 0], new Array(100).join('x')) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe scrollLeftBefore - - it "never goes negative", -> - presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expectStateUpdate presenter, -> presenter.setScrollLeft(-300) - expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe 0 - - it "is always 0 when soft wrapping is enabled", -> - presenter = buildPresenter(scrollLeft: 0, verticalScrollbarWidth: 0, contentFrameWidth: 85, baseCharacterWidth: 10) - - editor.update({softWrapped: false}) - presenter.setScrollLeft(Infinity) - expect(getState(presenter).content.scrollLeft).toBeGreaterThan 0 - - editor.update({softWrapped: true}) - expect(getState(presenter).content.scrollLeft).toBe 0 - presenter.setScrollLeft(10) - expect(getState(presenter).content.scrollLeft).toBe 0 - - it "is always 0 when `editor.autoWidth` is true", -> - editor.update({autoWidth: true}) - editor.setText('abcdefghijklm') - presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 30, verticalScrollbarWidth: 15, baseCharacterWidth: 10) - getState(presenter) # trigger a state update to store state in the presenter - - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText('n') - expect(getState(presenter).content.scrollLeft).toBe(0) - - editor.setText('abcdefghijklm\nnopqrstuvwxy') # make the vertical scrollbar appear - editor.setCursorBufferPosition([1, Infinity]) - editor.insertText('z') - expect(getState(presenter).content.scrollLeft).toBe(0) - - describe ".verticalScrollbar", -> - describe ".visible", -> - it "is true if the scrollHeight exceeds the computed client height", -> - presenter = buildPresenter - model: editor - explicitHeight: editor.getLineCount() * 10 - contentFrameWidth: editor.getMaxScreenLineLength() * 10 + 1 - baseCharacterWidth: 10 - lineHeight: 10 - horizontalScrollbarHeight: 10 - verticalScrollbarWidth: 10 - - expect(getState(presenter).verticalScrollbar.visible).toBe false - - # ::explicitHeight itself is smaller than scrollWidth - presenter.setExplicitHeight(editor.getLineCount() * 10 - 1) - expect(getState(presenter).verticalScrollbar.visible).toBe true - - # restore... - presenter.setExplicitHeight(editor.getLineCount() * 10) - expect(getState(presenter).verticalScrollbar.visible).toBe false - - # visible horizontal scrollbar makes the clientHeight smaller than the scrollHeight - presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10) - expect(getState(presenter).verticalScrollbar.visible).toBe true - - describe ".width", -> - it "is assigned based on ::verticalScrollbarWidth", -> - presenter = buildPresenter(verticalScrollbarWidth: 10) - expect(getState(presenter).verticalScrollbar.width).toBe 10 - expectStateUpdate presenter, -> presenter.setVerticalScrollbarWidth(20) - expect(getState(presenter).verticalScrollbar.width).toBe 20 - - describe ".bottom", -> - it "is ::horizontalScrollbarHeight if the horizontal scrollbar is visible and 0 otherwise", -> - presenter = buildPresenter - explicitHeight: editor.getLineCount() * 10 - 1 - contentFrameWidth: editor.getMaxScreenLineLength() * 10 + 50 - baseCharacterWidth: 10 - lineHeight: 10 - horizontalScrollbarHeight: 10 - verticalScrollbarWidth: 10 - - expect(getState(presenter).verticalScrollbar.bottom).toBe 0 - presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10) - expect(getState(presenter).verticalScrollbar.bottom).toBe 10 - - describe ".scrollHeight", -> - it "is initialized based on the lineHeight, the number of lines, and the height", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - - presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 500) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500 - - it "updates when new block decorations are measured, changed or destroyed", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - blockDecoration2 = addBlockDecorationBeforeScreenRow(3) - blockDecoration3 = addBlockDecorationBeforeScreenRow(7) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) - - linesHeight = editor.getScreenLineCount() * 10 - blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) - - blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - waitsForStateToUpdate presenter, -> blockDecoration3.destroy() - runs -> - blockDecorationsHeight = Math.round(35.8 + 100.3) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - it "updates when the ::lineHeight changes", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 20 - - it "updates when the line count changes", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - - it "updates when ::explicitHeight changes", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expectStateUpdate presenter, -> presenter.setExplicitHeight(500) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500 - - describe "scrollPastEnd", -> - it "adds the computed clientHeight to the computed scrollHeight if scrollPastEnd is enabled", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: true}) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: false}) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight - - describe ".scrollTop", -> - it "tracks the value of ::scrollTop", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 20, horizontalScrollbarHeight: 10) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollTop(50) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe 50 - - it "never exceeds the computed scrollHeight minus the computed clientHeight", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(100) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> presenter.setExplicitHeight(60) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollTopBefore = getState(presenter).verticalScrollbar.scrollTop - expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') - expect(getState(presenter).verticalScrollbar.scrollTop).toBe scrollTopBefore - - it "never goes negative", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(-100) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe 0 - - it "adds the computed clientHeight to the computed scrollHeight if scrollPastEnd is enabled", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - editor.update({scrollPastEnd: true}) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: false}) - expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - describe ".hiddenInput", -> - describe ".top/.left", -> - it "is positioned over the last cursor it is in view and the editor is focused", -> - editor.setCursorBufferPosition([3, 6]) - presenter = buildPresenter(focused: false, explicitHeight: 50, contentFrameWidth: 300, horizontalScrollbarHeight: 0, verticalScrollbarWidth: 0) - expectValues getState(presenter).hiddenInput, {top: 0, left: 0} - - expectStateUpdate presenter, -> presenter.setFocused(true) - expectValues getState(presenter).hiddenInput, {top: 3 * 10, left: 6 * 10} - - expectStateUpdate presenter, -> presenter.setScrollTop(15) - expectValues getState(presenter).hiddenInput, {top: (3 * 10) - 15, left: 6 * 10} - - expectStateUpdate presenter, -> presenter.setScrollLeft(35) - expectValues getState(presenter).hiddenInput, {top: (3 * 10) - 15, left: (6 * 10) - 35} - - expectStateUpdate presenter, -> presenter.setScrollTop(40) - expectValues getState(presenter).hiddenInput, {top: 0, left: (6 * 10) - 35} - - expectStateUpdate presenter, -> presenter.setScrollLeft(70) - expectValues getState(presenter).hiddenInput, {top: 0, left: 0} - - expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43]) - expectValues getState(presenter).hiddenInput, {top: 11 * 10 - presenter.getScrollTop(), left: 43 * 10 - presenter.getScrollLeft()} - - newCursor = null - expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10]) - expectValues getState(presenter).hiddenInput, {top: (6 * 10) - presenter.getScrollTop(), left: (10 * 10) - presenter.getScrollLeft()} - - expectStateUpdate presenter, -> newCursor.destroy() - expectValues getState(presenter).hiddenInput, {top: 50 - 10, left: 300 - 10} - - expectStateUpdate presenter, -> presenter.setFocused(false) - expectValues getState(presenter).hiddenInput, {top: 0, left: 0} - - describe ".height", -> - it "is assigned based on the line height", -> - presenter = buildPresenter() - expect(getState(presenter).hiddenInput.height).toBe 10 - - expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(getState(presenter).hiddenInput.height).toBe 20 - - describe ".width", -> - it "is assigned based on the width of the character following the cursor", -> - waitsForPromise -> atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - editor.setCursorBufferPosition([3, 6]) - presenter = buildPresenter() - expect(getState(presenter).hiddenInput.width).toBe 10 - - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) - expect(getState(presenter).hiddenInput.width).toBe 15 - - expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--storage.syntax--type.syntax--var.syntax--js'], 'r', 20) - presenter.measurementsChanged() - expect(getState(presenter).hiddenInput.width).toBe 20 - - it "is 2px at the end of lines", -> - presenter = buildPresenter() - editor.setCursorBufferPosition([3, Infinity]) - expect(getState(presenter).hiddenInput.width).toBe 2 - - describe ".content", -> - describe '.width', -> - describe "when `editor.autoWidth` is false (the default)", -> - it "equals to the max width between the content frame width and the content width + the vertical scrollbar width", -> - editor.setText('abc\ndef\nghi\njkl') - presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 33, verticalScrollbarWidth: 7, baseCharacterWidth: 10) - expect(getState(presenter).content.width).toBe(3 * 10 + 7 + 1) - presenter.setContentFrameWidth(50) - expect(getState(presenter).content.width).toBe(50) - presenter.setVerticalScrollbarWidth(27) - expect(getState(presenter).content.width).toBe(3 * 10 + 27 + 1) - - describe "when `editor.autoWidth` is true", -> - it "equals to the content width + the vertical scrollbar width", -> - editor.setText('abc\ndef\nghi\njkl') - presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 300, verticalScrollbarWidth: 7, baseCharacterWidth: 10) - expectStateUpdate presenter, -> editor.update({autoWidth: true}) - expect(getState(presenter).content.width).toBe(3 * 10 + 7 + 1) - editor.setText('abcdefghi\n') - expect(getState(presenter).content.width).toBe(9 * 10 + 7 + 1) - - it "ignores the vertical scrollbar width when it is unset", -> - editor.setText('abcdef\nghijkl') - presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 33, verticalScrollbarWidth: 7, baseCharacterWidth: 10) - presenter.setVerticalScrollbarWidth(null) - expect(getState(presenter).content.width).toBe(6 * 10 + 1) - - it "ignores the content frame width when it is unset", -> - editor.setText('abc\ndef\nghi\njkl') - presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 33, verticalScrollbarWidth: 7, baseCharacterWidth: 10) - getState(presenter) # trigger a state update, causing verticalScrollbarWidth to be stored in the presenter - presenter.setContentFrameWidth(null) - expect(getState(presenter).content.width).toBe(3 * 10 + 7 + 1) - - describe ".maxHeight", -> - it "changes based on boundingClientRect", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - - expectStateUpdate presenter, -> - presenter.setBoundingClientRect(left: 0, top: 0, height: 20, width: 0) - expect(getState(presenter).content.maxHeight).toBe(20) - - expectStateUpdate presenter, -> - presenter.setBoundingClientRect(left: 0, top: 0, height: 50, width: 0) - expect(getState(presenter).content.maxHeight).toBe(50) - - describe ".scrollHeight", -> - it "updates when new block decorations are measured, changed or destroyed", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - blockDecoration2 = addBlockDecorationBeforeScreenRow(3) - blockDecoration3 = addBlockDecorationBeforeScreenRow(7) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) - - linesHeight = editor.getScreenLineCount() * 10 - blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) - expect(getState(presenter).content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) - - blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) - expect(getState(presenter).content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - waitsForStateToUpdate presenter, -> blockDecoration3.destroy() - runs -> - blockDecorationsHeight = Math.round(35.8 + 100.3) - expect(getState(presenter).content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - it "is initialized based on the lineHeight, the number of lines, and the height", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expect(getState(presenter).content.scrollHeight).toBe editor.getScreenLineCount() * 10 - - presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 500) - expect(getState(presenter).content.scrollHeight).toBe 500 - - it "updates when the ::lineHeight changes", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(getState(presenter).content.scrollHeight).toBe editor.getScreenLineCount() * 20 - - it "updates when the line count changes", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(getState(presenter).content.scrollHeight).toBe editor.getScreenLineCount() * 10 - - it "updates when ::explicitHeight changes", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expectStateUpdate presenter, -> presenter.setExplicitHeight(500) - expect(getState(presenter).content.scrollHeight).toBe 500 - - it "adds the computed clientHeight to the computed scrollHeight if scrollPastEnd is enabled", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getState(presenter).content.scrollHeight).toBe presenter.contentHeight - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: true}) - expect(getState(presenter).content.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: false}) - expect(getState(presenter).content.scrollHeight).toBe presenter.contentHeight - - describe ".scrollWidth", -> - it "is initialized as the max of the computed clientWidth and the width of the longest line", -> - maxLineLength = editor.getMaxScreenLineLength() - - presenter = buildPresenter(explicitHeight: 100, contentFrameWidth: 50, baseCharacterWidth: 10, verticalScrollbarWidth: 10) - expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 - - presenter = buildPresenter(explicitHeight: 100, contentFrameWidth: 10 * maxLineLength + 20, baseCharacterWidth: 10, verticalScrollbarWidth: 10) - expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 20 - 10 # subtract vertical scrollbar width - - describe "when the longest screen row is the first one and it's hidden", -> - it "doesn't compute an invalid value (regression)", -> - presenter = buildPresenter(tileSize: 2, contentFrameWidth: 10, explicitHeight: 20) - editor.setText """ - a very long long long long long long line - b - c - d - e - """ - - expectStateUpdate presenter, -> presenter.setScrollTop(40) - expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - it "updates when the ::contentFrameWidth changes", -> - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) - expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 20 - - it "updates when character widths change", -> - waitsForPromise -> atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--meta.syntax--method-call.syntax--js', 'syntax--support.syntax--function.syntax--js'], 'p', 20) - presenter.measurementsChanged() - expect(getState(presenter).content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide - - it "updates when ::softWrapped changes on the editor", -> - presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) - expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - expectStateUpdate presenter, -> editor.update({softWrapped: true}) - expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe presenter.clientWidth - expectStateUpdate presenter, -> editor.update({softWrapped: false}) - expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - it "updates when the longest line changes", -> - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - expectStateUpdate presenter, -> editor.setCursorBufferPosition([editor.getLongestScreenRow(), 0]) - expectStateUpdate presenter, -> editor.insertText('xyz') - - expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - it "isn't clipped to 0 when the longest line is folded (regression)", -> - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - editor.foldBufferRow(0) - expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 - - describe ".scrollTop", -> - it "doesn't get stuck when repeatedly setting the same non-integer position in a scroll event listener", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) - expect(getState(presenter).content.scrollTop).toBe(0) - - presenter.onDidChangeScrollTop -> - presenter.setScrollTop(1.5) - getState(presenter) # trigger scroll update - - presenter.setScrollTop(1.5) - getState(presenter) # trigger scroll update - - expect(presenter.getScrollTop()).toBe(2) - expect(presenter.getRealScrollTop()).toBe(1.5) - - it "changes based on the scroll operation that was performed last", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) - expect(getState(presenter).content.scrollTop).toBe(0) - - presenter.setScrollTop(20) - editor.setCursorBufferPosition([5, 0]) - - expect(getState(presenter).content.scrollTop).toBe(50) - - editor.setCursorBufferPosition([8, 0]) - presenter.setScrollTop(10) - - expect(getState(presenter).content.scrollTop).toBe(10) - - it "corresponds to the passed logical coordinates when building the presenter", -> - editor.setFirstVisibleScreenRow(4) - presenter = buildPresenter(lineHeight: 10, explicitHeight: 20) - expect(getState(presenter).content.scrollTop).toBe(40) - - it "tracks the value of ::scrollTop", -> - presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 20) - expect(getState(presenter).content.scrollTop).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollTop(50) - expect(getState(presenter).content.scrollTop).toBe 50 - - it "keeps the model up to date with the corresponding logical coordinates", -> - presenter = buildPresenter(scrollTop: 0, explicitHeight: 20, horizontalScrollbarHeight: 10, lineHeight: 10) - - expectStateUpdate presenter, -> presenter.setScrollTop(50) - getState(presenter) # commits scroll position - expect(editor.getFirstVisibleScreenRow()).toBe 5 - - expectStateUpdate presenter, -> presenter.setScrollTop(57) - getState(presenter) # commits scroll position - expect(editor.getFirstVisibleScreenRow()).toBe 6 - - it "updates when the model's scroll position is changed directly", -> - presenter = buildPresenter(scrollTop: 0, explicitHeight: 20, horizontalScrollbarHeight: 10, lineHeight: 10) - expectStateUpdate presenter, -> editor.setFirstVisibleScreenRow(1) - expect(getState(presenter).content.scrollTop).toBe 10 - - it "reassigns the scrollTop if it exceeds the max possible value after lines are removed", -> - presenter = buildPresenter(scrollTop: 80, lineHeight: 10, explicitHeight: 50, horizontalScrollbarHeight: 0) - expect(getState(presenter).content.scrollTop).toBe(80) - buffer.deleteRows(10, 9, 8) - expect(getState(presenter).content.scrollTop).toBe(60) - - it "is always rounded to the nearest integer", -> - presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 20) - expect(getState(presenter).content.scrollTop).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollTop(11.4) - expect(getState(presenter).content.scrollTop).toBe 11 - expectStateUpdate presenter, -> presenter.setScrollTop(12.6) - expect(getState(presenter).content.scrollTop).toBe 13 - - it "scrolls down automatically when the model is changed", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) - - editor.setText("") - editor.insertNewline() - expect(getState(presenter).content.scrollTop).toBe(0) - - editor.insertNewline() - expect(getState(presenter).content.scrollTop).toBe(10) - - editor.insertNewline() - expect(getState(presenter).content.scrollTop).toBe(20) - - it "never exceeds the computed scroll height minus the computed client height", -> - didChangeScrollTopSpy = jasmine.createSpy() - presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - presenter.onDidChangeScrollTop(didChangeScrollTopSpy) - - expectStateUpdate presenter, -> presenter.setScrollTop(100) - expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight - expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight - - didChangeScrollTopSpy.reset() - expectStateUpdate presenter, -> presenter.setExplicitHeight(60) - expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight - expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight - - didChangeScrollTopSpy.reset() - expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) - expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight - expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight - - didChangeScrollTopSpy.reset() - expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) - expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight - expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight - - # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollTopBefore = getState(presenter).verticalScrollbar.scrollTop - didChangeScrollTopSpy.reset() - expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') - expect(getState(presenter).content.scrollTop).toBe scrollTopBefore - expect(presenter.getRealScrollTop()).toBe scrollTopBefore - expect(didChangeScrollTopSpy).not.toHaveBeenCalled() - - it "never goes negative", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(-100) - expect(getState(presenter).content.scrollTop).toBe 0 - - it "adds the computed clientHeight to the computed scrollHeight if scrollPastEnd is enabled", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getState(presenter).content.scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - editor.update({scrollPastEnd: true}) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getState(presenter).content.scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: false}) - expect(getState(presenter).content.scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - describe ".scrollLeft", -> - it "doesn't get stuck when repeatedly setting the same non-integer position in a scroll event listener", -> - presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 10) - expect(getState(presenter).content.scrollLeft).toBe(0) - - presenter.onDidChangeScrollLeft -> - presenter.setScrollLeft(1.5) - getState(presenter) # trigger scroll update - - presenter.setScrollLeft(1.5) - getState(presenter) # trigger scroll update - - expect(presenter.getScrollLeft()).toBe(2) - expect(presenter.getRealScrollLeft()).toBe(1.5) - - it "changes based on the scroll operation that was performed last", -> - presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 10) - expect(getState(presenter).content.scrollLeft).toBe(0) - - presenter.setScrollLeft(20) - editor.setCursorBufferPosition([0, 9]) - - expect(getState(presenter).content.scrollLeft).toBe(90) - - editor.setCursorBufferPosition([0, 18]) - presenter.setScrollLeft(50) - - expect(getState(presenter).content.scrollLeft).toBe(50) - - it "corresponds to the passed logical coordinates when building the presenter", -> - editor.setFirstVisibleScreenColumn(3) - presenter = buildPresenter(lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(getState(presenter).content.scrollLeft).toBe(30) - - it "tracks the value of ::scrollLeft", -> - presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(getState(presenter).content.scrollLeft).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollLeft(50) - expect(getState(presenter).content.scrollLeft).toBe 50 - - it "keeps the model up to date with the corresponding logical coordinates", -> - presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - - expectStateUpdate presenter, -> presenter.setScrollLeft(50) - getState(presenter) # commits scroll position - expect(editor.getFirstVisibleScreenColumn()).toBe 5 - - expectStateUpdate presenter, -> presenter.setScrollLeft(57) - getState(presenter) # commits scroll position - expect(editor.getFirstVisibleScreenColumn()).toBe 6 - - it "is always rounded to the nearest integer", -> - presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(getState(presenter).content.scrollLeft).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollLeft(11.4) - expect(getState(presenter).content.scrollLeft).toBe 11 - expectStateUpdate presenter, -> presenter.setScrollLeft(12.6) - expect(getState(presenter).content.scrollLeft).toBe 13 - - it "never exceeds the computed scrollWidth minus the computed clientWidth", -> - didChangeScrollLeftSpy = jasmine.createSpy() - presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - presenter.onDidChangeScrollLeft(didChangeScrollLeftSpy) - - expectStateUpdate presenter, -> presenter.setScrollLeft(300) - expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth - expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth - - didChangeScrollLeftSpy.reset() - expectStateUpdate presenter, -> presenter.setContentFrameWidth(600) - expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth - expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth - - didChangeScrollLeftSpy.reset() - expectStateUpdate presenter, -> presenter.setVerticalScrollbarWidth(5) - expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth - expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth - - didChangeScrollLeftSpy.reset() - expectStateUpdate presenter, -> editor.getBuffer().delete([[6, 0], [6, Infinity]]) - expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth - expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth - expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth - - # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollLeftBefore = getState(presenter).content.scrollLeft - didChangeScrollLeftSpy.reset() - expectStateUpdate presenter, -> editor.getBuffer().insert([6, 0], new Array(100).join('x')) - expect(getState(presenter).content.scrollLeft).toBe scrollLeftBefore - expect(presenter.getRealScrollLeft()).toBe scrollLeftBefore - expect(didChangeScrollLeftSpy).not.toHaveBeenCalled() - - it "never goes negative", -> - presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expectStateUpdate presenter, -> presenter.setScrollLeft(-300) - expect(getState(presenter).content.scrollLeft).toBe 0 - - describe ".backgroundColor", -> - it "is assigned to ::backgroundColor unless the editor is mini", -> - presenter = buildPresenter() - presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(getState(presenter).content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' - - editor.setMini(true) - presenter = buildPresenter() - presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(getState(presenter).content.backgroundColor).toBeNull() - - it "updates when ::backgroundColor changes", -> - presenter = buildPresenter() - presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(getState(presenter).content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' - expectStateUpdate presenter, -> presenter.setBackgroundColor('rgba(0, 0, 255, 0)') - expect(getState(presenter).content.backgroundColor).toBe 'rgba(0, 0, 255, 0)' - - it "updates when ::mini changes", -> - presenter = buildPresenter() - presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(getState(presenter).content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' - expectStateUpdate presenter, -> editor.setMini(true) - expect(getState(presenter).content.backgroundColor).toBeNull() - - describe ".placeholderText", -> - it "is present when the editor has no text", -> - editor.setPlaceholderText("the-placeholder-text") - presenter = buildPresenter() - expect(getState(presenter).content.placeholderText).toBeNull() - - expectStateUpdate presenter, -> editor.setText("") - expect(getState(presenter).content.placeholderText).toBe "the-placeholder-text" - - expectStateUpdate presenter, -> editor.setPlaceholderText("new-placeholder-text") - expect(getState(presenter).content.placeholderText).toBe "new-placeholder-text" - - describe ".tiles", -> - lineStateForScreenRow = (presenter, row) -> - 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 - - describe "[tileId].lines[lineId]", -> # line state objects - it "includes the state for visible lines in a tile", -> - presenter = buildPresenter(explicitHeight: 3, scrollTop: 4, lineHeight: 1, tileSize: 3, stoppedScrollingDelay: 200) - presenter.setExplicitHeight(3) - - expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() - 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", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileSize: 2) - - expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n") - - 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.update({showInvisibles: false, invisibles: {eol: 'X'}}) - - editor.setText("hello\nworld\r\n") - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) - 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') - - editor.update({showInvisibles: true}) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) - 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 ".{preceding,following}BlockDecorations", -> - stateForBlockDecorations = (blockDecorations) -> - state = {} - for blockDecoration in blockDecorations - state[blockDecoration.id] = { - decoration: blockDecoration, - screenRow: blockDecoration.getMarker().getHeadScreenPosition().row - } - state - - it "contains all block decorations that are present before/after a line, both initially and when decorations change", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - presenter = buildPresenter() - blockDecoration2 = addBlockDecorationBeforeScreenRow(3) - blockDecoration3 = addBlockDecorationBeforeScreenRow(7) - blockDecoration4 = null - - waitsForStateToUpdate presenter, -> - blockDecoration4 = addBlockDecorationAfterScreenRow(7) - - runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2])) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration3])) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration4])) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) - - waitsForStateToUpdate presenter, -> - blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) - blockDecoration2.getMarker().setHeadBufferPosition([9, 0]) - blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) - blockDecoration4.getMarker().setHeadBufferPosition([8, 0]) - - runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration4])) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2, blockDecoration3])) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) - - waitsForStateToUpdate presenter, -> - blockDecoration4.destroy() - blockDecoration3.destroy() - blockDecoration1.getMarker().setHeadBufferPosition([0, 0]) - - runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2])) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) - - waitsForStateToUpdate presenter, -> - editor.setCursorBufferPosition([0, 0]) - editor.insertNewline() - - runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2])) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) - - it "contains block decorations located in ::mouseWheelScreenRow even if they are off screen", -> - blockDecoration = addBlockDecorationBeforeScreenRow(0) - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) - lineId = presenter.displayLayer.getScreenLines(0, 1)[0].id - - expect(getState(presenter).content.tiles[0].lines[lineId].precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration])) - - presenter.setMouseWheelScreenRow(0) - expectStateUpdate presenter, -> presenter.setScrollTop(4) - expect(getState(presenter).content.tiles[0].lines[lineId].precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration])) - - advanceClock(presenter.stoppedScrollingDelay) - expect(getState(presenter).content.tiles[0]).toBeUndefined() - - it "inserts block decorations before the line unless otherwise specified", -> - blockDecoration = editor.decorateMarker(editor.markScreenPosition([4, 0]), {type: "block"}) - presenter = buildPresenter() - - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual stateForBlockDecorations([blockDecoration]) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual {} - - describe ".decorationClasses", -> - it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> - marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') - decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a') - presenter = buildPresenter() - marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') - decoration2 = null - - waitsForStateToUpdate presenter, -> decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b') - runs -> - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') - runs -> - expect(marker1.isValid()).toBe false - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> editor.undo() - runs -> - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) - runs -> - expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> decoration1.destroy() - runs -> - expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> marker2.destroy() - runs -> - expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - it "honors the 'onlyEmpty' option on line decorations", -> - presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 1]]) - decoration = editor.decorateMarker(marker, type: 'line', class: 'a', onlyEmpty: true) - - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> marker.clearTail() - - runs -> - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - it "honors the 'onlyNonEmpty' option on line decorations", -> - presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 2]]) - decoration = editor.decorateMarker(marker, type: 'line', class: 'a', onlyNonEmpty: true) - - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - waitsForStateToUpdate presenter, -> marker.clearTail() - - runs -> - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - it "honors the 'onlyHead' option on line decorations", -> - presenter = buildPresenter() - waitsForStateToUpdate presenter, -> - marker = editor.markBufferRange([[4, 0], [6, 2]]) - editor.decorateMarker(marker, type: 'line', class: 'a', onlyHead: true) - - runs -> - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - it "does not decorate the last line of a non-empty line decoration range if it ends at column 0", -> - presenter = buildPresenter() - waitsForStateToUpdate presenter, -> - marker = editor.markBufferRange([[4, 0], [6, 0]]) - editor.decorateMarker(marker, type: 'line', class: 'a') - - runs -> - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - it "does not apply line decorations to mini editors", -> - editor.setMini(true) - presenter = buildPresenter(explicitHeight: 10) - - waitsForStateToUpdate presenter, -> - marker = editor.markBufferRange([[0, 0], [0, 0]]) - decoration = editor.decorateMarker(marker, type: 'line', class: 'a') - - runs -> - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> editor.setMini(false) - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'a'] - - expectStateUpdate presenter, -> editor.setMini(true) - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull() - - it "only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> - editor.setText("a line that wraps, ok") - editor.update({softWrapped: true}) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(16) - marker = editor.markBufferRange([[0, 0], [0, 2]]) - editor.decorateMarker(marker, type: 'line', class: 'a') - presenter = buildPresenter(explicitHeight: 10) - - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> - marker.setBufferRange([[0, 0], [0, Infinity]]) - - runs -> - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' - - describe ".cursors", -> - stateForCursor = (presenter, cursorIndex) -> - getState(presenter).content.cursors[presenter.model.getCursors()[cursorIndex].id] - - it "contains pixelRects for empty selections that are visible on screen", -> - editor.update({showCursorOnSelection: false}) - editor.setSelectedBufferRanges([ - [[1, 2], [1, 2]], - [[2, 4], [2, 4]], - [[3, 4], [3, 5]] - [[5, 12], [5, 12]], - [[8, 4], [8, 4]] - ]) - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) - - expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 4)).toBeUndefined() - - it "is empty until all of the required measurements are assigned", -> - presenter = buildPresenterWithoutMeasurements() - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setExplicitHeight(25) - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setLineHeight(10) - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setScrollTop(0) - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setBaseCharacterWidth(8) - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setBoundingClientRect(top: 0, left: 0, width: 500, height: 130) - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setWindowSize(500, 130) - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setVerticalScrollbarWidth(10) - expect(getState(presenter).content.cursors).toEqual({}) - - presenter.setHorizontalScrollbarHeight(10) - expect(getState(presenter).content.cursors).not.toEqual({}) - - it "updates when block decorations change", -> - editor.update({showCursorOnSelection: false}) - editor.setSelectedBufferRanges([ - [[1, 2], [1, 2]], - [[2, 4], [2, 4]], - [[3, 4], [3, 5]] - [[5, 12], [5, 12]], - [[8, 4], [8, 4]] - ]) - presenter = buildPresenter(explicitHeight: 80, scrollTop: 0) - - expect(stateForCursor(presenter, 0)).toEqual {top: 10, left: 2 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 1)).toEqual {top: 20, left: 4 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10} - - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - blockDecoration2 = addBlockDecorationAfterScreenRow(1) - - waitsForStateToUpdate presenter, -> - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) - - runs -> - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10 + 30, left: 2 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10 + 30 + 10, left: 4 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toBeUndefined() - expect(stateForCursor(presenter, 4)).toBeUndefined() - - waitsForStateToUpdate presenter, -> - blockDecoration2.destroy() - editor.setCursorBufferPosition([0, 0]) - editor.insertNewline() - editor.setCursorBufferPosition([0, 0]) - - runs -> - expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 0, width: 10, height: 10} - - it "considers block decorations to be before a line by default", -> - editor.setCursorScreenPosition([4, 0]) - blockDecoration = editor.decorateMarker(editor.markScreenPosition([4, 0]), {type: "block"}) - presenter = buildPresenter() - presenter.setBlockDecorationDimensions(blockDecoration, 0, 6) - - expect(stateForCursor(presenter, 0)).toEqual {top: 4 * 10 + 6, left: 0, width: 10, height: 10} - - it "updates when ::scrollTop changes", -> - editor.setSelectedBufferRanges([ - [[1, 2], [1, 2]], - [[2, 4], [2, 4]], - [[3, 4], [3, 5]] - [[5, 12], [5, 12]], - [[8, 4], [8, 4]] - ]) - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) - - expectStateUpdate presenter, -> presenter.setScrollTop(5 * 10) - expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toBeUndefined() - expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 0, left: 12 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10 - 50, left: 4 * 10, width: 10, height: 10} - - it "updates when ::scrollTop changes after the model was changed", -> - editor.setCursorBufferPosition([8, 22]) - presenter = buildPresenter(explicitHeight: 50, scrollTop: 10 * 8) - - expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 10 * 22, width: 10, height: 10} - - expectStateUpdate presenter, -> - editor.getBuffer().deleteRow(12) - editor.getBuffer().deleteRow(11) - editor.getBuffer().deleteRow(10) - - expect(stateForCursor(presenter, 0)).toEqual {top: 20, left: 10 * 22, width: 10, height: 10} - - it "updates when ::explicitHeight changes", -> - editor.update({showCursorOnSelection: false}) - editor.setSelectedBufferRanges([ - [[1, 2], [1, 2]], - [[2, 4], [2, 4]], - [[3, 4], [3, 5]] - [[5, 12], [5, 12]], - [[8, 4], [8, 4]] - ]) - presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - - expectStateUpdate presenter, -> presenter.setExplicitHeight(30) - expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 4)).toBeUndefined() - - it "updates when ::lineHeight changes", -> - editor.setSelectedBufferRanges([ - [[1, 2], [1, 2]], - [[2, 4], [2, 4]], - [[3, 4], [3, 5]] - [[5, 12], [5, 12]], - [[8, 4], [8, 4]] - ]) - presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - - expectStateUpdate presenter, -> presenter.setLineHeight(5) - expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toBeUndefined() - expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5, left: 12 * 10, width: 10, height: 5} - expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5 - 20, left: 4 * 10, width: 10, height: 5} - - it "updates when scoped character widths change", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - editor.setCursorBufferPosition([1, 4]) - presenter = buildPresenter(explicitHeight: 20) - - expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--storage.syntax--type.syntax--var.syntax--js'], 'v', 20) - presenter.measurementsChanged() - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} - - expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--storage.syntax--type.syntax--var.syntax--js'], 'r', 20) - presenter.measurementsChanged() - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} - - it "updates when cursors are added, moved, hidden, shown, or destroyed", -> - editor.update({showCursorOnSelection: false}) - editor.setSelectedBufferRanges([ - [[1, 2], [1, 2]], - [[3, 4], [3, 5]] - ]) - presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - - # moving into view - expect(stateForCursor(presenter, 0)).toBeUndefined() - editor.getCursors()[0].setBufferPosition([2, 4]) - expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} - - # showing - expectStateUpdate presenter, -> editor.getSelections()[1].clear() - expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 5 * 10, width: 10, height: 10} - - # hiding - expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 5]]) - expect(stateForCursor(presenter, 1)).toBeUndefined() - - # moving out of view - expectStateUpdate presenter, -> editor.getCursors()[0].setBufferPosition([10, 4]) - expect(stateForCursor(presenter, 0)).toBeUndefined() - - # adding - expectStateUpdate presenter, -> editor.addCursorAtBufferPosition([4, 4]) - expect(stateForCursor(presenter, 2)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} - - # moving added cursor - expectStateUpdate presenter, -> editor.getCursors()[2].setBufferPosition([4, 6]) - expect(stateForCursor(presenter, 2)).toEqual {top: 0, left: 6 * 10, width: 10, height: 10} - - # destroying - destroyedCursor = editor.getCursors()[2] - expectStateUpdate presenter, -> destroyedCursor.destroy() - expect(getState(presenter).content.cursors[destroyedCursor.id]).toBeUndefined() - - it "makes cursors as wide as the ::baseCharacterWidth if they're at the end of a line", -> - editor.setCursorBufferPosition([1, Infinity]) - presenter = buildPresenter(explicitHeight: 20, scrollTop: 0) - expect(stateForCursor(presenter, 0).width).toBe 10 - - describe ".cursorsVisible", -> - it "alternates between true and false twice per ::cursorBlinkPeriod when the editor is focused", -> - cursorBlinkPeriod = 100 - cursorBlinkResumeDelay = 200 - presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay}) - presenter.setFocused(true) - - expect(getState(presenter).content.cursorsVisible).toBe true - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe true - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe true - - expectStateUpdate presenter, -> presenter.setFocused(false) - expect(getState(presenter).content.cursorsVisible).toBe false - advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - - expectStateUpdate presenter, -> presenter.setFocused(true) - expect(getState(presenter).content.cursorsVisible).toBe true - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - - it "stops alternating for ::cursorBlinkResumeDelay when a cursor moves or a cursor is added", -> - cursorBlinkPeriod = 100 - cursorBlinkResumeDelay = 200 - presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay}) - presenter.setFocused(true) - - expect(getState(presenter).content.cursorsVisible).toBe true - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - - expectStateUpdate presenter, -> editor.moveRight() - expect(getState(presenter).content.cursorsVisible).toBe true - - expectStateUpdate presenter, -> - advanceClock(cursorBlinkResumeDelay) - advanceClock(cursorBlinkPeriod / 2) - - expect(getState(presenter).content.cursorsVisible).toBe false - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe true - expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - - expectStateUpdate presenter, -> editor.addCursorAtBufferPosition([1, 0]) - expect(getState(presenter).content.cursorsVisible).toBe true - - expectStateUpdate presenter, -> - advanceClock(cursorBlinkResumeDelay) - advanceClock(cursorBlinkPeriod / 2) - expect(getState(presenter).content.cursorsVisible).toBe false - - describe ".highlights", -> - expectUndefinedStateForHighlight = (presenter, decoration) -> - for tileId of getState(presenter).content.tiles - state = stateForHighlightInTile(presenter, decoration, tileId) - expect(state).toBeUndefined() - - stateForHighlightInTile = (presenter, decoration, tile) -> - getState(presenter).content.tiles[tile]?.highlights[decoration.id] - - stateForSelectionInTile = (presenter, selectionIndex, tile) -> - selection = presenter.model.getSelections()[selectionIndex] - stateForHighlightInTile(presenter, selection.decoration, tile) - - expectUndefinedStateForSelection = (presenter, selectionIndex) -> - for tileId of getState(presenter).content.tiles - state = stateForSelectionInTile(presenter, selectionIndex, tileId) - expect(state).toBeUndefined() - - it "contains states for highlights that are visible on screen", -> - # off-screen above - marker1 = editor.markBufferRange([[0, 0], [1, 0]]) - highlight1 = editor.decorateMarker(marker1, type: 'highlight', class: 'a') - - # partially off-screen above, 1 of 2 regions on screen - marker2 = editor.markBufferRange([[1, 6], [2, 6]]) - highlight2 = editor.decorateMarker(marker2, type: 'highlight', class: 'b') - - # partially off-screen above, 2 of 3 regions on screen - marker3 = editor.markBufferRange([[0, 6], [3, 6]]) - highlight3 = editor.decorateMarker(marker3, type: 'highlight', class: 'c') - - # on-screen, spans over 2 tiles - marker4 = editor.markBufferRange([[2, 6], [4, 6]]) - highlight4 = editor.decorateMarker(marker4, type: 'highlight', class: 'd') - - # partially off-screen below, spans over 3 tiles, 2 of 3 regions on screen - marker5 = editor.markBufferRange([[3, 6], [6, 6]]) - highlight5 = editor.decorateMarker(marker5, type: 'highlight', class: 'e') - - # partially off-screen below, 1 of 3 regions on screen - marker6 = editor.markBufferRange([[5, 6], [7, 6]]) - highlight6 = editor.decorateMarker(marker6, type: 'highlight', class: 'f') - - # off-screen below - marker7 = editor.markBufferRange([[6, 6], [7, 6]]) - highlight7 = editor.decorateMarker(marker7, type: 'highlight', class: 'g') - - # on-screen, empty - marker8 = editor.markBufferRange([[2, 2], [2, 2]]) - highlight8 = editor.decorateMarker(marker8, type: 'highlight', class: 'h') - - # partially off-screen above, empty - marker9 = editor.markBufferRange([[0, 0], [2, 0]], invalidate: 'touch') - highlight9 = editor.decorateMarker(marker9, type: 'highlight', class: 'h') - - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2) - - expectUndefinedStateForHighlight(presenter, highlight1) - - expectValues stateForHighlightInTile(presenter, highlight2, 2), { - class: 'b' - regions: [ - {top: 0, left: 0 * 10, width: 6 * 10, height: 1 * 10} - ] - } - - expectValues stateForHighlightInTile(presenter, highlight3, 2), { - class: 'c' - regions: [ - {top: 0, left: 0 * 10, right: 0, height: 1 * 10} - {top: 10, left: 0 * 10, width: 6 * 10, height: 1 * 10} - ] - } - - expectValues stateForHighlightInTile(presenter, highlight4, 2), { - class: 'd' - regions: [ - {top: 0, left: 6 * 10, right: 0, height: 1 * 10} - {top: 10, left: 0, right: 0, height: 1 * 10} - ] - } - expectValues stateForHighlightInTile(presenter, highlight4, 4), { - class: 'd' - regions: [ - {top: 0, left: 0, width: 60, height: 1 * 10} - ] - } - - expectValues stateForHighlightInTile(presenter, highlight5, 2), { - class: 'e' - regions: [ - {top: 10, left: 6 * 10, right: 0, height: 1 * 10} - ] - } - - expectValues stateForHighlightInTile(presenter, highlight5, 4), { - class: 'e' - regions: [ - {top: 0, left: 0, right: 0, height: 1 * 10} - {top: 10, left: 0, right: 0, height: 1 * 10} - ] - } - - expect(stateForHighlightInTile(presenter, highlight5, 6)).toBeUndefined() - - expectValues stateForHighlightInTile(presenter, highlight6, 4), { - class: 'f' - regions: [ - {top: 10, left: 6 * 10, right: 0, height: 1 * 10} - ] - } - - expect(stateForHighlightInTile(presenter, highlight6, 6)).toBeUndefined() - - expectUndefinedStateForHighlight(presenter, highlight7) - expectUndefinedStateForHighlight(presenter, highlight8) - expectUndefinedStateForHighlight(presenter, highlight9) - - it "is empty until all of the required measurements are assigned", -> - editor.setSelectedBufferRanges([ - [[0, 2], [2, 4]], - ]) - - presenter = buildPresenterWithoutMeasurements(tileSize: 2) - for tileId, tileState of getState(presenter).content.tiles - expect(tileState.highlights).toEqual({}) - - presenter.setExplicitHeight(25) - for tileId, tileState of getState(presenter).content.tiles - expect(tileState.highlights).toEqual({}) - - presenter.setLineHeight(10) - for tileId, tileState of getState(presenter).content.tiles - expect(tileState.highlights).toEqual({}) - - presenter.setScrollTop(0) - for tileId, tileState of getState(presenter).content.tiles - expect(tileState.highlights).toEqual({}) - - presenter.setBaseCharacterWidth(8) - assignedAnyHighlight = false - for tileId, tileState of getState(presenter).content.tiles - assignedAnyHighlight ||= _.isEqual(tileState.highlights, {}) - - expect(assignedAnyHighlight).toBe(true) - - it "does not include highlights for invalid markers", -> - marker = editor.markBufferRange([[2, 2], [2, 4]], invalidate: 'touch') - highlight = editor.decorateMarker(marker, type: 'highlight', class: 'h') - - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2) - - expect(stateForHighlightInTile(presenter, highlight, 2)).toBeDefined() - - expectStateUpdate presenter, -> editor.getBuffer().insert([2, 2], "stuff") - - expectUndefinedStateForHighlight(presenter, highlight) - - it "does not include highlights that end before the first visible row", -> - editor.setText("Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.") - editor.update({softWrapped: true}) - editor.setWidth(100, true) - editor.setDefaultCharWidth(10) - - marker = editor.markBufferRange([[0, 0], [0, 4]], invalidate: 'never') - highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a') - presenter = buildPresenter(explicitHeight: 30, scrollTop: 10, tileSize: 2) - - expect(stateForHighlightInTile(presenter, highlight, 0)).toBeUndefined() - - it "handles highlights that extend to the left of the visible area (regression)", -> - editor.setSelectedBufferRanges([ - [[0, 2], [1, 4]], - ]) - - presenter = buildPresenter(explicitHeight: 20, scrollLeft: 0, tileSize: 2) - expectValues stateForSelectionInTile(presenter, 0, 0), { - regions: [ - {top: 0 * 10, height: 10, left: 2 * 10, right: 0 * 10}, - {top: 1 * 10, height: 10, left: 0 * 10, width: 4 * 10} - ] - } - - presenter = buildPresenter(explicitHeight: 20, scrollLeft: 20, tileSize: 2) - expectValues stateForSelectionInTile(presenter, 0, 0), { - regions: [ - {top: 0 * 10, height: 10, left: 2 * 10, right: 0 * 10}, - {top: 1 * 10, height: 10, left: 0 * 10, width: 4 * 10} - ] - } - - it "updates when ::scrollTop changes", -> - editor.setSelectedBufferRanges([ - [[6, 2], [6, 4]], - ]) - - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2) - - expectUndefinedStateForSelection(presenter, 0) - expectStateUpdate presenter, -> presenter.setScrollTop(5 * 10) - expect(stateForSelectionInTile(presenter, 0, 6)).toBeDefined() - expectStateUpdate presenter, -> presenter.setScrollTop(2 * 10) - expectUndefinedStateForSelection(presenter, 0) - - it "updates when ::explicitHeight changes", -> - editor.setSelectedBufferRanges([ - [[6, 2], [6, 4]], - ]) - - presenter = buildPresenter(explicitHeight: 20, scrollTop: 20, tileSize: 2) - - expectUndefinedStateForSelection(presenter, 0) - expectStateUpdate presenter, -> presenter.setExplicitHeight(60) - expect(stateForSelectionInTile(presenter, 0, 6)).toBeDefined() - expectStateUpdate presenter, -> presenter.setExplicitHeight(20) - expectUndefinedStateForSelection(presenter, 0) - - it "updates when ::lineHeight changes", -> - editor.setSelectedBufferRanges([ - [[2, 2], [2, 4]], - [[3, 4], [3, 6]], - ]) - - presenter = buildPresenter(explicitHeight: 20, scrollTop: 0, tileSize: 2) - - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [ - {top: 0, left: 2 * 10, width: 2 * 10, height: 10} - ] - } - expectUndefinedStateForSelection(presenter, 1) - - expectStateUpdate presenter, -> presenter.setLineHeight(5) - - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [ - {top: 0, left: 2 * 10, width: 2 * 10, height: 5} - ] - } - - expectValues stateForSelectionInTile(presenter, 1, 2), { - regions: [ - {top: 5, left: 4 * 10, width: 2 * 10, height: 5} - ] - } - - it "updates when scoped character widths change", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - editor.setSelectedBufferRanges([ - [[2, 4], [2, 6]], - ]) - - presenter = buildPresenter(explicitHeight: 20, scrollTop: 0, tileSize: 2) - - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] - } - expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--keyword.syntax--control.syntax--js'], 'i', 20) - presenter.measurementsChanged() - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] - } - - it "updates when highlight decorations are added, moved, hidden, shown, or destroyed", -> - editor.setSelectedBufferRanges([ - [[1, 2], [1, 4]], - [[3, 4], [3, 6]] - ]) - presenter = buildPresenter(explicitHeight: 20, scrollTop: 0, tileSize: 2) - - expectValues stateForSelectionInTile(presenter, 0, 0), { - regions: [{top: 10, left: 2 * 10, width: 2 * 10, height: 10}] - } - expectUndefinedStateForSelection(presenter, 1) - - # moving into view - waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) - runs -> - expectValues stateForSelectionInTile(presenter, 1, 2), { - regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] - } - - # becoming empty - runs -> - editor.getSelections()[1].clear(autoscroll: false) - waitsForStateToUpdate presenter - runs -> - expectUndefinedStateForSelection(presenter, 1) - - # becoming non-empty - runs -> - editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) - waitsForStateToUpdate presenter - runs -> - expectValues stateForSelectionInTile(presenter, 1, 2), { - regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] - } - - # moving out of view - runs -> - editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false) - waitsForStateToUpdate presenter - runs -> - expectUndefinedStateForSelection(presenter, 1) - - # adding - runs -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false) - waitsForStateToUpdate presenter - runs -> - expectValues stateForSelectionInTile(presenter, 2, 0), { - regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}] - } - - # moving added selection - runs -> - editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false) - waitsForStateToUpdate presenter - - [destroyedSelection, destroyedDecoration] = [] - runs -> - expectValues stateForSelectionInTile(presenter, 2, 0), { - regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}] - } - - # destroying - destroyedSelection = editor.getSelections()[2] - destroyedDecoration = destroyedSelection.decoration - - waitsForStateToUpdate presenter, -> destroyedSelection.destroy() - runs -> - expectUndefinedStateForHighlight(presenter, destroyedDecoration) - - it "updates when highlight decorations' properties are updated", -> - marker = editor.markBufferPosition([2, 2]) - highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a') - - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2) - - expectUndefinedStateForHighlight(presenter, highlight) - - waitsForStateToUpdate presenter, -> - marker.setBufferRange([[2, 2], [2, 4]]) - highlight.setProperties(class: 'b', type: 'highlight') - - runs -> - expectValues stateForHighlightInTile(presenter, highlight, 2), {class: 'b'} - - it "increments the .flashCount and sets the .flashClass and .flashDuration when the highlight model flashes", -> - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2) - - marker = editor.markBufferPosition([2, 2]) - highlight = null - waitsForStateToUpdate presenter, -> - highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a') - marker.setBufferRange([[2, 2], [5, 2]]) - highlight.flash('b', 500) - runs -> - expectValues stateForHighlightInTile(presenter, highlight, 2), { - needsFlash: true - flashClass: 'b' - flashDuration: 500 - flashCount: 1 - } - expectValues stateForHighlightInTile(presenter, highlight, 4), { - needsFlash: true - flashClass: 'b' - flashDuration: 500 - flashCount: 1 - } - - waitsForStateToUpdate presenter, -> highlight.flash('c', 600) - runs -> - expectValues stateForHighlightInTile(presenter, highlight, 2), { - needsFlash: true - flashClass: 'c' - flashDuration: 600 - flashCount: 2 - } - expectValues stateForHighlightInTile(presenter, highlight, 4), { - needsFlash: true - flashClass: 'c' - flashDuration: 600 - flashCount: 2 - } - - waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 2], [6, 2]]) - runs -> - expectValues stateForHighlightInTile(presenter, highlight, 2), {needsFlash: false} - expectValues stateForHighlightInTile(presenter, highlight, 4), {needsFlash: false} - - describe ".offScreenBlockDecorations", -> - stateForOffScreenBlockDecoration = (presenter, decoration) -> - getState(presenter).content.offScreenBlockDecorations[decoration.id] - - it "contains state for off-screen unmeasured block decorations, both initially and when they are updated or destroyed", -> - item = {} - blockDecoration1 = addBlockDecorationBeforeScreenRow(0, item) - blockDecoration2 = addBlockDecorationBeforeScreenRow(4, item) - blockDecoration3 = addBlockDecorationBeforeScreenRow(4, item) - blockDecoration4 = addBlockDecorationBeforeScreenRow(10, item) - presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBe(blockDecoration4) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration4, 0, 20) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBe(blockDecoration3) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBeUndefined() - - presenter.invalidateBlockDecorationDimensions(blockDecoration1) - presenter.invalidateBlockDecorationDimensions(blockDecoration4) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBe(blockDecoration4) - - blockDecoration4.destroy() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBeUndefined() - - it "contains state for off-screen block decorations that intersect a buffer change", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(9) - blockDecoration2 = addBlockDecorationBeforeScreenRow(10) - blockDecoration3 = addBlockDecorationBeforeScreenRow(11) - presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBe(blockDecoration3) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - - editor.setSelectedScreenRange([[10, 0], [12, 0]]) - editor.delete() - presenter.setScrollTop(0) # deleting the buffer causes the editor to autoscroll - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBe(blockDecoration3) - - it "contains state for all off-screen block decorations when content frame width, window size or bounding client rect change", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(10) - blockDecoration2 = addBlockDecorationBeforeScreenRow(11) - presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - - presenter.setBoundingClientRect({top: 0, left: 0, width: 50, height: 30}) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - - presenter.setContentFrameWidth(100) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - - presenter.setWindowSize(100, 200) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - - it "doesn't throw an error when setting the dimensions for a destroyed decoration", -> - blockDecoration = addBlockDecorationBeforeScreenRow(0) - presenter = buildPresenter() - blockDecoration.destroy() - presenter.setBlockDecorationDimensions(blockDecoration, 30, 30) - expect(getState(presenter).content.offScreenBlockDecorations).toEqual({}) - - describe ".overlays", -> - [item] = [] - stateForOverlay = (presenter, decoration) -> - getState(presenter).content.overlays[decoration.id] - - it "contains state for overlay decorations both initially and when their markers move", -> - marker = editor.addMarkerLayer(maintainHistory: true).markBufferPosition([2, 13], invalidate: 'touch') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) - - # Initial state - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} - } - - # Change range - waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]]) - runs -> - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} - } - - # Valid -> invalid - waitsForStateToUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x') - runs -> - expect(stateForOverlay(presenter, decoration)).toBeUndefined() - - # Invalid -> valid - waitsForStateToUpdate presenter, -> editor.undo() - runs -> - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} - } - - # Reverse direction - waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true) - runs -> - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} - } - - # Destroy - waitsForStateToUpdate presenter, -> decoration.destroy() - runs -> - expect(stateForOverlay(presenter, decoration)).toBeUndefined() - - # Add - decoration2 = null - waitsForStateToUpdate presenter, -> decoration2 = editor.decorateMarker(marker, {type: 'overlay', item}) - runs -> - expectValues stateForOverlay(presenter, decoration2), { - item: item - pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} - } - - it "updates when character widths changes", -> - scrollTop = 20 - marker = editor.markBufferPosition([2, 13], invalidate: 'touch') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter({explicitHeight: 30, scrollTop}) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} - } - - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(5) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 5} - } - - it "updates when ::lineHeight changes", -> - scrollTop = 20 - marker = editor.markBufferPosition([2, 13], invalidate: 'touch') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter({explicitHeight: 30, scrollTop}) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} - } - - expectStateUpdate presenter, -> presenter.setLineHeight(5) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 5 - scrollTop, left: 13 * 10} - } - - it "honors the 'position' option on overlay decorations", -> - scrollTop = 20 - marker = editor.markBufferRange([[2, 13], [4, 14]], invalidate: 'touch') - decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) - presenter = buildPresenter({explicitHeight: 30, scrollTop}) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} - } - - it "is empty until all of the required measurements are assigned", -> - marker = editor.markBufferRange([[2, 13], [4, 14]], invalidate: 'touch') - decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) - - presenter = buildPresenterWithoutMeasurements() - expect(getState(presenter).content.overlays).toEqual({}) - - presenter.setBaseCharacterWidth(10) - expect(getState(presenter).content.overlays).toEqual({}) - - presenter.setLineHeight(10) - expect(getState(presenter).content.overlays).toEqual({}) - - presenter.setWindowSize(500, 100) - expect(getState(presenter).content.overlays).toEqual({}) - - presenter.setVerticalScrollbarWidth(10) - expect(getState(presenter).content.overlays).toEqual({}) - - presenter.setHorizontalScrollbarHeight(10) - expect(getState(presenter).content.overlays).toEqual({}) - - presenter.setBoundingClientRect({top: 0, left: 0, height: 100, width: 500}) - expect(getState(presenter).content.overlays).not.toEqual({}) - - describe "when the overlay has been measured", -> - [gutterWidth, windowWidth, windowHeight, itemWidth, itemHeight, contentMargin, boundingClientRect, contentFrameWidth] = [] - beforeEach -> - item = {} - gutterWidth = 5 * 10 # 5 chars wide - contentFrameWidth = 30 * 10 - windowWidth = gutterWidth + contentFrameWidth - windowHeight = 9 * 10 - - itemWidth = 4 * 10 - itemHeight = 4 * 10 - contentMargin = 0 - - boundingClientRect = - top: 0 - left: 0, - width: windowWidth - height: windowHeight - - it "slides horizontally left when near the right edge", -> - scrollLeft = 20 - marker = editor.markBufferPosition([0, 26], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - - presenter = buildPresenter({scrollLeft, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) - expectStateUpdate presenter, -> - presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: 26 * 10 + gutterWidth - scrollLeft} - } - - expectStateUpdate presenter, -> editor.insertText('abc', autoscroll: false) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: windowWidth - itemWidth} - } - - expectStateUpdate presenter, -> editor.insertText('d', autoscroll: false) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: windowWidth - itemWidth} - } - - it "flips vertically when near the bottom edge", -> - scrollTop = 10 - marker = editor.markBufferPosition([5, 0], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - - presenter = buildPresenter({scrollTop, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) - expectStateUpdate presenter, -> - presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 6 * 10 - scrollTop, left: gutterWidth} - } - - expectStateUpdate presenter, -> - editor.insertNewline(autoscroll: false) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 6 * 10 - scrollTop - itemHeight, left: gutterWidth} - } - - it "when avoidOverflow is false, does not move horizontally when overflowing the editor's scrollView horizontally", -> - scrollLeft = 20 - marker = editor.markBufferPosition([0, 26], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item, avoidOverflow: false}) - - presenter = buildPresenter({scrollLeft, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) - expectStateUpdate presenter, -> - presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: 26 * 10 + gutterWidth - scrollLeft} - } - - expectStateUpdate presenter, -> editor.insertText('a', autoscroll: false) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: 27 * 10 + gutterWidth - scrollLeft} - } - - it "when avoidOverflow is false, does not flip vertically when overflowing the editor's scrollView vertically", -> - scrollTop = 10 - marker = editor.markBufferPosition([5, 0], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item, avoidOverflow: false}) - - presenter = buildPresenter({scrollTop, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) - expectStateUpdate presenter, -> - presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 6 * 10 - scrollTop, left: gutterWidth} - } - - expectStateUpdate presenter, -> - editor.insertNewline(autoscroll: false) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 7 * 10 - scrollTop, left: gutterWidth} - } - - describe "when the overlay item has a margin", -> - beforeEach -> - itemWidth = 12 * 10 - contentMargin = -(gutterWidth + 2 * 10) - - it "slides horizontally right when near the left edge with margin", -> - editor.setCursorBufferPosition([0, 3]) - cursor = editor.getLastCursor() - marker = cursor.marker - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - - presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) - expectStateUpdate presenter, -> - presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: 3 * 10 + gutterWidth} - } - - expectStateUpdate presenter, -> cursor.moveLeft() - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: -contentMargin} - } - - expectStateUpdate presenter, -> cursor.moveLeft() - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 1 * 10, left: -contentMargin} - } - - describe "when the editor is very small", -> - beforeEach -> - windowWidth = gutterWidth + 6 * 10 - windowHeight = 6 * 10 - contentFrameWidth = windowWidth - gutterWidth - boundingClientRect.width = windowWidth - boundingClientRect.height = windowHeight - - it "does not flip vertically and force the overlay to have a negative top", -> - marker = editor.markBufferPosition([1, 0], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - - presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) - expectStateUpdate presenter, -> - presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 2 * 10, left: 0 * 10 + gutterWidth} - } - - expectStateUpdate presenter, -> editor.insertNewline() - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10, left: gutterWidth} - } - - it "does not adjust horizontally and force the overlay to have a negative left", -> - itemWidth = 6 * 10 - - marker = editor.markBufferPosition([0, 0], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - - presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) - expectStateUpdate presenter, -> - presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) - - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 10, left: gutterWidth} - } - - windowWidth = gutterWidth + 5 * 10 - expectStateUpdate presenter, -> presenter.setWindowSize(windowWidth, windowHeight) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 10, left: windowWidth - itemWidth} - } - - windowWidth = gutterWidth + 1 * 10 - expectStateUpdate presenter, -> presenter.setWindowSize(windowWidth, windowHeight) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 10, left: 0} - } - - windowWidth = gutterWidth - expectStateUpdate presenter, -> presenter.setWindowSize(windowWidth, windowHeight) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 10, left: 0} - } - - describe ".width", -> - it "is null when `editor.autoWidth` is false (the default)", -> - presenter = buildPresenter(explicitHeight: 50, gutterWidth: 20, contentFrameWidth: 300, baseCharacterWidth: 10) - expect(getState(presenter).width).toBeNull() - - it "equals to sum of .content.width and the width of the gutter when `editor.autoWidth` is true", -> - editor.setText('abcdef') - editor.update({autoWidth: true}) - presenter = buildPresenter(explicitHeight: 50, gutterWidth: 20, contentFrameWidth: 300, baseCharacterWidth: 10) - expect(getState(presenter).width).toBe(20 + 6 * 10 + 1) - - describe ".height", -> - it "updates model's rows per page when it changes", -> - presenter = buildPresenter(explicitHeight: 50, lineHeightInPixels: 10, horizontalScrollbarHeight: 10) - - getState(presenter) # trigger state update - expect(editor.getRowsPerPage()).toBe(4) - - presenter.setExplicitHeight(100) - getState(presenter) # trigger state update - expect(editor.getRowsPerPage()).toBe(9) - - presenter.setHorizontalScrollbarHeight(0) - getState(presenter) # trigger state update - expect(editor.getRowsPerPage()).toBe(10) - - presenter.setLineHeight(5) - getState(presenter) # trigger state update - expect(editor.getRowsPerPage()).toBe(20) - - it "tracks the computed content height if ::autoHeight is true so the editor auto-expands vertically", -> - presenter = buildPresenter(explicitHeight: null) - presenter.setAutoHeight(true) - expect(getState(presenter).height).toBe editor.getScreenLineCount() * 10 - - expectStateUpdate presenter, -> presenter.setAutoHeight(false) - expect(getState(presenter).height).toBe null - - expectStateUpdate presenter, -> presenter.setAutoHeight(true) - expect(getState(presenter).height).toBe editor.getScreenLineCount() * 10 - - expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(getState(presenter).height).toBe editor.getScreenLineCount() * 20 - - expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(getState(presenter).height).toBe editor.getScreenLineCount() * 20 - - describe ".focused", -> - it "tracks the value of ::focused", -> - presenter = buildPresenter() - presenter.setFocused(false) - - expect(getState(presenter).focused).toBe false - expectStateUpdate presenter, -> presenter.setFocused(true) - expect(getState(presenter).focused).toBe true - expectStateUpdate presenter, -> presenter.setFocused(false) - expect(getState(presenter).focused).toBe false - - describe ".gutters", -> - getStateForGutterWithName = (presenter, gutterName) -> - gutterDescriptions = getState(presenter).gutters - for description in gutterDescriptions - gutter = description.gutter - return description if gutter.name is gutterName - - describe "the array itself, an array of gutter descriptions", -> - it "updates when gutters are added to the editor model, and keeps the gutters sorted by priority", -> - presenter = buildPresenter() - gutter1 = editor.addGutter({name: 'test-gutter-1', priority: -100, visible: true}) - gutter2 = editor.addGutter({name: 'test-gutter-2', priority: 100, visible: false}) - - expectedGutterOrder = [gutter1, editor.gutterWithName('line-number'), gutter2] - for gutterDescription, index in getState(presenter).gutters - expect(gutterDescription.gutter).toEqual expectedGutterOrder[index] - - it "updates when the visibility of a gutter changes", -> - presenter = buildPresenter() - gutter = editor.addGutter({name: 'test-gutter', visible: true}) - expect(getStateForGutterWithName(presenter, 'test-gutter').visible).toBe true - gutter.hide() - expect(getStateForGutterWithName(presenter, 'test-gutter').visible).toBe false - - it "updates when a gutter is removed", -> - presenter = buildPresenter() - gutter = editor.addGutter({name: 'test-gutter', visible: true}) - expect(getStateForGutterWithName(presenter, 'test-gutter').visible).toBe true - gutter.destroy() - expect(getStateForGutterWithName(presenter, 'test-gutter')).toBeUndefined() - - describe "for a gutter description that corresponds to the line-number gutter", -> - getLineNumberGutterState = (presenter) -> - gutterDescriptions = getState(presenter).gutters - for description in gutterDescriptions - gutter = description.gutter - return description if gutter.name is 'line-number' - - describe ".visible", -> - it "is true iff the editor isn't mini and has ::isLineNumberGutterVisible and ::doesShowLineNumbers set to true", -> - presenter = buildPresenter() - - expect(editor.isLineNumberGutterVisible()).toBe true - expect(getLineNumberGutterState(presenter).visible).toBe true - - expectStateUpdate presenter, -> editor.update({mini: true}) - expect(getLineNumberGutterState(presenter)).toBeUndefined() - - expectStateUpdate presenter, -> editor.update({mini: false}) - expect(getLineNumberGutterState(presenter).visible).toBe true - - expectStateUpdate presenter, -> editor.update({lineNumberGutterVisible: false}) - expect(getLineNumberGutterState(presenter).visible).toBe false - - expectStateUpdate presenter, -> editor.update({lineNumberGutterVisible: true}) - expect(getLineNumberGutterState(presenter).visible).toBe true - - expectStateUpdate presenter, -> editor.update({showLineNumbers: false}) - expect(getLineNumberGutterState(presenter).visible).toBe false - - describe ".content.maxLineNumberDigits", -> - it "is set to the number of digits used by the greatest line number", -> - presenter = buildPresenter() - expect(editor.getLastBufferRow()).toBe 12 - expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 2 - - editor.setText("1\n2\n3") - expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 2 - - describe ".content.tiles", -> - lineNumberStateForScreenRow = (presenter, screenRow) -> - tilesState = getLineNumberGutterState(presenter).content.tiles - line = presenter.linesByScreenRow.get(screenRow) - tilesState[presenter.tileForRow(screenRow)]?.lineNumbers[line?.id] - - tiledContentContract (presenter) -> getLineNumberGutterState(presenter).content - - describe ".lineNumbers[id]", -> - it "contains states for line numbers that are visible on screen", -> - editor.foldBufferRow(4) - editor.update({softWrapped: true}) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(51) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 3) - presenter.setScreenRowsToMeasure([9, 11]) - - expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false} - expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true} - expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false} - expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false} - expectValues lineNumberStateForScreenRow(presenter, 8), {screenRow: 8, bufferRow: 8, softWrapped: true} - expect(lineNumberStateForScreenRow(presenter, 9)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 11)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 12)).toBeUndefined() - - it "updates when the editor's content changes", -> - editor.foldBufferRow(4) - editor.update({softWrapped: true}) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 35, scrollTop: 30, tileSize: 2) - - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} - expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} - expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 9} - expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined() - - expectStateUpdate presenter, -> - editor.getBuffer().insert([3, Infinity], new Array(25).join("x ")) - - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 4} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 7} - expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} - expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined() - - it "correctly handles the first screen line being soft-wrapped", -> - editor.update({softWrapped: true}) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(30) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 50, tileSize: 2) - - expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 3, softWrapped: true} - expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 3, softWrapped: true} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 4, softWrapped: false} - - presenter.setContentFrameWidth(500) - - expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false} - expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 5, softWrapped: false} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 6, softWrapped: false} - - describe ".blockDecorationsHeight", -> - it "adds the sum of all block decorations' heights to the relevant line number state objects, both initially and when decorations change", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - presenter = buildPresenter(tileSize: 2, explicitHeight: 300) - blockDecoration2 = addBlockDecorationBeforeScreenRow(3) - blockDecoration3 = addBlockDecorationBeforeScreenRow(3) - blockDecoration4 = addBlockDecorationBeforeScreenRow(7) - blockDecoration5 = addBlockDecorationAfterScreenRow(7) - blockDecoration6 = addBlockDecorationAfterScreenRow(10) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 30) - presenter.setBlockDecorationDimensions(blockDecoration4, 0, 35) - presenter.setBlockDecorationDimensions(blockDecoration4, 0, 40) - presenter.setBlockDecorationDimensions(blockDecoration5, 0, 50) - - waitsForStateToUpdate presenter, -> presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60) - runs -> - expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(10) - expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(20 + 30) - expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) - expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) # 0 because we're at the start of a tile. - expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(60) - - waitsForStateToUpdate presenter, -> - blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) - blockDecoration2.getMarker().setHeadBufferPosition([5, 0]) - blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) - - runs -> - expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(10) - expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(20) - expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) - expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) # 0 because we're at the start of a tile. - expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(30) - expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(60) - - waitsForStateToUpdate presenter, -> - blockDecoration1.destroy() - blockDecoration3.destroy() - - runs -> - expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(20) - expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) - expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) # 0 because we're at the start of a tile. - expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) - expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(60) - - describe ".decorationClasses", -> - it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> - marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') - decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a') - marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') - decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b') - presenter = buildPresenter() - - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') - runs -> - expect(marker1.isValid()).toBe false - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> editor.undo() - runs -> - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) - runs -> - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> decoration1.destroy() - runs -> - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> marker2.destroy() - runs -> - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - it "honors the 'onlyEmpty' option on line-number decorations", -> - marker = editor.markBufferRange([[4, 0], [6, 1]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyEmpty: true) - presenter = buildPresenter() - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> marker.clearTail() - - runs -> - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - it "honors the 'onlyNonEmpty' option on line-number decorations", -> - marker = editor.markBufferRange([[4, 0], [6, 2]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyNonEmpty: true) - presenter = buildPresenter() - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - waitsForStateToUpdate presenter, -> marker.clearTail() - - runs -> - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - it "honors the 'onlyHead' option on line-number decorations", -> - marker = editor.markBufferRange([[4, 0], [6, 2]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyHead: true) - presenter = buildPresenter() - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - it "does not decorate the last line of a non-empty line-number decoration range if it ends at column 0", -> - marker = editor.markBufferRange([[4, 0], [6, 0]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a') - presenter = buildPresenter() - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - it "does not apply line-number decorations to mini editors", -> - editor.setMini(true) - presenter = buildPresenter() - marker = editor.markBufferRange([[0, 0], [0, 0]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a') - # A mini editor will have no gutters. - expect(getLineNumberGutterState(presenter)).toBeUndefined() - - expectStateUpdate presenter, -> editor.setMini(false) - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'cursor-line-no-selection', 'a'] - - expectStateUpdate presenter, -> editor.setMini(true) - expect(getLineNumberGutterState(presenter)).toBeUndefined() - - it "only applies line-number decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> - editor.setText("a line that wraps, ok") - editor.update({softWrapped: true}) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(16) - marker = editor.markBufferRange([[0, 0], [0, 2]]) - editor.decorateMarker(marker, type: 'line-number', class: 'a') - presenter = buildPresenter(explicitHeight: 10) - - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - - waitsForStateToUpdate presenter, -> marker.setBufferRange([[0, 0], [0, Infinity]]) - runs -> - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' - - describe "when a fold spans a single soft-wrapped buffer row", -> - it "applies the 'folded' decoration only to its initial screen row", -> - editor.update({softWrapped: true}) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(20) - editor.foldBufferRange([[0, 20], [0, 22]]) - editor.foldBufferRange([[0, 10], [0, 14]]) - presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2) - - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain('folded') - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - - describe "when a fold is at the end of a soft-wrapped buffer row", -> - it "applies the 'folded' decoration only to its initial screen row", -> - editor.update({softWrapped: true}) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(25) - editor.foldBufferRow(1) - presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2) - - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toContain('folded') - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - - describe ".foldable", -> - it "marks line numbers at the start of a foldable region as foldable", -> - presenter = buildPresenter() - expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe false - - it "updates the foldable class on the correct line numbers when the foldable positions change", -> - presenter = buildPresenter() - editor.getBuffer().insert([0, 0], '\n') - expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 6).foldable).toBe false - - it "updates the foldable class on a line number that becomes foldable", -> - presenter = buildPresenter() - expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false - - editor.getBuffer().insert([11, 44], '\n fold me') - expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe true - - editor.undo() - expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false - - describe "for a gutter description that corresponds to a custom gutter", -> - describe ".content", -> - getContentForGutterWithName = (presenter, gutterName) -> - fullState = getStateForGutterWithName(presenter, gutterName) - return fullState.content if fullState - - [presenter, gutter, decorationItem, decorationParams] = [] - [marker1, decoration1, marker2, decoration2, marker3, decoration3] = [] - - # Set the scrollTop to 0 to show the very top of the file. - # Set the explicitHeight to make 10 lines visible. - scrollTop = 0 - lineHeight = 10 - explicitHeight = lineHeight * 10 - - beforeEach -> - # At the beginning of each test, decoration1 and decoration2 are in visible range, - # but not decoration3. - presenter = buildPresenter({explicitHeight, scrollTop, lineHeight}) - gutter = editor.addGutter({name: 'test-gutter', visible: true}) - decorationItem = document.createElement('div') - decorationItem.class = 'decoration-item' - decorationParams = - type: 'gutter' - gutterName: 'test-gutter' - class: 'test-class' - item: decorationItem - marker1 = editor.markBufferRange([[0, 0], [1, 0]]) - decoration1 = editor.decorateMarker(marker1, decorationParams) - marker2 = editor.markBufferRange([[9, 0], [12, 0]]) - decoration2 = editor.decorateMarker(marker2, decorationParams) - marker3 = editor.markBufferRange([[13, 0], [14, 0]]) - decoration3 = editor.decorateMarker(marker3, decorationParams) - - # Clear any batched state updates. - getState(presenter) - - it "contains all decorations within the visible buffer range", -> - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row - expect(decorationState[decoration1.id].height).toBe lineHeight * marker1.getScreenRange().getRowCount() - expect(decorationState[decoration1.id].item).toBe decorationItem - expect(decorationState[decoration1.id].class).toBe 'test-class' - - expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row - expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() - expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration2.id].class).toBe 'test-class' - - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates all the gutters, even when a gutter with higher priority is hidden", -> - hiddenGutter = {name: 'test-gutter-1', priority: -150, visible: false} - editor.addGutter(hiddenGutter) - - # This update will scroll decoration1 out of view, and decoration3 into view. - expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration3.id].top).toBeDefined() - - it "updates when block decorations are added, changed or removed", -> - # block decoration before decoration1 - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 3) - # block decoration between decoration1 and decoration2 - blockDecoration2 = addBlockDecorationBeforeScreenRow(3) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 5) - # block decoration between decoration2 and decoration3 - blockDecoration3 = addBlockDecorationBeforeScreenRow(10) - blockDecoration4 = addBlockDecorationAfterScreenRow(10) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 7) - presenter.setBlockDecorationDimensions(blockDecoration4, 0, 11) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row + 3 - expect(decorationState[decoration1.id].height).toBe lineHeight * marker1.getScreenRange().getRowCount() - expect(decorationState[decoration1.id].item).toBe decorationItem - expect(decorationState[decoration1.id].class).toBe 'test-class' - expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 3 + 5 - expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + 11 - expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration2.id].class).toBe 'test-class' - expect(decorationState[decoration3.id]).toBeUndefined() - - presenter.setScrollTop(scrollTop + lineHeight * 5) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 3 + 5 - expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + 11 - expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration2.id].class).toBe 'test-class' - expect(decorationState[decoration3.id].top).toBe lineHeight * marker3.getScreenRange().start.row + 3 + 5 + 7 + 11 - expect(decorationState[decoration3.id].height).toBe lineHeight * marker3.getScreenRange().getRowCount() - expect(decorationState[decoration3.id].item).toBe decorationItem - expect(decorationState[decoration3.id].class).toBe 'test-class' - - waitsForStateToUpdate presenter, -> blockDecoration1.destroy() - runs -> - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 5 - expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + 11 - expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration2.id].class).toBe 'test-class' - expect(decorationState[decoration3.id].top).toBe lineHeight * marker3.getScreenRange().start.row + 5 + 7 + 11 - expect(decorationState[decoration3.id].height).toBe lineHeight * marker3.getScreenRange().getRowCount() - expect(decorationState[decoration3.id].item).toBe decorationItem - expect(decorationState[decoration3.id].class).toBe 'test-class' - - it "updates when ::scrollTop changes", -> - # This update will scroll decoration1 out of view, and decoration3 into view. - expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id].top).toBeDefined() - - it "updates when ::explicitHeight changes", -> - # This update will make all three decorations visible. - expectStateUpdate presenter, -> presenter.setExplicitHeight(explicitHeight + lineHeight * 5) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id].top).toBeDefined() - - it "updates when ::lineHeight changes", -> - # This update will make all three decorations visible. - expectStateUpdate presenter, -> presenter.setLineHeight(Math.ceil(1.0 * explicitHeight / marker3.getBufferRange().end.row)) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id].top).toBeDefined() - - it "updates when the editor's content changes", -> - # This update will add enough lines to push decoration2 out of view. - expectStateUpdate presenter, -> editor.setTextInBufferRange([[8, 0], [9, 0]], '\n\n\n\n\n') - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id]).toBeUndefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates when a decoration's marker is modified", -> - # This update will move decoration1 out of view. - waitsForStateToUpdate presenter, -> - newRange = new Range([13, 0], [14, 0]) - marker1.setBufferRange(newRange) - - runs -> - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - describe "when a decoration's properties are modified", -> - it "updates the item applied to the decoration, if the decoration item is changed", -> - # This changes the decoration class. The visibility of the decoration should not be affected. - newItem = document.createElement('div') - newItem.class = 'new-decoration-item' - newDecorationParams = - type: 'gutter' - gutterName: 'test-gutter' - class: 'test-class' - item: newItem - - waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) - - runs -> - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].item).toBe newItem - expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates the class applied to the decoration, if the decoration class is changed", -> - # This changes the decoration item. The visibility of the decoration should not be affected. - newDecorationParams = - type: 'gutter' - gutterName: 'test-gutter' - class: 'new-test-class' - item: decorationItem - waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) - - runs -> - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].class).toBe 'new-test-class' - expect(decorationState[decoration2.id].class).toBe 'test-class' - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates the type of the decoration, if the decoration type is changed", -> - # This changes the type of the decoration. This should remove the decoration from the gutter. - newDecorationParams = - type: 'line' - gutterName: 'test-gutter' # This is an invalid/meaningless option here, but it shouldn't matter. - class: 'test-class' - item: decorationItem - waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) - - runs -> - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates the gutter the decoration targets, if the decoration gutterName is changed", -> - # This changes which gutter this decoration applies to. Since this gutter does not exist, - # the decoration should not appear in the customDecorations state. - newDecorationParams = - type: 'gutter' - gutterName: 'test-gutter-2' - class: 'new-test-class' - item: decorationItem - waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) - - runs -> - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - # After adding the targeted gutter, the decoration will appear in the state for that gutter, - # since it should be visible. - expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) - newGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter-2') - expect(newGutterDecorationState[decoration1.id].top).toBeDefined() - expect(newGutterDecorationState[decoration2.id]).toBeUndefined() - expect(newGutterDecorationState[decoration3.id]).toBeUndefined() - oldGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(oldGutterDecorationState[decoration1.id]).toBeUndefined() - expect(oldGutterDecorationState[decoration2.id].top).toBeDefined() - expect(oldGutterDecorationState[decoration3.id]).toBeUndefined() - - it "updates when the editor's mini state changes, and is cleared when the editor is mini", -> - expectStateUpdate presenter, -> editor.setMini(true) - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState).toBeUndefined() - - # The decorations should return to the original state. - expectStateUpdate presenter, -> editor.setMini(false) - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates when a gutter's visibility changes, and is cleared when the gutter is not visible", -> - expectStateUpdate presenter, -> gutter.hide() - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id]).toBeUndefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - # The decorations should return to the original state. - expectStateUpdate presenter, -> gutter.show() - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates when a gutter is added to the editor", -> - decorationParams = - type: 'gutter' - gutterName: 'test-gutter-2' - class: 'test-class' - marker4 = editor.markBufferRange([[0, 0], [1, 0]]) - decoration4 = null - - waitsForStateToUpdate presenter, -> decoration4 = editor.decorateMarker(marker4, decorationParams) - - runs -> - expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter-2') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id]).toBeUndefined() - expect(decorationState[decoration3.id]).toBeUndefined() - expect(decorationState[decoration4.id].top).toBeDefined() - - it "updates when editor lines are folded", -> - oldDimensionsForDecoration1 = - top: lineHeight * marker1.getScreenRange().start.row - height: lineHeight * marker1.getScreenRange().getRowCount() - oldDimensionsForDecoration2 = - top: lineHeight * marker2.getScreenRange().start.row - height: lineHeight * marker2.getScreenRange().getRowCount() - - # Based on the contents of sample.js, this should affect all but the top - # part of decoration1. - expectStateUpdate presenter, -> editor.foldBufferRow(0) - - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBe oldDimensionsForDecoration1.top - expect(decorationState[decoration1.id].height).not.toBe oldDimensionsForDecoration1.height - # Due to the issue described here: https://github.com/atom/atom/issues/6454, decoration2 - # will be bumped up to the row that was folded and still made visible, instead of being - # entirely collapsed. (The same thing will happen to decoration3.) - expect(decorationState[decoration2.id].top).not.toBe oldDimensionsForDecoration2.top - expect(decorationState[decoration2.id].height).not.toBe oldDimensionsForDecoration2.height - - describe "regardless of what kind of gutter a gutter description corresponds to", -> - [customGutter] = [] - - getStylesForGutterWithName = (presenter, gutterName) -> - fullState = getStateForGutterWithName(presenter, gutterName) - return fullState.styles if fullState - - beforeEach -> - customGutter = editor.addGutter({name: 'test-gutter', priority: -1, visible: true}) - - afterEach -> - customGutter.destroy() - - describe ".scrollHeight", -> - it "updates when new block decorations are measured, changed or destroyed", -> - presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 - - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - blockDecoration2 = addBlockDecorationBeforeScreenRow(3) - blockDecoration3 = addBlockDecorationBeforeScreenRow(7) - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) - - linesHeight = editor.getScreenLineCount() * 10 - blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) - - blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - waitsForStateToUpdate presenter, -> blockDecoration3.destroy() - runs -> - blockDecorationsHeight = Math.round(35.8 + 100.3) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) - - it "is initialized based on ::lineHeight, the number of lines, and ::explicitHeight", -> - presenter = buildPresenter() - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 10 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe editor.getScreenLineCount() * 10 - - presenter = buildPresenter(explicitHeight: 500) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe 500 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe 500 - - it "updates when the ::lineHeight changes", -> - presenter = buildPresenter() - expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 20 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe editor.getScreenLineCount() * 20 - - it "updates when the line count changes", -> - presenter = buildPresenter() - expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 10 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe editor.getScreenLineCount() * 10 - - it "updates when ::explicitHeight changes", -> - presenter = buildPresenter() - expectStateUpdate presenter, -> presenter.setExplicitHeight(500) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe 500 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe 500 - - it "adds the computed clientHeight to the computed scrollHeight if scrollPastEnd is enabled", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe presenter.contentHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe presenter.contentHeight - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: true}) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: false}) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe presenter.contentHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe presenter.contentHeight - - describe ".scrollTop", -> - it "tracks the value of ::scrollTop", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 20) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe 10 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollTop(50) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe 50 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe 50 - - it "never exceeds the computed scrollHeight minus the computed clientHeight", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(100) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> presenter.setExplicitHeight(60) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollTopBefore = getState(presenter).verticalScrollbar.scrollTop - expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe scrollTopBefore - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe scrollTopBefore - - it "never goes negative", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(-100) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe 0 - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe 0 - - it "adds the computed clientHeight to the computed scrollHeight if scrollPastEnd is enabled", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.contentHeight - presenter.clientHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - editor.update({scrollPastEnd: true}) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> editor.update({scrollPastEnd: false}) - expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.contentHeight - presenter.clientHeight - expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - describe ".backgroundColor", -> - it "is assigned to ::gutterBackgroundColor if present, and to ::backgroundColor otherwise", -> - presenter = buildPresenter() - presenter.setBackgroundColor("rgba(255, 0, 0, 0)") - presenter.setGutterBackgroundColor("rgba(0, 255, 0, 0)") - expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(0, 255, 0, 0)" - expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(0, 255, 0, 0)" - - expectStateUpdate presenter, -> presenter.setGutterBackgroundColor("rgba(0, 0, 255, 0)") - expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(0, 0, 255, 0)" - expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(0, 0, 255, 0)" - - expectStateUpdate presenter, -> presenter.setGutterBackgroundColor("rgba(0, 0, 0, 0)") - expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(255, 0, 0, 0)" - expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(255, 0, 0, 0)" - - expectStateUpdate presenter, -> presenter.setBackgroundColor("rgba(0, 0, 255, 0)") - expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(0, 0, 255, 0)" - expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(0, 0, 255, 0)" - - # disabled until we fix an issue with display buffer markers not updating when - # they are moved on screen but not in the buffer - xdescribe "when the model and view measurements are mutated randomly", -> - [editor, buffer, presenterParams, presenter, statements] = [] - - recordStatement = (statement) -> statements.push(statement) - - it "correctly maintains the presenter state", -> - _.times 20, -> - waits(0) - runs -> - performSetup() - performRandomInitialization(recordStatement) - _.times 20, -> - performRandomAction recordStatement - expectValidState() - performTeardown() - - xit "works correctly for a particular stream of random actions", -> - performSetup() - # paste output from failing spec here - expectValidState() - performTeardown() - - performSetup = -> - buffer = new TextBuffer - editor = new TextEditor({buffer}) - editor.setEditorWidthInChars(80) - presenterParams = - model: editor - - presenter = new TextEditorPresenter(presenterParams) - statements = [] - - performRandomInitialization = (log) -> - actions = _.shuffle([ - changeScrollLeft - changeScrollTop - changeExplicitHeight - changeContentFrameWidth - changeLineHeight - changeBaseCharacterWidth - changeHorizontalScrollbarHeight - changeVerticalScrollbarWidth - ]) - for action in actions - action(log) - expectValidState() - - performTeardown = -> - buffer.destroy() - - expectValidState = -> - presenterParams.scrollTop = presenter.scrollTop - presenterParams.scrollLeft = presenter.scrollLeft - actualState = getState(presenter) - expectedState = new TextEditorPresenter(presenterParams).state - delete actualState.content.scrollingVertically - delete expectedState.content.scrollingVertically - - unless _.isEqual(actualState, expectedState) - console.log "Presenter states differ >>>>>>>>>>>>>>>>" - console.log "Actual:", actualState - console.log "Expected:", expectedState - console.log "Uncomment code below this line to see a JSON diff" - # {diff} = require 'json-diff' # !!! Run `npm install json-diff` in your `atom/` repository - # console.log "Difference:", diff(actualState, expectedState) - if statements.length > 0 - console.log """ - ===================================================== - Paste this code into the disabled spec in this file (and enable it) to repeat this failure: - - #{statements.join('\n')} - ===================================================== - """ - throw new Error("Unexpected presenter state after random mutation. Check console output for details.") - - performRandomAction = (log) -> - getRandomElement([ - changeScrollLeft - changeScrollTop - toggleSoftWrap - insertText - changeCursors - changeSelections - changeLineDecorations - ])(log) - - changeScrollTop = (log) -> - scrollHeight = (presenterParams.lineHeight ? 10) * editor.getScreenLineCount() - explicitHeight = (presenterParams.explicitHeight ? 500) - newScrollTop = Math.max(0, _.random(0, scrollHeight - explicitHeight)) - log "presenter.setScrollTop(#{newScrollTop})" - presenter.setScrollTop(newScrollTop) - - changeScrollLeft = (log) -> - scrollWidth = presenter.scrollWidth ? 300 - contentFrameWidth = presenter.contentFrameWidth ? 200 - newScrollLeft = Math.max(0, _.random(0, scrollWidth - contentFrameWidth)) - log """ - presenterParams.scrollLeft = #{newScrollLeft} - presenter.setScrollLeft(#{newScrollLeft}) - """ - presenterParams.scrollLeft = newScrollLeft - presenter.setScrollLeft(newScrollLeft) - - changeExplicitHeight = (log) -> - scrollHeight = (presenterParams.lineHeight ? 10) * editor.getScreenLineCount() - newExplicitHeight = _.random(30, scrollHeight * 1.5) - log """ - presenterParams.explicitHeight = #{newExplicitHeight} - presenter.setExplicitHeight(#{newExplicitHeight}) - """ - presenterParams.explicitHeight = newExplicitHeight - presenter.setExplicitHeight(newExplicitHeight) - - changeContentFrameWidth = (log) -> - scrollWidth = presenter.scrollWidth ? 300 - newContentFrameWidth = _.random(100, scrollWidth * 1.5) - log """ - presenterParams.contentFrameWidth = #{newContentFrameWidth} - presenter.setContentFrameWidth(#{newContentFrameWidth}) - """ - presenterParams.contentFrameWidth = newContentFrameWidth - presenter.setContentFrameWidth(newContentFrameWidth) - - changeLineHeight = (log) -> - newLineHeight = _.random(5, 15) - log """ - presenterParams.lineHeight = #{newLineHeight} - presenter.setLineHeight(#{newLineHeight}) - """ - presenterParams.lineHeight = newLineHeight - presenter.setLineHeight(newLineHeight) - - changeBaseCharacterWidth = (log) -> - newBaseCharacterWidth = _.random(5, 15) - log """ - presenterParams.baseCharacterWidth = #{newBaseCharacterWidth} - presenter.setBaseCharacterWidth(#{newBaseCharacterWidth}) - """ - presenterParams.baseCharacterWidth = newBaseCharacterWidth - presenter.setBaseCharacterWidth(newBaseCharacterWidth) - - changeHorizontalScrollbarHeight = (log) -> - newHorizontalScrollbarHeight = _.random(2, 15) - log """ - presenterParams.horizontalScrollbarHeight = #{newHorizontalScrollbarHeight} - presenter.setHorizontalScrollbarHeight(#{newHorizontalScrollbarHeight}) - """ - presenterParams.horizontalScrollbarHeight = newHorizontalScrollbarHeight - presenter.setHorizontalScrollbarHeight(newHorizontalScrollbarHeight) - - changeVerticalScrollbarWidth = (log) -> - newVerticalScrollbarWidth = _.random(2, 15) - log """ - presenterParams.verticalScrollbarWidth = #{newVerticalScrollbarWidth} - presenter.setVerticalScrollbarWidth(#{newVerticalScrollbarWidth}) - """ - presenterParams.verticalScrollbarWidth = newVerticalScrollbarWidth - presenter.setVerticalScrollbarWidth(newVerticalScrollbarWidth) - - toggleSoftWrap = (log) -> - softWrapped = not editor.isSoftWrapped() - log "editor.setSoftWrapped(#{softWrapped})" - editor.update({softWrapped: softWrapped}) - - insertText = (log) -> - range = buildRandomRange() - text = buildRandomText() - log "editor.setTextInBufferRange(#{JSON.stringify(range.serialize())}, #{JSON.stringify(text)})" - editor.setTextInBufferRange(range, text) - - changeCursors = (log) -> - actions = [addCursor, moveCursor] - actions.push(destroyCursor) if editor.getCursors().length > 1 - getRandomElement(actions)(log) - - addCursor = (log) -> - position = buildRandomPoint() - log "editor.addCursorAtBufferPosition(#{JSON.stringify(position.serialize())})" - editor.addCursorAtBufferPosition(position) - - moveCursor = (log) -> - index = _.random(0, editor.getCursors().length - 1) - position = buildRandomPoint() - log """ - cursor = editor.getCursors()[#{index}] - cursor.selection.clear() - cursor.setBufferPosition(#{JSON.stringify(position.serialize())}) - """ - cursor = editor.getCursors()[index] - cursor.selection.clear() - cursor.setBufferPosition(position) - - destroyCursor = (log) -> - index = _.random(0, editor.getCursors().length - 1) - log "editor.getCursors()[#{index}].destroy()" - editor.getCursors()[index].destroy() - - changeSelections = (log) -> - actions = [addSelection, changeSelection] - actions.push(destroySelection) if editor.getSelections().length > 1 - getRandomElement(actions)(log) - - addSelection = (log) -> - range = buildRandomRange() - log "editor.addSelectionForBufferRange(#{JSON.stringify(range.serialize())})" - editor.addSelectionForBufferRange(range) - - changeSelection = (log) -> - index = _.random(0, editor.getSelections().length - 1) - range = buildRandomRange() - log "editor.getSelections()[#{index}].setBufferRange(#{JSON.stringify(range.serialize())})" - editor.getSelections()[index].setBufferRange(range) - - destroySelection = (log) -> - index = _.random(0, editor.getSelections().length - 1) - log "editor.getSelections()[#{index}].destroy()" - editor.getSelections()[index].destroy() - - changeLineDecorations = (log) -> - actions = [addLineDecoration] - actions.push(changeLineDecoration, destroyLineDecoration) if editor.getLineDecorations().length > 0 - getRandomElement(actions)(log) - - addLineDecoration = (log) -> - range = buildRandomRange() - options = { - type: getRandomElement(['line', 'line-number']) - class: randomWords(exactly: 1)[0] - } - if Math.random() > .2 - options.onlyEmpty = true - else if Math.random() > .2 - options.onlyNonEmpty = true - else if Math.random() > .2 - options.onlyHead = true - - log """ - marker = editor.markBufferRange(#{JSON.stringify(range.serialize())}) - editor.decorateMarker(marker, #{JSON.stringify(options)}) - """ - - marker = editor.markBufferRange(range) - editor.decorateMarker(marker, options) - - changeLineDecoration = (log) -> - index = _.random(0, editor.getLineDecorations().length - 1) - range = buildRandomRange() - log "editor.getLineDecorations()[#{index}].getMarker().setBufferRange(#{JSON.stringify(range.serialize())})" - editor.getLineDecorations()[index].getMarker().setBufferRange(range) - - destroyLineDecoration = (log) -> - index = _.random(0, editor.getLineDecorations().length - 1) - log "editor.getLineDecorations()[#{index}].destroy()" - editor.getLineDecorations()[index].destroy() - - buildRandomPoint = -> - row = _.random(0, buffer.getLastRow()) - column = _.random(0, buffer.lineForRow(row).length) - new Point(row, column) - - buildRandomRange = -> - new Range(buildRandomPoint(), buildRandomPoint()) - - buildRandomText = -> - text = [] - - _.times _.random(20, 60), -> - if Math.random() < .2 - text += '\n' - else - text += " " if /\w$/.test(text) - text += randomWords(exactly: 1) - text - - getRandomElement = (array) -> - array[Math.floor(Math.random() * array.length)] diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js index ac5183cab..79d575a5f 100644 --- a/spec/text-editor-registry-spec.js +++ b/spec/text-editor-registry-spec.js @@ -19,7 +19,7 @@ describe('TextEditorRegistry', function () { packageManager: {deferredActivationHooks: null} }) - editor = new TextEditor() + editor = new TextEditor({autoHeight: false}) }) afterEach(function () { diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 6e6ba112f..12c5a3a55 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -99,23 +99,34 @@ describe "TextEditor", -> expect(editor.getAutoWidth()).toBeFalsy() expect(editor.getShowCursorOnSelection()).toBeTruthy() - editor.update({autoHeight: true, autoWidth: true, showCursorOnSelection: false}) + element = editor.getElement() + element.setHeight(100) + element.setWidth(100) + jasmine.attachToDOM(element) + + editor.update({showCursorOnSelection: false}) editor.setSelectedBufferRange([[1, 2], [3, 4]]) editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true) - editor.firstVisibleScreenRow = 5 - editor.firstVisibleScreenColumn = 5 + editor.setScrollTopRow(3) + expect(editor.getScrollTopRow()).toBe(3) + editor.setScrollLeftColumn(4) + expect(editor.getScrollLeftColumn()).toBe(4) editor.foldBufferRow(4) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() editor2 = editor.copy() + element2 = editor2.getElement() + element2.setHeight(100) + element2.setWidth(100) + jasmine.attachToDOM(element2) expect(editor2.id).not.toBe editor.id expect(editor2.getSelectedBufferRanges()).toEqual editor.getSelectedBufferRanges() expect(editor2.getSelections()[1].isReversed()).toBeTruthy() - expect(editor2.getFirstVisibleScreenRow()).toBe 5 - expect(editor2.getFirstVisibleScreenColumn()).toBe 5 + expect(editor2.getScrollTopRow()).toBe(3) + expect(editor2.getScrollLeftColumn()).toBe(4) expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor2.getAutoWidth()).toBeTruthy() - expect(editor2.getAutoHeight()).toBeTruthy() + expect(editor2.getAutoWidth()).toBe(false) + expect(editor2.getAutoHeight()).toBe(false) expect(editor2.getShowCursorOnSelection()).toBeFalsy() # editor2 can now diverge from its origin edit session @@ -137,7 +148,7 @@ describe "TextEditor", -> autoHeight: false }) - expect(returnedPromise).toBe(atom.views.getNextUpdatePromise()) + expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) expect(changeSpy.callCount).toBe(1) expect(editor.getTabLength()).toBe(6) expect(editor.getSoftTabs()).toBe(false) @@ -1877,8 +1888,6 @@ describe "TextEditor", -> [[4, 16], [4, 21]] [[4, 25], [4, 29]] ] - for cursor in editor.getCursors() - expect(cursor.isVisible()).toBeTruthy() it "skips lines that are too short to create a non-empty selection", -> editor.setSelectedBufferRange([[3, 31], [3, 38]]) @@ -2010,8 +2019,6 @@ describe "TextEditor", -> [[2, 16], [2, 21]] [[2, 37], [2, 40]] ] - for cursor in editor.getCursors() - expect(cursor.isVisible()).toBeTruthy() it "skips lines that are too short to create a non-empty selection", -> editor.setSelectedBufferRange([[6, 31], [6, 38]]) @@ -2181,54 +2188,6 @@ describe "TextEditor", -> editor.setCursorScreenPosition([3, 3]) expect(selection.isEmpty()).toBeTruthy() - describe "cursor visibility while there is a selection", -> - describe "when showCursorOnSelection is true", -> - it "is visible while there is no selection", -> - expect(selection.isEmpty()).toBeTruthy() - expect(editor.getShowCursorOnSelection()).toBeTruthy() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursors()[0].isVisible()).toBeTruthy() - - it "is visible while there is a selection", -> - expect(selection.isEmpty()).toBeTruthy() - editor.setSelectedBufferRange([[1, 2], [1, 5]]) - expect(selection.isEmpty()).toBeFalsy() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursors()[0].isVisible()).toBeTruthy() - - it "is visible while there are multiple selections", -> - expect(editor.getSelections().length).toBe 1 - editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]]) - expect(editor.getSelections().length).toBe 2 - expect(editor.getCursors().length).toBe 2 - expect(editor.getCursors()[0].isVisible()).toBeTruthy() - expect(editor.getCursors()[1].isVisible()).toBeTruthy() - - describe "when showCursorOnSelection is false", -> - it "is visible while there is no selection", -> - editor.update({showCursorOnSelection: false}) - expect(selection.isEmpty()).toBeTruthy() - expect(editor.getShowCursorOnSelection()).toBeFalsy() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursors()[0].isVisible()).toBeTruthy() - - it "is not visible while there is a selection", -> - editor.update({showCursorOnSelection: false}) - expect(selection.isEmpty()).toBeTruthy() - editor.setSelectedBufferRange([[1, 2], [1, 5]]) - expect(selection.isEmpty()).toBeFalsy() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursors()[0].isVisible()).toBeFalsy() - - it "is not visible while there are multiple selections", -> - editor.update({showCursorOnSelection: false}) - expect(editor.getSelections().length).toBe 1 - editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]]) - expect(editor.getSelections().length).toBe 2 - expect(editor.getCursors().length).toBe 2 - expect(editor.getCursors()[0].isVisible()).toBeFalsy() - expect(editor.getCursors()[1].isVisible()).toBeFalsy() - it "does not share selections between different edit sessions for the same buffer", -> editor2 = null waitsForPromise -> @@ -3295,7 +3254,6 @@ describe "TextEditor", -> expect(line).toBe " var ort = function(items) {" expect(editor.getCursorScreenPosition()).toEqual {row: 1, column: 6} expect(changeScreenRangeHandler).toHaveBeenCalled() - expect(editor.getLastCursor().isVisible()).toBeTruthy() describe "when the cursor is at the beginning of a line", -> it "joins it with the line above", -> @@ -4863,10 +4821,7 @@ describe "TextEditor", -> expect(buffer.getLineCount()).toBe(count - 1) describe "when the line being deleted preceeds a fold, and the command is undone", -> - # TODO: This seemed to have only been passing due to an accident in the text - # buffer implementation. Once we moved selections to a different layer it - # broke. We need to revisit our representation of folds and then reenable it. - xit "restores the line and preserves the fold", -> + it "restores the line and preserves the fold", -> editor.setCursorBufferPosition([4]) editor.foldCurrentRow() expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() @@ -5476,8 +5431,8 @@ describe "TextEditor", -> tokens = editor.tokensForScreenRow(0) expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']}, - {text: ' http://github.com', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']} + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} ] waitsForPromise -> @@ -5486,9 +5441,9 @@ describe "TextEditor", -> runs -> tokens = editor.tokensForScreenRow(0) expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']}, - {text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']} - {text: 'http://github.com', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--markup.syntax--underline.syntax--link.syntax--http.syntax--hyperlink']} + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + {text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']} ] describe "when the grammar is updated", -> @@ -5501,8 +5456,8 @@ describe "TextEditor", -> tokens = editor.tokensForScreenRow(0) expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']} + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} ] waitsForPromise -> @@ -5511,8 +5466,8 @@ describe "TextEditor", -> runs -> tokens = editor.tokensForScreenRow(0) expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']} + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} ] waitsForPromise -> @@ -5521,14 +5476,14 @@ describe "TextEditor", -> runs -> tokens = editor.tokensForScreenRow(0) expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']}, - {text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}, - {text: 'SELECT', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--other.syntax--DML.syntax--sql']}, - {text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}, - {text: '*', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--operator.syntax--star.syntax--sql']}, - {text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}, - {text: 'FROM', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--other.syntax--DML.syntax--sql']}, - {text: ' OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']} + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} ] describe ".normalizeTabsInBufferRange()", -> @@ -5549,7 +5504,11 @@ describe "TextEditor", -> describe ".pageUp/Down()", -> it "moves the cursor down one page length", -> - editor.setRowsPerPage(5) + editor.update(autoHeight: false) + element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = element.component.getLineHeight() * 5 + 'px' + element.measureDimensions() expect(editor.getCursorBufferPosition().row).toBe 0 @@ -5567,7 +5526,11 @@ describe "TextEditor", -> describe ".selectPageUp/Down()", -> it "selects one screen height of text up or down", -> - editor.setRowsPerPage(5) + editor.update(autoHeight: false) + element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = element.component.getLineHeight() * 5 + 'px' + element.measureDimensions() expect(editor.getCursorBufferPosition().row).toBe 0 @@ -5590,72 +5553,6 @@ describe "TextEditor", -> editor.selectPageUp() expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] - describe "::setFirstVisibleScreenRow() and ::getFirstVisibleScreenRow()", -> - beforeEach -> - line = Array(9).join('0123456789') - editor.setText([1..100].map(-> line).join('\n')) - expect(editor.getLineCount()).toBe 100 - expect(editor.lineTextForBufferRow(0).length).toBe 80 - - describe "when the editor doesn't have a height and lineHeightInPixels", -> - it "does not affect the editor's visible row range", -> - expect(editor.getVisibleRowRange()).toBeNull() - - editor.setFirstVisibleScreenRow(1) - expect(editor.getFirstVisibleScreenRow()).toEqual 1 - - editor.setFirstVisibleScreenRow(3) - expect(editor.getFirstVisibleScreenRow()).toEqual 3 - - expect(editor.getVisibleRowRange()).toBeNull() - expect(editor.getLastVisibleScreenRow()).toBeNull() - - describe "when the editor has a height and lineHeightInPixels", -> - beforeEach -> - editor.update({scrollPastEnd: true}) - editor.setHeight(100, true) - editor.setLineHeightInPixels(10) - - it "updates the editor's visible row range", -> - editor.setFirstVisibleScreenRow(2) - expect(editor.getFirstVisibleScreenRow()).toEqual 2 - expect(editor.getLastVisibleScreenRow()).toBe 12 - expect(editor.getVisibleRowRange()).toEqual [2, 12] - - it "notifies ::onDidChangeFirstVisibleScreenRow observers", -> - changeCount = 0 - editor.onDidChangeFirstVisibleScreenRow -> changeCount++ - - editor.setFirstVisibleScreenRow(2) - expect(changeCount).toBe 1 - - editor.setFirstVisibleScreenRow(2) - expect(changeCount).toBe 1 - - editor.setFirstVisibleScreenRow(3) - expect(changeCount).toBe 2 - - it "ensures that the top row is less than the buffer's line count", -> - editor.setFirstVisibleScreenRow(102) - expect(editor.getFirstVisibleScreenRow()).toEqual 99 - expect(editor.getVisibleRowRange()).toEqual [99, 99] - - it "ensures that the left column is less than the length of the longest screen line", -> - editor.setFirstVisibleScreenRow(10) - expect(editor.getFirstVisibleScreenRow()).toEqual 10 - - editor.setText("\n\n\n") - - editor.setFirstVisibleScreenRow(10) - expect(editor.getFirstVisibleScreenRow()).toEqual 3 - - describe "when the 'editor.scrollPastEnd' option is set to false", -> - it "ensures that the bottom row is less than the buffer's line count", -> - editor.update({scrollPastEnd: false}) - editor.setFirstVisibleScreenRow(95) - 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") @@ -5677,6 +5574,12 @@ describe "TextEditor", -> editor.update({scrollPastEnd: false}) expect(editor.getScrollPastEnd()).toBe(false) + it "always returns false when autoHeight is on", -> + editor.update({autoHeight: true, scrollPastEnd: true}) + expect(editor.getScrollPastEnd()).toBe(false) + editor.update({autoHeight: false}) + expect(editor.getScrollPastEnd()).toBe(true) + describe "auto height", -> it "returns true by default but can be customized", -> editor = new TextEditor @@ -5963,20 +5866,20 @@ describe "TextEditor", -> editor.update({showIndentGuide: false}) expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source.syntax--js']} + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} ] editor.update({showIndentGuide: true}) expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace indent-guide']}, - {text: 'foo', scopes: ['syntax--source.syntax--js']} + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} ] editor.setMini(true) expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source.syntax--js']} + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} ] describe "when the editor is constructed with the grammar option set", -> diff --git a/spec/tokenized-buffer-iterator-spec.js b/spec/tokenized-buffer-iterator-spec.js index cc703bbec..1b26f7b38 100644 --- a/spec/tokenized-buffer-iterator-spec.js +++ b/spec/tokenized-buffer-iterator-spec.js @@ -17,16 +17,6 @@ describe('TokenizedBufferIterator', () => { } else { return null } - }, - - grammar: { - scopeForId (id) { - return { - '-1': 'foo', '-2': 'foo', - '-3': 'bar', '-4': 'bar', - '-5': 'baz', '-6': 'baz' - }[id] - } } } @@ -34,57 +24,57 @@ describe('TokenizedBufferIterator', () => { expect(iterator.seek(Point(0, 0))).toEqual([]) expect(iterator.getPosition()).toEqual(Point(0, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['syntax--foo']) + expect(iterator.getCloseScopeIds()).toEqual([]) + expect(iterator.getOpenScopeIds()).toEqual([257]) iterator.moveToSuccessor() - expect(iterator.getCloseTags()).toEqual(['syntax--foo']) - expect(iterator.getOpenTags()).toEqual(['syntax--bar']) + expect(iterator.getCloseScopeIds()).toEqual([257]) + expect(iterator.getOpenScopeIds()).toEqual([259]) - expect(iterator.seek(Point(0, 1))).toEqual(['syntax--baz']) + expect(iterator.seek(Point(0, 1))).toEqual([261]) expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['syntax--bar']) + expect(iterator.getCloseScopeIds()).toEqual([]) + expect(iterator.getOpenScopeIds()).toEqual([259]) iterator.moveToSuccessor() expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual(['syntax--bar', 'syntax--baz']) - expect(iterator.getOpenTags()).toEqual(['syntax--baz']) + expect(iterator.getCloseScopeIds()).toEqual([259, 261]) + expect(iterator.getOpenScopeIds()).toEqual([261]) - expect(iterator.seek(Point(0, 3))).toEqual(['syntax--baz']) + expect(iterator.seek(Point(0, 3))).toEqual([261]) expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['syntax--bar']) + expect(iterator.getCloseScopeIds()).toEqual([]) + expect(iterator.getOpenScopeIds()).toEqual([259]) iterator.moveToSuccessor() expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual(['syntax--bar', 'syntax--baz']) - expect(iterator.getOpenTags()).toEqual(['syntax--baz']) + expect(iterator.getCloseScopeIds()).toEqual([259, 261]) + expect(iterator.getOpenScopeIds()).toEqual([261]) iterator.moveToSuccessor() expect(iterator.getPosition()).toEqual(Point(0, 7)) - expect(iterator.getCloseTags()).toEqual(['syntax--baz']) - expect(iterator.getOpenTags()).toEqual(['syntax--bar']) + expect(iterator.getCloseScopeIds()).toEqual([261]) + expect(iterator.getOpenScopeIds()).toEqual([259]) iterator.moveToSuccessor() expect(iterator.getPosition()).toEqual(Point(0, 7)) - expect(iterator.getCloseTags()).toEqual(['syntax--bar']) - expect(iterator.getOpenTags()).toEqual([]) + expect(iterator.getCloseScopeIds()).toEqual([259]) + expect(iterator.getOpenScopeIds()).toEqual([]) iterator.moveToSuccessor() expect(iterator.getPosition()).toEqual(Point(1, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual([]) + expect(iterator.getCloseScopeIds()).toEqual([]) + expect(iterator.getOpenScopeIds()).toEqual([]) - expect(iterator.seek(Point(0, 5))).toEqual(['syntax--baz']) + expect(iterator.seek(Point(0, 5))).toEqual([261]) expect(iterator.getPosition()).toEqual(Point(0, 7)) - expect(iterator.getCloseTags()).toEqual(['syntax--baz']) - expect(iterator.getOpenTags()).toEqual(['syntax--bar']) + expect(iterator.getCloseScopeIds()).toEqual([261]) + expect(iterator.getOpenScopeIds()).toEqual([259]) iterator.moveToSuccessor() expect(iterator.getPosition()).toEqual(Point(0, 7)) - expect(iterator.getCloseTags()).toEqual(['syntax--bar']) - expect(iterator.getOpenTags()).toEqual([]) + expect(iterator.getCloseScopeIds()).toEqual([259]) + expect(iterator.getOpenScopeIds()).toEqual([]) }) }) @@ -97,12 +87,6 @@ describe('TokenizedBufferIterator', () => { text: '', openScopes: [] } - }, - - grammar: { - scopeForId () { - return 'foo' - } } } @@ -110,80 +94,17 @@ describe('TokenizedBufferIterator', () => { iterator.seek(Point(0, 0)) expect(iterator.getPosition()).toEqual(Point(0, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['syntax--foo']) + expect(iterator.getCloseScopeIds()).toEqual([]) + expect(iterator.getOpenScopeIds()).toEqual([257]) iterator.moveToSuccessor() expect(iterator.getPosition()).toEqual(Point(0, 0)) - expect(iterator.getCloseTags()).toEqual(['syntax--foo']) - expect(iterator.getOpenTags()).toEqual(['syntax--foo']) + expect(iterator.getCloseScopeIds()).toEqual([257]) + expect(iterator.getOpenScopeIds()).toEqual([257]) iterator.moveToSuccessor() - expect(iterator.getCloseTags()).toEqual(['syntax--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] - } - } - }, - - grammar: { - scopeForId (id) { - if (id === -2 || id === -1) { - return 'foo' - } else if (id === -3) { - return 'qux' - } - } - } - } - - const iterator = new TokenizedBufferIterator(tokenizedBuffer) - - iterator.seek(Point(0, 0)) - expect(iterator.getPosition()).toEqual(Point(0, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['syntax--foo']) - - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual(['syntax--foo']) - expect(iterator.getOpenTags()).toEqual(['syntax--qux']) - - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual(['syntax--qux']) - expect(iterator.getOpenTags()).toEqual([]) - - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(1, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['syntax--foo']) - - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(2, 0)) - expect(iterator.getCloseTags()).toEqual(['syntax--foo']) - expect(iterator.getOpenTags()).toEqual([]) + expect(iterator.getCloseScopeIds()).toEqual([257]) + expect(iterator.getOpenScopeIds()).toEqual([]) }) }) }) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index eff79ec95..5b1863b35 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -590,43 +590,56 @@ describe "TokenizedBuffer", -> iterator.seek(Point(0, 0)) expectedBoundaries = [ - {position: Point(0, 0), closeTags: [], openTags: ["syntax--source.syntax--js", "syntax--storage.syntax--type.syntax--var.syntax--js"]} - {position: Point(0, 3), closeTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"], openTags: []} - {position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"]} - {position: Point(0, 9), closeTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"], openTags: []} - {position: Point(0, 10), closeTags: [], openTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"]} - {position: Point(0, 11), closeTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"], openTags: []} - {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment.syntax--block.syntax--js", "syntax--punctuation.syntax--definition.syntax--comment.syntax--js"]} - {position: Point(0, 14), closeTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js"], openTags: []} - {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js"]} - {position: Point(1, 7), closeTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js", "syntax--comment.syntax--block.syntax--js"], openTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"]} - {position: Point(1, 10), closeTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"], openTags: []} - {position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"]} - {position: Point(1, 16), closeTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"], openTags: []} - {position: Point(1, 17), closeTags: [], openTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"]} - {position: Point(1, 18), closeTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"], openTags: []} + {position: Point(0, 0), closeTags: [], openTags: ["syntax--source syntax--js", "syntax--storage syntax--type syntax--var syntax--js"]} + {position: Point(0, 3), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} + {position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} + {position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} + {position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} + {position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} + {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--js"]} + {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"], openTags: []} + {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"]} + {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]} + {position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} + {position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} + {position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} + {position: Point(1, 17), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} + {position: Point(1, 18), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} ] loop boundary = { position: iterator.getPosition(), - closeTags: iterator.getCloseTags(), - openTags: iterator.getOpenTags() + closeTags: iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)), + openTags: iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)) } expect(boundary).toEqual(expectedBoundaries.shift()) break unless iterator.moveToSuccessor() - expect(iterator.seek(Point(0, 1))).toEqual(["syntax--source.syntax--js", "syntax--storage.syntax--type.syntax--var.syntax--js"]) + expect(iterator.seek(Point(0, 1)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + "syntax--source syntax--js", + "syntax--storage syntax--type syntax--var syntax--js" + ]) expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.seek(Point(0, 8))).toEqual(["syntax--source.syntax--js"]) + expect(iterator.seek(Point(0, 8)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + "syntax--source syntax--js" + ]) expect(iterator.getPosition()).toEqual(Point(0, 8)) - expect(iterator.seek(Point(1, 0))).toEqual(["syntax--source.syntax--js", "syntax--comment.syntax--block.syntax--js"]) + expect(iterator.seek(Point(1, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + "syntax--source syntax--js", + "syntax--comment syntax--block syntax--js" + ]) expect(iterator.getPosition()).toEqual(Point(1, 0)) - expect(iterator.seek(Point(1, 18))).toEqual(["syntax--source.syntax--js", "syntax--constant.syntax--numeric.syntax--decimal.syntax--js"]) + expect(iterator.seek(Point(1, 18)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + "syntax--source syntax--js", + "syntax--constant syntax--numeric syntax--decimal syntax--js" + ]) expect(iterator.getPosition()).toEqual(Point(1, 18)) - expect(iterator.seek(Point(2, 0))).toEqual(["syntax--source.syntax--js"]) + expect(iterator.seek(Point(2, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + "syntax--source syntax--js" + ]) iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test) it "does not report columns beyond the length of the line", -> @@ -671,5 +684,5 @@ describe "TokenizedBuffer", -> iterator.seek(Point(1, 0)) expect(iterator.getPosition()).toEqual([1, 0]) - expect(iterator.getCloseTags()).toEqual ['syntax--blue.syntax--broken'] - expect(iterator.getOpenTags()).toEqual ['syntax--yellow.syntax--broken'] + expect(iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--blue syntax--broken'] + expect(iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--yellow syntax--broken'] diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index d805d17cb..4bae1d811 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -5,7 +5,6 @@ describe "ViewRegistry", -> beforeEach -> registry = new ViewRegistry - registry.initialize() afterEach -> registry.clearDocumentRequests() @@ -127,8 +126,6 @@ describe "ViewRegistry", -> spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) events = [] - registry.pollDocument -> events.push('poll') - registry.pollAfterNextUpdate() registry.updateDocument -> events.push('write 1') registry.readDocument -> registry.updateDocument -> events.push('write from read 1') @@ -147,108 +144,20 @@ describe "ViewRegistry", -> 'write 2' 'read 1' 'read 2' - 'poll' 'write from read 1' 'write from read 2' ] - it "pauses DOM polling when reads or writes are pending", -> - spyOn(window, 'setInterval').andCallFake(fakeSetInterval) - spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) - events = [] - - registry.pollDocument -> events.push('poll') - registry.updateDocument -> events.push('write') - registry.readDocument -> events.push('read') - - window.dispatchEvent(new UIEvent('resize')) - expect(events).toEqual [] - - frameRequests[0]() - expect(events).toEqual ['write', 'read', 'poll'] - - window.dispatchEvent(new UIEvent('resize')) - expect(events).toEqual ['write', 'read', 'poll', 'poll'] - - it "polls the document after updating when ::pollAfterNextUpdate() has been called", -> - events = [] - registry.pollDocument -> events.push('poll') - registry.updateDocument -> events.push('write') - registry.readDocument -> events.push('read') - frameRequests.shift()() - expect(events).toEqual ['write', 'read'] - - events = [] - registry.pollAfterNextUpdate() - registry.updateDocument -> events.push('write') - registry.readDocument -> events.push('read') - frameRequests.shift()() - expect(events).toEqual ['write', 'read', 'poll'] - - describe "::pollDocument(fn)", -> - [testElement, testStyleSheet, disposable1, disposable2, events] = [] - - beforeEach -> - testElement = document.createElement('div') - testStyleSheet = document.createElement('style') - testStyleSheet.textContent = 'body {}' - jasmineContent = document.getElementById('jasmine-content') - jasmineContent.appendChild(testElement) - jasmineContent.appendChild(testStyleSheet) - - events = [] - disposable1 = registry.pollDocument -> events.push('poll 1') - disposable2 = registry.pollDocument -> events.push('poll 2') - - it "calls all registered polling functions after document or stylesheet changes until they are disabled via a returned disposable", -> - jasmine.useRealClock() - expect(events).toEqual [] - - testElement.style.width = '400px' - - waitsFor "events to occur in response to DOM mutation", -> events.length > 0 - - runs -> - expect(events).toEqual ['poll 1', 'poll 2'] - events.length = 0 - - testStyleSheet.textContent = 'body {color: #333;}' - - waitsFor "events to occur in reponse to style sheet mutation", -> events.length > 0 - - runs -> - expect(events).toEqual ['poll 1', 'poll 2'] - events.length = 0 - - disposable1.dispose() - testElement.style.color = '#fff' - - waitsFor "more events to occur in response to DOM mutation", -> events.length > 0 - - runs -> - expect(events).toEqual ['poll 2'] - - it "calls all registered polling functions when the window resizes", -> - expect(events).toEqual [] - - window.dispatchEvent(new UIEvent('resize')) - - expect(events).toEqual ['poll 1', 'poll 2'] - describe "::getNextUpdatePromise()", -> it "returns a promise that resolves at the end of the next update cycle", -> updateCalled = false readCalled = false - pollCalled = false waitsFor 'getNextUpdatePromise to resolve', (done) -> registry.getNextUpdatePromise().then -> expect(updateCalled).toBe true expect(readCalled).toBe true - expect(pollCalled).toBe true done() registry.updateDocument -> updateCalled = true registry.readDocument -> readCalled = true - registry.pollDocument -> pollCalled = true - registry.pollAfterNextUpdate() diff --git a/spec/workspace-element-spec.js b/spec/workspace-element-spec.js index e35907e65..ea597c0aa 100644 --- a/spec/workspace-element-spec.js +++ b/spec/workspace-element-spec.js @@ -230,28 +230,32 @@ describe('WorkspaceElement', () => { editorElement = editor.getElement() }) - it("updates the font-size based on the 'editor.fontSize' config value", () => { + it("updates the font-size based on the 'editor.fontSize' config value", async () => { const initialCharWidth = editor.getDefaultCharWidth() expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px') + atom.config.set('editor.fontSize', atom.config.get('editor.fontSize') + 5) + await editorElement.component.getNextUpdatePromise() expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px') expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialCharWidth) }) - it("updates the font-family based on the 'editor.fontFamily' config value", () => { + it("updates the font-family based on the 'editor.fontFamily' config value", async () => { const initialCharWidth = editor.getDefaultCharWidth() let fontFamily = atom.config.get('editor.fontFamily') expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily) atom.config.set('editor.fontFamily', 'sans-serif') fontFamily = atom.config.get('editor.fontFamily') + await editorElement.component.getNextUpdatePromise() expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily) expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) }) - it("updates the line-height based on the 'editor.lineHeight' config value", () => { + it("updates the line-height based on the 'editor.lineHeight' config value", async () => { const initialLineHeight = editor.getLineHeightInPixels() atom.config.set('editor.lineHeight', '30px') + await editorElement.component.getNextUpdatePromise() expect(getComputedStyle(editorElement).lineHeight).toBe(atom.config.get('editor.lineHeight')) expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeight) }) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 959156184..a283608d9 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -135,6 +135,7 @@ class AtomEnvironment extends Model @deserializers = new DeserializerManager(this) @deserializeTimings = {} @views = new ViewRegistry(this) + TextEditor.setScheduler(@views) @notifications = new NotificationManager @updateProcessEnv ?= updateProcessEnv # For testing @@ -208,8 +209,6 @@ class AtomEnvironment extends Model @getStorageFolder().clear() @stateStore.clear() - @views.initialize() - ConfigSchema.projectHome = { type: 'string', default: path.join(fs.getHomeDirectory(), 'github'), @@ -251,9 +250,13 @@ class AtomEnvironment extends Model @attachSaveStateListeners() @windowEventHandler.initialize(@window, @document) + didChangeStyles = @didChangeStyles.bind(this) + @disposables.add(@styles.onDidAddStyleElement(didChangeStyles)) + @disposables.add(@styles.onDidUpdateStyleElement(didChangeStyles)) + @disposables.add(@styles.onDidRemoveStyleElement(didChangeStyles)) + @observeAutoHideMenuBar() - @history.initialize(@window.localStorage) @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) preloadPackages: -> @@ -261,7 +264,7 @@ class AtomEnvironment extends Model attachSaveStateListeners: -> saveState = _.debounce((=> - window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded + @window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded ), @saveStateDebounceInterval) @document.addEventListener('mousedown', saveState, true) @document.addEventListener('keydown', saveState, true) @@ -677,11 +680,8 @@ class AtomEnvironment extends Model # Call this method when establishing a real application window. startEditorWindow: -> @unloaded = false - updateProcessEnvPromise = @updateProcessEnv(@getLoadSettings().env) - updateProcessEnvPromise.then => - @shellEnvironmentLoaded = true - @emitter.emit('loaded-shell-environment') - @packages.triggerActivationHook('core:loaded-shell-environment') + + updateProcessEnvPromise = @updateProcessEnvAndTriggerHooks() loadStatePromise = @loadState().then (state) => @windowDimensions = state?.windowDimensions @@ -800,6 +800,17 @@ class AtomEnvironment extends Model @windowEventHandler?.unsubscribe() @windowEventHandler = null + didChangeStyles: (styleElement) -> + TextEditor.didUpdateStyles() + if styleElement.textContent.indexOf('scrollbar') >= 0 + TextEditor.didUpdateScrollbarStyles() + + updateProcessEnvAndTriggerHooks: -> + @updateProcessEnv(@getLoadSettings().env).then => + @shellEnvironmentLoaded = true + @emitter.emit('loaded-shell-environment') + @packages.triggerActivationHook('core:loaded-shell-environment') + ### Section: Messaging the User ### diff --git a/src/compile-cache.js b/src/compile-cache.js index a8faeb118..4209b30ab 100644 --- a/src/compile-cache.js +++ b/src/compile-cache.js @@ -114,16 +114,19 @@ function writeCachedJavascript (relativeCachePath, code) { var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg -let snapshotSourceMapConsumer -if (global.isGeneratingSnapshot) { - // Warm up the source map consumer to efficiently translate positions when - // generating stack traces containing a file that was snapshotted. - const {SourceMapConsumer} = require('source-map') - snapshotSourceMapConsumer = new SourceMapConsumer(snapshotAuxiliaryData.sourceMap) // eslint-disable-line no-undef - snapshotSourceMapConsumer.originalPositionFor({line: 42, column: 0}) -} - exports.install = function (resourcesPath, nodeRequire) { + const snapshotSourceMapConsumer = { + originalPositionFor ({line, column}) { + const {relativePath, row} = snapshotResult.translateSnapshotRow(line) + return { + column, + line: row, + source: path.join(resourcesPath, 'app', 'static', relativePath), + name: null + } + } + } + sourceMapSupport.install({ handleUncaughtExceptions: false, @@ -132,10 +135,7 @@ exports.install = function (resourcesPath, nodeRequire) { // code from our cache directory. retrieveSourceMap: function (filePath) { if (filePath === '') { - return { - map: snapshotSourceMapConsumer, - url: path.join(resourcesPath, 'app', 'static', 'index.js') - } + return {map: snapshotSourceMapConsumer} } if (!cacheDirectory || !fs.isFileSync(filePath)) { diff --git a/src/crash-reporter-start.js b/src/crash-reporter-start.js index 98b210d06..035a94e3d 100644 --- a/src/crash-reporter-start.js +++ b/src/crash-reporter-start.js @@ -4,7 +4,7 @@ module.exports = function (extra) { productName: 'Atom', companyName: 'GitHub', submitURL: 'https://crashreporter.atom.io', - autoSubmit: false, + uploadToServer: false, extra: extra }) } diff --git a/src/cursor.coffee b/src/cursor.coffee index 47e8c0594..74922ff51 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -12,20 +12,14 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g # of a {DisplayMarker}. module.exports = class Cursor extends Model - showCursorOnSelection: null screenPosition: null bufferPosition: null goalColumn: null - visible: true # Instantiated by a {TextEditor} - constructor: ({@editor, @marker, @showCursorOnSelection, id}) -> + constructor: ({@editor, @marker, id}) -> @emitter = new Emitter - - @showCursorOnSelection ?= true - @assignId(id) - @updateVisibility() destroy: -> @marker.destroy() @@ -57,15 +51,6 @@ class Cursor extends Model onDidDestroy: (callback) -> @emitter.on 'did-destroy', callback - # Public: Calls your `callback` when the cursor's visibility has changed - # - # * `callback` {Function} - # * `visibility` {Boolean} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeVisibility: (callback) -> - @emitter.on 'did-change-visibility', callback - ### Section: Managing Cursor Position ### @@ -568,21 +553,6 @@ class Cursor extends Model Section: Visibility ### - # Public: Sets whether the cursor is visible. - setVisible: (visible) -> - if @visible isnt visible - @visible = visible - @emitter.emit 'did-change-visibility', @visible - - # Public: Returns the visibility of the cursor. - isVisible: -> @visible - - updateVisibility: -> - if @showCursorOnSelection - @setVisible(true) - else - @setVisible(@marker.getBufferRange().isEmpty()) - ### Section: Comparing to another cursor ### @@ -599,9 +569,6 @@ class Cursor extends Model Section: Utilities ### - # Public: Prevents this cursor from causing scrolling. - clearAutoscroll: -> - # Public: Deselects the current selection. clearSelection: (options) -> @selection?.clear(options) @@ -651,11 +618,6 @@ class Cursor extends Model Section: Private ### - setShowCursorOnSelection: (value) -> - if value isnt @showCursorOnSelection - @showCursorOnSelection = value - @updateVisibility() - getNonWordCharacters: -> @editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray()) @@ -668,7 +630,8 @@ class Cursor extends Model {row, column} = @getScreenPosition() new Range(new Point(row, column), new Point(row, column + 1)) - autoscroll: (options) -> + autoscroll: (options = {}) -> + options.clip = false @editor.scrollToScreenRange(@getScreenRange(), options) getBeginningOfNextParagraphBufferPosition: -> diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee deleted file mode 100644 index 2de4f1ede..000000000 --- a/src/cursors-component.coffee +++ /dev/null @@ -1,58 +0,0 @@ -module.exports = -class CursorsComponent - oldState: null - - constructor: -> - @cursorNodesById = {} - @domNode = document.createElement('div') - @domNode.classList.add('cursors') - - getDomNode: -> - @domNode - - updateSync: (state) -> - newState = state.content - @oldState ?= {cursors: {}} - - # update blink class - if newState.cursorsVisible isnt @oldState.cursorsVisible - if newState.cursorsVisible - @domNode.classList.remove 'blink-off' - else - @domNode.classList.add 'blink-off' - @oldState.cursorsVisible = newState.cursorsVisible - - # remove cursors - for id of @oldState.cursors - unless newState.cursors[id]? - @cursorNodesById[id].remove() - delete @cursorNodesById[id] - delete @oldState.cursors[id] - - # add or update cursors - for id, cursorState of newState.cursors - unless @oldState.cursors[id]? - cursorNode = document.createElement('div') - cursorNode.classList.add('cursor') - @cursorNodesById[id] = cursorNode - @domNode.appendChild(cursorNode) - @updateCursorNode(id, cursorState) - - return - - updateCursorNode: (id, newCursorState) -> - cursorNode = @cursorNodesById[id] - oldCursorState = (@oldState.cursors[id] ?= {}) - - if newCursorState.top isnt oldCursorState.top or newCursorState.left isnt oldCursorState.left - cursorNode.style['-webkit-transform'] = "translate(#{newCursorState.left}px, #{newCursorState.top}px)" - oldCursorState.top = newCursorState.top - oldCursorState.left = newCursorState.left - - if newCursorState.height isnt oldCursorState.height - cursorNode.style.height = newCursorState.height + 'px' - oldCursorState.height = newCursorState.height - - if newCursorState.width isnt oldCursorState.width - cursorNode.style.width = newCursorState.width + 'px' - oldCursorState.width = newCursorState.width diff --git a/src/custom-gutter-component.coffee b/src/custom-gutter-component.coffee deleted file mode 100644 index 759fcf2c4..000000000 --- a/src/custom-gutter-component.coffee +++ /dev/null @@ -1,119 +0,0 @@ -# This class represents a gutter other than the 'line-numbers' gutter. -# The contents of this gutter may be specified by Decorations. - -module.exports = -class CustomGutterComponent - constructor: ({@gutter, @views}) -> - @decorationNodesById = {} - @decorationItemsById = {} - @visible = true - - @domNode = @gutter.getElement() - @decorationsNode = @domNode.firstChild - # Clear the contents in case the domNode is being reused. - @decorationsNode.innerHTML = '' - - getDomNode: -> - @domNode - - hideNode: -> - if @visible - @domNode.style.display = 'none' - @visible = false - - showNode: -> - if not @visible - @domNode.style.removeProperty('display') - @visible = true - - # `state` is a subset of the TextEditorPresenter state that is specific - # to this line number gutter. - updateSync: (state) -> - @oldDimensionsAndBackgroundState ?= {} - setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode) - - @oldDecorationPositionState ?= {} - decorationState = state.content - - updatedDecorationIds = new Set - for decorationId, decorationInfo of decorationState - updatedDecorationIds.add(decorationId) - existingDecoration = @decorationNodesById[decorationId] - if existingDecoration - @updateDecorationNode(existingDecoration, decorationId, decorationInfo) - else - newNode = @buildDecorationNode(decorationId, decorationInfo) - @decorationNodesById[decorationId] = newNode - @decorationsNode.appendChild(newNode) - - for decorationId, decorationNode of @decorationNodesById - if not updatedDecorationIds.has(decorationId) - decorationNode.remove() - delete @decorationNodesById[decorationId] - delete @decorationItemsById[decorationId] - delete @oldDecorationPositionState[decorationId] - - ### - Section: Private Methods - ### - - # Builds and returns an HTMLElement to represent the specified decoration. - buildDecorationNode: (decorationId, decorationInfo) -> - @oldDecorationPositionState[decorationId] = {} - newNode = document.createElement('div') - newNode.style.position = 'absolute' - @updateDecorationNode(newNode, decorationId, decorationInfo) - newNode - - # Updates the existing HTMLNode with the new decoration info. Attempts to - # minimize changes to the DOM. - updateDecorationNode: (node, decorationId, newDecorationInfo) -> - oldPositionState = @oldDecorationPositionState[decorationId] - - if oldPositionState.top isnt newDecorationInfo.top + 'px' - node.style.top = newDecorationInfo.top + 'px' - oldPositionState.top = newDecorationInfo.top + 'px' - - if oldPositionState.height isnt newDecorationInfo.height + 'px' - node.style.height = newDecorationInfo.height + 'px' - oldPositionState.height = newDecorationInfo.height + 'px' - - if newDecorationInfo.class and not node.classList.contains(newDecorationInfo.class) - node.className = 'decoration' - node.classList.add(newDecorationInfo.class) - else if not newDecorationInfo.class - node.className = 'decoration' - - @setDecorationItem(newDecorationInfo.item, newDecorationInfo.height, decorationId, node) - - # Sets the decorationItem on the decorationNode. - # If `decorationItem` is undefined, the decorationNode's child item will be cleared. - setDecorationItem: (newItem, decorationHeight, decorationId, decorationNode) -> - if newItem isnt @decorationItemsById[decorationId] - while decorationNode.firstChild - decorationNode.removeChild(decorationNode.firstChild) - delete @decorationItemsById[decorationId] - - if newItem - newItemNode = null - if newItem instanceof HTMLElement - newItemNode = newItem - else - newItemNode = newItem.element - - newItemNode.style.height = decorationHeight + 'px' - decorationNode.appendChild(newItemNode) - @decorationItemsById[decorationId] = newItem - -setDimensionsAndBackground = (oldState, newState, domNode) -> - if newState.scrollHeight isnt oldState.scrollHeight - domNode.style.height = newState.scrollHeight + 'px' - oldState.scrollHeight = newState.scrollHeight - - if newState.scrollTop isnt oldState.scrollTop - domNode.style['-webkit-transform'] = "translate3d(0px, #{-newState.scrollTop}px, 0px)" - oldState.scrollTop = newState.scrollTop - - if newState.backgroundColor isnt oldState.backgroundColor - domNode.style.backgroundColor = newState.backgroundColor - oldState.backgroundColor = newState.backgroundColor diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee deleted file mode 100644 index 05935f018..000000000 --- a/src/decoration-manager.coffee +++ /dev/null @@ -1,191 +0,0 @@ -{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) -> - 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 layerId of @decorationCountsByLayerId - layer = @displayLayer.getMarkerLayer(layerId) - for marker in layer.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) -> - if marker.isDestroyed() - error = new Error("Cannot decorate a destroyed marker") - error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()} - if marker.destroyStackTrace? - error.metadata.destroyStackTrace = marker.destroyStackTrace - if marker.bufferMarker?.destroyStackTrace? - error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace - throw error - 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) -> - throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed() - 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] - - didDestroyMarkerDecoration: (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-manager.js b/src/decoration-manager.js new file mode 100644 index 000000000..cf3301fd4 --- /dev/null +++ b/src/decoration-manager.js @@ -0,0 +1,289 @@ +const {Emitter} = require('event-kit') +const Decoration = require('./decoration') +const LayerDecoration = require('./layer-decoration') + +module.exports = +class DecorationManager { + constructor (editor) { + this.editor = editor + this.displayLayer = this.editor.displayLayer + + this.emitter = new Emitter() + this.decorationCountsByLayer = new Map() + this.markerDecorationCountsByLayer = new Map() + this.decorationsByMarker = new Map() + this.layerDecorationsByMarkerLayer = new Map() + this.overlayDecorations = new Set() + this.layerUpdateDisposablesByLayer = new WeakMap() + } + + observeDecorations (callback) { + const decorations = this.getDecorations() + for (let i = 0; i < decorations.length; i++) { + callback(decorations[i]) + } + return this.onDidAddDecoration(callback) + } + + onDidAddDecoration (callback) { + return this.emitter.on('did-add-decoration', callback) + } + + onDidRemoveDecoration (callback) { + return this.emitter.on('did-remove-decoration', callback) + } + + onDidUpdateDecorations (callback) { + return this.emitter.on('did-update-decorations', callback) + } + + getDecorations (propertyFilter) { + let allDecorations = [] + + this.decorationsByMarker.forEach((decorations) => { + decorations.forEach((decoration) => allDecorations.push(decoration)) + }) + if (propertyFilter != null) { + allDecorations = allDecorations.filter(function (decoration) { + for (let key in propertyFilter) { + const value = propertyFilter[key] + if (decoration.properties[key] !== value) return false + } + return true + }) + } + return allDecorations + } + + getLineDecorations (propertyFilter) { + return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line')) + } + + getLineNumberDecorations (propertyFilter) { + return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line-number')) + } + + getHighlightDecorations (propertyFilter) { + return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('highlight')) + } + + getOverlayDecorations (propertyFilter) { + const result = [] + result.push(...Array.from(this.overlayDecorations)) + if (propertyFilter != null) { + return result.filter(function (decoration) { + for (let key in propertyFilter) { + const value = propertyFilter[key] + if (decoration.properties[key] !== value) { + return false + } + } + return true + }) + } else { + return result + } + } + + decorationPropertiesByMarkerForScreenRowRange (startScreenRow, endScreenRow) { + const decorationPropertiesByMarker = new Map() + + this.decorationCountsByLayer.forEach((count, markerLayer) => { + const markers = markerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow - 1]}) + const layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer) + const hasMarkerDecorations = this.markerDecorationCountsByLayer.get(markerLayer) > 0 + + for (let i = 0; i < markers.length; i++) { + const marker = markers[i] + if (!marker.isValid()) continue + + let decorationPropertiesForMarker = decorationPropertiesByMarker.get(marker) + if (decorationPropertiesForMarker == null) { + decorationPropertiesForMarker = [] + decorationPropertiesByMarker.set(marker, decorationPropertiesForMarker) + } + + if (layerDecorations) { + layerDecorations.forEach((layerDecoration) => { + const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties() + decorationPropertiesForMarker.push(properties) + }) + } + + if (hasMarkerDecorations) { + const decorationsForMarker = this.decorationsByMarker.get(marker) + if (decorationsForMarker) { + decorationsForMarker.forEach((decoration) => { + decorationPropertiesForMarker.push(decoration.getProperties()) + }) + } + } + } + }) + + return decorationPropertiesByMarker + } + + decorationsForScreenRowRange (startScreenRow, endScreenRow) { + const decorationsByMarkerId = {} + for (const layer of this.decorationCountsByLayer.keys()) { + for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) { + const decorations = this.decorationsByMarker.get(marker) + if (decorations) { + decorationsByMarkerId[marker.id] = Array.from(decorations) + } + } + } + return decorationsByMarkerId + } + + decorationsStateForScreenRowRange (startScreenRow, endScreenRow) { + const decorationsState = {} + + for (const layer of this.decorationCountsByLayer.keys()) { + for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) { + if (marker.isValid()) { + const screenRange = marker.getScreenRange() + const bufferRange = marker.getBufferRange() + const rangeIsReversed = marker.isReversed() + + const decorations = this.decorationsByMarker.get(marker) + if (decorations) { + decorations.forEach((decoration) => { + decorationsState[decoration.id] = { + properties: decoration.properties, + screenRange, + bufferRange, + rangeIsReversed + } + }) + } + + const layerDecorations = this.layerDecorationsByMarkerLayer.get(layer) + if (layerDecorations) { + layerDecorations.forEach((layerDecoration) => { + const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties() + decorationsState[`${layerDecoration.id}-${marker.id}`] = { + properties, + screenRange, + bufferRange, + rangeIsReversed + } + }) + } + } + } + } + + return decorationsState + } + + decorateMarker (marker, decorationParams) { + if (marker.isDestroyed()) { + const error = new Error('Cannot decorate a destroyed marker') + error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()} + if (marker.destroyStackTrace != null) { + error.metadata.destroyStackTrace = marker.destroyStackTrace + } + if (marker.bufferMarker != null && marker.bufferMarker.destroyStackTrace != null) { + error.metadata.destroyStackTrace = marker.bufferMarker.destroyStackTrace + } + throw error + } + marker = this.displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id) + const decoration = new Decoration(marker, this, decorationParams) + let decorationsForMarker = this.decorationsByMarker.get(marker) + if (!decorationsForMarker) { + decorationsForMarker = new Set() + this.decorationsByMarker.set(marker, decorationsForMarker) + } + decorationsForMarker.add(decoration) + if (decoration.isType('overlay')) this.overlayDecorations.add(decoration) + this.observeDecoratedLayer(marker.layer, true) + this.editor.didAddDecoration(decoration) + this.emitDidUpdateDecorations() + this.emitter.emit('did-add-decoration', decoration) + return decoration + } + + decorateMarkerLayer (markerLayer, decorationParams) { + if (markerLayer.isDestroyed()) { + throw new Error('Cannot decorate a destroyed marker layer') + } + markerLayer = this.displayLayer.getMarkerLayer(markerLayer.id) + const decoration = new LayerDecoration(markerLayer, this, decorationParams) + let layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer) + if (layerDecorations == null) { + layerDecorations = new Set() + this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations) + } + layerDecorations.add(decoration) + this.observeDecoratedLayer(markerLayer, false) + this.emitDidUpdateDecorations() + return decoration + } + + emitDidUpdateDecorations () { + this.editor.scheduleComponentUpdate() + this.emitter.emit('did-update-decorations') + } + + decorationDidChangeType (decoration) { + if (decoration.isType('overlay')) { + this.overlayDecorations.add(decoration) + } else { + this.overlayDecorations.delete(decoration) + } + } + + didDestroyMarkerDecoration (decoration) { + const {marker} = decoration + const decorations = this.decorationsByMarker.get(marker) + if (decorations && decorations.has(decoration)) { + decorations.delete(decoration) + if (decorations.size === 0) this.decorationsByMarker.delete(marker) + this.overlayDecorations.delete(decoration) + this.unobserveDecoratedLayer(marker.layer, true) + this.emitter.emit('did-remove-decoration', decoration) + this.emitDidUpdateDecorations() + } + } + + didDestroyLayerDecoration (decoration) { + const {markerLayer} = decoration + const decorations = this.layerDecorationsByMarkerLayer.get(markerLayer) + + if (decorations && decorations.has(decoration)) { + decorations.delete(decoration) + if (decorations.size === 0) { + this.layerDecorationsByMarkerLayer.delete(markerLayer) + } + this.unobserveDecoratedLayer(markerLayer, true) + this.emitDidUpdateDecorations() + } + } + + observeDecoratedLayer (layer, isMarkerDecoration) { + const newCount = (this.decorationCountsByLayer.get(layer) || 0) + 1 + this.decorationCountsByLayer.set(layer, newCount) + if (newCount === 1) { + this.layerUpdateDisposablesByLayer.set(layer, layer.onDidUpdate(this.emitDidUpdateDecorations.bind(this))) + } + if (isMarkerDecoration) { + this.markerDecorationCountsByLayer.set(layer, (this.markerDecorationCountsByLayer.get(layer) || 0) + 1) + } + } + + unobserveDecoratedLayer (layer, isMarkerDecoration) { + const newCount = this.decorationCountsByLayer.get(layer) - 1 + if (newCount === 0) { + this.layerUpdateDisposablesByLayer.get(layer).dispose() + this.decorationCountsByLayer.delete(layer) + } else { + this.decorationCountsByLayer.set(layer, newCount) + } + if (isMarkerDecoration) { + this.markerDecorationCountsByLayer.set(this.markerDecorationCountsByLayer.get(layer) - 1) + } + } +} diff --git a/src/decoration.coffee b/src/decoration.coffee index 19d029f76..7be15d9f5 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -150,7 +150,7 @@ class Decoration @properties = translateDecorationParamsOldToNew(newProperties) if newProperties.type? @decorationManager.decorationDidChangeType(this) - @decorationManager.scheduleUpdateDecorationsEvent() + @decorationManager.emitDidUpdateDecorations() @emitter.emit 'did-change-properties', {oldProperties, newProperties} ### @@ -171,9 +171,8 @@ class Decoration true flash: (klass, duration=500) -> - @properties.flashCount ?= 0 - @properties.flashCount++ + @properties.flashRequested = true @properties.flashClass = klass @properties.flashDuration = duration - @decorationManager.scheduleUpdateDecorationsEvent() + @decorationManager.emitDidUpdateDecorations() @emitter.emit 'did-flash' diff --git a/src/dom-element-pool.js b/src/dom-element-pool.js deleted file mode 100644 index 0fef02dee..000000000 --- a/src/dom-element-pool.js +++ /dev/null @@ -1,89 +0,0 @@ -module.exports = -class DOMElementPool { - constructor () { - this.managedElements = new Set() - this.freeElementsByTagName = new Map() - this.freedElements = new Set() - } - - clear () { - this.managedElements.clear() - this.freedElements.clear() - this.freeElementsByTagName.clear() - } - - buildElement (tagName, className) { - const elements = this.freeElementsByTagName.get(tagName) - let element = elements ? elements.pop() : null - if (element) { - for (let dataId in element.dataset) { delete element.dataset[dataId] } - element.removeAttribute('style') - if (className) { - element.className = className - } else { - element.removeAttribute('class') - } - while (element.firstChild) { - element.removeChild(element.firstChild) - } - this.freedElements.delete(element) - } else { - element = document.createElement(tagName) - if (className) { - element.className = className - } - this.managedElements.add(element) - } - return element - } - - buildText (textContent) { - const elements = this.freeElementsByTagName.get('#text') - let element = elements ? elements.pop() : null - if (element) { - element.textContent = textContent - this.freedElements.delete(element) - } else { - element = document.createTextNode(textContent) - this.managedElements.add(element) - } - return element - } - - freeElementAndDescendants (element) { - this.free(element) - element.remove() - } - - freeDescendants (element) { - while (element.firstChild) { - this.free(element.firstChild) - element.removeChild(element.firstChild) - } - } - - free (element) { - if (element == null) { throw new Error('The element cannot be null or undefined.') } - if (!this.managedElements.has(element)) return - if (this.freedElements.has(element)) { - atom.assert(false, 'The element has already been freed!', { - content: element instanceof window.Text ? element.textContent : element.outerHTML - }) - return - } - - const tagName = element.nodeName.toLowerCase() - let elements = this.freeElementsByTagName.get(tagName) - if (!elements) { - elements = [] - this.freeElementsByTagName.set(tagName, elements) - } - elements.push(element) - this.freedElements.add(element) - - for (let i = element.childNodes.length - 1; i >= 0; i--) { - const descendant = element.childNodes[i] - this.free(descendant) - } - } -} diff --git a/src/first-mate-helpers.js b/src/first-mate-helpers.js new file mode 100644 index 000000000..0ca312834 --- /dev/null +++ b/src/first-mate-helpers.js @@ -0,0 +1,11 @@ +module.exports = { + fromFirstMateScopeId (firstMateScopeId) { + let atomScopeId = -firstMateScopeId + if ((atomScopeId & 1) === 0) atomScopeId-- + return atomScopeId + 256 + }, + + toFirstMateScopeId (atomScopeId) { + return -(atomScopeId - 256) + } +} diff --git a/src/gutter-container-component.coffee b/src/gutter-container-component.coffee deleted file mode 100644 index ebb2d8597..000000000 --- a/src/gutter-container-component.coffee +++ /dev/null @@ -1,112 +0,0 @@ -_ = require 'underscore-plus' -CustomGutterComponent = require './custom-gutter-component' -LineNumberGutterComponent = require './line-number-gutter-component' - -# The GutterContainerComponent manages the GutterComponents of a particular -# TextEditorComponent. - -module.exports = -class GutterContainerComponent - constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool, @views}) -> - # An array of objects of the form: {name: {String}, component: {Object}} - @gutterComponents = [] - @gutterComponentsByGutterName = {} - @lineNumberGutterComponent = null - - @domNode = document.createElement('div') - @domNode.classList.add('gutter-container') - @domNode.style.display = 'flex' - - destroy: -> - for {component} in @gutterComponents - component.destroy?() - return - - getDomNode: -> - @domNode - - getLineNumberGutterComponent: -> - @lineNumberGutterComponent - - updateSync: (state) -> - # The GutterContainerComponent expects the gutters to be sorted in the order - # they should appear. - newState = state.gutters - - newGutterComponents = [] - newGutterComponentsByGutterName = {} - for {gutter, visible, styles, content} in newState - gutterComponent = @gutterComponentsByGutterName[gutter.name] - if not gutterComponent - if gutter.name is 'line-number' - gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool, @views}) - @lineNumberGutterComponent = gutterComponent - else - gutterComponent = new CustomGutterComponent({gutter, @views}) - - if visible then gutterComponent.showNode() else gutterComponent.hideNode() - # Pass the gutter only the state that it needs. - if gutter.name is 'line-number' - # For ease of use in the line number gutter component, set the shared - # 'styles' as a field under the 'content'. - gutterSubstate = _.clone(content) - gutterSubstate.styles = styles - else - # Custom gutter 'content' is keyed on gutter name, so we cannot set - # 'styles' as a subfield directly under it. - gutterSubstate = {content, styles} - gutterComponent.updateSync(gutterSubstate) - - newGutterComponents.push({ - name: gutter.name, - component: gutterComponent, - }) - newGutterComponentsByGutterName[gutter.name] = gutterComponent - - @reorderGutters(newGutterComponents, newGutterComponentsByGutterName) - - @gutterComponents = newGutterComponents - @gutterComponentsByGutterName = newGutterComponentsByGutterName - - ### - Section: Private Methods - ### - - reorderGutters: (newGutterComponents, newGutterComponentsByGutterName) -> - # First, insert new gutters into the DOM. - indexInOldGutters = 0 - oldGuttersLength = @gutterComponents.length - - for gutterComponentDescription in newGutterComponents - gutterComponent = gutterComponentDescription.component - gutterName = gutterComponentDescription.name - - if @gutterComponentsByGutterName[gutterName] - # If the gutter existed previously, we first try to move the cursor to - # the point at which it occurs in the previous gutters. - matchingGutterFound = false - while indexInOldGutters < oldGuttersLength - existingGutterComponentDescription = @gutterComponents[indexInOldGutters] - existingGutterComponent = existingGutterComponentDescription.component - indexInOldGutters++ - if existingGutterComponent is gutterComponent - matchingGutterFound = true - break - if not matchingGutterFound - # If we've reached this point, the gutter previously existed, but its - # position has moved. Remove it from the DOM and re-insert it. - gutterComponent.getDomNode().remove() - @domNode.appendChild(gutterComponent.getDomNode()) - - else - if indexInOldGutters is oldGuttersLength - @domNode.appendChild(gutterComponent.getDomNode()) - else - @domNode.insertBefore(gutterComponent.getDomNode(), @domNode.children[indexInOldGutters]) - indexInOldGutters += 1 - - # Remove any gutters that were not present in the new gutters state. - for gutterComponentDescription in @gutterComponents - if not newGutterComponentsByGutterName[gutterComponentDescription.name] - gutterComponent = gutterComponentDescription.component - gutterComponent.getDomNode().remove() diff --git a/src/gutter-container.coffee b/src/gutter-container.coffee index 084e1e1ad..677fa4521 100644 --- a/src/gutter-container.coffee +++ b/src/gutter-container.coffee @@ -8,6 +8,9 @@ class GutterContainer @textEditor = textEditor @emitter = new Emitter + scheduleComponentUpdate: -> + @textEditor.scheduleComponentUpdate() + destroy: -> # Create a copy, because `Gutter::destroy` removes the gutter from # GutterContainer's @gutters. @@ -36,6 +39,7 @@ class GutterContainer break if not inserted @gutters.push newGutter + @scheduleComponentUpdate() @emitter.emit 'did-add-gutter', newGutter return newGutter @@ -67,6 +71,7 @@ class GutterContainer index = @gutters.indexOf(gutter) if index > -1 @gutters.splice(index, 1) + @scheduleComponentUpdate() @emitter.emit 'did-remove-gutter', gutter.name else throw new Error 'The given gutter cannot be removed because it is not ' + diff --git a/src/gutter.coffee b/src/gutter.coffee index 64535efa4..6b39398dd 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -1,4 +1,5 @@ {Emitter} = require 'event-kit' +CustomGutterComponent = null DefaultPriority = -100 @@ -28,19 +29,6 @@ class Gutter @emitter.emit 'did-destroy' @emitter.dispose() - getElement: -> - unless @element? - @element = document.createElement('div') - @element.classList.add('gutter') - @element.setAttribute('gutter-name', @name) - childNode = document.createElement('div') - if @name is 'line-number' - childNode.classList.add('line-numbers') - else - childNode.classList.add('custom-decorations') - @element.appendChild(childNode) - @element - ### Section: Event Subscription ### @@ -70,12 +58,14 @@ class Gutter hide: -> if @visible @visible = false + @gutterContainer.scheduleComponentUpdate() @emitter.emit 'did-change-visible', this # Essential: Show the gutter. show: -> if not @visible @visible = true + @gutterContainer.scheduleComponentUpdate() @emitter.emit 'did-change-visible', this # Essential: Determine whether the gutter is visible. @@ -100,3 +90,6 @@ class Gutter # Returns a {Decoration} object decorateMarker: (marker, options) -> @gutterContainer.addGutterDecoration(this, marker, options) + + getElement: -> + @element ?= document.createElement('div') diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee deleted file mode 100644 index e5c1db60e..000000000 --- a/src/highlights-component.coffee +++ /dev/null @@ -1,119 +0,0 @@ -RegionStyleProperties = ['top', 'left', 'right', 'width', 'height'] -SpaceRegex = /\s+/ - -module.exports = -class HighlightsComponent - oldState: null - - constructor: (@domElementPool) -> - @highlightNodesById = {} - @regionNodesByHighlightId = {} - - @domNode = @domElementPool.buildElement("div", "highlights") - - getDomNode: -> - @domNode - - updateSync: (state) -> - newState = state.highlights - @oldState ?= {} - - # remove highlights - for id of @oldState - unless newState[id]? - @domElementPool.freeElementAndDescendants(@highlightNodesById[id]) - delete @highlightNodesById[id] - delete @regionNodesByHighlightId[id] - delete @oldState[id] - - # add or update highlights - for id, highlightState of newState - unless @oldState[id]? - highlightNode = @domElementPool.buildElement("div", "highlight") - @highlightNodesById[id] = highlightNode - @regionNodesByHighlightId[id] = {} - @domNode.appendChild(highlightNode) - @updateHighlightNode(id, highlightState) - - return - - updateHighlightNode: (id, newHighlightState) -> - highlightNode = @highlightNodesById[id] - oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0}) - - # update class - if newHighlightState.class isnt oldHighlightState.class - if oldHighlightState.class? - if SpaceRegex.test(oldHighlightState.class) - highlightNode.classList.remove(oldHighlightState.class.split(SpaceRegex)...) - else - highlightNode.classList.remove(oldHighlightState.class) - - if SpaceRegex.test(newHighlightState.class) - highlightNode.classList.add(newHighlightState.class.split(SpaceRegex)...) - else - highlightNode.classList.add(newHighlightState.class) - - oldHighlightState.class = newHighlightState.class - - @updateHighlightRegions(id, newHighlightState) - @flashHighlightNodeIfRequested(id, newHighlightState) - - updateHighlightRegions: (id, newHighlightState) -> - oldHighlightState = @oldState[id] - highlightNode = @highlightNodesById[id] - - # remove regions - while oldHighlightState.regions.length > newHighlightState.regions.length - oldHighlightState.regions.pop() - @domElementPool.freeElementAndDescendants(@regionNodesByHighlightId[id][oldHighlightState.regions.length]) - delete @regionNodesByHighlightId[id][oldHighlightState.regions.length] - - # add or update regions - for newRegionState, i in newHighlightState.regions - unless oldHighlightState.regions[i]? - oldHighlightState.regions[i] = {} - regionNode = @domElementPool.buildElement("div", "region") - # This prevents highlights at the tiles boundaries to be hidden by the - # subsequent tile. When this happens, subpixel anti-aliasing gets - # disabled. - regionNode.style.boxSizing = "border-box" - regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass? - @regionNodesByHighlightId[id][i] = regionNode - highlightNode.appendChild(regionNode) - - oldRegionState = oldHighlightState.regions[i] - regionNode = @regionNodesByHighlightId[id][i] - - for property in RegionStyleProperties - if newRegionState[property] isnt oldRegionState[property] - oldRegionState[property] = newRegionState[property] - if newRegionState[property]? - regionNode.style[property] = newRegionState[property] + 'px' - else - regionNode.style[property] = '' - - return - - flashHighlightNodeIfRequested: (id, newHighlightState) -> - oldHighlightState = @oldState[id] - if newHighlightState.needsFlash and oldHighlightState.flashCount isnt newHighlightState.flashCount - highlightNode = @highlightNodesById[id] - - addFlashClass = => - highlightNode.classList.add(newHighlightState.flashClass) - oldHighlightState.flashClass = newHighlightState.flashClass - @flashTimeoutId = setTimeout(removeFlashClass, newHighlightState.flashDuration) - - removeFlashClass = => - highlightNode.classList.remove(oldHighlightState.flashClass) - oldHighlightState.flashClass = null - clearTimeout(@flashTimeoutId) - - if oldHighlightState.flashClass? - removeFlashClass() - requestAnimationFrame(addFlashClass) - else - addFlashClass() - - oldHighlightState.flashCount = newHighlightState.flashCount diff --git a/src/history-manager.js b/src/history-manager.js index cd151e660..a8ddbaae9 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -17,10 +17,6 @@ export class HistoryManager { this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))) } - initialize (localStorage) { - this.localStorage = localStorage - } - destroy () { this.disposables.dispose() } @@ -98,10 +94,6 @@ export class HistoryManager { async loadState () { let history = await this.stateStore.load('history-manager') - if (!history) { - history = JSON.parse(this.localStorage.getItem('history')) - } - if (history && history.projects) { this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) this.didChangeProjects({reloaded: true}) diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index 307fd6430..0c4c0a391 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -59,6 +59,7 @@ if global.isGeneratingSnapshot clipboard = new Clipboard TextEditor.setClipboard(clipboard) +TextEditor.viewForItem = (item) -> atom.views.getView(item) global.atom = new AtomEnvironment({ clipboard, diff --git a/src/initialize-benchmark-window.js b/src/initialize-benchmark-window.js index a8f1aafe6..7ba99c468 100644 --- a/src/initialize-benchmark-window.js +++ b/src/initialize-benchmark-window.js @@ -54,6 +54,7 @@ export default async function () { const clipboard = new Clipboard() TextEditor.setClipboard(clipboard) + TextEditor.viewForItem = (item) => atom.views.getView(item) const applicationDelegate = new ApplicationDelegate() const environmentParams = { diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index e87586374..5ad10670a 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -70,6 +70,7 @@ module.exports = ({blobStore}) -> clipboard = new Clipboard TextEditor.setClipboard(clipboard) + TextEditor.viewForItem = (item) -> atom.views.getView(item) testRunner = require(testRunnerPath) legacyTestRunner = require(legacyTestRunnerPath) diff --git a/src/input-component.coffee b/src/input-component.coffee deleted file mode 100644 index 27543a2fd..000000000 --- a/src/input-component.coffee +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = -class InputComponent - constructor: (@domNode) -> - - updateSync: (state) -> - @oldState ?= {} - newState = state.hiddenInput - - if newState.top isnt @oldState.top - @domNode.style.top = newState.top + 'px' - @oldState.top = newState.top - - if newState.left isnt @oldState.left - @domNode.style.left = newState.left + 'px' - @oldState.left = newState.left - - if newState.width isnt @oldState.width - @domNode.style.width = newState.width + 'px' - @oldState.width = newState.width - - if newState.height isnt @oldState.height - @domNode.style.height = newState.height + 'px' - @oldState.height = newState.height diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 06990bad5..1839f1c59 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -189,7 +189,7 @@ class LanguageMode # row is a comment. isLineCommentedAtBufferRow: (bufferRow) -> return false unless 0 <= bufferRow <= @editor.getLastBufferRow() - @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() + @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() ? false # 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 diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee index fb544948f..e20921b4d 100644 --- a/src/layer-decoration.coffee +++ b/src/layer-decoration.coffee @@ -9,7 +9,7 @@ class LayerDecoration @id = nextId() @destroyed = false @markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy() - @overridePropertiesByMarkerId = {} + @overridePropertiesByMarker = null # Essential: Destroys the decoration. destroy: -> @@ -42,7 +42,7 @@ class LayerDecoration setProperties: (newProperties) -> return if @destroyed @properties = newProperties - @decorationManager.scheduleUpdateDecorationsEvent() + @decorationManager.emitDidUpdateDecorations() # Essential: Override the decoration properties for a specific marker. # @@ -52,8 +52,13 @@ class LayerDecoration # Pass `null` to clear the override. setPropertiesForMarker: (marker, properties) -> return if @destroyed + @overridePropertiesByMarker ?= new Map() + marker = @markerLayer.getMarker(marker.id) if properties? - @overridePropertiesByMarkerId[marker.id] = properties + @overridePropertiesByMarker.set(marker, properties) else - delete @overridePropertiesByMarkerId[marker.id] - @decorationManager.scheduleUpdateDecorationsEvent() + @overridePropertiesByMarker.delete(marker) + @decorationManager.emitDidUpdateDecorations() + + getPropertiesForMarker: (marker) -> + @overridePropertiesByMarker?.get(marker) diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee deleted file mode 100644 index f0f150ff2..000000000 --- a/src/line-number-gutter-component.coffee +++ /dev/null @@ -1,99 +0,0 @@ -TiledComponent = require './tiled-component' -LineNumbersTileComponent = require './line-numbers-tile-component' - -module.exports = -class LineNumberGutterComponent extends TiledComponent - dummyLineNumberNode: null - - constructor: ({@onMouseDown, @editor, @gutter, @domElementPool, @views}) -> - @visible = true - - @dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool) - - @domNode = @gutter.getElement() - @lineNumbersNode = @domNode.firstChild - @lineNumbersNode.innerHTML = '' - - @domNode.addEventListener 'click', @onClick - @domNode.addEventListener 'mousedown', @onMouseDown - - destroy: -> - @domNode.removeEventListener 'click', @onClick - @domNode.removeEventListener 'mousedown', @onMouseDown - - getDomNode: -> - @domNode - - hideNode: -> - if @visible - @domNode.style.display = 'none' - @visible = false - - showNode: -> - if not @visible - @domNode.style.removeProperty('display') - @visible = true - - buildEmptyState: -> - { - tiles: {} - styles: {} - } - - getNewState: (state) -> state - - getTilesNode: -> @lineNumbersNode - - beforeUpdateSync: (state) -> - @appendDummyLineNumber() unless @dummyLineNumberNode? - - if @newState.styles.maxHeight isnt @oldState.styles.maxHeight - @lineNumbersNode.style.height = @newState.styles.maxHeight + 'px' - @oldState.maxHeight = @newState.maxHeight - - if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor - @lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor - @oldState.styles.backgroundColor = @newState.styles.backgroundColor - - if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits - @updateDummyLineNumber() - @oldState.styles = {} - @oldState.maxLineNumberDigits = @newState.maxLineNumberDigits - - buildComponentForTile: (id) -> new LineNumbersTileComponent({id, @domElementPool}) - - shouldRecreateAllTilesOnUpdate: -> - @newState.continuousReflow - - ### - Section: Private Methods - ### - - # This dummy line number element holds the gutter to the appropriate width, - # since the real line numbers are absolutely positioned for performance reasons. - appendDummyLineNumber: -> - @dummyLineNumberComponent.newState = @newState - @dummyLineNumberNode = @dummyLineNumberComponent.buildLineNumberNode({bufferRow: -1}) - @lineNumbersNode.appendChild(@dummyLineNumberNode) - - updateDummyLineNumber: -> - @dummyLineNumberComponent.newState = @newState - @dummyLineNumberComponent.setLineNumberInnerNodes(0, false, @dummyLineNumberNode) - - onMouseDown: (event) => - {target} = event - lineNumber = target.parentNode - - unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') - @onMouseDown(event) - - onClick: (event) => - {target} = event - lineNumber = target.parentNode - - if target.classList.contains('icon-right') - bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row')) - if lineNumber.classList.contains('folded') - @editor.unfoldBufferRow(bufferRow) - else if lineNumber.classList.contains('foldable') - @editor.foldBufferRow(bufferRow) diff --git a/src/line-numbers-tile-component.coffee b/src/line-numbers-tile-component.coffee deleted file mode 100644 index d552e2056..000000000 --- a/src/line-numbers-tile-component.coffee +++ /dev/null @@ -1,157 +0,0 @@ -_ = require 'underscore-plus' - -module.exports = -class LineNumbersTileComponent - @createDummy: (domElementPool) -> - new LineNumbersTileComponent({id: -1, domElementPool}) - - constructor: ({@id, @domElementPool}) -> - @lineNumberNodesById = {} - @domNode = @domElementPool.buildElement("div") - @domNode.style.position = "absolute" - @domNode.style.display = "block" - @domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber - - destroy: -> - @domElementPool.freeElementAndDescendants(@domNode) - - getDomNode: -> - @domNode - - updateSync: (state) -> - @newState = state - unless @oldState - @oldState = {tiles: {}, styles: {}} - @oldState.tiles[@id] = {lineNumbers: {}} - - @newTileState = @newState.tiles[@id] - @oldTileState = @oldState.tiles[@id] - - if @newTileState.display isnt @oldTileState.display - @domNode.style.display = @newTileState.display - @oldTileState.display = @newTileState.display - - if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor - @domNode.style.backgroundColor = @newState.styles.backgroundColor - @oldState.styles.backgroundColor = @newState.styles.backgroundColor - - if @newTileState.height isnt @oldTileState.height - @domNode.style.height = @newTileState.height + 'px' - @oldTileState.height = @newTileState.height - - if @newTileState.top isnt @oldTileState.top - @domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)" - @oldTileState.top = @newTileState.top - - if @newTileState.zIndex isnt @oldTileState.zIndex - @domNode.style.zIndex = @newTileState.zIndex - @oldTileState.zIndex = @newTileState.zIndex - - if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits - for id, node of @lineNumberNodesById - @domElementPool.freeElementAndDescendants(node) - - @oldState.tiles[@id] = {lineNumbers: {}} - @oldTileState = @oldState.tiles[@id] - @lineNumberNodesById = {} - @oldState.maxLineNumberDigits = @newState.maxLineNumberDigits - - @updateLineNumbers() - - updateLineNumbers: -> - newLineNumberIds = null - newLineNumberNodes = null - - for id, lineNumberState of @oldTileState.lineNumbers - unless @newTileState.lineNumbers.hasOwnProperty(id) - @domElementPool.freeElementAndDescendants(@lineNumberNodesById[id]) - delete @lineNumberNodesById[id] - delete @oldTileState.lineNumbers[id] - - for id, lineNumberState of @newTileState.lineNumbers - if @oldTileState.lineNumbers.hasOwnProperty(id) - @updateLineNumberNode(id, lineNumberState) - else - newLineNumberIds ?= [] - newLineNumberNodes ?= [] - newLineNumberIds.push(id) - newLineNumberNodes.push(@buildLineNumberNode(lineNumberState)) - @oldTileState.lineNumbers[id] = _.clone(lineNumberState) - - return unless newLineNumberIds? - - for id, i in newLineNumberIds - lineNumberNode = newLineNumberNodes[i] - @lineNumberNodesById[id] = lineNumberNode - if nextNode = @findNodeNextTo(lineNumberNode) - @domNode.insertBefore(lineNumberNode, nextNode) - else - @domNode.appendChild(lineNumberNode) - - findNodeNextTo: (node) -> - for nextNode in @domNode.children - return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode) - return - - screenRowForNode: (node) -> parseInt(node.dataset.screenRow) - - buildLineNumberNode: (lineNumberState) -> - {screenRow, bufferRow, softWrapped, blockDecorationsHeight} = lineNumberState - - className = @buildLineNumberClassName(lineNumberState) - lineNumberNode = @domElementPool.buildElement("div", className) - lineNumberNode.dataset.screenRow = screenRow - lineNumberNode.dataset.bufferRow = bufferRow - lineNumberNode.style.marginTop = blockDecorationsHeight + "px" - - @setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode) - lineNumberNode - - setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) -> - @domElementPool.freeDescendants(lineNumberNode) - - {maxLineNumberDigits} = @newState - - if softWrapped - lineNumber = "•" - else - lineNumber = (bufferRow + 1).toString() - padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length) - - textNode = @domElementPool.buildText(padding + lineNumber) - iconRight = @domElementPool.buildElement("div", "icon-right") - - lineNumberNode.appendChild(textNode) - lineNumberNode.appendChild(iconRight) - - updateLineNumberNode: (lineNumberId, newLineNumberState) -> - oldLineNumberState = @oldTileState.lineNumbers[lineNumberId] - node = @lineNumberNodesById[lineNumberId] - - unless oldLineNumberState.foldable is newLineNumberState.foldable and _.isEqual(oldLineNumberState.decorationClasses, newLineNumberState.decorationClasses) - node.className = @buildLineNumberClassName(newLineNumberState) - oldLineNumberState.foldable = newLineNumberState.foldable - oldLineNumberState.decorationClasses = _.clone(newLineNumberState.decorationClasses) - - unless oldLineNumberState.screenRow is newLineNumberState.screenRow and oldLineNumberState.bufferRow is newLineNumberState.bufferRow - @setLineNumberInnerNodes(newLineNumberState.bufferRow, newLineNumberState.softWrapped, node) - node.dataset.screenRow = newLineNumberState.screenRow - node.dataset.bufferRow = newLineNumberState.bufferRow - oldLineNumberState.screenRow = newLineNumberState.screenRow - oldLineNumberState.bufferRow = newLineNumberState.bufferRow - - unless oldLineNumberState.blockDecorationsHeight is newLineNumberState.blockDecorationsHeight - node.style.marginTop = newLineNumberState.blockDecorationsHeight + "px" - oldLineNumberState.blockDecorationsHeight = newLineNumberState.blockDecorationsHeight - - buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) -> - className = "line-number" - className += " " + decorationClasses.join(' ') if decorationClasses? - className += " foldable" if foldable and not softWrapped - className - - lineNumberNodeForScreenRow: (screenRow) -> - for id, lineNumberState of @oldTileState.lineNumbers - if lineNumberState.screenRow is screenRow - return @lineNumberNodesById[id] - null diff --git a/src/lines-component.coffee b/src/lines-component.coffee deleted file mode 100644 index 02d396021..000000000 --- a/src/lines-component.coffee +++ /dev/null @@ -1,109 +0,0 @@ -CursorsComponent = require './cursors-component' -LinesTileComponent = require './lines-tile-component' -TiledComponent = require './tiled-component' - -module.exports = -class LinesComponent extends TiledComponent - placeholderTextDiv: null - - constructor: ({@views, @presenter, @domElementPool, @assert}) -> - @DummyLineNode = document.createElement('div') - @DummyLineNode.className = 'line' - @DummyLineNode.style.position = 'absolute' - @DummyLineNode.style.visibility = 'hidden' - @DummyLineNode.appendChild(document.createElement('span')) - @DummyLineNode.appendChild(document.createElement('span')) - @DummyLineNode.appendChild(document.createElement('span')) - @DummyLineNode.appendChild(document.createElement('span')) - @DummyLineNode.children[0].textContent = 'x' - @DummyLineNode.children[1].textContent = '我' - @DummyLineNode.children[2].textContent = 'ハ' - @DummyLineNode.children[3].textContent = '세' - - @domNode = document.createElement('div') - @domNode.classList.add('lines') - @tilesNode = document.createElement("div") - # Create a new stacking context, so that tiles z-index does not interfere - # with other visual elements. - @tilesNode.style.isolation = "isolate" - @tilesNode.style.zIndex = 0 - @domNode.appendChild(@tilesNode) - - @cursorsComponent = new CursorsComponent - @domNode.appendChild(@cursorsComponent.getDomNode()) - - getDomNode: -> - @domNode - - shouldRecreateAllTilesOnUpdate: -> - @newState.continuousReflow - - beforeUpdateSync: (state) -> - if @newState.maxHeight isnt @oldState.maxHeight - @domNode.style.height = @newState.maxHeight + 'px' - @oldState.maxHeight = @newState.maxHeight - - if @newState.backgroundColor isnt @oldState.backgroundColor - @domNode.style.backgroundColor = @newState.backgroundColor - @oldState.backgroundColor = @newState.backgroundColor - - afterUpdateSync: (state) -> - if @newState.placeholderText isnt @oldState.placeholderText - @placeholderTextDiv?.remove() - if @newState.placeholderText? - @placeholderTextDiv = document.createElement('div') - @placeholderTextDiv.classList.add('placeholder-text') - @placeholderTextDiv.textContent = @newState.placeholderText - @domNode.appendChild(@placeholderTextDiv) - @oldState.placeholderText = @newState.placeholderText - - # Removing and updating block decorations needs to be done in two different - # steps, so that the same decoration node can be moved from one tile to - # another in the same animation frame. - for component in @getComponents() - component.removeDeletedBlockDecorations() - for component in @getComponents() - component.updateBlockDecorations() - - @cursorsComponent.updateSync(state) - - buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @views}) - - buildEmptyState: -> - {tiles: {}} - - getNewState: (state) -> - state.content - - getTilesNode: -> @tilesNode - - measureLineHeightAndDefaultCharWidth: -> - @domNode.appendChild(@DummyLineNode) - - lineHeightInPixels = @DummyLineNode.getBoundingClientRect().height - defaultCharWidth = @DummyLineNode.children[0].getBoundingClientRect().width - doubleWidthCharWidth = @DummyLineNode.children[1].getBoundingClientRect().width - halfWidthCharWidth = @DummyLineNode.children[2].getBoundingClientRect().width - koreanCharWidth = @DummyLineNode.children[3].getBoundingClientRect().width - - @domNode.removeChild(@DummyLineNode) - - @presenter.setLineHeight(lineHeightInPixels) - @presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) - - measureBlockDecorations: -> - for component in @getComponents() - component.measureBlockDecorations() - return - - lineIdForScreenRow: (screenRow) -> - tile = @presenter.tileForRow(screenRow) - @getComponentForTile(tile)?.lineIdForScreenRow(screenRow) - - lineNodeForScreenRow: (screenRow) -> - tile = @presenter.tileForRow(screenRow) - @getComponentForTile(tile)?.lineNodeForScreenRow(screenRow) - - textNodesForScreenRow: (screenRow) -> - tile = @presenter.tileForRow(screenRow) - @getComponentForTile(tile)?.textNodesForScreenRow(screenRow) diff --git a/src/lines-tile-component.js b/src/lines-tile-component.js deleted file mode 100644 index 202e16708..000000000 --- a/src/lines-tile-component.js +++ /dev/null @@ -1,401 +0,0 @@ -const HighlightsComponent = require('./highlights-component') -const ZERO_WIDTH_NBSP = '\ufeff' - -module.exports = class LinesTileComponent { - constructor ({presenter, id, domElementPool, assert, views}) { - this.id = id - this.presenter = presenter - this.views = views - this.domElementPool = domElementPool - this.assert = assert - this.lineNodesByLineId = {} - this.screenRowsByLineId = {} - this.lineIdsByScreenRow = {} - this.textNodesByLineId = {} - this.blockDecorationNodesByLineIdAndDecorationId = {} - this.domNode = this.domElementPool.buildElement('div') - this.domNode.style.position = 'absolute' - this.domNode.style.display = 'block' - this.highlightsComponent = new HighlightsComponent(this.domElementPool) - this.domNode.appendChild(this.highlightsComponent.getDomNode()) - } - - destroy () { - this.removeLineNodes() - this.domElementPool.freeElementAndDescendants(this.domNode) - } - - getDomNode () { - return this.domNode - } - - updateSync (state) { - this.newState = state - if (this.oldState == null) { - this.oldState = {tiles: {}} - this.oldState.tiles[this.id] = {lines: {}} - } - - this.newTileState = this.newState.tiles[this.id] - this.oldTileState = this.oldState.tiles[this.id] - - if (this.newState.backgroundColor !== this.oldState.backgroundColor) { - this.domNode.style.backgroundColor = this.newState.backgroundColor - this.oldState.backgroundColor = this.newState.backgroundColor - } - - if (this.newTileState.zIndex !== this.oldTileState.zIndex) { - this.domNode.style.zIndex = this.newTileState.zIndex - this.oldTileState.zIndex = this.newTileState.zIndex - } - - if (this.newTileState.display !== this.oldTileState.display) { - this.domNode.style.display = this.newTileState.display - this.oldTileState.display = this.newTileState.display - } - - if (this.newTileState.height !== this.oldTileState.height) { - this.domNode.style.height = this.newTileState.height + 'px' - this.oldTileState.height = this.newTileState.height - } - - if (this.newState.width !== this.oldState.width) { - this.domNode.style.width = this.newState.width + 'px' - this.oldState.width = this.newState.width - } - - if (this.newTileState.top !== this.oldTileState.top || this.newTileState.left !== this.oldTileState.left) { - this.domNode.style.transform = `translate3d(${this.newTileState.left}px, ${this.newTileState.top}px, 0px)` - this.oldTileState.top = this.newTileState.top - this.oldTileState.left = this.newTileState.left - } - - this.updateLineNodes() - this.highlightsComponent.updateSync(this.newTileState) - } - - removeLineNodes () { - for (const id of Object.keys(this.oldTileState.lines)) { - this.removeLineNode(id) - } - } - - removeLineNode (lineId) { - this.domElementPool.freeElementAndDescendants(this.lineNodesByLineId[lineId]) - for (const decorationId of Object.keys(this.oldTileState.lines[lineId].precedingBlockDecorations)) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - topRulerNode.remove() - blockDecorationNode.remove() - bottomRulerNode.remove() - } - for (const decorationId of Object.keys(this.oldTileState.lines[lineId].followingBlockDecorations)) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - topRulerNode.remove() - blockDecorationNode.remove() - bottomRulerNode.remove() - } - - delete this.blockDecorationNodesByLineIdAndDecorationId[lineId] - delete this.lineNodesByLineId[lineId] - delete this.textNodesByLineId[lineId] - delete this.lineIdsByScreenRow[this.screenRowsByLineId[lineId]] - delete this.screenRowsByLineId[lineId] - delete this.oldTileState.lines[lineId] - } - - updateLineNodes () { - for (const id of Object.keys(this.oldTileState.lines)) { - if (!this.newTileState.lines.hasOwnProperty(id)) { - this.removeLineNode(id) - } - } - - const newLineIds = [] - const newLineNodes = [] - for (const id of Object.keys(this.newTileState.lines)) { - const lineState = this.newTileState.lines[id] - if (this.oldTileState.lines.hasOwnProperty(id)) { - this.updateLineNode(id) - } else { - newLineIds.push(id) - newLineNodes.push(this.buildLineNode(id)) - this.screenRowsByLineId[id] = lineState.screenRow - this.lineIdsByScreenRow[lineState.screenRow] = id - this.oldTileState.lines[id] = Object.assign({}, lineState) - // Avoid assigning state for block decorations, because we need to - // process it later when updating the DOM. - this.oldTileState.lines[id].precedingBlockDecorations = {} - this.oldTileState.lines[id].followingBlockDecorations = {} - } - } - - while (newLineIds.length > 0) { - const id = newLineIds.shift() - const lineNode = newLineNodes.shift() - this.lineNodesByLineId[id] = lineNode - const nextNode = this.findNodeNextTo(lineNode) - if (nextNode == null) { - this.domNode.appendChild(lineNode) - } else { - this.domNode.insertBefore(lineNode, nextNode) - } - } - } - - findNodeNextTo (node) { - let i = 1 // skip highlights node - while (i < this.domNode.children.length) { - const nextNode = this.domNode.children[i] - if (this.screenRowForNode(node) < this.screenRowForNode(nextNode)) { - return nextNode - } - i++ - } - return null - } - - screenRowForNode (node) { - return parseInt(node.dataset.screenRow) - } - - buildLineNode (id) { - const {lineText, tagCodes, screenRow, decorationClasses} = this.newTileState.lines[id] - - const lineNode = this.domElementPool.buildElement('div', 'line') - lineNode.dataset.screenRow = screenRow - if (decorationClasses != null) { - for (const decorationClass of decorationClasses) { - lineNode.classList.add(decorationClass) - } - } - - const textNodes = [] - let startIndex = 0 - let openScopeNode = lineNode - for (const tagCode of tagCodes) { - if (tagCode !== 0) { - if (this.presenter.isCloseTagCode(tagCode)) { - openScopeNode = openScopeNode.parentElement - } else if (this.presenter.isOpenTagCode(tagCode)) { - const scope = this.presenter.tagForCode(tagCode) - const newScopeNode = this.domElementPool.buildElement('span', scope.replace(/\.+/g, ' ')) - openScopeNode.appendChild(newScopeNode) - openScopeNode = newScopeNode - } else { - const textNode = this.domElementPool.buildText(lineText.substr(startIndex, tagCode)) - startIndex += tagCode - openScopeNode.appendChild(textNode) - textNodes.push(textNode) - } - } - } - - if (startIndex === 0) { - const textNode = this.domElementPool.buildText(' ') - lineNode.appendChild(textNode) - textNodes.push(textNode) - } - - if (lineText.endsWith(this.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. - const textNode = this.domElementPool.buildText(ZERO_WIDTH_NBSP) - lineNode.appendChild(textNode) - textNodes.push(textNode) - } - - this.textNodesByLineId[id] = textNodes - return lineNode - } - - updateLineNode (id) { - const oldLineState = this.oldTileState.lines[id] - const newLineState = this.newTileState.lines[id] - const lineNode = this.lineNodesByLineId[id] - const newDecorationClasses = newLineState.decorationClasses - const oldDecorationClasses = oldLineState.decorationClasses - - if (oldDecorationClasses != null) { - for (const decorationClass of oldDecorationClasses) { - if (newDecorationClasses == null || !newDecorationClasses.includes(decorationClass)) { - lineNode.classList.remove(decorationClass) - } - } - } - - if (newDecorationClasses != null) { - for (const decorationClass of newDecorationClasses) { - if (oldDecorationClasses == null || !oldDecorationClasses.includes(decorationClass)) { - lineNode.classList.add(decorationClass) - } - } - } - - oldLineState.decorationClasses = newLineState.decorationClasses - - if (newLineState.screenRow !== oldLineState.screenRow) { - lineNode.dataset.screenRow = newLineState.screenRow - this.lineIdsByScreenRow[newLineState.screenRow] = id - this.screenRowsByLineId[id] = newLineState.screenRow - } - - oldLineState.screenRow = newLineState.screenRow - } - - removeDeletedBlockDecorations () { - for (const lineId of Object.keys(this.newTileState.lines)) { - const oldLineState = this.oldTileState.lines[lineId] - const newLineState = this.newTileState.lines[lineId] - for (const decorationId of Object.keys(oldLineState.precedingBlockDecorations)) { - if (!newLineState.precedingBlockDecorations.hasOwnProperty(decorationId)) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - topRulerNode.remove() - blockDecorationNode.remove() - bottomRulerNode.remove() - delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - delete oldLineState.precedingBlockDecorations[decorationId] - } - } - for (const decorationId of Object.keys(oldLineState.followingBlockDecorations)) { - if (!newLineState.followingBlockDecorations.hasOwnProperty(decorationId)) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - topRulerNode.remove() - blockDecorationNode.remove() - bottomRulerNode.remove() - delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - delete oldLineState.followingBlockDecorations[decorationId] - } - } - } - } - - updateBlockDecorations () { - for (const lineId of Object.keys(this.newTileState.lines)) { - const oldLineState = this.oldTileState.lines[lineId] - const newLineState = this.newTileState.lines[lineId] - const lineNode = this.lineNodesByLineId[lineId] - if (!this.blockDecorationNodesByLineIdAndDecorationId.hasOwnProperty(lineId)) { - this.blockDecorationNodesByLineIdAndDecorationId[lineId] = {} - } - for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) { - const oldBlockDecorationState = oldLineState.precedingBlockDecorations[decorationId] - const newBlockDecorationState = newLineState.precedingBlockDecorations[decorationId] - if (oldBlockDecorationState != null) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) { - topRulerNode.remove() - blockDecorationNode.remove() - bottomRulerNode.remove() - topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(topRulerNode, lineNode) - blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(blockDecorationNode, lineNode) - bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(bottomRulerNode, lineNode) - } - } else { - const topRulerNode = document.createElement('div') - topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(topRulerNode, lineNode) - const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item) - blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(blockDecorationNode, lineNode) - const bottomRulerNode = document.createElement('div') - bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(bottomRulerNode, lineNode) - - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] = - {topRulerNode, blockDecorationNode, bottomRulerNode} - } - oldLineState.precedingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState) - } - for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) { - const oldBlockDecorationState = oldLineState.followingBlockDecorations[decorationId] - const newBlockDecorationState = newLineState.followingBlockDecorations[decorationId] - if (oldBlockDecorationState != null) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) { - topRulerNode.remove() - blockDecorationNode.remove() - bottomRulerNode.remove() - bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling) - blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling) - topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(topRulerNode, lineNode.nextSibling) - } - } else { - const bottomRulerNode = document.createElement('div') - bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling) - const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item) - blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling) - const topRulerNode = document.createElement('div') - topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow - this.domNode.insertBefore(topRulerNode, lineNode.nextSibling) - - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] = - {topRulerNode, blockDecorationNode, bottomRulerNode} - } - oldLineState.followingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState) - } - } - } - - measureBlockDecorations () { - for (const lineId of Object.keys(this.newTileState.lines)) { - const newLineState = this.newTileState.lines[lineId] - - for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - const width = blockDecorationNode.offsetWidth - const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop - const {decoration} = newLineState.precedingBlockDecorations[decorationId] - this.presenter.setBlockDecorationDimensions(decoration, width, height) - } - for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) { - const {topRulerNode, blockDecorationNode, bottomRulerNode} = - this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] - const width = blockDecorationNode.offsetWidth - const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop - const {decoration} = newLineState.followingBlockDecorations[decorationId] - this.presenter.setBlockDecorationDimensions(decoration, width, height) - } - } - } - - lineNodeForScreenRow (screenRow) { - return this.lineNodesByLineId[this.lineIdsByScreenRow[screenRow]] - } - - lineNodeForLineId (lineId) { - return this.lineNodesByLineId[lineId] - } - - textNodesForLineId (lineId) { - return this.textNodesByLineId[lineId].slice() - } - - lineIdForScreenRow (screenRow) { - return this.lineIdsByScreenRow[screenRow] - } - - textNodesForScreenRow (screenRow) { - const textNodes = this.textNodesByLineId[this.lineIdsByScreenRow[screenRow]] - if (textNodes == null) { - return null - } else { - return textNodes.slice() - } - } -} diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee deleted file mode 100644 index 308cc5af0..000000000 --- a/src/lines-yardstick.coffee +++ /dev/null @@ -1,133 +0,0 @@ -{Point} = require 'text-buffer' -{isPairedCharacter} = require './text-utils' - -module.exports = -class LinesYardstick - constructor: (@model, @lineNodesProvider, @lineTopIndex) -> - @rangeForMeasurement = document.createRange() - @invalidateCache() - - invalidateCache: -> - @leftPixelPositionCache = {} - - measuredRowForPixelPosition: (pixelPosition) -> - targetTop = pixelPosition.top - row = Math.floor(targetTop / @model.getLineHeightInPixels()) - row if 0 <= row - - screenPositionForPixelPosition: (pixelPosition) -> - targetTop = pixelPosition.top - row = Math.max(0, @lineTopIndex.rowForPixelPosition(targetTop)) - lineNode = @lineNodesProvider.lineNodeForScreenRow(row) - unless lineNode - lastScreenRow = @model.getLastScreenRow() - if row > lastScreenRow - return Point(lastScreenRow, @model.lineLengthForScreenRow(lastScreenRow)) - else - return Point(row, 0) - - targetLeft = pixelPosition.left - targetLeft = 0 if targetTop < 0 or targetLeft < 0 - - textNodes = @lineNodesProvider.textNodesForScreenRow(row) - lineOffset = lineNode.getBoundingClientRect().left - targetLeft += lineOffset - - 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 - - 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 - characterIndex = nextCharIndex - break - - textNodeStartColumn = 0 - textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1 - Point(row, textNodeStartColumn + characterIndex) - - pixelPositionForScreenPosition: (screenPosition) -> - targetRow = screenPosition.row - targetColumn = screenPosition.column - - top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow) - left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) - - {top, left} - - leftPixelPositionForScreenPosition: (row, column) -> - lineNode = @lineNodesProvider.lineNodeForScreenRow(row) - lineId = @lineNodesProvider.lineIdForScreenRow(row) - - if lineNode? - if @leftPixelPositionCache[lineId]?[column]? - @leftPixelPositionCache[lineId][column] - else - textNodes = @lineNodesProvider.textNodesForScreenRow(row) - textNodeStartColumn = 0 - for textNode in textNodes - textNodeEndColumn = textNodeStartColumn + textNode.textContent.length - if textNodeEndColumn > column - indexInTextNode = column - textNodeStartColumn - break - else - textNodeStartColumn = textNodeEndColumn - - if textNode? - 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 - else - 0 - - clientRectForRange: (textNode, startIndex, endIndex) -> - @rangeForMeasurement.setStart(textNode, startIndex) - @rangeForMeasurement.setEnd(textNode, endIndex) - clientRects = @rangeForMeasurement.getClientRects() - if clientRects.length is 1 - clientRects[0] - else - @rangeForMeasurement.getBoundingClientRect() diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index b9d51ae12..d920fa5d2 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -26,17 +26,16 @@ class AtomWindow options = show: false title: 'Atom' - # Add an opaque backgroundColor (instead of keeping the default - # transparent one) to prevent subpixel anti-aliasing from being disabled. - # We believe this is a regression introduced with Electron 0.37.3, and - # thus we should remove this as soon as a fix gets released. - backgroundColor: "#fff" webPreferences: # Prevent specs from throttling when the window is in the background: # this should result in faster CI builds, and an improvement in the # local development experience when running specs through the UI (which # now won't pause when e.g. minimizing the window). backgroundThrottling: not @isSpec + # Disable the `auxclick` feature so that `click` events are triggered in + # response to a middle-click. + # (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960) + disableBlinkFeatures: 'Auxclick' # Don't set icon on Windows so the exe's ico will be used as window and # taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. @@ -315,4 +314,4 @@ class AtomWindow copy: -> @browserWindow.copy() disableZoom: -> - @browserWindow.webContents.setZoomLevelLimits(1, 1) + @browserWindow.webContents.setVisualZoomLevelLimits(1, 1) diff --git a/src/main-process/main.js b/src/main-process/main.js index 0beabdcba..ee7b96232 100644 --- a/src/main-process/main.js +++ b/src/main-process/main.js @@ -1,5 +1,5 @@ if (typeof snapshotResult !== 'undefined') { - snapshotResult.setGlobals(global, process, global, {}, console, require) // eslint-disable-line no-undef + snapshotResult.setGlobals(global, process, global, {}, console, require) } const startTime = Date.now() diff --git a/src/main-process/start.js b/src/main-process/start.js index d711d5f64..fae78a07e 100644 --- a/src/main-process/start.js +++ b/src/main-process/start.js @@ -22,6 +22,8 @@ module.exports = function start (resourcePath, startTime) { const previousConsoleLog = console.log console.log = nslog + app.commandLine.appendSwitch('enable-experimental-web-platform-features') + const args = parseCommandLine(process.argv.slice(1)) atomPaths.setAtomHome(app.getPath('home')) atomPaths.setUserData(app) diff --git a/src/marker-observation-window.coffee b/src/marker-observation-window.coffee deleted file mode 100644 index ffb92c0ab..000000000 --- a/src/marker-observation-window.coffee +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = -class MarkerObservationWindow - constructor: (@decorationManager, @bufferWindow) -> - - setScreenRange: (range) -> - @bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range)) - - setBufferRange: (range) -> - @bufferWindow.setRange(range) - - destroy: -> - @bufferWindow.destroy() diff --git a/src/native-compile-cache.js b/src/native-compile-cache.js index 09a62b186..cc947e84b 100644 --- a/src/native-compile-cache.js +++ b/src/native-compile-cache.js @@ -83,7 +83,7 @@ class NativeCompileCache { compiledWrapper = compilationResult.result } - let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global] + let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global, Buffer] return compiledWrapper.apply(moduleSelf.exports, args) } } diff --git a/src/off-screen-block-decorations-component.js b/src/off-screen-block-decorations-component.js deleted file mode 100644 index 0460c854e..000000000 --- a/src/off-screen-block-decorations-component.js +++ /dev/null @@ -1,62 +0,0 @@ -module.exports = class OffScreenBlockDecorationsComponent { - constructor ({presenter, views}) { - this.presenter = presenter - this.views = views - this.newState = {offScreenBlockDecorations: {}, width: 0} - this.oldState = {offScreenBlockDecorations: {}, width: 0} - this.domNode = document.createElement('div') - this.domNode.style.visibility = 'hidden' - this.domNode.style.position = 'absolute' - this.blockDecorationNodesById = {} - } - - getDomNode () { - return this.domNode - } - - updateSync (state) { - this.newState = state.content - - if (this.newState.width !== this.oldState.width) { - this.domNode.style.width = `${this.newState.width}px` - this.oldState.width = this.newState.width - } - - for (const id of Object.keys(this.oldState.offScreenBlockDecorations)) { - if (!this.newState.offScreenBlockDecorations.hasOwnProperty(id)) { - const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id] - topRuler.remove() - blockDecoration.remove() - bottomRuler.remove() - delete this.blockDecorationNodesById[id] - delete this.oldState.offScreenBlockDecorations[id] - } - } - - for (const id of Object.keys(this.newState.offScreenBlockDecorations)) { - const decoration = this.newState.offScreenBlockDecorations[id] - if (!this.oldState.offScreenBlockDecorations.hasOwnProperty(id)) { - const topRuler = document.createElement('div') - this.domNode.appendChild(topRuler) - const blockDecoration = this.views.getView(decoration.getProperties().item) - this.domNode.appendChild(blockDecoration) - const bottomRuler = document.createElement('div') - this.domNode.appendChild(bottomRuler) - - this.blockDecorationNodesById[id] = {topRuler, blockDecoration, bottomRuler} - } - - this.oldState.offScreenBlockDecorations[id] = decoration - } - } - - measureBlockDecorations () { - for (const id of Object.keys(this.blockDecorationNodesById)) { - const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id] - const width = blockDecoration.offsetWidth - const height = bottomRuler.offsetTop - topRuler.offsetTop - const decoration = this.newState.offScreenBlockDecorations[id] - this.presenter.setBlockDecorationDimensions(decoration, width, height) - } - } -} diff --git a/src/overlay-manager.coffee b/src/overlay-manager.coffee index 83787ad10..c8c7f4b27 100644 --- a/src/overlay-manager.coffee +++ b/src/overlay-manager.coffee @@ -1,3 +1,6 @@ +ElementResizeDetector = require('element-resize-detector') +elementResizeDetector = null + module.exports = class OverlayManager constructor: (@presenter, @container, @views) -> @@ -12,6 +15,7 @@ class OverlayManager unless state.content.overlays.hasOwnProperty(id) delete @overlaysById[id] overlayNode.remove() + elementResizeDetector.uninstall(overlayNode) shouldUpdateOverlay: (decorationId, overlay) -> cachedOverlay = @overlaysById[decorationId] @@ -19,10 +23,6 @@ class OverlayManager cachedOverlay.pixelPosition?.top isnt overlay.pixelPosition?.top or cachedOverlay.pixelPosition?.left isnt overlay.pixelPosition?.left - measureOverlays: -> - for decorationId, {itemView} of @overlaysById - @measureOverlay(decorationId, itemView) - measureOverlay: (decorationId, itemView) -> contentMargin = parseInt(getComputedStyle(itemView)['margin-left']) ? 0 @presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin) @@ -33,13 +33,20 @@ class OverlayManager unless overlayNode = cachedOverlay?.overlayNode overlayNode = document.createElement('atom-overlay') overlayNode.classList.add(klass) if klass? + elementResizeDetector ?= ElementResizeDetector({strategy: 'scroll'}) + elementResizeDetector.listenTo(overlayNode, => + if overlayNode.parentElement? + @measureOverlay(decorationId, itemView) + ) @container.appendChild(overlayNode) @overlaysById[decorationId] = cachedOverlay = {overlayNode, itemView} # The same node may be used in more than one overlay. This steals the node # back if it has been displayed in another overlay. - overlayNode.appendChild(itemView) if overlayNode.childNodes.length is 0 + overlayNode.appendChild(itemView) unless overlayNode.contains(itemView) cachedOverlay.pixelPosition = pixelPosition overlayNode.style.top = pixelPosition.top + 'px' overlayNode.style.left = pixelPosition.left + 'px' + + @measureOverlay(decorationId, itemView) diff --git a/src/package.coffee b/src/package.coffee index fbe4bce23..039ccf9d3 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -751,7 +751,7 @@ class Package "installed-packages:#{@name}:#{@metadata.version}:build-error" getIncompatibleNativeModulesStorageKey: -> - electronVersion = process.versions['electron'] ? process.versions['atom-shell'] + electronVersion = process.versions.electron "installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules" getCanDeferMainModuleRequireStorageKey: -> diff --git a/src/pane-element.coffee b/src/pane-element.coffee index 8d2952880..c4866816a 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -27,7 +27,7 @@ class PaneElement extends HTMLElement subscribeToDOMEvents: -> handleFocus = (event) => - @model.focus() unless @isActivating or @contains(event.relatedTarget) + @model.focus() unless @isActivating or @model.isDestroyed() or @contains(event.relatedTarget) if event.target is this and view = @getActiveView() view.focus() event.stopPropagation() diff --git a/src/scrollbar-component.coffee b/src/scrollbar-component.coffee deleted file mode 100644 index 9d418e8c6..000000000 --- a/src/scrollbar-component.coffee +++ /dev/null @@ -1,79 +0,0 @@ -module.exports = -class ScrollbarComponent - constructor: ({@orientation, @onScroll}) -> - @domNode = document.createElement('div') - @domNode.classList.add "#{@orientation}-scrollbar" - @domNode.style['-webkit-transform'] = 'translateZ(0)' # See atom/atom#3559 - @domNode.style.left = 0 if @orientation is 'horizontal' - - @contentNode = document.createElement('div') - @contentNode.classList.add "scrollbar-content" - @domNode.appendChild(@contentNode) - - @domNode.addEventListener 'scroll', @onScrollCallback - - destroy: -> - @domNode.removeEventListener 'scroll', @onScrollCallback - @onScroll = null - - getDomNode: -> - @domNode - - updateSync: (state) -> - @oldState ?= {} - switch @orientation - when 'vertical' - @newState = state.verticalScrollbar - @updateVertical() - when 'horizontal' - @newState = state.horizontalScrollbar - @updateHorizontal() - - if @newState.visible isnt @oldState.visible - if @newState.visible - @domNode.style.display = '' - else - @domNode.style.display = 'none' - @oldState.visible = @newState.visible - - updateVertical: -> - if @newState.width isnt @oldState.width - @domNode.style.width = @newState.width + 'px' - @oldState.width = @newState.width - - if @newState.bottom isnt @oldState.bottom - @domNode.style.bottom = @newState.bottom + 'px' - @oldState.bottom = @newState.bottom - - if @newState.scrollHeight isnt @oldState.scrollHeight - @contentNode.style.height = @newState.scrollHeight + 'px' - @oldState.scrollHeight = @newState.scrollHeight - - if @newState.scrollTop isnt @oldState.scrollTop - @domNode.scrollTop = @newState.scrollTop - @oldState.scrollTop = @newState.scrollTop - - updateHorizontal: -> - if @newState.height isnt @oldState.height - @domNode.style.height = @newState.height + 'px' - @oldState.height = @newState.height - - if @newState.right isnt @oldState.right - @domNode.style.right = @newState.right + 'px' - @oldState.right = @newState.right - - if @newState.scrollWidth isnt @oldState.scrollWidth - @contentNode.style.width = @newState.scrollWidth + 'px' - @oldState.scrollWidth = @newState.scrollWidth - - if @newState.scrollLeft isnt @oldState.scrollLeft - @domNode.scrollLeft = @newState.scrollLeft - @oldState.scrollLeft = @newState.scrollLeft - - - onScrollCallback: => - switch @orientation - when 'vertical' - @onScroll(@domNode.scrollTop) - when 'horizontal' - @onScroll(@domNode.scrollLeft) diff --git a/src/scrollbar-corner-component.coffee b/src/scrollbar-corner-component.coffee deleted file mode 100644 index bc059f12c..000000000 --- a/src/scrollbar-corner-component.coffee +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = -class ScrollbarCornerComponent - constructor: -> - @domNode = document.createElement('div') - @domNode.classList.add('scrollbar-corner') - - @contentNode = document.createElement('div') - @domNode.appendChild(@contentNode) - - getDomNode: -> - @domNode - - updateSync: (state) -> - @oldState ?= {} - @newState ?= {} - - newHorizontalState = state.horizontalScrollbar - newVerticalState = state.verticalScrollbar - @newState.visible = newHorizontalState.visible and newVerticalState.visible - @newState.height = newHorizontalState.height - @newState.width = newVerticalState.width - - if @newState.visible isnt @oldState.visible - if @newState.visible - @domNode.style.display = '' - else - @domNode.style.display = 'none' - @oldState.visible = @newState.visible - - if @newState.height isnt @oldState.height - @domNode.style.height = @newState.height + 'px' - @contentNode.style.height = @newState.height + 1 + 'px' - @oldState.height = @newState.height - - if @newState.width isnt @oldState.width - @domNode.style.width = @newState.width + 'px' - @contentNode.style.width = @newState.width + 1 + 'px' - @oldState.width = @newState.width diff --git a/src/selection.coffee b/src/selection.coffee index 8aa86157e..935a15b13 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -769,8 +769,6 @@ class Selection extends Model {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e {textChanged} = e - @cursor.updateVisibility() - unless oldHeadScreenPosition.isEqual(newHeadScreenPosition) @cursor.goalColumn = null cursorMovedEvent = { diff --git a/src/task-bootstrap.coffee b/src/task-bootstrap.coffee deleted file mode 100644 index ebb5cdc2b..000000000 --- a/src/task-bootstrap.coffee +++ /dev/null @@ -1,54 +0,0 @@ -{userAgent, taskPath} = process.env -handler = null - -setupGlobals = -> - global.attachEvent = -> - console = - warn: -> emit 'task:warn', arguments... - log: -> emit 'task:log', arguments... - error: -> emit 'task:error', arguments... - trace: -> - global.__defineGetter__ 'console', -> console - - global.document = - createElement: -> - setAttribute: -> - getElementsByTagName: -> [] - appendChild: -> - documentElement: - insertBefore: -> - removeChild: -> - getElementById: -> {} - createComment: -> {} - createDocumentFragment: -> {} - - global.emit = (event, args...) -> - process.send({event, args}) - global.navigator = {userAgent} - global.window = global - -handleEvents = -> - process.on 'uncaughtException', (error) -> - console.error(error.message, error.stack) - process.on 'message', ({event, args}={}) -> - return unless event is 'start' - - isAsync = false - async = -> - isAsync = true - (result) -> - emit('task:completed', result) - result = handler.bind({async})(args...) - emit('task:completed', result) unless isAsync - -setupDeprecations = -> - Grim = require 'grim' - Grim.on 'updated', -> - deprecations = Grim.getDeprecations().map (deprecation) -> deprecation.serialize() - Grim.clearDeprecations() - emit('task:deprecations', deprecations) - -setupGlobals() -handleEvents() -setupDeprecations() -handler = require(taskPath) diff --git a/src/task-bootstrap.js b/src/task-bootstrap.js new file mode 100644 index 000000000..a7076ebd4 --- /dev/null +++ b/src/task-bootstrap.js @@ -0,0 +1,68 @@ +const {userAgent} = process.env +const [compileCachePath, taskPath] = process.argv.slice(2) + +const CompileCache = require('./compile-cache') +CompileCache.setCacheDirectory(compileCachePath) +CompileCache.install(`${process.resourcesPath}`, require) + +const setupGlobals = function () { + global.attachEvent = function () {} + const console = { + warn () { return global.emit('task:warn', ...arguments) }, + log () { return global.emit('task:log', ...arguments) }, + error () { return global.emit('task:error', ...arguments) }, + trace () {} + } + global.__defineGetter__('console', () => console) + + global.document = { + createElement () { + return { + setAttribute () {}, + getElementsByTagName () { return [] }, + appendChild () {} + } + }, + documentElement: { + insertBefore () {}, + removeChild () {} + }, + getElementById () { return {} }, + createComment () { return {} }, + createDocumentFragment () { return {} } + } + + global.emit = (event, ...args) => process.send({event, args}) + global.navigator = {userAgent} + return (global.window = global) +} + +const handleEvents = function () { + process.on('uncaughtException', error => console.error(error.message, error.stack)) + + return process.on('message', function ({event, args} = {}) { + if (event !== 'start') { return } + + let isAsync = false + const async = function () { + isAsync = true + return result => global.emit('task:completed', result) + } + const result = handler.bind({async})(...args) + if (!isAsync) { return global.emit('task:completed', result) } + }) +} + +const setupDeprecations = function () { + const Grim = require('grim') + return Grim.on('updated', function () { + const deprecations = Grim.getDeprecations().map(deprecation => deprecation.serialize()) + Grim.clearDeprecations() + return global.emit('task:deprecations', deprecations) + }) +} + +setupGlobals() +handleEvents() +setupDeprecations() +const handler = require(taskPath) diff --git a/src/task.coffee b/src/task.coffee index fb2a9f97f..b8283cb01 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -66,22 +66,11 @@ class Task constructor: (taskPath) -> @emitter = new Emitter - compileCacheRequire = "require('#{require.resolve('./compile-cache')}')" compileCachePath = require('./compile-cache').getCacheDirectory() - taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');" - bootstrap = """ - CompileCache = #{compileCacheRequire} - CompileCache.setCacheDirectory('#{compileCachePath}'); - CompileCache.install("#{process.resourcesPath}", require) - #{taskBootstrapRequire} - """ - bootstrap = bootstrap.replace(/\\/g, "\\\\") - taskPath = require.resolve(taskPath) - taskPath = taskPath.replace(/\\/g, "\\\\") - env = _.extend({}, process.env, {taskPath, userAgent: navigator.userAgent}) - @childProcess = ChildProcess.fork '--eval', [bootstrap], {env, silent: true} + env = Object.assign({}, process.env, {userAgent: navigator.userAgent}) + @childProcess = ChildProcess.fork require.resolve('./task-bootstrap'), [compileCachePath, taskPath], {env, silent: true} @on "task:log", -> console.log(arguments...) @on "task:warn", -> console.warn(arguments...) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee deleted file mode 100644 index 89e8f0783..000000000 --- a/src/text-editor-component.coffee +++ /dev/null @@ -1,976 +0,0 @@ -scrollbarStyle = require 'scrollbar-style' -{Range, Point} = require 'text-buffer' -{CompositeDisposable} = require 'event-kit' -{ipcRenderer} = require 'electron' -Grim = require 'grim' - -TextEditorPresenter = require './text-editor-presenter' -GutterContainerComponent = require './gutter-container-component' -InputComponent = require './input-component' -LinesComponent = require './lines-component' -OffScreenBlockDecorationsComponent = require './off-screen-block-decorations-component' -ScrollbarComponent = require './scrollbar-component' -ScrollbarCornerComponent = require './scrollbar-corner-component' -OverlayManager = require './overlay-manager' -DOMElementPool = require './dom-element-pool' -LinesYardstick = require './lines-yardstick' -LineTopIndex = require 'line-top-index' - -module.exports = -class TextEditorComponent - cursorBlinkPeriod: 800 - cursorBlinkResumeDelay: 100 - tileSize: 12 - - pendingScrollTop: null - pendingScrollLeft: null - updateRequested: false - updatesPaused: false - updateRequestedWhilePaused: false - heightAndWidthMeasurementRequested: false - inputEnabled: true - measureScrollbarsWhenShown: true - measureLineHeightAndDefaultCharWidthWhenShown: true - stylingChangeAnimationFrameRequested: false - gutterComponent: null - mounted: true - initialized: false - - Object.defineProperty @prototype, "domNode", - get: -> @domNodeValue - set: (domNode) -> - @assert domNode?, "TextEditorComponent::domNode was set to null." - @domNodeValue = domNode - - constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert, hiddenInputElement}) -> - @tileSize = tileSize if tileSize? - @disposables = new CompositeDisposable - - lineTopIndex = new LineTopIndex({ - defaultLineHeight: @editor.getLineHeightInPixels() - }) - @presenter = new TextEditorPresenter - model: @editor - tileSize: tileSize - cursorBlinkPeriod: @cursorBlinkPeriod - cursorBlinkResumeDelay: @cursorBlinkResumeDelay - stoppedScrollingDelay: 200 - lineTopIndex: lineTopIndex - autoHeight: @editor.getAutoHeight() - - @presenter.onDidUpdateState(@requestUpdate) - - @domElementPool = new DOMElementPool - @domNode = document.createElement('div') - @domNode.classList.add('editor-contents--private') - - @overlayManager = new OverlayManager(@presenter, @domNode, @views) - - @scrollViewNode = document.createElement('div') - @scrollViewNode.classList.add('scroll-view') - @domNode.appendChild(@scrollViewNode) - - @hiddenInputComponent = new InputComponent(hiddenInputElement) - @scrollViewNode.appendChild(hiddenInputElement) - # Add a getModel method to the hidden input component to make it easy to - # access the editor in response to DOM events or when using - # document.activeElement. - hiddenInputElement.getModel = => @editor - - @linesComponent = new LinesComponent({@presenter, @domElementPool, @assert, @grammars, @views}) - @scrollViewNode.appendChild(@linesComponent.getDomNode()) - - @offScreenBlockDecorationsComponent = new OffScreenBlockDecorationsComponent({@presenter, @views}) - @scrollViewNode.appendChild(@offScreenBlockDecorationsComponent.getDomNode()) - - @linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex) - @presenter.setLinesYardstick(@linesYardstick) - - @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) - @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode()) - - @verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll}) - @domNode.appendChild(@verticalScrollbarComponent.getDomNode()) - - @scrollbarCornerComponent = new ScrollbarCornerComponent - @domNode.appendChild(@scrollbarCornerComponent.getDomNode()) - - @observeEditor() - @listenForDOMEvents() - - @disposables.add @styles.onDidAddStyleElement @onStylesheetsChanged - @disposables.add @styles.onDidUpdateStyleElement @onStylesheetsChanged - @disposables.add @styles.onDidRemoveStyleElement @onStylesheetsChanged - unless @themes.isInitialLoadComplete() - @disposables.add @themes.onDidChangeActiveThemes @onAllThemesLoaded - @disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars - - @disposables.add @views.pollDocument(@pollDOM) - - @updateSync() - @checkForVisibilityChange() - @initialized = true - - destroy: -> - @mounted = false - @disposables.dispose() - @presenter.destroy() - @gutterContainerComponent?.destroy() - @domElementPool.clear() - - @verticalScrollbarComponent.destroy() - @horizontalScrollbarComponent.destroy() - - @onVerticalScroll = null - @onHorizontalScroll = null - - getDomNode: -> - @domNode - - updateSync: -> - @updateSyncPreMeasurement() - - @oldState ?= {width: null} - @newState = @presenter.getPostMeasurementState() - - if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty() - @domNode.classList.add('has-selection') - else - @domNode.classList.remove('has-selection') - - if @newState.focused isnt @oldState.focused - @domNode.classList.toggle('is-focused', @newState.focused) - - @performedInitialMeasurement = false if @editor.isDestroyed() - - if @performedInitialMeasurement - if @newState.height isnt @oldState.height - if @newState.height? - @domNode.style.height = @newState.height + 'px' - else - @domNode.style.height = '' - - if @newState.width isnt @oldState.width - if @newState.width? - @hostElement.style.width = @newState.width + 'px' - else - @hostElement.style.width = '' - @oldState.width = @newState.width - - if @newState.gutters.length - @mountGutterContainerComponent() unless @gutterContainerComponent? - @gutterContainerComponent.updateSync(@newState) - else - @gutterContainerComponent?.getDomNode()?.remove() - @gutterContainerComponent = null - - @hiddenInputComponent.updateSync(@newState) - @offScreenBlockDecorationsComponent.updateSync(@newState) - @linesComponent.updateSync(@newState) - @horizontalScrollbarComponent.updateSync(@newState) - @verticalScrollbarComponent.updateSync(@newState) - @scrollbarCornerComponent.updateSync(@newState) - - @overlayManager?.render(@newState) - - if @clearPoolAfterUpdate - @domElementPool.clear() - @clearPoolAfterUpdate = false - - if @editor.isAlive() - @updateParentViewFocusedClassIfNeeded() - @updateParentViewMiniClass() - - updateSyncPreMeasurement: -> - @linesComponent.updateSync(@presenter.getPreMeasurementState()) - - readAfterUpdateSync: => - @overlayManager?.measureOverlays() - @linesComponent.measureBlockDecorations() - @offScreenBlockDecorationsComponent.measureBlockDecorations() - - mountGutterContainerComponent: -> - @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views}) - @domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild) - - becameVisible: -> - @updatesPaused = true - # Always invalidate LinesYardstick measurements when the editor becomes - # visible again, because content might have been reflowed and measurements - # could be outdated. - @invalidateMeasurements() - @measureScrollbars() if @measureScrollbarsWhenShown - @sampleFontStyling() - @sampleBackgroundColors() - @measureWindowSize() - @measureDimensions() - @measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown - @editor.setVisible(true) - @performedInitialMeasurement = true - @updatesPaused = false - @updateSync() if @canUpdate() - - requestUpdate: => - return unless @canUpdate() - - if @updatesPaused - @updateRequestedWhilePaused = true - return - - if @hostElement.isUpdatedSynchronously() - @updateSync() - else unless @updateRequested - @updateRequested = true - @views.updateDocument => - @updateRequested = false - @updateSync() if @canUpdate() - @views.readDocument(@readAfterUpdateSync) - - canUpdate: -> - @mounted and @editor.isAlive() - - requestAnimationFrame: (fn) -> - @updatesPaused = true - requestAnimationFrame => - fn() - @updatesPaused = false - if @updateRequestedWhilePaused and @canUpdate() - @updateRequestedWhilePaused = false - @requestUpdate() - - getTopmostDOMNode: -> - @hostElement - - observeEditor: -> - @disposables.add @editor.observeGrammar(@onGrammarChanged) - - listenForDOMEvents: -> - @domNode.addEventListener 'mousewheel', @onMouseWheel - @domNode.addEventListener 'textInput', @onTextInput - @scrollViewNode.addEventListener 'mousedown', @onMouseDown - @scrollViewNode.addEventListener 'scroll', @onScrollViewScroll - - @detectAccentedCharacterMenu() - @listenForIMEEvents() - @trackSelectionClipboard() if process.platform is 'linux' - - detectAccentedCharacterMenu: -> - # We need to get clever to detect when the accented character menu is - # opened on macOS. Usually, every keydown event that could cause input is - # followed by a corresponding keypress. However, pressing and holding - # long enough to open the accented character menu causes additional keydown - # events to fire that aren't followed by their own keypress and textInput - # events. - # - # Therefore, we assume the accented character menu has been deployed if, - # before observing any keyup event, we observe events in the following - # sequence: - # - # keydown(keyCode: X), keypress, keydown(keyCode: X) - # - # The keyCode X must be the same in the keydown events that bracket the - # keypress, meaning we're *holding* the _same_ key we intially pressed. - # Got that? - lastKeydown = null - lastKeydownBeforeKeypress = null - - @domNode.addEventListener 'keydown', (event) => - if lastKeydownBeforeKeypress - if lastKeydownBeforeKeypress.keyCode is event.keyCode - @openedAccentedCharacterMenu = true - lastKeydownBeforeKeypress = null - else - lastKeydown = event - - @domNode.addEventListener 'keypress', => - lastKeydownBeforeKeypress = lastKeydown - lastKeydown = null - - # This cancels the accented character behavior if we type a key normally - # with the menu open. - @openedAccentedCharacterMenu = false - - @domNode.addEventListener 'keyup', -> - lastKeydownBeforeKeypress = null - lastKeydown = null - - listenForIMEEvents: -> - # The IME composition events work like this: - # - # User types 's', chromium pops up the completion helper - # 1. compositionstart fired - # 2. compositionupdate fired; event.data == 's' - # User hits arrow keys to move around in completion helper - # 3. compositionupdate fired; event.data == 's' for each arry key press - # User escape to cancel - # 4. compositionend fired - # OR User chooses a completion - # 4. compositionend fired - # 5. textInput fired; event.data == the completion string - - checkpoint = null - @domNode.addEventListener 'compositionstart', => - if @openedAccentedCharacterMenu - @editor.selectLeft() - @openedAccentedCharacterMenu = false - checkpoint = @editor.createCheckpoint() - @domNode.addEventListener 'compositionupdate', (event) => - @editor.insertText(event.data, select: true) - @domNode.addEventListener 'compositionend', (event) => - @editor.revertToCheckpoint(checkpoint) - event.target.value = '' - - # Listen for selection changes and store the currently selected text - # in the selection clipboard. This is only applicable on Linux. - trackSelectionClipboard: -> - timeoutId = null - writeSelectedTextToSelectionClipboard = => - return if @editor.isDestroyed() - if selectedText = @editor.getSelectedText() - # This uses ipcRenderer.send instead of clipboard.writeText because - # clipboard.writeText is a sync ipcRenderer call on Linux and that - # will slow down selections. - ipcRenderer.send('write-text-to-selection-clipboard', selectedText) - @disposables.add @editor.onDidChangeSelectionRange -> - clearTimeout(timeoutId) - timeoutId = setTimeout(writeSelectedTextToSelectionClipboard) - - onGrammarChanged: => - if @scopedConfigDisposables? - @scopedConfigDisposables.dispose() - @disposables.remove(@scopedConfigDisposables) - - @scopedConfigDisposables = new CompositeDisposable - @disposables.add(@scopedConfigDisposables) - - focused: -> - if @mounted - @presenter.setFocused(true) - - blurred: -> - if @mounted - @presenter.setFocused(false) - - onTextInput: (event) => - event.stopPropagation() - - # WARNING: If we call preventDefault on the input of a space character, - # then the browser interprets the spacebar keypress as a page-down command, - # causing spaces to scroll elements containing editors. This is impossible - # to test. - event.preventDefault() if event.data isnt ' ' - - return unless @isInputEnabled() - - # Workaround of the accented character suggestion feature in macOS. - # This will only occur when the user is not composing in IME mode. - # When the user selects a modified character from the macOS menu, `textInput` - # will occur twice, once for the initial character, and once for the - # modified character. However, only a single keypress will have fired. If - # this is the case, select backward to replace the original character. - if @openedAccentedCharacterMenu - @editor.selectLeft() - @openedAccentedCharacterMenu = false - - @editor.insertText(event.data, groupUndo: true) - - onVerticalScroll: (scrollTop) => - return if @updateRequested or scrollTop is @presenter.getScrollTop() - - animationFramePending = @pendingScrollTop? - @pendingScrollTop = scrollTop - unless animationFramePending - @requestAnimationFrame => - pendingScrollTop = @pendingScrollTop - @pendingScrollTop = null - @presenter.setScrollTop(pendingScrollTop) - @presenter.commitPendingScrollTopPosition() - - onHorizontalScroll: (scrollLeft) => - return if @updateRequested or scrollLeft is @presenter.getScrollLeft() - - animationFramePending = @pendingScrollLeft? - @pendingScrollLeft = scrollLeft - unless animationFramePending - @requestAnimationFrame => - @presenter.setScrollLeft(@pendingScrollLeft) - @presenter.commitPendingScrollLeftPosition() - @pendingScrollLeft = null - - onMouseWheel: (event) => - # Only scroll in one direction at a time - {wheelDeltaX, wheelDeltaY} = event - - if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) - # Scrolling horizontally - previousScrollLeft = @presenter.getScrollLeft() - updatedScrollLeft = previousScrollLeft - Math.round(wheelDeltaX * @editor.getScrollSensitivity() / 100) - - event.preventDefault() if @presenter.canScrollLeftTo(updatedScrollLeft) - @presenter.setScrollLeft(updatedScrollLeft) - else - # Scrolling vertically - @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target)) - previousScrollTop = @presenter.getScrollTop() - updatedScrollTop = previousScrollTop - Math.round(wheelDeltaY * @editor.getScrollSensitivity() / 100) - - event.preventDefault() if @presenter.canScrollTopTo(updatedScrollTop) - @presenter.setScrollTop(updatedScrollTop) - - onScrollViewScroll: => - if @mounted - @scrollViewNode.scrollTop = 0 - @scrollViewNode.scrollLeft = 0 - - onDidChangeScrollTop: (callback) -> - @presenter.onDidChangeScrollTop(callback) - - onDidChangeScrollLeft: (callback) -> - @presenter.onDidChangeScrollLeft(callback) - - setScrollLeft: (scrollLeft) -> - @presenter.setScrollLeft(scrollLeft) - - setScrollRight: (scrollRight) -> - @presenter.setScrollRight(scrollRight) - - setScrollTop: (scrollTop) -> - @presenter.setScrollTop(scrollTop) - - setScrollBottom: (scrollBottom) -> - @presenter.setScrollBottom(scrollBottom) - - getScrollTop: -> - @presenter.getScrollTop() - - getScrollLeft: -> - @presenter.getScrollLeft() - - getScrollRight: -> - @presenter.getScrollRight() - - getScrollBottom: -> - @presenter.getScrollBottom() - - getScrollHeight: -> - @presenter.getScrollHeight() - - getScrollWidth: -> - @presenter.getScrollWidth() - - getMaxScrollTop: -> - @presenter.getMaxScrollTop() - - getVerticalScrollbarWidth: -> - @presenter.getVerticalScrollbarWidth() - - getHorizontalScrollbarHeight: -> - @presenter.getHorizontalScrollbarHeight() - - getVisibleRowRange: -> - @presenter.getVisibleRowRange() - - pixelPositionForScreenPosition: (screenPosition, clip=true) -> - screenPosition = Point.fromObject(screenPosition) - screenPosition = @editor.clipScreenPosition(screenPosition) if clip - - unless @presenter.isRowRendered(screenPosition.row) - @presenter.setScreenRowsToMeasure([screenPosition.row]) - - unless @linesComponent.lineNodeForScreenRow(screenPosition.row)? - @updateSyncPreMeasurement() - - pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition) - @presenter.clearScreenRowsToMeasure() - pixelPosition - - screenPositionForPixelPosition: (pixelPosition) -> - row = @linesYardstick.measuredRowForPixelPosition(pixelPosition) - if row? and not @presenter.isRowRendered(row) - @presenter.setScreenRowsToMeasure([row]) - @updateSyncPreMeasurement() - - position = @linesYardstick.screenPositionForPixelPosition(pixelPosition) - @presenter.clearScreenRowsToMeasure() - position - - pixelRectForScreenRange: (screenRange) -> - rowsToMeasure = [] - unless @presenter.isRowRendered(screenRange.start.row) - rowsToMeasure.push(screenRange.start.row) - unless @presenter.isRowRendered(screenRange.end.row) - rowsToMeasure.push(screenRange.end.row) - - if rowsToMeasure.length > 0 - @presenter.setScreenRowsToMeasure(rowsToMeasure) - @updateSyncPreMeasurement() - - rect = @presenter.absolutePixelRectForScreenRange(screenRange) - - if rowsToMeasure.length > 0 - @presenter.clearScreenRowsToMeasure() - - rect - - pixelRangeForScreenRange: (screenRange, clip=true) -> - {start, end} = Range.fromObject(screenRange) - {start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)} - - pixelPositionForBufferPosition: (bufferPosition) -> - @pixelPositionForScreenPosition( - @editor.screenPositionForBufferPosition(bufferPosition) - ) - - invalidateBlockDecorationDimensions: -> - @presenter.invalidateBlockDecorationDimensions(arguments...) - - onMouseDown: (event) => - # Handle middle mouse button on linux platform only (paste clipboard) - if event.button is 1 and process.platform is 'linux' - if selection = require('./safe-clipboard').readText('selection') - screenPosition = @screenPositionForMouseEvent(event) - @editor.setCursorScreenPosition(screenPosition, autoscroll: false) - @editor.insertText(selection) - return - - # Handle mouse down events for left mouse button only - # (except middle mouse button on linux platform, see above) - unless event.button is 0 - return - - return if event.target?.classList.contains('horizontal-scrollbar') - - {detail, shiftKey, metaKey, ctrlKey} = event - - # CTRL+click brings up the context menu on macOS, so don't handle those either - return if ctrlKey and process.platform is 'darwin' - - # Prevent focusout event on hidden input if editor is already focused - event.preventDefault() if @oldState.focused - - screenPosition = @screenPositionForMouseEvent(event) - - if event.target?.classList.contains('fold-marker') - bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition) - @editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition]) - return - - switch detail - when 1 - if shiftKey - @editor.selectToScreenPosition(screenPosition) - else if metaKey or (ctrlKey and process.platform isnt 'darwin') - cursorAtScreenPosition = @editor.getCursorAtScreenPosition(screenPosition) - if cursorAtScreenPosition and @editor.hasMultipleCursors() - cursorAtScreenPosition.destroy() - else - @editor.addCursorAtScreenPosition(screenPosition, autoscroll: false) - else - @editor.setCursorScreenPosition(screenPosition, autoscroll: false) - when 2 - @editor.getLastSelection().selectWord(autoscroll: false) - when 3 - @editor.getLastSelection().selectLine(null, autoscroll: false) - - @handleDragUntilMouseUp (screenPosition) => - @editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false) - - onLineNumberGutterMouseDown: (event) => - return unless event.button is 0 # only handle the left mouse button - - {shiftKey, metaKey, ctrlKey} = event - - if shiftKey - @onGutterShiftClick(event) - else if metaKey or (ctrlKey and process.platform isnt 'darwin') - @onGutterMetaClick(event) - else - @onGutterClick(event) - - onGutterClick: (event) => - clickedScreenRow = @screenPositionForMouseEvent(event).row - clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow) - initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]]) - @editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false) - @handleGutterDrag(initialScreenRange) - - onGutterMetaClick: (event) => - clickedScreenRow = @screenPositionForMouseEvent(event).row - clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow) - initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]]) - @editor.addSelectionForScreenRange(initialScreenRange, autoscroll: false) - @handleGutterDrag(initialScreenRange) - - onGutterShiftClick: (event) => - tailScreenPosition = @editor.getLastSelection().getTailScreenPosition() - clickedScreenRow = @screenPositionForMouseEvent(event).row - clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow) - clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]]) - - if clickedScreenRow < tailScreenPosition.row - @editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false) - else - @editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false) - - @handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition)) - - handleGutterDrag: (initialRange) -> - @handleDragUntilMouseUp (screenPosition) => - dragRow = screenPosition.row - if dragRow < initialRange.start.row - startPosition = @editor.clipScreenPosition([dragRow, 0], skipSoftWrapIndentation: true) - screenRange = new Range(startPosition, startPosition).union(initialRange) - @editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true) - else - endPosition = @editor.clipScreenPosition([dragRow + 1, 0], clipDirection: 'backward') - screenRange = new Range(endPosition, endPosition).union(initialRange) - @editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true) - - onStylesheetsChanged: (styleElement) => - return unless @performedInitialMeasurement - return unless @themes.isInitialLoadComplete() - - # This delay prevents the styling from going haywire when stylesheets are - # reloaded in dev mode. It seems like a workaround for a browser bug, but - # not totally sure. - - unless @stylingChangeAnimationFrameRequested - @stylingChangeAnimationFrameRequested = true - requestAnimationFrame => - @stylingChangeAnimationFrameRequested = false - if @mounted - @refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet) - @handleStylingChange() - - onAllThemesLoaded: => - @refreshScrollbars() - @handleStylingChange() - - handleStylingChange: => - @sampleFontStyling() - @sampleBackgroundColors() - @invalidateMeasurements() - - handleDragUntilMouseUp: (dragHandler) -> - dragging = false - lastMousePosition = {} - animationLoop = => - @requestAnimationFrame => - if dragging and @mounted - linesClientRect = @linesComponent.getDomNode().getBoundingClientRect() - autoscroll(lastMousePosition, linesClientRect) - screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect) - dragHandler(screenPosition) - animationLoop() - else if not @mounted - stopDragging() - - onMouseMove = (event) -> - lastMousePosition.clientX = event.clientX - lastMousePosition.clientY = event.clientY - - # Start the animation loop when the mouse moves prior to a mouseup event - unless dragging - dragging = true - animationLoop() - - # Stop dragging when cursor enters dev tools because we can't detect mouseup - onMouseUp() if event.which is 0 - - onMouseUp = (event) => - if dragging - stopDragging() - @editor.finalizeSelections() - @editor.mergeIntersectingSelections() - - stopDragging = -> - dragging = false - window.removeEventListener('mousemove', onMouseMove) - window.removeEventListener('mouseup', onMouseUp) - disposables.dispose() - - autoscroll = (mouseClientPosition) => - {top, bottom, left, right} = @scrollViewNode.getBoundingClientRect() - top += 30 - bottom -= 30 - left += 30 - right -= 30 - - if mouseClientPosition.clientY < top - mouseYDelta = top - mouseClientPosition.clientY - yDirection = -1 - else if mouseClientPosition.clientY > bottom - mouseYDelta = mouseClientPosition.clientY - bottom - yDirection = 1 - - if mouseClientPosition.clientX < left - mouseXDelta = left - mouseClientPosition.clientX - xDirection = -1 - else if mouseClientPosition.clientX > right - mouseXDelta = mouseClientPosition.clientX - right - xDirection = 1 - - if mouseYDelta? - @presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta)) - @presenter.commitPendingScrollTopPosition() - - if mouseXDelta? - @presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta)) - @presenter.commitPendingScrollLeftPosition() - - scaleScrollDelta = (scrollDelta) -> - Math.pow(scrollDelta / 2, 3) / 280 - - window.addEventListener('mousemove', onMouseMove) - window.addEventListener('mouseup', onMouseUp) - disposables = new CompositeDisposable - disposables.add(@editor.getBuffer().onWillChange(onMouseUp)) - disposables.add(@editor.onDidDestroy(stopDragging)) - - isVisible: -> - # Investigating an exception that occurs here due to ::domNode being null. - @assert @domNode?, "TextEditorComponent::domNode was null.", (error) => - error.metadata = {@initialized} - - @domNode? and (@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0) - - pollDOM: => - unless @checkForVisibilityChange() - @sampleBackgroundColors() - @measureWindowSize() - @measureDimensions() - @sampleFontStyling() - @overlayManager?.measureOverlays() - - checkForVisibilityChange: -> - if @isVisible() - if @wasVisible - false - else - @becameVisible() - @wasVisible = true - else - @wasVisible = false - - # Measure explicitly-styled height and width and relay them to the model. If - # these values aren't explicitly styled, we assume the editor is unconstrained - # and use the scrollHeight / scrollWidth as its height and width in - # calculations. - measureDimensions: -> - # If we don't assign autoHeight explicitly, we try to automatically disable - # auto-height in certain circumstances. This is legacy behavior that we - # would rather not implement, but we can't remove it without risking - # breakage currently. - unless @editor.autoHeight? - {position, top, bottom} = getComputedStyle(@hostElement) - hasExplicitTopAndBottom = (position is 'absolute' and top isnt 'auto' and bottom isnt 'auto') - hasInlineHeight = @hostElement.style.height.length > 0 - - if hasInlineHeight or hasExplicitTopAndBottom - if @presenter.autoHeight - @presenter.setAutoHeight(false) - if hasExplicitTopAndBottom - Grim.deprecate(""" - Assigning editor #{@editor.id}'s height explicitly via `position: 'absolute'` and an assigned `top` and `bottom` implicitly assigns the `autoHeight` property to false on the editor. - This behavior is deprecated and will not be supported in the future. Please explicitly assign `autoHeight` on this editor. - """) - else if hasInlineHeight - Grim.deprecate(""" - Assigning editor #{@editor.id}'s height explicitly via an inline style implicitly assigns the `autoHeight` property to false on the editor. - This behavior is deprecated and will not be supported in the future. Please explicitly assign `autoHeight` on this editor. - """) - else - @presenter.setAutoHeight(true) - - if @presenter.autoHeight - @presenter.setExplicitHeight(null) - else if @hostElement.offsetHeight > 0 - @presenter.setExplicitHeight(@hostElement.offsetHeight) - - clientWidth = @scrollViewNode.clientWidth - paddingLeft = parseInt(getComputedStyle(@scrollViewNode).paddingLeft) - clientWidth -= paddingLeft - if clientWidth > 0 - @presenter.setContentFrameWidth(clientWidth) - - @presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0) - @presenter.setBoundingClientRect(@hostElement.getBoundingClientRect()) - - measureWindowSize: -> - return unless @mounted - - # FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value - # when window gets resized through `atom.setWindowDimensions({width: - # windowWidth, height: windowHeight})`. - @presenter.setWindowSize(window.innerWidth, window.innerHeight) - - sampleFontStyling: => - oldFontSize = @fontSize - oldFontFamily = @fontFamily - oldLineHeight = @lineHeight - - {@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode()) - - if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight - @clearPoolAfterUpdate = true - @measureLineHeightAndDefaultCharWidth() - @invalidateMeasurements() - - sampleBackgroundColors: (suppressUpdate) -> - {backgroundColor} = getComputedStyle(@hostElement) - @presenter.setBackgroundColor(backgroundColor) - - lineNumberGutter = @gutterContainerComponent?.getLineNumberGutterComponent() - if lineNumberGutter - gutterBackgroundColor = getComputedStyle(lineNumberGutter.getDomNode()).backgroundColor - @presenter.setGutterBackgroundColor(gutterBackgroundColor) - - measureLineHeightAndDefaultCharWidth: -> - if @isVisible() - @measureLineHeightAndDefaultCharWidthWhenShown = false - @linesComponent.measureLineHeightAndDefaultCharWidth() - else - @measureLineHeightAndDefaultCharWidthWhenShown = true - - measureScrollbars: -> - @measureScrollbarsWhenShown = false - - cornerNode = @scrollbarCornerComponent.getDomNode() - originalDisplayValue = cornerNode.style.display - - cornerNode.style.display = 'block' - - width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15 - height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15 - - @presenter.setVerticalScrollbarWidth(width) - @presenter.setHorizontalScrollbarHeight(height) - - cornerNode.style.display = originalDisplayValue - - containsScrollbarSelector: (stylesheet) -> - for rule in stylesheet.cssRules - if rule.selectorText?.indexOf('scrollbar') > -1 - return true - false - - refreshScrollbars: => - if @isVisible() - @measureScrollbarsWhenShown = false - else - @measureScrollbarsWhenShown = true - return - - verticalNode = @verticalScrollbarComponent.getDomNode() - horizontalNode = @horizontalScrollbarComponent.getDomNode() - cornerNode = @scrollbarCornerComponent.getDomNode() - - originalVerticalDisplayValue = verticalNode.style.display - originalHorizontalDisplayValue = horizontalNode.style.display - originalCornerDisplayValue = cornerNode.style.display - - # First, hide all scrollbars in case they are visible so they take on new - # styles when they are shown again. - verticalNode.style.display = 'none' - horizontalNode.style.display = 'none' - cornerNode.style.display = 'none' - - # Force a reflow - cornerNode.offsetWidth - - # Now measure the new scrollbar dimensions - @measureScrollbars() - - # Now restore the display value for all scrollbars, since they were - # previously hidden - verticalNode.style.display = originalVerticalDisplayValue - horizontalNode.style.display = originalHorizontalDisplayValue - cornerNode.style.display = originalCornerDisplayValue - - consolidateSelections: (e) -> - e.abortKeyBinding() unless @editor.consolidateSelections() - - lineNodeForScreenRow: (screenRow) -> - @linesComponent.lineNodeForScreenRow(screenRow) - - lineNumberNodeForScreenRow: (screenRow) -> - tileRow = @presenter.tileForRow(screenRow) - gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent() - tileComponent = gutterComponent.getComponentForTile(tileRow) - - tileComponent?.lineNumberNodeForScreenRow(screenRow) - - tileNodesForLines: -> - @linesComponent.getTiles() - - tileNodesForLineNumbers: -> - gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent() - gutterComponent.getTiles() - - screenRowForNode: (node) -> - while node? - if screenRow = node.dataset?.screenRow - return parseInt(screenRow) - node = node.parentElement - null - - getFontSize: -> - parseInt(getComputedStyle(@getTopmostDOMNode()).fontSize) - - setFontSize: (fontSize) -> - @getTopmostDOMNode().style.fontSize = fontSize + 'px' - @sampleFontStyling() - @invalidateMeasurements() - - getFontFamily: -> - getComputedStyle(@getTopmostDOMNode()).fontFamily - - setFontFamily: (fontFamily) -> - @getTopmostDOMNode().style.fontFamily = fontFamily - @sampleFontStyling() - @invalidateMeasurements() - - setLineHeight: (lineHeight) -> - @getTopmostDOMNode().style.lineHeight = lineHeight - @sampleFontStyling() - @invalidateMeasurements() - - invalidateMeasurements: -> - @linesYardstick.invalidateCache() - @presenter.measurementsChanged() - - screenPositionForMouseEvent: (event, linesClientRect) -> - pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect) - @screenPositionForPixelPosition(pixelPosition) - - pixelPositionForMouseEvent: (event, linesClientRect) -> - {clientX, clientY} = event - - linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect() - top = clientY - linesClientRect.top + @presenter.getRealScrollTop() - left = clientX - linesClientRect.left + @presenter.getRealScrollLeft() - bottom = linesClientRect.top + @presenter.getRealScrollTop() + linesClientRect.height - clientY - right = linesClientRect.left + @presenter.getRealScrollLeft() + linesClientRect.width - clientX - - {top, left, bottom, right} - - getGutterWidth: -> - @presenter.getGutterWidth() - - getModel: -> - @editor - - isInputEnabled: -> @inputEnabled - - setInputEnabled: (@inputEnabled) -> @inputEnabled - - setContinuousReflow: (continuousReflow) -> - @presenter.setContinuousReflow(continuousReflow) - - updateParentViewFocusedClassIfNeeded: -> - if @oldState.focused isnt @newState.focused - @hostElement.classList.toggle('is-focused', @newState.focused) - @oldState.focused = @newState.focused - - updateParentViewMiniClass: -> - @hostElement.classList.toggle('mini', @editor.isMini()) diff --git a/src/text-editor-component.js b/src/text-editor-component.js new file mode 100644 index 000000000..c5a154eb6 --- /dev/null +++ b/src/text-editor-component.js @@ -0,0 +1,3968 @@ +/* global ResizeObserver */ + +const etch = require('etch') +const {Point, Range} = require('text-buffer') +const LineTopIndex = require('line-top-index') +const TextEditor = require('./text-editor') +const {isPairedCharacter} = require('./text-utils') +const clipboard = require('./safe-clipboard') +const electron = require('electron') +const $ = etch.dom + +let TextEditorElement + +const DEFAULT_ROWS_PER_TILE = 6 +const NORMAL_WIDTH_CHARACTER = 'x' +const DOUBLE_WIDTH_CHARACTER = '我' +const HALF_WIDTH_CHARACTER = 'ハ' +const KOREAN_CHARACTER = '세' +const NBSP_CHARACTER = '\u00a0' +const ZERO_WIDTH_NBSP_CHARACTER = '\ufeff' +const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40 +const CURSOR_BLINK_RESUME_DELAY = 300 +const CURSOR_BLINK_PERIOD = 800 + +function scaleMouseDragAutoscrollDelta (delta) { + return Math.pow(delta / 3, 3) / 280 +} + +module.exports = +class TextEditorComponent { + static setScheduler (scheduler) { + etch.setScheduler(scheduler) + } + + static didUpdateStyles () { + if (this.attachedComponents) { + this.attachedComponents.forEach((component) => { + component.didUpdateStyles() + }) + } + } + + static didUpdateScrollbarStyles () { + if (this.attachedComponents) { + this.attachedComponents.forEach((component) => { + component.didUpdateScrollbarStyles() + }) + } + } + + constructor (props) { + this.props = props + + if (!props.model) { + props.model = new TextEditor({mini: props.mini}) + } + this.props.model.component = this + + if (props.element) { + this.element = props.element + } else { + if (!TextEditorElement) TextEditorElement = require('./text-editor-element') + this.element = new TextEditorElement() + } + this.element.initialize(this) + this.virtualNode = $('atom-text-editor') + this.virtualNode.domNode = this.element + this.refs = {} + + this.updateSync = this.updateSync.bind(this) + this.didBlurHiddenInput = this.didBlurHiddenInput.bind(this) + this.didFocusHiddenInput = this.didFocusHiddenInput.bind(this) + this.didTextInput = this.didTextInput.bind(this) + this.didKeydown = this.didKeydown.bind(this) + this.didKeyup = this.didKeyup.bind(this) + this.didKeypress = this.didKeypress.bind(this) + this.didCompositionStart = this.didCompositionStart.bind(this) + this.didCompositionUpdate = this.didCompositionUpdate.bind(this) + this.didCompositionEnd = this.didCompositionEnd.bind(this) + + this.updatedSynchronously = this.props.updatedSynchronously + this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this) + this.didMouseDownOnContent = this.didMouseDownOnContent.bind(this) + this.debouncedResumeCursorBlinking = debounce( + this.resumeCursorBlinking.bind(this), + (this.props.cursorBlinkResumeDelay || CURSOR_BLINK_RESUME_DELAY) + ) + this.lineTopIndex = new LineTopIndex() + this.updateScheduled = false + this.suppressUpdates = false + this.hasInitialMeasurements = false + this.measurements = { + lineHeight: 0, + baseCharacterWidth: 0, + doubleWidthCharacterWidth: 0, + halfWidthCharacterWidth: 0, + koreanCharacterWidth: 0, + gutterContainerWidth: 0, + lineNumberGutterWidth: 0, + clientContainerHeight: 0, + clientContainerWidth: 0, + verticalScrollbarWidth: 0, + horizontalScrollbarHeight: 0, + longestLineWidth: 0 + } + this.derivedDimensionsCache = {} + this.visible = false + this.cursorsBlinking = false + this.cursorsBlinkedOff = false + this.nextUpdateOnlyBlinksCursors = null + this.extraLinesToMeasure = null + this.extraRenderedScreenLines = null + this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure + this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions + this.blockDecorationsToMeasure = new Set() + this.lineNodesByScreenLineId = new Map() + this.textNodesByScreenLineId = new Map() + this.overlayComponents = new Set() + this.shouldRenderDummyScrollbars = true + this.remeasureScrollbars = false + this.pendingAutoscroll = null + this.scrollTopPending = false + this.scrollLeftPending = false + this.scrollTop = 0 + this.scrollLeft = 0 + this.previousScrollWidth = 0 + this.previousScrollHeight = 0 + this.lastKeydown = null + this.lastKeydownBeforeKeypress = null + this.accentedCharacterMenuIsOpen = false + this.remeasureGutterDimensions = false + this.guttersToRender = [this.props.model.getLineNumberGutter()] + this.guttersVisibility = [this.guttersToRender[0].visible] + this.idsByTileStartRow = new Map() + this.nextTileId = 0 + this.renderedTileStartRows = [] + this.lineNumbersToRender = { + maxDigits: 2, + bufferRows: [], + keys: [], + softWrappedFlags: [], + foldableFlags: [] + } + this.decorationsToRender = { + lineNumbers: null, + lines: null, + highlights: new Map(), + cursors: [], + overlays: [], + customGutter: new Map(), + blocks: new Map(), + text: [] + } + this.decorationsToMeasure = { + highlights: new Map(), + cursors: new Map() + } + this.textDecorationsByMarker = new Map() + this.textDecorationBoundaries = [] + this.pendingScrollTopRow = this.props.initialScrollTopRow + this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn + + this.measuredContent = false + this.queryGuttersToRender() + this.queryMaxLineNumberDigits() + this.observeBlockDecorations() + this.updateClassList() + etch.updateSync(this) + } + + update (props) { + this.props = props + this.scheduleUpdate() + } + + pixelPositionForScreenPosition ({row, column}) { + const top = this.pixelPositionAfterBlocksForRow(row) + let left = column === 0 ? 0 : this.pixelLeftForRowAndColumn(row, column) + if (left == null) { + this.requestHorizontalMeasurement(row, column) + this.updateSync() + left = this.pixelLeftForRowAndColumn(row, column) + } + return {top, left} + } + + scheduleUpdate (nextUpdateOnlyBlinksCursors = false) { + if (!this.visible) return + if (this.suppressUpdates) return + + this.nextUpdateOnlyBlinksCursors = + this.nextUpdateOnlyBlinksCursors !== false && nextUpdateOnlyBlinksCursors === true + + if (this.updatedSynchronously) { + this.updateSync() + } else if (!this.updateScheduled) { + this.updateScheduled = true + etch.getScheduler().updateDocument(() => { + if (this.updateScheduled) this.updateSync(true) + }) + } + } + + updateSync (useScheduler = false) { + this.updateScheduled = false + + // Don't proceed if we know we are not visible + if (!this.visible) return + + // Don't proceed if we have to pay for a measurement anyway and detect + // that we are no longer visible. + if ((this.remeasureCharacterDimensions || this.remeasureAllBlockDecorations) && !this.isVisible()) { + if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise() + return + } + + const onlyBlinkingCursors = this.nextUpdateOnlyBlinksCursors + this.nextUpdateOnlyBlinksCursors = null + if (useScheduler && onlyBlinkingCursors) { + this.refs.cursorsAndInput.updateCursorBlinkSync(this.cursorsBlinkedOff) + if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise() + return + } + + if (this.remeasureCharacterDimensions) { + const originalLineHeight = this.getLineHeight() + const originalBaseCharacterWidth = this.getBaseCharacterWidth() + const scrollTopRow = this.getScrollTopRow() + const scrollLeftColumn = this.getScrollLeftColumn() + + this.measureCharacterDimensions() + this.measureGutterDimensions() + + if (this.getLineHeight() !== originalLineHeight) { + this.setScrollTopRow(scrollTopRow) + } + if (this.getBaseCharacterWidth() !== originalBaseCharacterWidth) { + this.setScrollLeftColumn(scrollLeftColumn) + } + this.remeasureCharacterDimensions = false + } + + this.measureBlockDecorations() + + this.measuredContent = false + this.updateSyncBeforeMeasuringContent() + if (useScheduler === true) { + const scheduler = etch.getScheduler() + scheduler.readDocument(() => { + this.measureContentDuringUpdateSync() + this.measuredContent = true + scheduler.updateDocument(() => { + this.updateSyncAfterMeasuringContent() + }) + }) + } else { + this.measureContentDuringUpdateSync() + this.measuredContent = true + this.updateSyncAfterMeasuringContent() + } + } + + measureBlockDecorations () { + if (this.remeasureAllBlockDecorations) { + this.remeasureAllBlockDecorations = false + + const decorations = this.props.model.getDecorations() + for (var i = 0; i < decorations.length; i++) { + const decoration = decorations[i] + if (decoration.getProperties().type === 'block') { + this.blockDecorationsToMeasure.add(decoration) + } + } + + // Update the width of the line tiles to ensure block decorations are + // measured with the most recent width. + if (this.blockDecorationsToMeasure.size > 0) { + this.updateSyncBeforeMeasuringContent() + } + } + + if (this.blockDecorationsToMeasure.size > 0) { + const {blockDecorationMeasurementArea} = this.refs + const sentinelElements = new Set() + + blockDecorationMeasurementArea.appendChild(document.createElement('div')) + this.blockDecorationsToMeasure.forEach((decoration) => { + const {item} = decoration.getProperties() + const decorationElement = TextEditor.viewForItem(item) + if (document.contains(decorationElement)) { + const parentElement = decorationElement.parentElement + + if (!decorationElement.previousSibling) { + const sentinelElement = document.createElement('div') + parentElement.insertBefore(sentinelElement, decorationElement) + sentinelElements.add(sentinelElement) + } + + if (!decorationElement.nextSibling) { + const sentinelElement = document.createElement('div') + parentElement.appendChild(sentinelElement) + sentinelElements.add(sentinelElement) + } + + this.didMeasureVisibleBlockDecoration = true + } else { + blockDecorationMeasurementArea.appendChild(decorationElement) + blockDecorationMeasurementArea.appendChild(document.createElement('div')) + } + }) + + this.blockDecorationsToMeasure.forEach((decoration) => { + const {item} = decoration.getProperties() + const decorationElement = TextEditor.viewForItem(item) + const {previousSibling, nextSibling} = decorationElement + const height = nextSibling.getBoundingClientRect().top - previousSibling.getBoundingClientRect().bottom + this.lineTopIndex.resizeBlock(decoration, height) + }) + + sentinelElements.forEach((sentinelElement) => sentinelElement.remove()) + while (blockDecorationMeasurementArea.firstChild) { + blockDecorationMeasurementArea.firstChild.remove() + } + this.blockDecorationsToMeasure.clear() + } + } + + updateSyncBeforeMeasuringContent () { + this.derivedDimensionsCache = {} + this.updateModelSoftWrapColumn() + if (this.pendingAutoscroll) { + const {screenRange, options} = this.pendingAutoscroll + this.autoscrollVertically(screenRange, options) + this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column) + this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column) + } + this.populateVisibleRowRange() + this.populateVisibleTiles() + this.queryScreenLinesToRender() + this.queryLineNumbersToRender() + this.queryGuttersToRender() + this.queryDecorationsToRender() + this.shouldRenderDummyScrollbars = !this.remeasureScrollbars + etch.updateSync(this) + this.updateClassList() + this.shouldRenderDummyScrollbars = true + this.didMeasureVisibleBlockDecoration = false + } + + measureContentDuringUpdateSync () { + if (this.remeasureGutterDimensions) { + this.measureGutterDimensions() + this.remeasureGutterDimensions = false + } + const wasHorizontalScrollbarVisible = this.isHorizontalScrollbarVisible() + + this.extraRenderedScreenLines = this.extraLinesToMeasure + this.extraLinesToMeasure = null + this.measureLongestLineWidth() + this.measureHorizontalPositions() + this.updateAbsolutePositionedDecorations() + + if (this.pendingAutoscroll) { + this.derivedDimensionsCache = {} + const {screenRange, options} = this.pendingAutoscroll + this.autoscrollHorizontally(screenRange, options) + if (!wasHorizontalScrollbarVisible && this.isHorizontalScrollbarVisible()) { + this.autoscrollVertically(screenRange, options) + } + this.pendingAutoscroll = null + } + } + + updateSyncAfterMeasuringContent () { + this.derivedDimensionsCache = {} + etch.updateSync(this) + + this.currentFrameLineNumberGutterProps = null + this.scrollTopPending = false + this.scrollLeftPending = false + if (this.remeasureScrollbars) { + // Flush stored scroll positions to the vertical and the horizontal + // scrollbars. This is because they have just been destroyed and recreated + // as a result of their remeasurement, but we could not assign the scroll + // top while they were initialized because they were not attached to the + // DOM yet. + this.refs.verticalScrollbar.flushScrollPosition() + this.refs.horizontalScrollbar.flushScrollPosition() + + this.measureScrollbarDimensions() + this.remeasureScrollbars = false + etch.updateSync(this) + } + + this.derivedDimensionsCache = {} + if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise() + } + + render () { + const {model} = this.props + const style = {} + + if (!model.getAutoHeight() && !model.getAutoWidth()) { + style.contain = 'size' + } + + let clientContainerHeight = '100%' + let clientContainerWidth = '100%' + if (this.hasInitialMeasurements) { + if (model.getAutoHeight()) { + clientContainerHeight = this.getContentHeight() + if (this.isHorizontalScrollbarVisible()) clientContainerHeight += this.getHorizontalScrollbarHeight() + clientContainerHeight += 'px' + } + if (model.getAutoWidth()) { + style.width = 'min-content' + clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() + if (this.isVerticalScrollbarVisible()) clientContainerWidth += this.getVerticalScrollbarWidth() + clientContainerWidth += 'px' + } else { + style.width = this.element.style.width + } + } + + let attributes = null + if (model.isMini()) { + attributes = {mini: ''} + } + + const dataset = {encoding: model.getEncoding()} + const grammar = model.getGrammar() + if (grammar && grammar.scopeName) { + dataset.grammar = grammar.scopeName.replace(/\./g, ' ') + } + + return $('atom-text-editor', + { + // See this.updateClassList() for construction of the class name + style, + attributes, + dataset, + tabIndex: -1, + on: {mousewheel: this.didMouseWheel} + }, + $.div( + { + ref: 'clientContainer', + style: { + position: 'relative', + contain: 'strict', + overflow: 'hidden', + backgroundColor: 'inherit', + height: clientContainerHeight, + width: clientContainerWidth + } + }, + this.renderGutterContainer(), + this.renderScrollContainer() + ), + this.renderOverlayDecorations() + ) + } + + renderGutterContainer () { + if (this.props.model.isMini()) { + return null + } else { + return $(GutterContainerComponent, { + ref: 'gutterContainer', + key: 'gutterContainer', + rootComponent: this, + hasInitialMeasurements: this.hasInitialMeasurements, + measuredContent: this.measuredContent, + scrollTop: this.getScrollTop(), + scrollHeight: this.getScrollHeight(), + lineNumberGutterWidth: this.getLineNumberGutterWidth(), + lineHeight: this.getLineHeight(), + renderedStartRow: this.getRenderedStartRow(), + renderedEndRow: this.getRenderedEndRow(), + rowsPerTile: this.getRowsPerTile(), + guttersToRender: this.guttersToRender, + decorationsToRender: this.decorationsToRender, + isLineNumberGutterVisible: this.props.model.isLineNumberGutterVisible(), + lineNumbersToRender: this.lineNumbersToRender, + didMeasureVisibleBlockDecoration: this.didMeasureVisibleBlockDecoration + }) + } + } + + renderScrollContainer () { + const style = { + position: 'absolute', + contain: 'strict', + overflow: 'hidden', + top: 0, + bottom: 0, + backgroundColor: 'inherit' + } + + if (this.hasInitialMeasurements) { + style.left = this.getGutterContainerWidth() + 'px' + style.width = this.getScrollContainerWidth() + 'px' + } + + return $.div( + { + ref: 'scrollContainer', + key: 'scrollContainer', + className: 'scroll-view', + style + }, + this.renderContent(), + this.renderDummyScrollbars() + ) + } + + renderContent () { + let children + let style = { + contain: 'strict', + overflow: 'hidden', + backgroundColor: 'inherit' + } + if (this.hasInitialMeasurements) { + style.width = this.getScrollWidth() + 'px' + style.height = this.getScrollHeight() + 'px' + style.willChange = 'transform' + style.transform = `translate(${-this.getScrollLeft()}px, ${-this.getScrollTop()}px)` + children = [ + this.renderCursorsAndInput(), + this.renderLineTiles(), + this.renderBlockDecorationMeasurementArea(), + this.renderCharacterMeasurementLine(), + this.renderPlaceholderText() + ] + } else { + children = [ + this.renderBlockDecorationMeasurementArea(), + this.renderCharacterMeasurementLine() + ] + } + + return $.div( + { + ref: 'content', + on: {mousedown: this.didMouseDownOnContent}, + style + }, + children + ) + } + + renderLineTiles () { + const {lineNodesByScreenLineId, textNodesByScreenLineId} = this + + const startRow = this.getRenderedStartRow() + const endRow = this.getRenderedEndRow() + const rowsPerTile = this.getRowsPerTile() + const tileWidth = this.getScrollWidth() + + const displayLayer = this.props.model.displayLayer + const tileNodes = new Array(this.renderedTileStartRows.length) + + for (let i = 0; i < this.renderedTileStartRows.length; i++) { + const tileStartRow = this.renderedTileStartRows[i] + const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile) + const tileHeight = this.pixelPositionBeforeBlocksForRow(tileEndRow) - this.pixelPositionBeforeBlocksForRow(tileStartRow) + + tileNodes[i] = $(LinesTileComponent, { + key: this.idsByTileStartRow.get(tileStartRow), + measuredContent: this.measuredContent, + height: tileHeight, + width: tileWidth, + top: this.pixelPositionBeforeBlocksForRow(tileStartRow), + lineHeight: this.getLineHeight(), + renderedStartRow: startRow, + tileStartRow, + tileEndRow, + screenLines: this.renderedScreenLines.slice(tileStartRow - startRow, tileEndRow - startRow), + lineDecorations: this.decorationsToRender.lines.slice(tileStartRow - startRow, tileEndRow - startRow), + textDecorations: this.decorationsToRender.text.slice(tileStartRow - startRow, tileEndRow - startRow), + blockDecorations: this.decorationsToRender.blocks.get(tileStartRow), + highlightDecorations: this.decorationsToRender.highlights.get(tileStartRow), + displayLayer, + lineNodesByScreenLineId, + textNodesByScreenLineId + }) + } + + if (this.extraLinesToMeasure) { + this.extraLinesToMeasure.forEach((screenLine, screenRow) => { + if (screenRow < startRow || screenRow >= endRow) { + tileNodes.push($(LineComponent, { + key: 'extra-' + screenLine.id, + screenLine, + screenRow, + displayLayer, + lineNodesByScreenLineId, + textNodesByScreenLineId + })) + } + }) + } + + return $.div({ + key: 'lineTiles', + ref: 'lineTiles', + className: 'lines', + style: { + position: 'absolute', + contain: 'strict', + overflow: 'hidden', + width: this.getScrollWidth() + 'px', + height: this.getScrollHeight() + 'px' + } + }, tileNodes) + } + + renderCursorsAndInput () { + return $(CursorsAndInputComponent, { + ref: 'cursorsAndInput', + key: 'cursorsAndInput', + didBlurHiddenInput: this.didBlurHiddenInput, + didFocusHiddenInput: this.didFocusHiddenInput, + didTextInput: this.didTextInput, + didKeydown: this.didKeydown, + didKeyup: this.didKeyup, + didKeypress: this.didKeypress, + didCompositionStart: this.didCompositionStart, + didCompositionUpdate: this.didCompositionUpdate, + didCompositionEnd: this.didCompositionEnd, + measuredContent: this.measuredContent, + lineHeight: this.getLineHeight(), + scrollHeight: this.getScrollHeight(), + scrollWidth: this.getScrollWidth(), + decorationsToRender: this.decorationsToRender, + cursorsBlinkedOff: this.cursorsBlinkedOff, + hiddenInputPosition: this.hiddenInputPosition + }) + } + + renderPlaceholderText () { + const {model} = this.props + if (model.isEmpty()) { + const placeholderText = model.getPlaceholderText() + if (placeholderText != null) { + return $.div({className: 'placeholder-text'}, placeholderText) + } + } + return null + } + + renderCharacterMeasurementLine () { + return $.div( + { + key: 'characterMeasurementLine', + ref: 'characterMeasurementLine', + className: 'line dummy', + style: {position: 'absolute', visibility: 'hidden'} + }, + $.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER), + $.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER), + $.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER), + $.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER) + ) + } + + renderBlockDecorationMeasurementArea () { + return $.div({ + ref: 'blockDecorationMeasurementArea', + key: 'blockDecorationMeasurementArea', + style: { + contain: 'strict', + position: 'absolute', + visibility: 'hidden' + } + }) + } + + renderDummyScrollbars () { + if (this.shouldRenderDummyScrollbars && !this.props.model.isMini()) { + let scrollHeight, scrollTop, horizontalScrollbarHeight + let scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible + + if (this.hasInitialMeasurements) { + scrollHeight = this.getScrollHeight() + scrollWidth = this.getScrollWidth() + scrollTop = this.getScrollTop() + scrollLeft = this.getScrollLeft() + horizontalScrollbarHeight = + this.isHorizontalScrollbarVisible() + ? this.getHorizontalScrollbarHeight() + : 0 + verticalScrollbarWidth = + this.isVerticalScrollbarVisible() + ? this.getVerticalScrollbarWidth() + : 0 + forceScrollbarVisible = this.remeasureScrollbars + } else { + forceScrollbarVisible = true + } + + const dummyScrollbarVnodes = [ + $(DummyScrollbarComponent, { + ref: 'verticalScrollbar', + orientation: 'vertical', + didScroll: this.didScrollDummyScrollbar, + didMousedown: this.didMouseDownOnContent, + scrollHeight, + scrollTop, + horizontalScrollbarHeight, + forceScrollbarVisible + }), + $(DummyScrollbarComponent, { + ref: 'horizontalScrollbar', + orientation: 'horizontal', + didScroll: this.didScrollDummyScrollbar, + didMousedown: this.didMouseDownOnContent, + scrollWidth, + scrollLeft, + verticalScrollbarWidth, + forceScrollbarVisible + }) + ] + + // If both scrollbars are visible, push a dummy element to force a "corner" + // to render where the two scrollbars meet at the lower right + if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) { + dummyScrollbarVnodes.push($.div( + { + ref: 'scrollbarCorner', + className: 'scrollbar-corner', + style: { + position: 'absolute', + height: '20px', + width: '20px', + bottom: 0, + right: 0, + overflow: 'scroll' + } + } + )) + } + + return dummyScrollbarVnodes + } else { + return null + } + } + + renderOverlayDecorations () { + return this.decorationsToRender.overlays.map((overlayProps) => + $(OverlayComponent, Object.assign( + { + key: overlayProps.element, + overlayComponents: this.overlayComponents, + didResize: () => { this.updateSync() } + }, + overlayProps + )) + ) + } + + // Imperatively manipulate the class list of the root element to avoid + // clearing classes assigned by package authors. + updateClassList () { + const {model} = this.props + + const oldClassList = this.classList + const newClassList = ['editor'] + if (this.focused) newClassList.push('is-focused') + if (model.isMini()) newClassList.push('mini') + for (var i = 0; i < model.selections.length; i++) { + if (!model.selections[i].isEmpty()) { + newClassList.push('has-selection') + break + } + } + + if (oldClassList) { + for (let i = 0; i < oldClassList.length; i++) { + const className = oldClassList[i] + if (!newClassList.includes(className)) { + this.element.classList.remove(className) + } + } + } + + for (let i = 0; i < newClassList.length; i++) { + const className = newClassList[i] + if (!oldClassList || !oldClassList.includes(className)) { + this.element.classList.add(className) + } + } + + this.classList = newClassList + } + + queryScreenLinesToRender () { + const {model} = this.props + + this.renderedScreenLines = model.displayLayer.getScreenLines( + this.getRenderedStartRow(), + this.getRenderedEndRow() + ) + + const longestLineRow = model.getApproximateLongestScreenRow() + const longestLine = model.screenLineForScreenRow(longestLineRow) + if (longestLine !== this.previousLongestLine) { + this.requestExtraLineToMeasure(longestLineRow, longestLine) + this.longestLineToMeasure = longestLine + this.previousLongestLine = longestLine + } + } + + queryLineNumbersToRender () { + const {model} = this.props + if (!model.isLineNumberGutterVisible()) return + + this.queryMaxLineNumberDigits() + + const startRow = this.getRenderedStartRow() + const endRow = this.getRenderedEndRow() + const renderedRowCount = this.getRenderedRowCount() + + const bufferRows = model.bufferRowsForScreenRows(startRow, endRow) + const keys = new Array(renderedRowCount) + const foldableFlags = new Array(renderedRowCount) + const softWrappedFlags = new Array(renderedRowCount) + + let previousBufferRow = (startRow > 0) ? model.bufferRowForScreenRow(startRow - 1) : -1 + let softWrapCount = 0 + for (let row = startRow; row < endRow; row++) { + const i = row - startRow + const bufferRow = bufferRows[i] + if (bufferRow === previousBufferRow) { + softWrapCount++ + softWrappedFlags[i] = true + keys[i] = bufferRow + '-' + softWrapCount + } else { + softWrapCount = 0 + softWrappedFlags[i] = false + keys[i] = bufferRow + } + + const nextBufferRow = bufferRows[i + 1] + if (bufferRow !== nextBufferRow) { + foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow) + } else { + foldableFlags[i] = false + } + + previousBufferRow = bufferRow + } + + // Delete extra buffer row at the end because it's not currently on screen. + bufferRows.pop() + + this.lineNumbersToRender.bufferRows = bufferRows + this.lineNumbersToRender.keys = keys + this.lineNumbersToRender.foldableFlags = foldableFlags + this.lineNumbersToRender.softWrappedFlags = softWrappedFlags + } + + queryMaxLineNumberDigits () { + const {model} = this.props + if (model.isLineNumberGutterVisible()) { + const maxDigits = Math.max(2, model.getLineCount().toString().length) + if (maxDigits !== this.lineNumbersToRender.maxDigits) { + this.remeasureGutterDimensions = true + this.lineNumbersToRender.maxDigits = maxDigits + } + } + } + + renderedScreenLineForRow (row) { + return ( + this.renderedScreenLines[row - this.getRenderedStartRow()] || + (this.extraRenderedScreenLines ? this.extraRenderedScreenLines.get(row) : null) + ) + } + + queryGuttersToRender () { + const oldGuttersToRender = this.guttersToRender + const oldGuttersVisibility = this.guttersVisibility + this.guttersToRender = this.props.model.getGutters() + this.guttersVisibility = this.guttersToRender.map(g => g.visible) + + if (!oldGuttersToRender || oldGuttersToRender.length !== this.guttersToRender.length) { + this.remeasureGutterDimensions = true + } else { + for (let i = 0, length = this.guttersToRender.length; i < length; i++) { + if (this.guttersToRender[i] !== oldGuttersToRender[i] || this.guttersVisibility[i] !== oldGuttersVisibility[i]) { + this.remeasureGutterDimensions = true + break + } + } + } + } + + queryDecorationsToRender () { + this.decorationsToRender.lineNumbers = [] + this.decorationsToRender.lines = [] + this.decorationsToRender.overlays.length = 0 + this.decorationsToRender.customGutter.clear() + this.decorationsToRender.blocks = new Map() + this.decorationsToRender.text = [] + this.decorationsToMeasure.highlights.clear() + this.decorationsToMeasure.cursors.clear() + this.textDecorationsByMarker.clear() + this.textDecorationBoundaries.length = 0 + + const decorationsByMarker = + this.props.model.decorationManager.decorationPropertiesByMarkerForScreenRowRange( + this.getRenderedStartRow(), + this.getRenderedEndRow() + ) + + decorationsByMarker.forEach((decorations, marker) => { + const screenRange = marker.getScreenRange() + const reversed = marker.isReversed() + for (let i = 0; i < decorations.length; i++) { + const decoration = decorations[i] + this.addDecorationToRender(decoration.type, decoration, marker, screenRange, reversed) + } + }) + + this.populateTextDecorationsToRender() + } + + addDecorationToRender (type, decoration, marker, screenRange, reversed) { + if (Array.isArray(type)) { + for (let i = 0, length = type.length; i < length; i++) { + this.addDecorationToRender(type[i], decoration, marker, screenRange, reversed) + } + } else { + switch (type) { + case 'line': + case 'line-number': + this.addLineDecorationToRender(type, decoration, screenRange, reversed) + break + case 'highlight': + this.addHighlightDecorationToMeasure(decoration, screenRange, marker.id) + break + case 'cursor': + this.addCursorDecorationToMeasure(decoration, marker, screenRange, reversed) + break + case 'overlay': + this.addOverlayDecorationToRender(decoration, marker) + break + case 'gutter': + this.addCustomGutterDecorationToRender(decoration, screenRange) + break + case 'block': + this.addBlockDecorationToRender(decoration, screenRange, reversed) + break + case 'text': + this.addTextDecorationToRender(decoration, screenRange, marker) + break + } + } + } + + addLineDecorationToRender (type, decoration, screenRange, reversed) { + const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers + + let omitLastRow = false + if (screenRange.isEmpty()) { + if (decoration.onlyNonEmpty) return + } else { + if (decoration.onlyEmpty) return + if (decoration.omitEmptyLastRow !== false) { + omitLastRow = screenRange.end.column === 0 + } + } + + const renderedStartRow = this.getRenderedStartRow() + let rangeStartRow = screenRange.start.row + let rangeEndRow = screenRange.end.row + + if (decoration.onlyHead) { + if (reversed) { + rangeEndRow = rangeStartRow + } else { + rangeStartRow = rangeEndRow + } + } + + rangeStartRow = Math.max(rangeStartRow, this.getRenderedStartRow()) + rangeEndRow = Math.min(rangeEndRow, this.getRenderedEndRow() - 1) + + for (let row = rangeStartRow; row <= rangeEndRow; row++) { + if (omitLastRow && row === screenRange.end.row) break + const currentClassName = decorationsToRender[row - renderedStartRow] + const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class + decorationsToRender[row - renderedStartRow] = newClassName + } + } + + addHighlightDecorationToMeasure (decoration, screenRange, key) { + screenRange = constrainRangeToRows(screenRange, this.getRenderedStartRow(), this.getRenderedEndRow()) + if (screenRange.isEmpty()) return + + const {class: className, flashRequested, flashClass, flashDuration} = decoration + decoration.flashRequested = false + + let tileStartRow = this.tileStartRowForRow(screenRange.start.row) + const rowsPerTile = this.getRowsPerTile() + + while (tileStartRow <= screenRange.end.row) { + const tileEndRow = tileStartRow + rowsPerTile + const screenRangeInTile = constrainRangeToRows(screenRange, tileStartRow, tileEndRow) + + let tileHighlights = this.decorationsToMeasure.highlights.get(tileStartRow) + if (!tileHighlights) { + tileHighlights = [] + this.decorationsToMeasure.highlights.set(tileStartRow, tileHighlights) + } + + tileHighlights.push({ + screenRange: screenRangeInTile, + key, + className, + flashRequested, + flashClass, + flashDuration + }) + + this.requestHorizontalMeasurement(screenRangeInTile.start.row, screenRangeInTile.start.column) + this.requestHorizontalMeasurement(screenRangeInTile.end.row, screenRangeInTile.end.column) + + tileStartRow = tileStartRow + rowsPerTile + } + } + + addCursorDecorationToMeasure (decoration, marker, screenRange, reversed) { + const {model} = this.props + if (!model.getShowCursorOnSelection() && !screenRange.isEmpty()) return + + let decorationToMeasure = this.decorationsToMeasure.cursors.get(marker) + if (!decorationToMeasure) { + const isLastCursor = model.getLastCursor().getMarker() === marker + const screenPosition = reversed ? screenRange.start : screenRange.end + const {row, column} = screenPosition + + if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return + + this.requestHorizontalMeasurement(row, column) + let columnWidth = 0 + if (model.lineLengthForScreenRow(row) > column) { + columnWidth = 1 + this.requestHorizontalMeasurement(row, column + 1) + } + decorationToMeasure = {screenPosition, columnWidth, isLastCursor} + this.decorationsToMeasure.cursors.set(marker, decorationToMeasure) + } + + if (decoration.class) { + if (decorationToMeasure.className) { + decorationToMeasure.className += ' ' + decoration.class + } else { + decorationToMeasure.className = decoration.class + } + } + + if (decoration.style) { + if (decorationToMeasure.style) { + Object.assign(decorationToMeasure.style, decoration.style) + } else { + decorationToMeasure.style = Object.assign({}, decoration.style) + } + } + } + + addOverlayDecorationToRender (decoration, marker) { + const {class: className, item, position, avoidOverflow} = decoration + const element = TextEditor.viewForItem(item) + const screenPosition = (position === 'tail') + ? marker.getTailScreenPosition() + : marker.getHeadScreenPosition() + + this.requestHorizontalMeasurement(screenPosition.row, screenPosition.column) + this.decorationsToRender.overlays.push({className, element, avoidOverflow, screenPosition}) + } + + addCustomGutterDecorationToRender (decoration, screenRange) { + let decorations = this.decorationsToRender.customGutter.get(decoration.gutterName) + if (!decorations) { + decorations = [] + this.decorationsToRender.customGutter.set(decoration.gutterName, decorations) + } + const top = this.pixelPositionAfterBlocksForRow(screenRange.start.row) + const height = this.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) - top + + decorations.push({ + className: decoration.class, + element: TextEditor.viewForItem(decoration.item), + top, + height + }) + } + + addBlockDecorationToRender (decoration, screenRange, reversed) { + const screenPosition = reversed ? screenRange.start : screenRange.end + const tileStartRow = this.tileStartRowForRow(screenPosition.row) + const screenLine = this.renderedScreenLines[screenPosition.row - this.getRenderedStartRow()] + + let decorationsByScreenLine = this.decorationsToRender.blocks.get(tileStartRow) + if (!decorationsByScreenLine) { + decorationsByScreenLine = new Map() + this.decorationsToRender.blocks.set(tileStartRow, decorationsByScreenLine) + } + + let decorations = decorationsByScreenLine.get(screenLine.id) + if (!decorations) { + decorations = [] + decorationsByScreenLine.set(screenLine.id, decorations) + } + decorations.push(decoration) + } + + addTextDecorationToRender (decoration, screenRange, marker) { + if (screenRange.isEmpty()) return + + let decorationsForMarker = this.textDecorationsByMarker.get(marker) + if (!decorationsForMarker) { + decorationsForMarker = [] + this.textDecorationsByMarker.set(marker, decorationsForMarker) + this.textDecorationBoundaries.push({position: screenRange.start, starting: [marker]}) + this.textDecorationBoundaries.push({position: screenRange.end, ending: [marker]}) + } + decorationsForMarker.push(decoration) + } + + populateTextDecorationsToRender () { + // Sort all boundaries in ascending order of position + this.textDecorationBoundaries.sort((a, b) => a.position.compare(b.position)) + + // Combine adjacent boundaries with the same position + for (let i = 0; i < this.textDecorationBoundaries.length;) { + const boundary = this.textDecorationBoundaries[i] + const nextBoundary = this.textDecorationBoundaries[i + 1] + if (nextBoundary && nextBoundary.position.isEqual(boundary.position)) { + if (nextBoundary.starting) { + if (boundary.starting) { + boundary.starting.push(...nextBoundary.starting) + } else { + boundary.starting = nextBoundary.starting + } + } + + if (nextBoundary.ending) { + if (boundary.ending) { + boundary.ending.push(...nextBoundary.ending) + } else { + boundary.ending = nextBoundary.ending + } + } + + this.textDecorationBoundaries.splice(i + 1, 1) + } else { + i++ + } + } + + const renderedStartRow = this.getRenderedStartRow() + const renderedEndRow = this.getRenderedEndRow() + const containingMarkers = [] + + // Iterate over boundaries to build up text decorations. + for (let i = 0; i < this.textDecorationBoundaries.length; i++) { + const boundary = this.textDecorationBoundaries[i] + + // If multiple markers start here, sort them by order of nesting (markers ending later come first) + if (boundary.starting && boundary.starting.length > 1) { + boundary.starting.sort((a, b) => a.compare(b)) + } + + // If multiple markers start here, sort them by order of nesting (markers starting earlier come first) + if (boundary.ending && boundary.ending.length > 1) { + boundary.ending.sort((a, b) => b.compare(a)) + } + + // Remove markers ending here from containing markers array + if (boundary.ending) { + for (let j = boundary.ending.length - 1; j >= 0; j--) { + containingMarkers.splice(containingMarkers.lastIndexOf(boundary.ending[j]), 1) + } + } + // Add markers starting here to containing markers array + if (boundary.starting) containingMarkers.push(...boundary.starting) + + // Determine desired className and style based on containing markers + let className, style + for (let j = 0; j < containingMarkers.length; j++) { + const marker = containingMarkers[j] + const decorations = this.textDecorationsByMarker.get(marker) + for (let k = 0; k < decorations.length; k++) { + const decoration = decorations[k] + if (decoration.class) { + if (className) { + className += ' ' + decoration.class + } else { + className = decoration.class + } + } + if (decoration.style) { + if (style) { + Object.assign(style, decoration.style) + } else { + style = Object.assign({}, decoration.style) + } + } + } + } + + // Add decoration start with className/style for current position's column, + // and also for the start of every row up until the next decoration boundary + if (boundary.position.row >= renderedStartRow) { + this.addTextDecorationStart(boundary.position.row, boundary.position.column, className, style) + } + const nextBoundary = this.textDecorationBoundaries[i + 1] + if (nextBoundary) { + let row = Math.max(boundary.position.row + 1, renderedStartRow) + const endRow = Math.min(nextBoundary.position.row, renderedEndRow) + for (; row < endRow; row++) { + this.addTextDecorationStart(row, 0, className, style) + } + + if (row === nextBoundary.position.row && nextBoundary.position.column !== 0) { + this.addTextDecorationStart(row, 0, className, style) + } + } + } + } + + addTextDecorationStart (row, column, className, style) { + const renderedStartRow = this.getRenderedStartRow() + let decorationStarts = this.decorationsToRender.text[row - renderedStartRow] + if (!decorationStarts) { + decorationStarts = [] + this.decorationsToRender.text[row - renderedStartRow] = decorationStarts + } + decorationStarts.push({column, className, style}) + } + + updateAbsolutePositionedDecorations () { + this.updateHighlightsToRender() + this.updateCursorsToRender() + this.updateOverlaysToRender() + } + + updateHighlightsToRender () { + this.decorationsToRender.highlights.clear() + this.decorationsToMeasure.highlights.forEach((highlights, tileRow) => { + for (let i = 0, length = highlights.length; i < length; i++) { + const highlight = highlights[i] + const {start, end} = highlight.screenRange + highlight.startPixelTop = this.pixelPositionAfterBlocksForRow(start.row) + highlight.startPixelLeft = this.pixelLeftForRowAndColumn(start.row, start.column) + highlight.endPixelTop = this.pixelPositionBeforeBlocksForRow(end.row + 1) + highlight.endPixelLeft = this.pixelLeftForRowAndColumn(end.row, end.column) + } + this.decorationsToRender.highlights.set(tileRow, highlights) + }) + } + + updateCursorsToRender () { + this.decorationsToRender.cursors.length = 0 + + this.decorationsToMeasure.cursors.forEach((cursor) => { + const {screenPosition, className, style} = cursor + const {row, column} = screenPosition + + const pixelTop = this.pixelPositionAfterBlocksForRow(row) + const pixelLeft = this.pixelLeftForRowAndColumn(row, column) + let pixelWidth + if (cursor.columnWidth === 0) { + pixelWidth = this.getBaseCharacterWidth() + } else { + pixelWidth = this.pixelLeftForRowAndColumn(row, column + 1) - pixelLeft + } + + const cursorPosition = {pixelTop, pixelLeft, pixelWidth, className, style} + this.decorationsToRender.cursors.push(cursorPosition) + if (cursor.isLastCursor) this.hiddenInputPosition = cursorPosition + }) + } + + updateOverlaysToRender () { + const overlayCount = this.decorationsToRender.overlays.length + if (overlayCount === 0) return null + + const windowInnerHeight = this.getWindowInnerHeight() + const windowInnerWidth = this.getWindowInnerWidth() + const contentClientRect = this.refs.content.getBoundingClientRect() + for (let i = 0; i < overlayCount; i++) { + const decoration = this.decorationsToRender.overlays[i] + const {element, screenPosition, avoidOverflow} = decoration + const {row, column} = screenPosition + let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight() + let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column) + + if (avoidOverflow !== false) { + const computedStyle = window.getComputedStyle(element) + const elementHeight = element.offsetHeight + const elementTop = wrapperTop + parseInt(computedStyle.marginTop) + const elementBottom = elementTop + elementHeight + const flippedElementTop = wrapperTop - this.getLineHeight() - elementHeight - parseInt(computedStyle.marginBottom) + const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft) + const elementRight = elementLeft + element.offsetWidth + + if (elementBottom > windowInnerHeight && flippedElementTop >= 0) { + wrapperTop -= (elementTop - flippedElementTop) + } + if (elementLeft < 0) { + wrapperLeft -= elementLeft + } else if (elementRight > windowInnerWidth) { + wrapperLeft -= (elementRight - windowInnerWidth) + } + } + + decoration.pixelTop = wrapperTop + decoration.pixelLeft = wrapperLeft + } + } + + didAttach () { + if (!this.attached) { + this.attached = true + this.intersectionObserver = new IntersectionObserver((entries) => { + const {intersectionRect} = entries[entries.length - 1] + if (intersectionRect.width > 0 || intersectionRect.height > 0) { + this.didShow() + } else { + this.didHide() + } + }) + this.intersectionObserver.observe(this.element) + + this.resizeObserver = new ResizeObserver(this.didResize.bind(this)) + this.resizeObserver.observe(this.element) + + if (this.refs.gutterContainer) { + this.gutterContainerResizeObserver = new ResizeObserver(this.didResizeGutterContainer.bind(this)) + this.gutterContainerResizeObserver.observe(this.refs.gutterContainer.element) + } + + this.overlayComponents.forEach((component) => component.didAttach()) + + if (this.isVisible()) { + this.didShow() + } else { + this.didHide() + } + if (!this.constructor.attachedComponents) { + this.constructor.attachedComponents = new Set() + } + this.constructor.attachedComponents.add(this) + } + } + + didDetach () { + if (this.attached) { + this.intersectionObserver.disconnect() + this.resizeObserver.disconnect() + if (this.gutterContainerResizeObserver) this.gutterContainerResizeObserver.disconnect() + this.overlayComponents.forEach((component) => component.didDetach()) + + this.didHide() + this.attached = false + this.constructor.attachedComponents.delete(this) + } + } + + didShow () { + if (!this.visible && this.isVisible()) { + if (!this.hasInitialMeasurements) this.measureDimensions() + this.visible = true + this.props.model.setVisible(true) + this.updateSync() + this.flushPendingLogicalScrollPosition() + } + } + + didHide () { + if (this.visible) { + this.visible = false + this.props.model.setVisible(false) + } + } + + // Called by TextEditorElement so that focus events can be handled before + // the element is attached to the DOM. + didFocus () { + // This element can be focused from a parent custom element's + // attachedCallback before *its* attachedCallback is fired. This protects + // against that case. + if (!this.attached) this.didAttach() + + // The element can be focused before the intersection observer detects that + // it has been shown for the first time. If this element is being focused, + // it is necessarily visible, so we call `didShow` to ensure the hidden + // input is rendered before we try to shift focus to it. + if (!this.visible) this.didShow() + + if (!this.focused) { + this.focused = true + this.startCursorBlinking() + this.scheduleUpdate() + } + + // Transfer focus to the hidden input, but first ensure the input is in the + // visible part of the scrolled content to avoid the browser trying to + // auto-scroll to the form-field. + const {hiddenInput} = this.refs.cursorsAndInput.refs + hiddenInput.style.top = this.getScrollTop() + 'px' + hiddenInput.style.left = this.getScrollLeft() + 'px' + + hiddenInput.focus() + + // Restore the previous position of the field now that it is already focused + // and won't cause unwanted scrolling. + if (this.hiddenInputPosition) { + hiddenInput.style.top = this.hiddenInputPosition.pixelTop + 'px' + hiddenInput.style.left = this.hiddenInputPosition.pixelLeft + 'px' + } else { + hiddenInput.style.top = 0 + hiddenInput.style.left = 0 + } + } + + // Called by TextEditorElement so that this function is always the first + // listener to be fired, even if other listeners are bound before creating + // the component. + didBlur (event) { + const {cursorsAndInput} = this.refs + if (cursorsAndInput && event.relatedTarget === cursorsAndInput.refs.hiddenInput) { + event.stopImmediatePropagation() + } + } + + didBlurHiddenInput (event) { + if (this.element !== event.relatedTarget && !this.element.contains(event.relatedTarget)) { + this.focused = false + this.stopCursorBlinking() + this.scheduleUpdate() + this.element.dispatchEvent(new FocusEvent(event.type, event)) + } + } + + didFocusHiddenInput () { + if (!this.focused) { + this.focused = true + this.startCursorBlinking() + this.scheduleUpdate() + } + } + + didMouseWheel (event) { + const scrollSensitivity = this.props.mouseWheelScrollSensitivity || 0.8 + + let {deltaX, deltaY} = event + deltaX = deltaX * scrollSensitivity + deltaY = deltaY * scrollSensitivity + + if (this.getPlatform() !== 'darwin' && event.shiftKey) { + let temp = deltaX + deltaX = deltaY + deltaY = temp + } + + const scrollPositionChanged = + this.setScrollLeft(this.getScrollLeft() + deltaX) || + this.setScrollTop(this.getScrollTop() + deltaY) + + if (scrollPositionChanged) this.updateSync() + } + + didResize () { + // Prevent the component from measuring the client container dimensions when + // getting spurious resize events. + if (this.isVisible()) { + const clientContainerWidthChanged = this.measureClientContainerWidth() + const clientContainerHeightChanged = this.measureClientContainerHeight() + if (clientContainerWidthChanged || clientContainerHeightChanged) { + if (clientContainerWidthChanged) { + this.remeasureAllBlockDecorations = true + } + + this.resizeObserver.disconnect() + this.scheduleUpdate() + process.nextTick(() => { this.resizeObserver.observe(this.element) }) + } + } + } + + didResizeGutterContainer () { + // Prevent the component from measuring the gutter dimensions when getting + // spurious resize events. + if (this.isVisible() && this.measureGutterDimensions()) { + this.gutterContainerResizeObserver.disconnect() + this.scheduleUpdate() + process.nextTick(() => { this.gutterContainerResizeObserver.observe(this.refs.gutterContainer.element) }) + } + } + + didScrollDummyScrollbar () { + let scrollTopChanged = false + let scrollLeftChanged = false + if (!this.scrollTopPending) { + scrollTopChanged = this.setScrollTop(this.refs.verticalScrollbar.element.scrollTop) + } + if (!this.scrollLeftPending) { + scrollLeftChanged = this.setScrollLeft(this.refs.horizontalScrollbar.element.scrollLeft) + } + if (scrollTopChanged || scrollLeftChanged) this.updateSync() + } + + didUpdateStyles () { + this.remeasureCharacterDimensions = true + this.horizontalPixelPositionsByScreenLineId.clear() + this.scheduleUpdate() + } + + didUpdateScrollbarStyles () { + if (!this.props.model.isMini()) { + this.remeasureScrollbars = true + this.scheduleUpdate() + } + } + + didTextInput (event) { + if (!this.isInputEnabled()) return + + event.stopPropagation() + + // WARNING: If we call preventDefault on the input of a space character, + // then the browser interprets the spacebar keypress as a page-down command, + // causing spaces to scroll elements containing editors. This is impossible + // to test. + if (event.data !== ' ') event.preventDefault() + + if (this.compositionCheckpoint) { + this.props.model.revertToCheckpoint(this.compositionCheckpoint) + this.compositionCheckpoint = null + } + + // If the input event is fired while the accented character menu is open it + // means that the user has chosen one of the accented alternatives. Thus, we + // will replace the original non accented character with the selected + // alternative. + if (this.accentedCharacterMenuIsOpen) { + this.props.model.selectLeft() + } + + this.props.model.insertText(event.data, {groupUndo: true}) + } + + // We need to get clever to detect when the accented character menu is + // opened on macOS. Usually, every keydown event that could cause input is + // followed by a corresponding keypress. However, pressing and holding + // long enough to open the accented character menu causes additional keydown + // events to fire that aren't followed by their own keypress and textInput + // events. + // + // Therefore, we assume the accented character menu has been deployed if, + // before observing any keyup event, we observe events in the following + // sequence: + // + // keydown(code: X), keypress, keydown(code: X) + // + // The code X must be the same in the keydown events that bracket the + // keypress, meaning we're *holding* the _same_ key we intially pressed. + // Got that? + didKeydown (event) { + if (this.lastKeydownBeforeKeypress != null) { + if (this.lastKeydownBeforeKeypress.code === event.code) { + this.accentedCharacterMenuIsOpen = true + } + + this.lastKeydownBeforeKeypress = null + } + + this.lastKeydown = event + } + + didKeypress (event) { + this.lastKeydownBeforeKeypress = this.lastKeydown + this.lastKeydown = null + + // This cancels the accented character behavior if we type a key normally + // with the menu open. + this.accentedCharacterMenuIsOpen = false + } + + didKeyup (event) { + if (this.lastKeydownBeforeKeypress && this.lastKeydownBeforeKeypress.code === event.code) { + this.lastKeydownBeforeKeypress = null + this.lastKeydown = null + } + } + + // The IME composition events work like this: + // + // User types 's', chromium pops up the completion helper + // 1. compositionstart fired + // 2. compositionupdate fired; event.data == 's' + // User hits arrow keys to move around in completion helper + // 3. compositionupdate fired; event.data == 's' for each arry key press + // User escape to cancel OR User chooses a completion + // 4. compositionend fired + // 5. textInput fired; event.data == the completion string + didCompositionStart () { + this.compositionCheckpoint = this.props.model.createCheckpoint() + if (this.accentedCharacterMenuIsOpen) { + this.props.model.selectLeft() + } + } + + didCompositionUpdate (event) { + this.props.model.insertText(event.data, {select: true}) + } + + didCompositionEnd (event) { + event.target.value = '' + } + + didMouseDownOnContent (event) { + const {model} = this.props + const {target, button, detail, ctrlKey, shiftKey, metaKey} = event + + // Only handle mousedown events for left mouse button (or the middle mouse + // button on Linux where it pastes the selection clipboard). + if (!(button === 0 || (this.getPlatform() === 'linux' && button === 1))) return + + const screenPosition = this.screenPositionForMouseEvent(event) + + if (target && target.matches('.fold-marker')) { + const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) + model.destroyFoldsIntersectingBufferRange(Range(bufferPosition, bufferPosition)) + return + } + + // Handle middle mouse button only on Linux (paste clipboard) + if (this.getPlatform() === 'linux' && button === 1) { + const selection = clipboard.readText('selection') + model.setCursorScreenPosition(screenPosition, {autoscroll: false}) + model.insertText(selection) + return + } + + const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin') + + switch (detail) { + case 1: + if (addOrRemoveSelection) { + const existingSelection = model.getSelectionAtScreenPosition(screenPosition) + if (existingSelection) { + if (model.hasMultipleCursors()) existingSelection.destroy() + } else { + model.addCursorAtScreenPosition(screenPosition) + } + } else { + if (shiftKey) { + model.selectToScreenPosition(screenPosition) + } else { + model.setCursorScreenPosition(screenPosition) + } + } + break + case 2: + if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition) + model.getLastSelection().selectWord({autoscroll: false}) + break + case 3: + if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition) + model.getLastSelection().selectLine(null, {autoscroll: false}) + break + } + + this.handleMouseDragUntilMouseUp({ + didDrag: (event) => { + this.autoscrollOnMouseDrag(event) + const screenPosition = this.screenPositionForMouseEvent(event) + model.selectToScreenPosition(screenPosition, {suppressSelectionMerge: true, autoscroll: false}) + this.updateSync() + }, + didStopDragging: () => { + model.finalizeSelections() + model.mergeIntersectingSelections() + this.updateSync() + } + }) + } + + didMouseDownOnLineNumberGutter (event) { + const {model} = this.props + const {target, button, ctrlKey, shiftKey, metaKey} = event + + // Only handle mousedown events for left mouse button + if (button !== 0) return + + const clickedScreenRow = this.screenPositionForMouseEvent(event).row + const startBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, 0]).row + + if (target && (target.matches('.foldable .icon-right') || target.matches('.folded .icon-right'))) { + model.toggleFoldAtBufferRow(startBufferRow) + return + } + + const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin') + const endBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, Infinity]).row + const clickedLineBufferRange = Range(Point(startBufferRow, 0), Point(endBufferRow + 1, 0)) + + let initialBufferRange + if (shiftKey) { + const lastSelection = model.getLastSelection() + initialBufferRange = lastSelection.getBufferRange() + lastSelection.setBufferRange(initialBufferRange.union(clickedLineBufferRange), { + reversed: clickedScreenRow < lastSelection.getScreenRange().start.row, + autoscroll: false, + preserveFolds: true, + suppressSelectionMerge: true + }) + } else { + initialBufferRange = clickedLineBufferRange + if (addOrRemoveSelection) { + model.addSelectionForBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true}) + } else { + model.setSelectedBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true}) + } + } + + const initialScreenRange = model.screenRangeForBufferRange(initialBufferRange) + this.handleMouseDragUntilMouseUp({ + didDrag: (event) => { + this.autoscrollOnMouseDrag(event, true) + const dragRow = this.screenPositionForMouseEvent(event).row + const draggedLineScreenRange = Range(Point(dragRow, 0), Point(dragRow + 1, 0)) + model.getLastSelection().setScreenRange(draggedLineScreenRange.union(initialScreenRange), { + reversed: dragRow < initialScreenRange.start.row, + autoscroll: false, + preserveFolds: true + }) + this.updateSync() + }, + didStopDragging: () => { + model.mergeIntersectingSelections() + this.updateSync() + } + }) + } + + handleMouseDragUntilMouseUp ({didDrag, didStopDragging}) { + let dragging = false + let lastMousemoveEvent + + const animationFrameLoop = () => { + window.requestAnimationFrame(() => { + if (dragging && this.visible) { + didDrag(lastMousemoveEvent) + animationFrameLoop() + } + }) + } + + function didMouseMove (event) { + lastMousemoveEvent = event + if (!dragging) { + dragging = true + animationFrameLoop() + } + } + + function didMouseUp () { + window.removeEventListener('mousemove', didMouseMove) + window.removeEventListener('mouseup', didMouseUp) + if (dragging) { + dragging = false + didStopDragging() + } + } + + window.addEventListener('mousemove', didMouseMove) + window.addEventListener('mouseup', didMouseUp) + } + + autoscrollOnMouseDrag ({clientX, clientY}, verticalOnly = false) { + var {top, bottom, left, right} = this.refs.scrollContainer.getBoundingClientRect() // Using var to avoid deopt on += assignments below + top += MOUSE_DRAG_AUTOSCROLL_MARGIN + bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN + left += MOUSE_DRAG_AUTOSCROLL_MARGIN + right -= MOUSE_DRAG_AUTOSCROLL_MARGIN + + let yDelta, yDirection + if (clientY < top) { + yDelta = top - clientY + yDirection = -1 + } else if (clientY > bottom) { + yDelta = clientY - bottom + yDirection = 1 + } + + let xDelta, xDirection + if (clientX < left) { + xDelta = left - clientX + xDirection = -1 + } else if (clientX > right) { + xDelta = clientX - right + xDirection = 1 + } + + let scrolled = false + if (yDelta != null) { + const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection + scrolled = this.setScrollTop(this.getScrollTop() + scaledDelta) + } + + if (!verticalOnly && xDelta != null) { + const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection + scrolled = this.setScrollLeft(this.getScrollLeft() + scaledDelta) + } + + if (scrolled) this.updateSync() + } + + screenPositionForMouseEvent (event) { + return this.screenPositionForPixelPosition(this.pixelPositionForMouseEvent(event)) + } + + pixelPositionForMouseEvent ({clientX, clientY}) { + const scrollContainerRect = this.refs.scrollContainer.getBoundingClientRect() + clientX = Math.min(scrollContainerRect.right, Math.max(scrollContainerRect.left, clientX)) + clientY = Math.min(scrollContainerRect.bottom, Math.max(scrollContainerRect.top, clientY)) + const linesRect = this.refs.lineTiles.getBoundingClientRect() + return { + top: clientY - linesRect.top, + left: clientX - linesRect.left + } + } + + didUpdateSelections () { + this.pauseCursorBlinking() + this.scheduleUpdate() + } + + pauseCursorBlinking () { + this.stopCursorBlinking() + this.debouncedResumeCursorBlinking() + } + + resumeCursorBlinking () { + this.cursorsBlinkedOff = true + this.startCursorBlinking() + } + + stopCursorBlinking () { + if (this.cursorsBlinking) { + this.cursorsBlinkedOff = false + this.cursorsBlinking = false + window.clearInterval(this.cursorBlinkIntervalHandle) + this.cursorBlinkIntervalHandle = null + this.scheduleUpdate() + } + } + + startCursorBlinking () { + if (!this.cursorsBlinking) { + this.cursorBlinkIntervalHandle = window.setInterval(() => { + this.cursorsBlinkedOff = !this.cursorsBlinkedOff + this.scheduleUpdate(true) + }, (this.props.cursorBlinkPeriod || CURSOR_BLINK_PERIOD) / 2) + this.cursorsBlinking = true + this.scheduleUpdate(true) + } + } + + didRequestAutoscroll (autoscroll) { + this.pendingAutoscroll = autoscroll + this.scheduleUpdate() + } + + flushPendingLogicalScrollPosition () { + let changedScrollTop = false + if (this.pendingScrollTopRow > 0) { + changedScrollTop = this.setScrollTopRow(this.pendingScrollTopRow, false) + this.pendingScrollTopRow = null + } + + let changedScrollLeft = false + if (this.pendingScrollLeftColumn > 0) { + changedScrollLeft = this.setScrollLeftColumn(this.pendingScrollLeftColumn, false) + this.pendingScrollLeftColumn = null + } + + if (changedScrollTop || changedScrollLeft) { + this.updateSync() + } + } + + autoscrollVertically (screenRange, options) { + const screenRangeTop = this.pixelPositionAfterBlocksForRow(screenRange.start.row) + const screenRangeBottom = this.pixelPositionAfterBlocksForRow(screenRange.end.row) + this.getLineHeight() + const verticalScrollMargin = this.getVerticalAutoscrollMargin() + + let desiredScrollTop, desiredScrollBottom + if (options && options.center) { + const desiredScrollCenter = (screenRangeTop + screenRangeBottom) / 2 + if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) { + desiredScrollTop = desiredScrollCenter - this.getScrollContainerClientHeight() / 2 + desiredScrollBottom = desiredScrollCenter + this.getScrollContainerClientHeight() / 2 + } + } else { + desiredScrollTop = screenRangeTop - verticalScrollMargin + desiredScrollBottom = screenRangeBottom + verticalScrollMargin + } + + if (!options || options.reversed !== false) { + if (desiredScrollBottom > this.getScrollBottom()) { + this.setScrollBottom(desiredScrollBottom) + } + if (desiredScrollTop < this.getScrollTop()) { + this.setScrollTop(desiredScrollTop) + } + } else { + if (desiredScrollTop < this.getScrollTop()) { + this.setScrollTop(desiredScrollTop) + } + if (desiredScrollBottom > this.getScrollBottom()) { + this.setScrollBottom(desiredScrollBottom) + } + } + + return false + } + + autoscrollHorizontally (screenRange, options) { + const horizontalScrollMargin = this.getHorizontalAutoscrollMargin() + + const gutterContainerWidth = this.getGutterContainerWidth() + let left = this.pixelLeftForRowAndColumn(screenRange.start.row, screenRange.start.column) + gutterContainerWidth + let right = this.pixelLeftForRowAndColumn(screenRange.end.row, screenRange.end.column) + gutterContainerWidth + const desiredScrollLeft = Math.max(0, left - horizontalScrollMargin - gutterContainerWidth) + const desiredScrollRight = Math.min(this.getScrollWidth(), right + horizontalScrollMargin) + + if (!options || options.reversed !== false) { + if (desiredScrollRight > this.getScrollRight()) { + this.setScrollRight(desiredScrollRight) + } + if (desiredScrollLeft < this.getScrollLeft()) { + this.setScrollLeft(desiredScrollLeft) + } + } else { + if (desiredScrollLeft < this.getScrollLeft()) { + this.setScrollLeft(desiredScrollLeft) + } + if (desiredScrollRight > this.getScrollRight()) { + this.setScrollRight(desiredScrollRight) + } + } + } + + getVerticalAutoscrollMargin () { + const maxMarginInLines = Math.floor( + (this.getScrollContainerClientHeight() / this.getLineHeight() - 1) / 2 + ) + const marginInLines = Math.min( + this.props.model.verticalScrollMargin, + maxMarginInLines + ) + return marginInLines * this.getLineHeight() + } + + getHorizontalAutoscrollMargin () { + const maxMarginInBaseCharacters = Math.floor( + (this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() - 1) / 2 + ) + const marginInBaseCharacters = Math.min( + this.props.model.horizontalScrollMargin, + maxMarginInBaseCharacters + ) + return marginInBaseCharacters * this.getBaseCharacterWidth() + } + + updateModelSoftWrapColumn () { + const {model} = this.props + const newEditorWidthInChars = this.getScrollContainerClientWidthInBaseCharacters() + if (newEditorWidthInChars !== model.getEditorWidthInChars()) { + this.suppressUpdates = true + this.props.model.setEditorWidthInChars(newEditorWidthInChars) + // Wrapping may cause a vertical scrollbar to appear, which will change the width again. + this.props.model.setEditorWidthInChars(this.getScrollContainerClientWidthInBaseCharacters()) + this.suppressUpdates = false + } + } + + // This method exists because it existed in the previous implementation and some + // package tests relied on it + measureDimensions () { + this.measureCharacterDimensions() + this.measureGutterDimensions() + this.measureClientContainerHeight() + this.measureClientContainerWidth() + this.measureScrollbarDimensions() + this.hasInitialMeasurements = true + } + + measureCharacterDimensions () { + this.measurements.lineHeight = this.refs.characterMeasurementLine.getBoundingClientRect().height + this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width + this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width + this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width + this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().width + + this.props.model.setLineHeightInPixels(this.measurements.lineHeight) + this.props.model.setDefaultCharWidth( + this.measurements.baseCharacterWidth, + this.measurements.doubleWidthCharacterWidth, + this.measurements.halfWidthCharacterWidth, + this.measurements.koreanCharacterWidth + ) + this.lineTopIndex.setDefaultLineHeight(this.measurements.lineHeight) + } + + measureGutterDimensions () { + let dimensionsChanged = false + + if (this.refs.gutterContainer) { + const gutterContainerWidth = this.refs.gutterContainer.element.offsetWidth + if (gutterContainerWidth !== this.measurements.gutterContainerWidth) { + dimensionsChanged = true + this.measurements.gutterContainerWidth = gutterContainerWidth + } + } else { + this.measurements.gutterContainerWidth = 0 + } + + if (this.refs.gutterContainer && this.refs.gutterContainer.refs.lineNumberGutter) { + const lineNumberGutterWidth = this.refs.gutterContainer.refs.lineNumberGutter.element.offsetWidth + if (lineNumberGutterWidth !== this.measurements.lineNumberGutterWidth) { + dimensionsChanged = true + this.measurements.lineNumberGutterWidth = lineNumberGutterWidth + } + } else { + this.measurements.lineNumberGutterWidth = 0 + } + + return dimensionsChanged + } + + measureClientContainerHeight () { + const clientContainerHeight = this.refs.clientContainer.offsetHeight + if (clientContainerHeight !== this.measurements.clientContainerHeight) { + this.measurements.clientContainerHeight = clientContainerHeight + return true + } else { + return false + } + } + + measureClientContainerWidth () { + const clientContainerWidth = this.refs.clientContainer.offsetWidth + if (clientContainerWidth !== this.measurements.clientContainerWidth) { + this.measurements.clientContainerWidth = clientContainerWidth + return true + } else { + return false + } + } + + measureScrollbarDimensions () { + if (this.props.model.isMini()) { + this.measurements.verticalScrollbarWidth = 0 + this.measurements.horizontalScrollbarHeight = 0 + } else { + this.measurements.verticalScrollbarWidth = this.refs.verticalScrollbar.getRealScrollbarWidth() + this.measurements.horizontalScrollbarHeight = this.refs.horizontalScrollbar.getRealScrollbarHeight() + } + } + + measureLongestLineWidth () { + if (this.longestLineToMeasure) { + this.measurements.longestLineWidth = this.lineNodesByScreenLineId.get(this.longestLineToMeasure.id).firstChild.offsetWidth + this.longestLineToMeasure = null + } + } + + requestExtraLineToMeasure (row, screenLine) { + if (!this.extraLinesToMeasure) this.extraLinesToMeasure = new Map() + this.extraLinesToMeasure.set(row, screenLine) + } + + requestHorizontalMeasurement (row, column) { + if (column === 0) return + + if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) { + this.requestExtraLineToMeasure(row, this.props.model.screenLineForScreenRow(row)) + } + + let columns = this.horizontalPositionsToMeasure.get(row) + if (columns == null) { + columns = [] + this.horizontalPositionsToMeasure.set(row, columns) + } + columns.push(column) + } + + measureHorizontalPositions () { + this.horizontalPositionsToMeasure.forEach((columnsToMeasure, row) => { + columnsToMeasure.sort((a, b) => a - b) + + const screenLine = this.renderedScreenLineForRow(row) + const lineNode = this.lineNodesByScreenLineId.get(screenLine.id) + + if (!lineNode) { + const error = new Error('Requested measurement of a line that is not currently rendered') + error.metadata = {row, columnsToMeasure} + throw error + } + + const textNodes = this.textNodesByScreenLineId.get(screenLine.id) + let positionsForLine = this.horizontalPixelPositionsByScreenLineId.get(screenLine.id) + if (positionsForLine == null) { + positionsForLine = new Map() + this.horizontalPixelPositionsByScreenLineId.set(screenLine.id, positionsForLine) + } + + this.measureHorizontalPositionsOnLine(lineNode, textNodes, columnsToMeasure, positionsForLine) + }) + this.horizontalPositionsToMeasure.clear() + } + + measureHorizontalPositionsOnLine (lineNode, textNodes, columnsToMeasure, positions) { + let lineNodeClientLeft = -1 + let textNodeStartColumn = 0 + let textNodesIndex = 0 + let lastTextNodeRight = null + + columnLoop: // eslint-disable-line no-labels + for (let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++) { + const nextColumnToMeasure = columnsToMeasure[columnsIndex] + while (textNodesIndex < textNodes.length) { + if (nextColumnToMeasure === 0) { + positions.set(0, 0) + continue columnLoop // eslint-disable-line no-labels + } + + if (positions.has(nextColumnToMeasure)) continue columnLoop // eslint-disable-line no-labels + const textNode = textNodes[textNodesIndex] + const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length + + if (nextColumnToMeasure < textNodeEndColumn) { + let clientPixelPosition + if (nextColumnToMeasure === textNodeStartColumn) { + clientPixelPosition = clientRectForRange(textNode, 0, 1).left + } else { + clientPixelPosition = clientRectForRange(textNode, 0, nextColumnToMeasure - textNodeStartColumn).right + } + + if (lineNodeClientLeft === -1) { + lineNodeClientLeft = lineNode.getBoundingClientRect().left + } + + positions.set(nextColumnToMeasure, Math.round(clientPixelPosition - lineNodeClientLeft)) + continue columnLoop // eslint-disable-line no-labels + } else { + textNodesIndex++ + textNodeStartColumn = textNodeEndColumn + } + } + + if (lastTextNodeRight == null) { + const lastTextNode = textNodes[textNodes.length - 1] + lastTextNodeRight = clientRectForRange(lastTextNode, 0, lastTextNode.textContent.length).right + } + + if (lineNodeClientLeft === -1) { + lineNodeClientLeft = lineNode.getBoundingClientRect().left + } + + positions.set(nextColumnToMeasure, Math.round(lastTextNodeRight - lineNodeClientLeft)) + } + } + + rowForPixelPosition (pixelPosition) { + return Math.max(0, this.lineTopIndex.rowForPixelPosition(pixelPosition)) + } + + pixelPositionBeforeBlocksForRow (row) { + return this.lineTopIndex.pixelPositionBeforeBlocksForRow(row) + } + + pixelPositionAfterBlocksForRow (row) { + return this.lineTopIndex.pixelPositionAfterBlocksForRow(row) + } + + pixelLeftForRowAndColumn (row, column) { + if (column === 0) return 0 + const screenLine = this.renderedScreenLineForRow(row) + if (screenLine) { + const horizontalPositionsByColumn = this.horizontalPixelPositionsByScreenLineId.get(screenLine.id) + if (horizontalPositionsByColumn) { + return horizontalPositionsByColumn.get(column) + } + } + } + + screenPositionForPixelPosition ({top, left}) { + const {model} = this.props + + const row = Math.min( + this.rowForPixelPosition(top), + model.getApproximateScreenLineCount() - 1 + ) + + let screenLine = this.renderedScreenLineForRow(row) + if (!screenLine) { + this.requestExtraLineToMeasure(row, model.screenLineForScreenRow(row)) + this.updateSyncBeforeMeasuringContent() + this.measureContentDuringUpdateSync() + screenLine = this.renderedScreenLineForRow(row) + } + + const linesClientLeft = this.refs.lineTiles.getBoundingClientRect().left + const targetClientLeft = linesClientLeft + Math.max(0, left) + const textNodes = this.textNodesByScreenLineId.get(screenLine.id) + + let containingTextNodeIndex + { + let low = 0 + let high = textNodes.length - 1 + while (low <= high) { + const mid = low + ((high - low) >> 1) + const textNode = textNodes[mid] + const textNodeRect = clientRectForRange(textNode, 0, textNode.length) + + if (targetClientLeft < textNodeRect.left) { + high = mid - 1 + containingTextNodeIndex = Math.max(0, mid - 1) + } else if (targetClientLeft > textNodeRect.right) { + low = mid + 1 + containingTextNodeIndex = Math.min(textNodes.length - 1, mid + 1) + } else { + containingTextNodeIndex = mid + break + } + } + } + const containingTextNode = textNodes[containingTextNodeIndex] + let characterIndex = 0 + { + let low = 0 + let high = containingTextNode.length - 1 + while (low <= high) { + const charIndex = low + ((high - low) >> 1) + const nextCharIndex = isPairedCharacter(containingTextNode.textContent, charIndex) + ? charIndex + 2 + : charIndex + 1 + + const rangeRect = clientRectForRange(containingTextNode, charIndex, nextCharIndex) + if (targetClientLeft < rangeRect.left) { + high = charIndex - 1 + characterIndex = Math.max(0, charIndex - 1) + } else if (targetClientLeft > rangeRect.right) { + low = nextCharIndex + characterIndex = Math.min(containingTextNode.textContent.length, nextCharIndex) + } else { + if (targetClientLeft <= ((rangeRect.left + rangeRect.right) / 2)) { + characterIndex = charIndex + } else { + characterIndex = nextCharIndex + } + break + } + } + } + + let textNodeStartColumn = 0 + for (let i = 0; i < containingTextNodeIndex; i++) { + textNodeStartColumn = textNodeStartColumn + textNodes[i].length + } + const column = textNodeStartColumn + characterIndex + + return Point(row, column) + } + + didResetDisplayLayer () { + this.spliceLineTopIndex(0, Infinity, Infinity) + this.scheduleUpdate() + } + + didChangeDisplayLayer (changes) { + for (let i = 0; i < changes.length; i++) { + const {start, oldExtent, newExtent} = changes[i] + this.spliceLineTopIndex(start.row, oldExtent.row, newExtent.row) + } + + this.scheduleUpdate() + } + + didChangeSelectionRange () { + const {model} = this.props + + if (this.getPlatform() === 'linux') { + if (this.selectionClipboardImmediateId) { + clearImmediate(this.selectionClipboardImmediateId) + } + + this.selectionClipboardImmediateId = setImmediate(() => { + this.selectionClipboardImmediateId = null + + if (model.isDestroyed()) return + + const selectedText = model.getSelectedText() + if (selectedText) { + // This uses ipcRenderer.send instead of clipboard.writeText because + // clipboard.writeText is a sync ipcRenderer call on Linux and that + // will slow down selections. + electron.ipcRenderer.send('write-text-to-selection-clipboard', selectedText) + } + }) + } + } + + observeBlockDecorations () { + const {model} = this.props + const decorations = model.getDecorations({type: 'block'}) + for (let i = 0; i < decorations.length; i++) { + this.didAddBlockDecoration(decorations[i]) + } + } + + didAddBlockDecoration (decoration) { + const marker = decoration.getMarker() + const {position} = decoration.getProperties() + const row = marker.getHeadScreenPosition().row + this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after') + + this.blockDecorationsToMeasure.add(decoration) + + const didUpdateDisposable = marker.bufferMarker.onDidChange((e) => { + if (!e.textChanged) { + this.lineTopIndex.moveBlock(decoration, marker.getHeadScreenPosition().row) + this.scheduleUpdate() + } + }) + const didDestroyDisposable = decoration.onDidDestroy(() => { + this.blockDecorationsToMeasure.delete(decoration) + this.lineTopIndex.removeBlock(decoration) + didUpdateDisposable.dispose() + didDestroyDisposable.dispose() + this.scheduleUpdate() + }) + } + + invalidateBlockDecorationDimensions (decoration) { + this.blockDecorationsToMeasure.add(decoration) + this.scheduleUpdate() + } + + spliceLineTopIndex (startRow, oldExtent, newExtent) { + const invalidatedBlockDecorations = this.lineTopIndex.splice(startRow, oldExtent, newExtent) + invalidatedBlockDecorations.forEach((decoration) => { + const newPosition = decoration.getMarker().getHeadScreenPosition() + this.lineTopIndex.moveBlock(decoration, newPosition.row) + }) + } + + isVisible () { + return this.element.offsetWidth > 0 || this.element.offsetHeight > 0 + } + + getWindowInnerHeight () { + return window.innerHeight + } + + getWindowInnerWidth () { + return window.innerWidth + } + + getLineHeight () { + return this.measurements.lineHeight + } + + getBaseCharacterWidth () { + return this.measurements.baseCharacterWidth + } + + getLongestLineWidth () { + return this.measurements.longestLineWidth + } + + getClientContainerHeight () { + return this.measurements.clientContainerHeight + } + + getClientContainerWidth () { + return this.measurements.clientContainerWidth + } + + getScrollContainerWidth () { + if (this.props.model.getAutoWidth()) { + return this.getScrollWidth() + } else { + return this.getClientContainerWidth() - this.getGutterContainerWidth() + } + } + + getScrollContainerHeight () { + if (this.props.model.getAutoHeight()) { + return this.getScrollHeight() + } else { + return this.getClientContainerHeight() + } + } + + getScrollContainerClientWidth () { + if (this.isVerticalScrollbarVisible()) { + return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth() + } else { + return this.getScrollContainerWidth() + } + } + + getScrollContainerClientHeight () { + if (this.isHorizontalScrollbarVisible()) { + return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() + } else { + return this.getScrollContainerHeight() + } + } + + isVerticalScrollbarVisible () { + const {model} = this.props + if (model.isMini()) return false + if (model.getAutoHeight()) return false + if (this.getContentHeight() > this.getScrollContainerHeight()) return true + return ( + this.getContentWidth() > this.getScrollContainerWidth() && + this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()) + ) + } + + isHorizontalScrollbarVisible () { + const {model} = this.props + if (model.isMini()) return false + if (model.getAutoWidth()) return false + if (model.isSoftWrapped()) return false + if (this.getContentWidth() > this.getScrollContainerWidth()) return true + return ( + this.getContentHeight() > this.getScrollContainerHeight() && + this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()) + ) + } + + getScrollHeight () { + if (this.props.model.getScrollPastEnd()) { + return this.getContentHeight() + Math.max( + 3 * this.getLineHeight(), + this.getScrollContainerClientHeight() - (3 * this.getLineHeight()) + ) + } else if (this.props.model.getAutoHeight()) { + return this.getContentHeight() + } else { + return Math.max(this.getContentHeight(), this.getScrollContainerClientHeight()) + } + } + + getScrollWidth () { + const {model} = this.props + + if (model.isSoftWrapped()) { + return this.getScrollContainerClientWidth() + } else if (model.getAutoWidth()) { + return this.getContentWidth() + } else { + return Math.max(this.getContentWidth(), this.getScrollContainerClientWidth()) + } + } + + getContentHeight () { + return this.pixelPositionAfterBlocksForRow(this.props.model.getApproximateScreenLineCount()) + } + + getContentWidth () { + return Math.round(this.getLongestLineWidth() + this.getBaseCharacterWidth()) + } + + getScrollContainerClientWidthInBaseCharacters () { + return Math.floor(this.getScrollContainerClientWidth() / this.getBaseCharacterWidth()) + } + + getGutterContainerWidth () { + return this.measurements.gutterContainerWidth + } + + getLineNumberGutterWidth () { + return this.measurements.lineNumberGutterWidth + } + + getVerticalScrollbarWidth () { + return this.measurements.verticalScrollbarWidth + } + + getHorizontalScrollbarHeight () { + return this.measurements.horizontalScrollbarHeight + } + + getRowsPerTile () { + return this.props.rowsPerTile || DEFAULT_ROWS_PER_TILE + } + + tileStartRowForRow (row) { + return row - (row % this.getRowsPerTile()) + } + + getRenderedStartRow () { + if (this.derivedDimensionsCache.renderedStartRow == null) { + this.derivedDimensionsCache.renderedStartRow = this.tileStartRowForRow(this.getFirstVisibleRow()) + } + + return this.derivedDimensionsCache.renderedStartRow + } + + getRenderedEndRow () { + if (this.derivedDimensionsCache.renderedEndRow == null) { + this.derivedDimensionsCache.renderedEndRow = Math.min( + this.props.model.getApproximateScreenLineCount(), + this.getRenderedStartRow() + this.getVisibleTileCount() * this.getRowsPerTile() + ) + } + + return this.derivedDimensionsCache.renderedEndRow + } + + getRenderedRowCount () { + if (this.derivedDimensionsCache.renderedRowCount == null) { + this.derivedDimensionsCache.renderedRowCount = Math.max(0, this.getRenderedEndRow() - this.getRenderedStartRow()) + } + + return this.derivedDimensionsCache.renderedRowCount + } + + getRenderedTileCount () { + if (this.derivedDimensionsCache.renderedTileCount == null) { + this.derivedDimensionsCache.renderedTileCount = Math.ceil(this.getRenderedRowCount() / this.getRowsPerTile()) + } + + return this.derivedDimensionsCache.renderedTileCount + } + + getFirstVisibleRow () { + if (this.derivedDimensionsCache.firstVisibleRow == null) { + this.derivedDimensionsCache.firstVisibleRow = this.rowForPixelPosition(this.getScrollTop()) + } + + return this.derivedDimensionsCache.firstVisibleRow + } + + getLastVisibleRow () { + if (this.derivedDimensionsCache.lastVisibleRow == null) { + this.derivedDimensionsCache.lastVisibleRow = Math.min( + this.props.model.getApproximateScreenLineCount() - 1, + this.rowForPixelPosition(this.getScrollBottom()) + ) + } + + return this.derivedDimensionsCache.lastVisibleRow + } + + // We may render more tiles than needed if some contain block decorations, + // but keeping this calculation simple ensures the number of tiles remains + // fixed for a given editor height, which eliminates situations where a + // tile is repeatedly added and removed during scrolling in certain + // comibinations of editor height and line height. + getVisibleTileCount () { + if (this.derivedDimensionsCache.visibleTileCount == null) { + const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() / this.getRowsPerTile() + this.derivedDimensionsCache.visibleTileCount = Math.ceil(editorHeightInTiles) + 1 + } + return this.derivedDimensionsCache.visibleTileCount + } + + getFirstVisibleColumn () { + return Math.floor(this.getScrollLeft() / this.getBaseCharacterWidth()) + } + + getScrollTop () { + this.scrollTop = Math.min(this.getMaxScrollTop(), this.scrollTop) + return this.scrollTop + } + + setScrollTop (scrollTop) { + scrollTop = Math.round(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop))) + if (scrollTop !== this.scrollTop) { + this.derivedDimensionsCache = {} + this.scrollTopPending = true + this.scrollTop = scrollTop + this.element.emitter.emit('did-change-scroll-top', scrollTop) + return true + } else { + return false + } + } + + getMaxScrollTop () { + return Math.max(0, this.getScrollHeight() - this.getScrollContainerClientHeight()) + } + + getScrollBottom () { + return this.getScrollTop() + this.getScrollContainerClientHeight() + } + + setScrollBottom (scrollBottom) { + return this.setScrollTop(scrollBottom - this.getScrollContainerClientHeight()) + } + + getScrollLeft () { + return this.scrollLeft + } + + setScrollLeft (scrollLeft) { + scrollLeft = Math.round(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft))) + if (scrollLeft !== this.scrollLeft) { + this.scrollLeftPending = true + this.scrollLeft = scrollLeft + this.element.emitter.emit('did-change-scroll-left', scrollLeft) + return true + } else { + return false + } + } + + getMaxScrollLeft () { + return Math.max(0, this.getScrollWidth() - this.getScrollContainerClientWidth()) + } + + getScrollRight () { + return this.getScrollLeft() + this.getScrollContainerClientWidth() + } + + setScrollRight (scrollRight) { + return this.setScrollLeft(scrollRight - this.getScrollContainerClientWidth()) + } + + setScrollTopRow (scrollTopRow, scheduleUpdate = true) { + if (this.hasInitialMeasurements) { + const didScroll = this.setScrollTop(this.pixelPositionBeforeBlocksForRow(scrollTopRow)) + if (didScroll && scheduleUpdate) { + this.scheduleUpdate() + } + return didScroll + } else { + this.pendingScrollTopRow = scrollTopRow + return false + } + } + + getScrollTopRow () { + if (this.hasInitialMeasurements) { + return this.rowForPixelPosition(this.getScrollTop()) + } else { + return this.pendingScrollTopRow || 0 + } + } + + setScrollLeftColumn (scrollLeftColumn, scheduleUpdate = true) { + if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) { + const didScroll = this.setScrollLeft(scrollLeftColumn * this.getBaseCharacterWidth()) + if (didScroll && scheduleUpdate) { + this.scheduleUpdate() + } + return didScroll + } else { + this.pendingScrollLeftColumn = scrollLeftColumn + return false + } + } + + getScrollLeftColumn () { + if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) { + return Math.round(this.getScrollLeft() / this.getBaseCharacterWidth()) + } else { + return this.pendingScrollLeftColumn || 0 + } + } + + // Ensure the spatial index is populated with rows that are currently + // visible so we *at least* get the longest row in the visible range. + populateVisibleRowRange () { + const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() + const visibleTileCount = Math.ceil(editorHeightInTiles) + 1 + const lastRenderedRow = this.getRenderedStartRow() + (visibleTileCount * this.getRowsPerTile()) + this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, lastRenderedRow) + } + + populateVisibleTiles () { + const startRow = this.getRenderedStartRow() + const endRow = this.getRenderedEndRow() + const freeTileIds = [] + for (let i = 0; i < this.renderedTileStartRows.length; i++) { + const tileStartRow = this.renderedTileStartRows[i] + if (tileStartRow < startRow || tileStartRow >= endRow) { + const tileId = this.idsByTileStartRow.get(tileStartRow) + freeTileIds.push(tileId) + this.idsByTileStartRow.delete(tileStartRow) + } + } + + const rowsPerTile = this.getRowsPerTile() + this.renderedTileStartRows.length = this.getRenderedTileCount() + for (let tileStartRow = startRow, i = 0; tileStartRow < endRow; tileStartRow = tileStartRow + rowsPerTile, i++) { + this.renderedTileStartRows[i] = tileStartRow + if (!this.idsByTileStartRow.has(tileStartRow)) { + if (freeTileIds.length > 0) { + this.idsByTileStartRow.set(tileStartRow, freeTileIds.shift()) + } else { + this.idsByTileStartRow.set(tileStartRow, this.nextTileId++) + } + } + } + + this.renderedTileStartRows.sort((a, b) => this.idsByTileStartRow.get(a) - this.idsByTileStartRow.get(b)) + } + + getNextUpdatePromise () { + if (!this.nextUpdatePromise) { + this.nextUpdatePromise = new Promise((resolve) => { + this.resolveNextUpdatePromise = () => { + this.nextUpdatePromise = null + this.resolveNextUpdatePromise = null + resolve() + } + }) + } + return this.nextUpdatePromise + } + + setInputEnabled (inputEnabled) { + this.props.inputEnabled = inputEnabled + } + + isInputEnabled (inputEnabled) { + return this.props.inputEnabled != null ? this.props.inputEnabled : true + } + + getPlatform () { + return this.props.platform || process.platform + } +} + +class DummyScrollbarComponent { + constructor (props) { + this.props = props + etch.initialize(this) + } + + update (newProps) { + const oldProps = this.props + this.props = newProps + etch.updateSync(this) + + const shouldFlushScrollPosition = ( + newProps.scrollTop !== oldProps.scrollTop || + newProps.scrollLeft !== oldProps.scrollLeft + ) + if (shouldFlushScrollPosition) this.flushScrollPosition() + } + + flushScrollPosition () { + if (this.props.orientation === 'horizontal') { + this.element.scrollLeft = this.props.scrollLeft + } else { + this.element.scrollTop = this.props.scrollTop + } + } + + render () { + const outerStyle = { + position: 'absolute', + contain: 'strict', + zIndex: 1 + } + const innerStyle = {} + if (this.props.orientation === 'horizontal') { + let right = (this.props.verticalScrollbarWidth || 0) + outerStyle.bottom = 0 + outerStyle.left = 0 + outerStyle.right = right + 'px' + outerStyle.height = '20px' + outerStyle.overflowY = 'hidden' + outerStyle.overflowX = this.props.forceScrollbarVisible ? 'scroll' : 'auto' + innerStyle.height = '20px' + innerStyle.width = (this.props.scrollWidth || 0) + 'px' + } else { + let bottom = (this.props.horizontalScrollbarHeight || 0) + outerStyle.right = 0 + outerStyle.top = 0 + outerStyle.bottom = bottom + 'px' + outerStyle.width = '20px' + outerStyle.overflowX = 'hidden' + outerStyle.overflowY = this.props.forceScrollbarVisible ? 'scroll' : 'auto' + innerStyle.width = '20px' + innerStyle.height = (this.props.scrollHeight || 0) + 'px' + } + + return $.div( + { + className: `${this.props.orientation}-scrollbar`, + style: outerStyle, + on: { + scroll: this.props.didScroll, + mousedown: this.didMousedown + } + }, + $.div({style: innerStyle}) + ) + } + + didMousedown (event) { + let {bottom, right} = this.element.getBoundingClientRect() + const clickedOnScrollbar = (this.props.orientation === 'horizontal') + ? event.clientY >= (bottom - this.getRealScrollbarHeight()) + : event.clientX >= (right - this.getRealScrollbarWidth()) + if (!clickedOnScrollbar) this.props.didMousedown(event) + } + + getRealScrollbarWidth () { + return this.element.offsetWidth - this.element.clientWidth + } + + getRealScrollbarHeight () { + return this.element.offsetHeight - this.element.clientHeight + } +} + +class GutterContainerComponent { + constructor (props) { + this.props = props + etch.initialize(this) + } + + update (props) { + if (this.shouldUpdate(props)) { + this.props = props + etch.updateSync(this) + } + } + + shouldUpdate (props) { + return ( + !props.measuredContent || + props.lineNumberGutterWidth !== this.props.lineNumberGutterWidth + ) + } + + render () { + const {hasInitialMeasurements, scrollTop, scrollHeight, guttersToRender, decorationsToRender} = this.props + + const innerStyle = { + willChange: 'transform', + display: 'flex' + } + + if (hasInitialMeasurements) { + innerStyle.transform = `translateY(${-scrollTop}px)` + } + + return $.div( + { + ref: 'gutterContainer', + key: 'gutterContainer', + className: 'gutter-container', + style: { + position: 'relative', + zIndex: 1, + backgroundColor: 'inherit' + } + }, + $.div({style: innerStyle}, + guttersToRender.map((gutter) => { + if (gutter.name === 'line-number') { + return this.renderLineNumberGutter(gutter) + } else { + return $(CustomGutterComponent, { + key: gutter, + element: gutter.getElement(), + name: gutter.name, + visible: gutter.isVisible(), + height: scrollHeight, + decorations: decorationsToRender.customGutter.get(gutter.name) + }) + } + }) + ) + ) + } + + renderLineNumberGutter (gutter) { + const { + rootComponent, isLineNumberGutterVisible, hasInitialMeasurements, lineNumbersToRender, + renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, + scrollHeight, lineNumberGutterWidth, lineHeight + } = this.props + + if (!isLineNumberGutterVisible) return null + + if (hasInitialMeasurements) { + const {maxDigits, keys, bufferRows, softWrappedFlags, foldableFlags} = lineNumbersToRender + return $(LineNumberGutterComponent, { + ref: 'lineNumberGutter', + element: gutter.getElement(), + rootComponent: rootComponent, + startRow: renderedStartRow, + endRow: renderedEndRow, + rowsPerTile: rowsPerTile, + maxDigits: maxDigits, + keys: keys, + bufferRows: bufferRows, + softWrappedFlags: softWrappedFlags, + foldableFlags: foldableFlags, + decorations: decorationsToRender.lineNumbers, + blockDecorations: decorationsToRender.blocks, + didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, + height: scrollHeight, + width: lineNumberGutterWidth, + lineHeight: lineHeight + }) + } else { + return $(LineNumberGutterComponent, { + ref: 'lineNumberGutter', + element: gutter.getElement(), + maxDigits: lineNumbersToRender.maxDigits + }) + } + } +} + +class LineNumberGutterComponent { + constructor (props) { + this.props = props + this.element = this.props.element + this.virtualNode = $.div(null) + this.virtualNode.domNode = this.element + etch.updateSync(this) + } + + update (newProps) { + if (this.shouldUpdate(newProps)) { + this.props = newProps + etch.updateSync(this) + } + } + + render () { + const { + rootComponent, height, width, lineHeight, startRow, endRow, rowsPerTile, + maxDigits, keys, bufferRows, softWrappedFlags, foldableFlags, decorations + } = this.props + + let children = null + + if (bufferRows) { + children = new Array(rootComponent.renderedTileStartRows.length) + for (let i = 0; i < rootComponent.renderedTileStartRows.length; i++) { + const tileStartRow = rootComponent.renderedTileStartRows[i] + const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile) + const tileChildren = new Array(tileEndRow - tileStartRow) + for (let row = tileStartRow; row < tileEndRow; row++) { + const j = row - startRow + const key = keys[j] + const softWrapped = softWrappedFlags[j] + const foldable = foldableFlags[j] + const bufferRow = bufferRows[j] + + let className = 'line-number' + if (foldable) className = className + ' foldable' + + const decorationsForRow = decorations[row - startRow] + if (decorationsForRow) className = className + ' ' + decorationsForRow + + let number = softWrapped ? '•' : bufferRow + 1 + number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number + + const lineNumberProps = { + key, + className, + style: {width: width + 'px'}, + dataset: {bufferRow} + } + const currentRowTop = rootComponent.pixelPositionAfterBlocksForRow(row) + const previousRowBottom = rootComponent.pixelPositionAfterBlocksForRow(row - 1) + lineHeight + if (currentRowTop > previousRowBottom) { + lineNumberProps.style.marginTop = (currentRowTop - previousRowBottom) + 'px' + } + + tileChildren[row - tileStartRow] = $.div(lineNumberProps, + number, + $.div({className: 'icon-right'}) + ) + } + + const tileTop = rootComponent.pixelPositionBeforeBlocksForRow(tileStartRow) + const tileBottom = rootComponent.pixelPositionBeforeBlocksForRow(tileEndRow) + const tileHeight = tileBottom - tileTop + + children[i] = $.div({ + key: rootComponent.idsByTileStartRow.get(tileStartRow), + style: { + contain: 'strict', + overflow: 'hidden', + position: 'absolute', + top: 0, + height: tileHeight + 'px', + width: width + 'px', + willChange: 'transform', + transform: `translateY(${tileTop}px)`, + backgroundColor: 'inherit' + } + }, ...tileChildren) + } + } + + return $.div( + { + className: 'gutter line-bufferRows', + attributes: {'gutter-name': 'line-number'}, + style: {position: 'relative', height: height + 'px'}, + on: { + mousedown: this.didMouseDown + } + }, + $.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}}, + '0'.repeat(maxDigits), + $.div({className: 'icon-right'}) + ), + children + ) + } + + shouldUpdate (newProps) { + const oldProps = this.props + + if (oldProps.height !== newProps.height) return true + if (oldProps.width !== newProps.width) return true + if (oldProps.lineHeight !== newProps.lineHeight) return true + if (oldProps.startRow !== newProps.startRow) return true + if (oldProps.endRow !== newProps.endRow) return true + if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true + if (oldProps.maxDigits !== newProps.maxDigits) return true + if (newProps.didMeasureVisibleBlockDecoration) return true + if (!arraysEqual(oldProps.keys, newProps.keys)) return true + if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true + if (!arraysEqual(oldProps.foldableFlags, newProps.foldableFlags)) return true + if (!arraysEqual(oldProps.decorations, newProps.decorations)) return true + + let oldTileStartRow = oldProps.startRow + let newTileStartRow = newProps.startRow + while (oldTileStartRow < oldProps.endRow || newTileStartRow < newProps.endRow) { + let oldTileBlockDecorations = oldProps.blockDecorations.get(oldTileStartRow) + let newTileBlockDecorations = newProps.blockDecorations.get(newTileStartRow) + + if (oldTileBlockDecorations && newTileBlockDecorations) { + if (oldTileBlockDecorations.size !== newTileBlockDecorations.size) return true + + let blockDecorationsChanged = false + + oldTileBlockDecorations.forEach((oldDecorations, screenLineId) => { + if (!blockDecorationsChanged) { + const newDecorations = newTileBlockDecorations.get(screenLineId) + blockDecorationsChanged = (newDecorations == null || !arraysEqual(oldDecorations, newDecorations)) + } + }) + if (blockDecorationsChanged) return true + + newTileBlockDecorations.forEach((newDecorations, screenLineId) => { + if (!blockDecorationsChanged) { + const oldDecorations = oldTileBlockDecorations.get(screenLineId) + blockDecorationsChanged = (oldDecorations == null) + } + }) + if (blockDecorationsChanged) return true + } else if (oldTileBlockDecorations) { + return true + } else if (newTileBlockDecorations) { + return true + } + + oldTileStartRow += oldProps.rowsPerTile + newTileStartRow += newProps.rowsPerTile + } + + return false + } + + didMouseDown (event) { + this.props.rootComponent.didMouseDownOnLineNumberGutter(event) + } +} + +class CustomGutterComponent { + constructor (props) { + this.props = props + this.element = this.props.element + this.virtualNode = $.div(null) + this.virtualNode.domNode = this.element + etch.updateSync(this) + } + + update (props) { + this.props = props + etch.updateSync(this) + } + + destroy () { + etch.destroy(this) + } + + render () { + return $.div( + { + className: 'gutter', + attributes: {'gutter-name': this.props.name}, + style: { + display: this.props.visible ? '' : 'none' + } + }, + $.div( + { + className: 'custom-decorations', + style: {height: this.props.height + 'px'} + }, + this.renderDecorations() + ) + ) + } + + renderDecorations () { + if (!this.props.decorations) return null + + return this.props.decorations.map(({className, element, top, height}) => { + return $(CustomGutterDecorationComponent, { + className, + element, + top, + height + }) + }) + } +} + +class CustomGutterDecorationComponent { + constructor (props) { + this.props = props + this.element = document.createElement('div') + const {top, height, className, element} = this.props + + this.element.style.position = 'absolute' + this.element.style.top = top + 'px' + this.element.style.height = height + 'px' + if (className != null) this.element.className = className + if (element != null) this.element.appendChild(element) + } + + update (newProps) { + const oldProps = this.props + this.props = newProps + + if (newProps.top !== oldProps.top) this.element.style.top = newProps.top + 'px' + if (newProps.height !== oldProps.height) this.element.style.height = newProps.height + 'px' + if (newProps.className !== oldProps.className) this.element.className = newProps.className || '' + if (newProps.element !== oldProps.element) { + if (this.element.firstChild) this.element.firstChild.remove() + this.element.appendChild(newProps.element) + } + } +} + +class CursorsAndInputComponent { + constructor (props) { + this.props = props + etch.initialize(this) + } + + update (props) { + if (props.measuredContent) { + this.props = props + etch.updateSync(this) + } + } + + updateCursorBlinkSync (cursorsBlinkedOff) { + this.props.cursorsBlinkedOff = cursorsBlinkedOff + const className = this.getCursorsClassName() + this.refs.cursors.className = className + this.virtualNode.props.className = className + } + + render () { + const {lineHeight, decorationsToRender, scrollHeight, scrollWidth} = this.props + + const className = this.getCursorsClassName() + const cursorHeight = lineHeight + 'px' + + const children = [this.renderHiddenInput()] + for (let i = 0; i < decorationsToRender.cursors.length; i++) { + const {pixelLeft, pixelTop, pixelWidth, className: extraCursorClassName, style: extraCursorStyle} = decorationsToRender.cursors[i] + let cursorClassName = 'cursor' + if (extraCursorClassName) cursorClassName += ' ' + extraCursorClassName + + const cursorStyle = { + height: cursorHeight, + width: pixelWidth + 'px', + transform: `translate(${pixelLeft}px, ${pixelTop}px)` + } + if (extraCursorStyle) Object.assign(cursorStyle, extraCursorStyle) + + children.push($.div({ + className: cursorClassName, + style: cursorStyle + })) + } + + return $.div({ + key: 'cursors', + ref: 'cursors', + className, + style: { + position: 'absolute', + contain: 'strict', + zIndex: 1, + width: scrollWidth + 'px', + height: scrollHeight + 'px', + pointerEvents: 'none' + } + }, children) + } + + getCursorsClassName () { + return this.props.cursorsBlinkedOff ? 'cursors blink-off' : 'cursors' + } + + renderHiddenInput () { + const { + lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput, + didTextInput, didKeydown, didKeyup, didKeypress, didCompositionStart, + didCompositionUpdate, didCompositionEnd + } = this.props + + let top, left + if (hiddenInputPosition) { + top = hiddenInputPosition.pixelTop + left = hiddenInputPosition.pixelLeft + } else { + top = 0 + left = 0 + } + + return $.input({ + ref: 'hiddenInput', + key: 'hiddenInput', + className: 'hidden-input', + on: { + blur: didBlurHiddenInput, + focus: didFocusHiddenInput, + textInput: didTextInput, + keydown: didKeydown, + keyup: didKeyup, + keypress: didKeypress, + compositionstart: didCompositionStart, + compositionupdate: didCompositionUpdate, + compositionend: didCompositionEnd + }, + tabIndex: -1, + style: { + position: 'absolute', + width: '1px', + height: lineHeight + 'px', + top: top + 'px', + left: left + 'px', + opacity: 0, + padding: 0, + border: 0 + } + }) + } +} + +class LinesTileComponent { + constructor (props) { + this.props = props + etch.initialize(this) + this.createLines() + this.updateBlockDecorations({}, props) + } + + update (newProps) { + if (this.shouldUpdate(newProps)) { + const oldProps = this.props + this.props = newProps + etch.updateSync(this) + if (!newProps.measuredContent) { + this.updateLines(oldProps, newProps) + this.updateBlockDecorations(oldProps, newProps) + } + } + } + + destroy () { + for (let i = 0; i < this.lineComponents.length; i++) { + this.lineComponents[i].destroy() + } + } + + render () { + const {height, width, top} = this.props + + return $.div( + { + style: { + contain: 'strict', + position: 'absolute', + height: height + 'px', + width: width + 'px', + willChange: 'transform', + transform: `translateY(${top}px)`, + backgroundColor: 'inherit' + } + }, + this.renderHighlights() + // Lines and block decorations will be manually inserted here for efficiency + ) + } + + renderHighlights () { + const {top, lineHeight, highlightDecorations} = this.props + + let children = null + if (highlightDecorations) { + const decorationCount = highlightDecorations.length + children = new Array(decorationCount) + for (let i = 0; i < decorationCount; i++) { + const highlightProps = Object.assign( + {parentTileTop: top, lineHeight}, + highlightDecorations[i] + ) + children[i] = $(HighlightComponent, highlightProps) + highlightDecorations[i].flashRequested = false + } + } + + return $.div( + { + className: 'highlights', + style: {contain: 'layout'} + }, + children + ) + } + + createLines () { + const { + tileStartRow, screenLines, lineDecorations, textDecorations, + displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId + } = this.props + + this.lineComponents = [] + for (let i = 0, length = screenLines.length; i < length; i++) { + const component = new LineComponent({ + screenLine: screenLines[i], + screenRow: tileStartRow + i, + lineDecoration: lineDecorations[i], + textDecorations: textDecorations[i], + displayLayer, + lineNodesByScreenLineId, + textNodesByScreenLineId + }) + this.element.appendChild(component.element) + this.lineComponents.push(component) + } + } + + updateLines (oldProps, newProps) { + var { + screenLines, tileStartRow, lineDecorations, textDecorations, + displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId + } = newProps + + var oldScreenLines = oldProps.screenLines + var newScreenLines = screenLines + var oldScreenLinesEndIndex = oldScreenLines.length + var newScreenLinesEndIndex = newScreenLines.length + var oldScreenLineIndex = 0 + var newScreenLineIndex = 0 + var lineComponentIndex = 0 + + while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) { + var oldScreenLine = oldScreenLines[oldScreenLineIndex] + var newScreenLine = newScreenLines[newScreenLineIndex] + + if (oldScreenLineIndex >= oldScreenLinesEndIndex) { + var newScreenLineComponent = new LineComponent({ + screenLine: newScreenLine, + screenRow: tileStartRow + newScreenLineIndex, + lineDecoration: lineDecorations[newScreenLineIndex], + textDecorations: textDecorations[newScreenLineIndex], + displayLayer, + lineNodesByScreenLineId, + textNodesByScreenLineId + }) + this.element.appendChild(newScreenLineComponent.element) + this.lineComponents.push(newScreenLineComponent) + + newScreenLineIndex++ + lineComponentIndex++ + } else if (newScreenLineIndex >= newScreenLinesEndIndex) { + this.lineComponents[lineComponentIndex].destroy() + this.lineComponents.splice(lineComponentIndex, 1) + + oldScreenLineIndex++ + } else if (oldScreenLine === newScreenLine) { + var lineComponent = this.lineComponents[lineComponentIndex] + lineComponent.update({ + screenRow: tileStartRow + newScreenLineIndex, + lineDecoration: lineDecorations[newScreenLineIndex], + textDecorations: textDecorations[newScreenLineIndex] + }) + + oldScreenLineIndex++ + newScreenLineIndex++ + lineComponentIndex++ + } else { + var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine) + var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine) + if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) { + var newScreenLineComponents = [] + while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { + var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare + screenLine: newScreenLines[newScreenLineIndex], + screenRow: tileStartRow + newScreenLineIndex, + lineDecoration: lineDecorations[newScreenLineIndex], + textDecorations: textDecorations[newScreenLineIndex], + displayLayer, + lineNodesByScreenLineId, + textNodesByScreenLineId + }) + this.element.insertBefore(newScreenLineComponent.element, this.getFirstElementForScreenLine(oldProps, oldScreenLine)) + newScreenLineComponents.push(newScreenLineComponent) + + newScreenLineIndex++ + } + + this.lineComponents.splice(lineComponentIndex, 0, ...newScreenLineComponents) + lineComponentIndex = lineComponentIndex + newScreenLineComponents.length + } else if (oldScreenLineIndex < newScreenLineIndexInOldScreenLines && newScreenLineIndexInOldScreenLines < oldScreenLinesEndIndex) { + while (oldScreenLineIndex < newScreenLineIndexInOldScreenLines) { + this.lineComponents[lineComponentIndex].destroy() + this.lineComponents.splice(lineComponentIndex, 1) + + oldScreenLineIndex++ + } + } else { + var oldScreenLineComponent = this.lineComponents[lineComponentIndex] + var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare + screenLine: newScreenLines[newScreenLineIndex], + screenRow: tileStartRow + newScreenLineIndex, + lineDecoration: lineDecorations[newScreenLineIndex], + textDecorations: textDecorations[newScreenLineIndex], + displayLayer, + lineNodesByScreenLineId, + textNodesByScreenLineId + }) + this.element.insertBefore(newScreenLineComponent.element, oldScreenLineComponent.element) + oldScreenLineComponent.destroy() + this.lineComponents[lineComponentIndex] = newScreenLineComponent + + oldScreenLineIndex++ + newScreenLineIndex++ + lineComponentIndex++ + } + } + } + } + + getFirstElementForScreenLine (oldProps, screenLine) { + var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null + if (blockDecorations) { + var blockDecorationElementsBeforeOldScreenLine = [] + for (let i = 0; i < blockDecorations.length; i++) { + var decoration = blockDecorations[i] + if (decoration.position !== 'after') { + blockDecorationElementsBeforeOldScreenLine.push( + TextEditor.viewForItem(decoration.item) + ) + } + } + + for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) { + var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i] + if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) { + return blockDecorationElement + } + } + } + + return oldProps.lineNodesByScreenLineId.get(screenLine.id) + } + + updateBlockDecorations (oldProps, newProps) { + var {blockDecorations, lineNodesByScreenLineId} = newProps + + if (oldProps.blockDecorations) { + oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { + var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null + for (var i = 0; i < oldDecorations.length; i++) { + var oldDecoration = oldDecorations[i] + if (newDecorations && newDecorations.includes(oldDecoration)) continue + + var element = TextEditor.viewForItem(oldDecoration.item) + if (element.parentElement !== this.element) continue + + element.remove() + } + }) + } + + if (blockDecorations) { + blockDecorations.forEach((newDecorations, screenLineId) => { + var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null + for (var i = 0; i < newDecorations.length; i++) { + var newDecoration = newDecorations[i] + if (oldDecorations && oldDecorations.includes(newDecoration)) continue + + var element = TextEditor.viewForItem(newDecoration.item) + var lineNode = lineNodesByScreenLineId.get(screenLineId) + if (newDecoration.position === 'after') { + this.element.insertBefore(element, lineNode.nextSibling) + } else { + this.element.insertBefore(element, lineNode) + } + } + }) + } + } + + shouldUpdate (newProps) { + const oldProps = this.props + if (oldProps.top !== newProps.top) return true + if (oldProps.height !== newProps.height) return true + if (oldProps.width !== newProps.width) return true + if (oldProps.lineHeight !== newProps.lineHeight) return true + if (oldProps.tileStartRow !== newProps.tileStartRow) return true + if (oldProps.tileEndRow !== newProps.tileEndRow) return true + if (!arraysEqual(oldProps.screenLines, newProps.screenLines)) return true + if (!arraysEqual(oldProps.lineDecorations, newProps.lineDecorations)) return true + + if (!oldProps.highlightDecorations && newProps.highlightDecorations) return true + if (oldProps.highlightDecorations && !newProps.highlightDecorations) return true + + if (oldProps.highlightDecorations && newProps.highlightDecorations) { + if (oldProps.highlightDecorations.length !== newProps.highlightDecorations.length) return true + + for (let i = 0, length = oldProps.highlightDecorations.length; i < length; i++) { + const oldHighlight = oldProps.highlightDecorations[i] + const newHighlight = newProps.highlightDecorations[i] + if (oldHighlight.className !== newHighlight.className) return true + if (newHighlight.flashRequested) return true + if (oldHighlight.startPixelTop !== newHighlight.startPixelTop) return true + if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true + if (oldHighlight.endPixelTop !== newHighlight.endPixelTop) return true + if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true + if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true + } + } + + if (oldProps.blockDecorations && newProps.blockDecorations) { + if (oldProps.blockDecorations.size !== newProps.blockDecorations.size) return true + + let blockDecorationsChanged = false + + oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { + if (!blockDecorationsChanged) { + const newDecorations = newProps.blockDecorations.get(screenLineId) + blockDecorationsChanged = (newDecorations == null || !arraysEqual(oldDecorations, newDecorations)) + } + }) + if (blockDecorationsChanged) return true + + newProps.blockDecorations.forEach((newDecorations, screenLineId) => { + if (!blockDecorationsChanged) { + const oldDecorations = oldProps.blockDecorations.get(screenLineId) + blockDecorationsChanged = (oldDecorations == null) + } + }) + if (blockDecorationsChanged) return true + } else if (oldProps.blockDecorations) { + return true + } else if (newProps.blockDecorations) { + return true + } + + if (oldProps.textDecorations.length !== newProps.textDecorations.length) return true + for (let i = 0; i < oldProps.textDecorations.length; i++) { + if (!textDecorationsEqual(oldProps.textDecorations[i], newProps.textDecorations[i])) return true + } + + return false + } +} + +class LineComponent { + constructor (props) { + const {screenRow, screenLine, lineNodesByScreenLineId} = props + this.props = props + this.element = document.createElement('div') + this.element.className = this.buildClassName() + this.element.dataset.screenRow = screenRow + lineNodesByScreenLineId.set(screenLine.id, this.element) + this.appendContents() + } + + update (newProps) { + if (this.props.lineDecoration !== newProps.lineDecoration) { + this.props.lineDecoration = newProps.lineDecoration + this.element.className = this.buildClassName() + } + + if (this.props.screenRow !== newProps.screenRow) { + this.props.screenRow = newProps.screenRow + this.element.dataset.screenRow = newProps.screenRow + } + + if (!textDecorationsEqual(this.props.textDecorations, newProps.textDecorations)) { + this.props.textDecorations = newProps.textDecorations + this.element.firstChild.remove() + this.appendContents() + } + } + + destroy () { + const {lineNodesByScreenLineId, textNodesByScreenLineId, screenLine} = this.props + if (lineNodesByScreenLineId.get(screenLine.id) === this.element) { + lineNodesByScreenLineId.delete(screenLine.id) + textNodesByScreenLineId.delete(screenLine.id) + } + + this.element.remove() + } + + appendContents () { + const {displayLayer, screenLine, textDecorations, textNodesByScreenLineId} = this.props + + const textNodes = [] + textNodesByScreenLineId.set(screenLine.id, textNodes) + + const {lineText, tags} = screenLine + let openScopeNode = document.createElement('span') + this.element.appendChild(openScopeNode) + + let decorationIndex = 0 + let column = 0 + let activeClassName = null + let activeStyle = null + let nextDecoration = textDecorations ? textDecorations[decorationIndex] : null + if (nextDecoration && nextDecoration.column === 0) { + column = nextDecoration.column + activeClassName = nextDecoration.className + activeStyle = nextDecoration.style + nextDecoration = textDecorations[++decorationIndex] + } + + for (let i = 0; i < tags.length; i++) { + const tag = tags[i] + if (tag !== 0) { + if (displayLayer.isCloseTag(tag)) { + openScopeNode = openScopeNode.parentElement + } else if (displayLayer.isOpenTag(tag)) { + const newScopeNode = document.createElement('span') + newScopeNode.className = displayLayer.classNameForTag(tag) + openScopeNode.appendChild(newScopeNode) + openScopeNode = newScopeNode + } else { + const nextTokenColumn = column + tag + while (nextDecoration && nextDecoration.column <= nextTokenColumn) { + const text = lineText.substring(column, nextDecoration.column) + this.appendTextNode(textNodes, openScopeNode, text, activeClassName, activeStyle) + column = nextDecoration.column + activeClassName = nextDecoration.className + activeStyle = nextDecoration.style + nextDecoration = textDecorations[++decorationIndex] + } + + if (column < nextTokenColumn) { + const text = lineText.substring(column, nextTokenColumn) + this.appendTextNode(textNodes, openScopeNode, text, activeClassName, activeStyle) + column = nextTokenColumn + } + } + } + } + + if (column === 0) { + const textNode = document.createTextNode(' ') + this.element.appendChild(textNode) + textNodes.push(textNode) + } + + if (lineText.endsWith(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. + const textNode = document.createTextNode(ZERO_WIDTH_NBSP_CHARACTER) + this.element.appendChild(textNode) + textNodes.push(textNode) + } + } + + appendTextNode (textNodes, openScopeNode, text, activeClassName, activeStyle) { + if (activeClassName || activeStyle) { + const decorationNode = document.createElement('span') + if (activeClassName) decorationNode.className = activeClassName + if (activeStyle) Object.assign(decorationNode.style, activeStyle) + openScopeNode.appendChild(decorationNode) + openScopeNode = decorationNode + } + + const textNode = document.createTextNode(text) + openScopeNode.appendChild(textNode) + textNodes.push(textNode) + } + + buildClassName () { + const {lineDecoration} = this.props + let className = 'line' + if (lineDecoration != null) className = className + ' ' + lineDecoration + return className + } +} + +class HighlightComponent { + constructor (props) { + this.props = props + etch.initialize(this) + if (this.props.flashRequested) this.performFlash() + } + + update (newProps) { + this.props = newProps + etch.updateSync(this) + if (newProps.flashRequested) this.performFlash() + } + + performFlash () { + const {flashClass, flashDuration} = this.props + if (!this.timeoutsByClassName) this.timeoutsByClassName = new Map() + + // If a flash of this class is already in progress, clear it early and + // flash again on the next frame to ensure CSS transitions apply to the + // second flash. + if (this.timeoutsByClassName.has(flashClass)) { + window.clearTimeout(this.timeoutsByClassName.get(flashClass)) + this.timeoutsByClassName.delete(flashClass) + this.element.classList.remove(flashClass) + requestAnimationFrame(() => this.performFlash()) + } else { + this.element.classList.add(flashClass) + this.timeoutsByClassName.set(flashClass, window.setTimeout(() => { + this.element.classList.remove(flashClass) + }, flashDuration)) + } + } + + render () { + let {startPixelTop, endPixelTop} = this.props + const { + className, screenRange, parentTileTop, lineHeight, + startPixelLeft, endPixelLeft + } = this.props + startPixelTop -= parentTileTop + endPixelTop -= parentTileTop + + let regionClassName = 'region ' + className + let children + if (screenRange.start.row === screenRange.end.row) { + children = $.div({ + className: regionClassName, + style: { + position: 'absolute', + boxSizing: 'border-box', + top: startPixelTop + 'px', + left: startPixelLeft + 'px', + width: endPixelLeft - startPixelLeft + 'px', + height: lineHeight + 'px' + } + }) + } else { + children = [] + children.push($.div({ + className: regionClassName, + style: { + position: 'absolute', + boxSizing: 'border-box', + top: startPixelTop + 'px', + left: startPixelLeft + 'px', + right: 0, + height: lineHeight + 'px' + } + })) + + if (screenRange.end.row - screenRange.start.row > 1) { + children.push($.div({ + className: regionClassName, + style: { + position: 'absolute', + boxSizing: 'border-box', + top: startPixelTop + lineHeight + 'px', + left: 0, + right: 0, + height: endPixelTop - startPixelTop - (lineHeight * 2) + 'px' + } + })) + } + + if (endPixelLeft > 0) { + children.push($.div({ + className: regionClassName, + style: { + position: 'absolute', + boxSizing: 'border-box', + top: endPixelTop - lineHeight + 'px', + left: 0, + width: endPixelLeft + 'px', + height: lineHeight + 'px' + } + })) + } + } + + return $.div({className: 'highlight ' + className}, children) + } +} + +class OverlayComponent { + constructor (props) { + this.props = props + this.element = document.createElement('atom-overlay') + if (this.props.className != null) this.element.classList.add(this.props.className) + this.element.appendChild(this.props.element) + this.element.style.position = 'fixed' + this.element.style.zIndex = 4 + this.element.style.top = (this.props.pixelTop || 0) + 'px' + this.element.style.left = (this.props.pixelLeft || 0) + 'px' + + // Synchronous DOM updates in response to resize events might trigger a + // "loop limit exceeded" error. We disconnect the observer before + // potentially mutating the DOM, and then reconnect it on the next tick. + this.resizeObserver = new ResizeObserver(() => { + this.resizeObserver.disconnect() + this.props.didResize() + process.nextTick(() => { this.resizeObserver.observe(this.element) }) + }) + this.didAttach() + this.props.overlayComponents.add(this) + } + + destroy () { + this.props.overlayComponents.delete(this) + this.didDetach() + } + + update (newProps) { + const oldProps = this.props + this.props = newProps + if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px' + if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px' + if (newProps.className !== oldProps.className) { + if (oldProps.className != null) this.element.classList.remove(oldProps.className) + if (newProps.className != null) this.element.classList.add(newProps.className) + } + } + + didAttach () { + this.resizeObserver.observe(this.element) + } + + didDetach () { + this.resizeObserver.disconnect() + } +} + +let rangeForMeasurement +function clientRectForRange (textNode, startIndex, endIndex) { + if (!rangeForMeasurement) rangeForMeasurement = document.createRange() + rangeForMeasurement.setStart(textNode, startIndex) + rangeForMeasurement.setEnd(textNode, endIndex) + return rangeForMeasurement.getBoundingClientRect() +} + +function textDecorationsEqual (oldDecorations, newDecorations) { + if (!oldDecorations && newDecorations) return false + if (oldDecorations && !newDecorations) return false + if (oldDecorations && newDecorations) { + if (oldDecorations.length !== newDecorations.length) return false + for (let j = 0; j < oldDecorations.length; j++) { + if (oldDecorations[j].column !== newDecorations[j].column) return false + if (oldDecorations[j].className !== newDecorations[j].className) return false + if (!objectsEqual(oldDecorations[j].style, newDecorations[j].style)) return false + } + } + return true +} + +function arraysEqual (a, b) { + if (a.length !== b.length) return false + for (let i = 0, length = a.length; i < length; i++) { + if (a[i] !== b[i]) return false + } + return true +} + +function objectsEqual (a, b) { + if (!a && b) return false + if (a && !b) return false + if (a && b) { + for (const key in a) { + if (a[key] !== b[key]) return false + } + for (const key in b) { + if (a[key] !== b[key]) return false + } + } + return true +} + +function constrainRangeToRows (range, startRow, endRow) { + if (range.start.row < startRow || range.end.row >= endRow) { + range = range.copy() + if (range.start.row < startRow) { + range.start.row = startRow + range.start.column = 0 + } + if (range.end.row >= endRow) { + range.end.row = endRow + range.end.column = 0 + } + } + return range +} + +function debounce (fn, wait) { + let timestamp, timeout + + function later () { + const last = Date.now() - timestamp + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last) + } else { + timeout = null + fn() + } + } + + return function () { + timestamp = Date.now() + if (!timeout) timeout = setTimeout(later, wait) + } +} diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee deleted file mode 100644 index 26e3bae12..000000000 --- a/src/text-editor-element.coffee +++ /dev/null @@ -1,331 +0,0 @@ -Grim = require 'grim' -{Emitter, CompositeDisposable} = require 'event-kit' -TextBuffer = require 'text-buffer' -TextEditorComponent = require './text-editor-component' - -class TextEditorElement extends HTMLElement - model: null - componentDescriptor: null - component: null - attached: false - tileSize: null - focusOnAttach: false - hasTiledRendering: true - logicalDisplayBuffer: true - lightDOM: true - - createdCallback: -> - # Use globals when the following instance variables aren't set. - @themes = atom.themes - @workspace = atom.workspace - @assert = atom.assert - @views = atom.views - @styles = atom.styles - - @emitter = new Emitter - @subscriptions = new CompositeDisposable - - @hiddenInputElement = document.createElement('input') - @hiddenInputElement.classList.add('hidden-input') - @hiddenInputElement.setAttribute('tabindex', -1) - @hiddenInputElement.setAttribute('data-react-skip-selection-restoration', true) - @hiddenInputElement.style['-webkit-transform'] = 'translateZ(0)' - @hiddenInputElement.addEventListener 'paste', (event) -> event.preventDefault() - - @addEventListener 'focus', @focused.bind(this) - @addEventListener 'blur', @blurred.bind(this) - @hiddenInputElement.addEventListener 'focus', @focused.bind(this) - @hiddenInputElement.addEventListener 'blur', @inputNodeBlurred.bind(this) - - @classList.add('editor') - @setAttribute('tabindex', -1) - - initializeContent: (attributes) -> - Object.defineProperty(this, 'shadowRoot', { - get: => - Grim.deprecate(""" - The contents of `atom-text-editor` elements are no longer encapsulated - within a shadow DOM boundary. Please, stop using `shadowRoot` and access - the editor contents directly instead. - """) - this - }) - @rootElement = document.createElement('div') - @rootElement.classList.add('editor--private') - @appendChild(@rootElement) - - attachedCallback: -> - @buildModel() unless @getModel()? - @assert(@model.isAlive(), "Attaching a view for a destroyed editor") - @mountComponent() unless @component? - @listenForComponentEvents() - @component.checkForVisibilityChange() - if @hasFocus() - @focused() - @emitter.emit("did-attach") - - detachedCallback: -> - @unmountComponent() - @subscriptions.dispose() - @subscriptions = new CompositeDisposable - @emitter.emit("did-detach") - - listenForComponentEvents: -> - @subscriptions.add @component.onDidChangeScrollTop => - @emitter.emit("did-change-scroll-top", arguments...) - @subscriptions.add @component.onDidChangeScrollLeft => - @emitter.emit("did-change-scroll-left", arguments...) - - initialize: (model, {@views, @themes, @workspace, @assert, @styles}) -> - throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views? - 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 an assert parameter when initializing TextEditorElements") unless @assert? - throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles? - - @setModel(model) - this - - setModel: (model) -> - throw new Error("Model already assigned on TextEditorElement") if @model? - return if model.isDestroyed() - - @model = model - @model.setUpdatedSynchronously(@isUpdatedSynchronously()) - @initializeContent() - @mountComponent() - @addGrammarScopeAttribute() - @addMiniAttribute() if @model.isMini() - @addEncodingAttribute() - @model.onDidChangeGrammar => @addGrammarScopeAttribute() - @model.onDidChangeEncoding => @addEncodingAttribute() - @model.onDidDestroy => @unmountComponent() - @model.onDidChangeMini (mini) => if mini then @addMiniAttribute() else @removeMiniAttribute() - @model - - getModel: -> - @model ? @buildModel() - - buildModel: -> - @setModel(@workspace.buildTextEditor( - buffer: new TextBuffer({ - text: @textContent - shouldDestroyOnFileDelete: - -> atom.config.get('core.closeDeletedFileTabs')}) - softWrapped: false - tabLength: 2 - softTabs: true - mini: @hasAttribute('mini') - lineNumberGutterVisible: not @hasAttribute('gutter-hidden') - placeholderText: @getAttribute('placeholder-text') - )) - - mountComponent: -> - @component = new TextEditorComponent( - hostElement: this - editor: @model - tileSize: @tileSize - views: @views - themes: @themes - styles: @styles - workspace: @workspace - assert: @assert, - hiddenInputElement: @hiddenInputElement - ) - @rootElement.appendChild(@component.getDomNode()) - - unmountComponent: -> - if @component? - @component.destroy() - @component.getDomNode().remove() - @component = null - - focused: (event) -> - @component?.focused() - @hiddenInputElement.focus() - - blurred: (event) -> - if event.relatedTarget is @hiddenInputElement - event.stopImmediatePropagation() - return - @component?.blurred() - - inputNodeBlurred: (event) -> - if event.relatedTarget isnt this - @dispatchEvent(new FocusEvent('blur', relatedTarget: event.relatedTarget, bubbles: false)) - - addGrammarScopeAttribute: -> - @dataset.grammar = @model.getGrammar()?.scopeName?.replace(/\./g, ' ') - - addMiniAttribute: -> - @setAttributeNode(document.createAttribute("mini")) - - removeMiniAttribute: -> - @removeAttribute("mini") - - addEncodingAttribute: -> - @dataset.encoding = @model.getEncoding() - - hasFocus: -> - this is document.activeElement or @contains(document.activeElement) - - setUpdatedSynchronously: (@updatedSynchronously) -> - @model?.setUpdatedSynchronously(@updatedSynchronously) - @updatedSynchronously - - isUpdatedSynchronously: -> @updatedSynchronously - - # Extended: Continuously reflows lines and line numbers. (Has performance overhead) - # - # * `continuousReflow` A {Boolean} indicating whether to keep reflowing or not. - setContinuousReflow: (continuousReflow) -> - @component?.setContinuousReflow(continuousReflow) - - # Extended: get the width of a character of text displayed in this element. - # - # Returns a {Number} of pixels. - getDefaultCharacterWidth: -> - @getModel().getDefaultCharWidth() - - # Extended: Get the maximum scroll top that can be applied to this element. - # - # Returns a {Number} of pixels. - getMaxScrollTop: -> - @component?.getMaxScrollTop() - - # Extended: Converts a buffer position to a pixel position. - # - # * `bufferPosition` An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # - # Returns an {Object} with two values: `top` and `left`, representing the pixel position. - pixelPositionForBufferPosition: (bufferPosition) -> - @component.pixelPositionForBufferPosition(bufferPosition) - - # Extended: Converts a screen position to a pixel position. - # - # * `screenPosition` An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # - # Returns an {Object} with two values: `top` and `left`, representing the pixel positions. - pixelPositionForScreenPosition: (screenPosition) -> - @component.pixelPositionForScreenPosition(screenPosition) - - # Extended: Retrieves the number of the row that is visible and currently at the - # top of the editor. - # - # Returns a {Number}. - getFirstVisibleScreenRow: -> - @getVisibleRowRange()[0] - - # Extended: Retrieves the number of the row that is visible and currently at the - # bottom of the editor. - # - # Returns a {Number}. - getLastVisibleScreenRow: -> - @getVisibleRowRange()[1] - - # Extended: call the given `callback` when the editor is attached to the DOM. - # - # * `callback` {Function} - onDidAttach: (callback) -> - @emitter.on("did-attach", callback) - - # Extended: call the given `callback` when the editor is detached from the DOM. - # - # * `callback` {Function} - onDidDetach: (callback) -> - @emitter.on("did-detach", callback) - - onDidChangeScrollTop: (callback) -> - @emitter.on("did-change-scroll-top", callback) - - onDidChangeScrollLeft: (callback) -> - @emitter.on("did-change-scroll-left", callback) - - setScrollLeft: (scrollLeft) -> - @component.setScrollLeft(scrollLeft) - - setScrollRight: (scrollRight) -> - @component.setScrollRight(scrollRight) - - setScrollTop: (scrollTop) -> - @component.setScrollTop(scrollTop) - - setScrollBottom: (scrollBottom) -> - @component.setScrollBottom(scrollBottom) - - # Essential: Scrolls the editor to the top - scrollToTop: -> - @setScrollTop(0) - - # Essential: Scrolls the editor to the bottom - scrollToBottom: -> - @setScrollBottom(Infinity) - - getScrollTop: -> - @component?.getScrollTop() or 0 - - getScrollLeft: -> - @component?.getScrollLeft() or 0 - - getScrollRight: -> - @component?.getScrollRight() or 0 - - getScrollBottom: -> - @component?.getScrollBottom() or 0 - - getScrollHeight: -> - @component?.getScrollHeight() or 0 - - getScrollWidth: -> - @component?.getScrollWidth() or 0 - - getVerticalScrollbarWidth: -> - @component?.getVerticalScrollbarWidth() or 0 - - getHorizontalScrollbarHeight: -> - @component?.getHorizontalScrollbarHeight() or 0 - - getVisibleRowRange: -> - @component?.getVisibleRowRange() or [0, 0] - - intersectsVisibleRowRange: (startRow, endRow) -> - [visibleStart, visibleEnd] = @getVisibleRowRange() - not (endRow <= visibleStart or visibleEnd <= startRow) - - selectionIntersectsVisibleRowRange: (selection) -> - {start, end} = selection.getScreenRange() - @intersectsVisibleRowRange(start.row, end.row + 1) - - screenPositionForPixelPosition: (pixelPosition) -> - @component.screenPositionForPixelPosition(pixelPosition) - - pixelRectForScreenRange: (screenRange) -> - @component.pixelRectForScreenRange(screenRange) - - pixelRangeForScreenRange: (screenRange) -> - @component.pixelRangeForScreenRange(screenRange) - - setWidth: (width) -> - @style.width = (@component.getGutterWidth() + width) + "px" - - getWidth: -> - @offsetWidth - @component.getGutterWidth() - - setHeight: (height) -> - @style.height = height + "px" - - getHeight: -> - @offsetHeight - - # Experimental: Invalidate the passed block {Decoration} dimensions, forcing - # them to be recalculated and the surrounding content to be adjusted on the - # next animation frame. - # - # * {blockDecoration} A {Decoration} representing the block decoration you - # want to update the dimensions of. - invalidateBlockDecorationDimensions: -> - @component.invalidateBlockDecorationDimensions(arguments...) - -module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype diff --git a/src/text-editor-element.js b/src/text-editor-element.js new file mode 100644 index 000000000..d56c5596b --- /dev/null +++ b/src/text-editor-element.js @@ -0,0 +1,346 @@ +const {Emitter, Range} = require('atom') +const Grim = require('grim') +const TextEditorComponent = require('./text-editor-component') +const dedent = require('dedent') + +class TextEditorElement extends HTMLElement { + initialize (component) { + this.component = component + return this + } + + get shadowRoot () { + Grim.deprecate(dedent` + The contents of \`atom-text-editor\` elements are no longer encapsulated + within a shadow DOM boundary. Please, stop using \`shadowRoot\` and access + the editor contents directly instead. + `) + + return this + } + + get rootElement () { + Grim.deprecate(dedent` + The contents of \`atom-text-editor\` elements are no longer encapsulated + within a shadow DOM boundary. Please, stop using \`rootElement\` and access + the editor contents directly instead. + `) + + return this + } + + createdCallback () { + this.emitter = new Emitter() + this.initialText = this.textContent + this.tabIndex = -1 + this.addEventListener('focus', (event) => this.getComponent().didFocus(event)) + this.addEventListener('blur', (event) => this.getComponent().didBlur(event)) + } + + attachedCallback () { + this.getComponent().didAttach() + this.emitter.emit('did-attach') + } + + detachedCallback () { + this.emitter.emit('did-detach') + this.getComponent().didDetach() + } + + attributeChangedCallback (name, oldValue, newValue) { + if (this.component) { + switch (name) { + case 'mini': + this.getModel().update({mini: newValue != null}) + break + case 'placeholder-text': + this.getModel().update({placeholderText: newValue}) + break + case 'gutter-hidden': + this.getModel().update({lineNumberGutterVisible: newValue == null}) + break + } + } + } + + // Extended: Get a promise that resolves the next time the element's DOM + // is updated in any way. + // + // This can be useful when you've made a change to the model and need to + // be sure this change has been flushed to the DOM. + // + // Returns a {Promise}. + getNextUpdatePromise () { + return this.getComponent().getNextUpdatePromise() + } + + getModel () { + return this.getComponent().props.model + } + + setModel (model) { + this.getComponent().update({model}) + this.updateModelFromAttributes() + } + + updateModelFromAttributes () { + const props = {mini: this.hasAttribute('mini')} + if (this.hasAttribute('placeholder-text')) props.placeholderText = this.getAttribute('placeholder-text') + if (this.hasAttribute('gutter-hidden')) props.lineNumberGutterVisible = false + + this.getModel().update(props) + if (this.initialText) this.getModel().setText(this.initialText) + } + + onDidAttach (callback) { + return this.emitter.on('did-attach', callback) + } + + onDidDetach (callback) { + return this.emitter.on('did-detach', callback) + } + + measureDimensions () { + this.getComponent().measureDimensions() + } + + setWidth (width) { + this.style.width = this.getComponent().getGutterContainerWidth() + width + 'px' + } + + getWidth () { + return this.getComponent().getScrollContainerWidth() + } + + setHeight (height) { + this.style.height = height + 'px' + } + + getHeight () { + return this.getComponent().getScrollContainerHeight() + } + + onDidChangeScrollLeft (callback) { + return this.emitter.on('did-change-scroll-left', callback) + } + + onDidChangeScrollTop (callback) { + return this.emitter.on('did-change-scroll-top', callback) + } + + // Deprecated: get the width of an `x` character displayed in this element. + // + // Returns a {Number} of pixels. + getDefaultCharacterWidth () { + return this.getComponent().getBaseCharacterWidth() + } + + // Extended: get the width of an `x` character displayed in this element. + // + // Returns a {Number} of pixels. + getBaseCharacterWidth () { + return this.getComponent().getBaseCharacterWidth() + } + + getMaxScrollTop () { + return this.getComponent().getMaxScrollTop() + } + + getScrollHeight () { + return this.getComponent().getScrollHeight() + } + + getScrollWidth () { + return this.getComponent().getScrollWidth() + } + + getVerticalScrollbarWidth () { + return this.getComponent().getVerticalScrollbarWidth() + } + + getHorizontalScrollbarHeight () { + return this.getComponent().getHorizontalScrollbarHeight() + } + + getScrollTop () { + return this.getComponent().getScrollTop() + } + + setScrollTop (scrollTop) { + const component = this.getComponent() + component.setScrollTop(scrollTop) + component.scheduleUpdate() + } + + getScrollBottom () { + return this.getComponent().getScrollBottom() + } + + setScrollBottom (scrollBottom) { + return this.getComponent().setScrollBottom(scrollBottom) + } + + getScrollLeft () { + return this.getComponent().getScrollLeft() + } + + setScrollLeft (scrollLeft) { + const component = this.getComponent() + component.setScrollLeft(scrollLeft) + component.scheduleUpdate() + } + + getScrollRight () { + return this.getComponent().getScrollRight() + } + + setScrollRight (scrollRight) { + return this.getComponent().setScrollRight(scrollRight) + } + + // Essential: Scrolls the editor to the top. + scrollToTop () { + this.setScrollTop(0) + } + + // Essential: Scrolls the editor to the bottom. + scrollToBottom () { + this.setScrollTop(Infinity) + } + + hasFocus () { + return this.getComponent().focused + } + + // Extended: Converts a buffer position to a pixel position. + // + // * `bufferPosition` A {Point}-like object that represents a buffer position. + // + // Be aware that calling this method with a column that does not translate + // to column 0 on screen could cause a synchronous DOM update in order to + // measure the requested horizontal pixel position if it isn't already + // cached. + // + // Returns an {Object} with two values: `top` and `left`, representing the + // pixel position. + pixelPositionForBufferPosition (bufferPosition) { + const screenPosition = this.getModel().screenPositionForBufferPosition(bufferPosition) + return this.getComponent().pixelPositionForScreenPosition(screenPosition) + } + + // Extended: Converts a screen position to a pixel position. + // + // * `screenPosition` A {Point}-like object that represents a buffer position. + // + // Be aware that calling this method with a non-zero column value could + // cause a synchronous DOM update in order to measure the requested + // horizontal pixel position if it isn't already cached. + // + // Returns an {Object} with two values: `top` and `left`, representing the + // pixel position. + pixelPositionForScreenPosition (screenPosition) { + screenPosition = this.getModel().clipScreenPosition(screenPosition) + return this.getComponent().pixelPositionForScreenPosition(screenPosition) + } + + screenPositionForPixelPosition (pixelPosition) { + return this.getComponent().screenPositionForPixelPosition(pixelPosition) + } + + pixelRectForScreenRange (range) { + range = Range.fromObject(range) + + const start = this.pixelPositionForScreenPosition(range.start) + const end = this.pixelPositionForScreenPosition(range.end) + const lineHeight = this.getComponent().getLineHeight() + + return { + top: start.top, + left: start.left, + height: end.top + lineHeight - start.top, + width: end.left - start.left + } + } + + pixelRangeForScreenRange (range) { + range = Range.fromObject(range) + return { + start: this.pixelPositionForScreenPosition(range.start), + end: this.pixelPositionForScreenPosition(range.end) + } + } + + getComponent () { + if (!this.component) { + this.component = new TextEditorComponent({ + element: this, + mini: this.hasAttribute('mini'), + updatedSynchronously: this.updatedSynchronously + }) + this.updateModelFromAttributes() + } + + return this.component + } + + setUpdatedSynchronously (updatedSynchronously) { + this.updatedSynchronously = updatedSynchronously + if (this.component) this.component.updatedSynchronously = updatedSynchronously + return updatedSynchronously + } + + isUpdatedSynchronously () { + return this.component ? this.component.updatedSynchronously : this.updatedSynchronously + } + + // Experimental: Invalidate the passed block {Decoration}'s dimensions, + // forcing them to be recalculated and the surrounding content to be adjusted + // on the next animation frame. + // + // * {blockDecoration} A {Decoration} representing the block decoration you + // want to update the dimensions of. + invalidateBlockDecorationDimensions () { + this.getComponent().invalidateBlockDecorationDimensions(...arguments) + } + + setFirstVisibleScreenRow (row) { + this.getModel().setFirstVisibleScreenRow(row) + } + + getFirstVisibleScreenRow () { + return this.getModel().getFirstVisibleScreenRow() + } + + getLastVisibleScreenRow () { + return this.getModel().getLastVisibleScreenRow() + } + + getVisibleRowRange () { + return this.getModel().getVisibleRowRange() + } + + intersectsVisibleRowRange (startRow, endRow) { + return !( + endRow <= this.getFirstVisibleScreenRow() || + this.getLastVisibleScreenRow() <= startRow + ) + } + + selectionIntersectsVisibleRowRange (selection) { + const {start, end} = selection.getScreenRange() + return this.intersectsVisibleRowRange(start.row, end.row + 1) + } + + setFirstVisibleScreenColumn (column) { + return this.getModel().setFirstVisibleScreenColumn(column) + } + + getFirstVisibleScreenColumn () { + return this.getModel().getFirstVisibleScreenColumn() + } +} + +module.exports = +document.registerElement('atom-text-editor', { + prototype: TextEditorElement.prototype +}) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee deleted file mode 100644 index 1106cee09..000000000 --- a/src/text-editor-presenter.coffee +++ /dev/null @@ -1,1562 +0,0 @@ -{CompositeDisposable, Emitter} = require 'event-kit' -{Point, Range} = require 'text-buffer' -_ = require 'underscore-plus' -Decoration = require './decoration' - -module.exports = -class TextEditorPresenter - toggleCursorBlinkHandle: null - startBlinkingCursorsAfterDelay: null - stoppedScrollingTimeoutId: null - mouseWheelScreenRow: null - overlayDimensions: null - minimumReflowInterval: 200 - - constructor: (params) -> - {@model, @lineTopIndex} = params - @model.presenter = this - {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize, @autoHeight} = params - {@contentFrameWidth} = params - {@displayLayer} = @model - - @gutterWidth = 0 - @tileSize ?= 6 - @realScrollTop = @scrollTop - @realScrollLeft = @scrollLeft - @disposables = new CompositeDisposable - @emitter = new Emitter - @linesByScreenRow = new Map - @visibleHighlights = {} - @characterWidthsByScope = {} - @lineDecorationsByScreenRow = {} - @lineNumberDecorationsByScreenRow = {} - @customGutterDecorationsByGutterName = {} - @overlayDimensions = {} - @observedBlockDecorations = new Set() - @invalidatedDimensionsByBlockDecoration = new Set() - @invalidateAllBlockDecorationsDimensions = false - @precedingBlockDecorationsByScreenRowAndId = {} - @followingBlockDecorationsByScreenRowAndId = {} - @screenRowsToMeasure = [] - @flashCountsByDecorationId = {} - @transferMeasurementsToModel() - @transferMeasurementsFromModel() - @observeModel() - @buildState() - @invalidateState() - @startBlinkingCursors() if @focused - @startReflowing() if @continuousReflow - @updating = false - - setLinesYardstick: (@linesYardstick) -> - - getLinesYardstick: -> @linesYardstick - - destroy: -> - @disposables.dispose() - clearTimeout(@stoppedScrollingTimeoutId) if @stoppedScrollingTimeoutId? - clearInterval(@reflowingInterval) if @reflowingInterval? - @stopBlinkingCursors() - - # Calls your `callback` when some changes in the model occurred and the current state has been updated. - onDidUpdateState: (callback) -> - @emitter.on 'did-update-state', callback - - emitDidUpdateState: -> - @emitter.emit "did-update-state" if @isBatching() - - transferMeasurementsToModel: -> - @model.setLineHeightInPixels(@lineHeight) if @lineHeight? - @model.setDefaultCharWidth(@baseCharacterWidth) if @baseCharacterWidth? - - transferMeasurementsFromModel: -> - @editorWidthInChars = @model.getEditorWidthInChars() - - # Private: Determines whether {TextEditorPresenter} is currently batching changes. - # Returns a {Boolean}, `true` if is collecting changes, `false` if is applying them. - isBatching: -> - @updating is false - - getPreMeasurementState: -> - @updating = true - - @updateVerticalDimensions() - @updateScrollbarDimensions() - - @commitPendingLogicalScrollTopPosition() - @commitPendingScrollTopPosition() - - @updateStartRow() - @updateEndRow() - @updateCommonGutterState() - @updateReflowState() - - @updateLines() - - if @shouldUpdateDecorations - @fetchDecorations() - @updateLineDecorations() - @updateBlockDecorations() - - @updateTilesState() - - @updating = false - @state - - getPostMeasurementState: -> - @updating = true - - @updateHorizontalDimensions() - @commitPendingLogicalScrollLeftPosition() - @commitPendingScrollLeftPosition() - @clearPendingScrollPosition() - @updateRowsPerPage() - - @updateLines() - - @updateVerticalScrollState() - @updateHorizontalScrollState() - @updateScrollbarsState() - @updateHiddenInputState() - @updateContentState() - @updateFocusedState() - @updateHeightState() - @updateWidthState() - @updateHighlightDecorations() if @shouldUpdateDecorations - @updateTilesState() - @updateCursorsState() - @updateOverlaysState() - @updateLineNumberGutterState() - @updateGutterOrderState() - @updateCustomGutterDecorationState() - @updating = false - - @resetTrackedUpdates() - @state - - resetTrackedUpdates: -> - @shouldUpdateDecorations = false - - invalidateState: -> - @shouldUpdateDecorations = true - - observeModel: -> - @disposables.add @model.displayLayer.onDidReset => - @spliceBlockDecorationsInRange(0, Infinity, Infinity) - @shouldUpdateDecorations = true - @emitDidUpdateState() - - @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() - - @disposables.add @model.onDidUpdateDecorations => - @shouldUpdateDecorations = true - @emitDidUpdateState() - - @disposables.add @model.onDidAddDecoration(@didAddBlockDecoration.bind(this)) - - for decoration in @model.getDecorations({type: 'block'}) - this.didAddBlockDecoration(decoration) - - @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) - @disposables.add @model.onDidChangePlaceholderText(@emitDidUpdateState.bind(this)) - @disposables.add @model.onDidChangeMini => - @shouldUpdateDecorations = true - @emitDidUpdateState() - - @disposables.add @model.onDidChangeLineNumberGutterVisible(@emitDidUpdateState.bind(this)) - - @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this)) - @disposables.add @model.onDidRequestAutoscroll(@requestAutoscroll.bind(this)) - @disposables.add @model.onDidChangeFirstVisibleScreenRow(@didChangeFirstVisibleScreenRow.bind(this)) - @observeCursor(cursor) for cursor in @model.getCursors() - @disposables.add @model.onDidAddGutter(@didAddGutter.bind(this)) - return - - didChangeScrollPastEnd: -> - @updateScrollHeight() - @emitDidUpdateState() - - didChangeShowLineNumbers: -> - @emitDidUpdateState() - - didChangeGrammar: -> - @emitDidUpdateState() - - buildState: -> - @state = - horizontalScrollbar: {} - verticalScrollbar: {} - hiddenInput: {} - content: - scrollingVertically: false - cursorsVisible: false - tiles: {} - highlights: {} - overlays: {} - cursors: {} - offScreenBlockDecorations: {} - gutters: [] - # Shared state that is copied into ``@state.gutters`. - @sharedGutterStyles = {} - @customGutterDecorations = {} - @lineNumberGutter = - tiles: {} - - setContinuousReflow: (@continuousReflow) -> - if @continuousReflow - @startReflowing() - else - @stopReflowing() - - updateReflowState: -> - @state.content.continuousReflow = @continuousReflow - @lineNumberGutter.continuousReflow = @continuousReflow - - startReflowing: -> - @reflowingInterval = setInterval(@emitDidUpdateState.bind(this), @minimumReflowInterval) - - stopReflowing: -> - clearInterval(@reflowingInterval) - @reflowingInterval = null - - updateFocusedState: -> - @state.focused = @focused - - updateHeightState: -> - if @autoHeight - @state.height = @contentHeight - else - @state.height = null - - updateWidthState: -> - if @model.getAutoWidth() - @state.width = @state.content.width + @gutterWidth - else - @state.width = null - - updateVerticalScrollState: -> - @state.content.scrollHeight = @scrollHeight - @sharedGutterStyles.scrollHeight = @scrollHeight - @state.verticalScrollbar.scrollHeight = @scrollHeight - - @state.content.scrollTop = @scrollTop - @sharedGutterStyles.scrollTop = @scrollTop - @state.verticalScrollbar.scrollTop = @scrollTop - - updateHorizontalScrollState: -> - @state.content.scrollWidth = @scrollWidth - @state.horizontalScrollbar.scrollWidth = @scrollWidth - - @state.content.scrollLeft = @scrollLeft - @state.horizontalScrollbar.scrollLeft = @scrollLeft - - updateScrollbarsState: -> - @state.horizontalScrollbar.visible = @horizontalScrollbarHeight > 0 - @state.horizontalScrollbar.height = @measuredHorizontalScrollbarHeight - @state.horizontalScrollbar.right = @verticalScrollbarWidth - - @state.verticalScrollbar.visible = @verticalScrollbarWidth > 0 - @state.verticalScrollbar.width = @measuredVerticalScrollbarWidth - @state.verticalScrollbar.bottom = @horizontalScrollbarHeight - - updateHiddenInputState: -> - return unless lastCursor = @model.getLastCursor() - - {top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange()) - - if @focused - @state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0) - @state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0) - else - @state.hiddenInput.top = 0 - @state.hiddenInput.left = 0 - - @state.hiddenInput.height = height - @state.hiddenInput.width = Math.max(width, 2) - - updateContentState: -> - if @boundingClientRect? - @sharedGutterStyles.maxHeight = @boundingClientRect.height - @state.content.maxHeight = @boundingClientRect.height - - verticalScrollbarWidth = @verticalScrollbarWidth ? 0 - contentFrameWidth = @contentFrameWidth ? 0 - contentWidth = @contentWidth ? 0 - if @model.getAutoWidth() - @state.content.width = contentWidth + verticalScrollbarWidth - else - @state.content.width = Math.max(contentWidth + verticalScrollbarWidth, contentFrameWidth) - @state.content.scrollWidth = @scrollWidth - @state.content.scrollLeft = @scrollLeft - @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor - @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null - - tileForRow: (row) -> - row - (row % @tileSize) - - getStartTileRow: -> - @tileForRow(@startRow ? 0) - - getEndTileRow: -> - @tileForRow(@endRow ? 0) - - getScreenRowsToRender: -> - startRow = @getStartTileRow() - endRow = @getEndTileRow() + @tileSize - - screenRows = [startRow...endRow] - longestScreenRow = @model.getApproximateLongestScreenRow() - if longestScreenRow? - screenRows.push(longestScreenRow) - if @screenRowsToMeasure? - screenRows.push(@screenRowsToMeasure...) - - screenRows = screenRows.filter (row) -> row >= 0 - 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 - - @screenRowsToMeasure = screenRows - @shouldUpdateDecorations = true - - clearScreenRowsToMeasure: -> - @screenRowsToMeasure = [] - - updateTilesState: -> - return unless @startRow? and @endRow? and @lineHeight? - - screenRows = @getScreenRowsToRender() - visibleTiles = {} - startRow = screenRows[0] - endRow = screenRows[screenRows.length - 1] - screenRowIndex = screenRows.length - 1 - zIndex = 0 - - for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize - tileEndRow = tileStartRow + @tileSize - rowsWithinTile = [] - - while screenRowIndex >= 0 - currentScreenRow = screenRows[screenRowIndex] - break if currentScreenRow < tileStartRow - rowsWithinTile.push(currentScreenRow) - screenRowIndex-- - - continue if rowsWithinTile.length is 0 - - top = Math.round(@lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow)) - bottom = Math.round(@lineTopIndex.pixelPositionBeforeBlocksForRow(tileEndRow)) - height = bottom - top - - tile = @state.content.tiles[tileStartRow] ?= {} - tile.top = top - @scrollTop - tile.left = -@scrollLeft - tile.height = height - tile.display = "block" - tile.zIndex = zIndex - tile.highlights ?= {} - - gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {} - gutterTile.top = top - @scrollTop - gutterTile.height = height - gutterTile.display = "block" - gutterTile.zIndex = zIndex - - @updateLinesState(tile, rowsWithinTile) - @updateLineNumbersState(gutterTile, rowsWithinTile) - - visibleTiles[tileStartRow] = true - zIndex++ - - mouseWheelTileId = @tileForRow(@mouseWheelScreenRow) if @mouseWheelScreenRow? - - for id, tile of @state.content.tiles - continue if visibleTiles.hasOwnProperty(id) - - if Number(id) is mouseWheelTileId - @state.content.tiles[id].display = "none" - @lineNumberGutter.tiles[id].display = "none" - else - delete @state.content.tiles[id] - delete @lineNumberGutter.tiles[id] - - updateLinesState: (tileState, screenRows) -> - tileState.lines ?= {} - visibleLineIds = {} - for screenRow in screenRows - line = @linesByScreenRow.get(screenRow) - continue unless line? - - visibleLineIds[line.id] = true - precedingBlockDecorations = @precedingBlockDecorationsByScreenRowAndId[screenRow] ? {} - followingBlockDecorations = @followingBlockDecorationsByScreenRowAndId[screenRow] ? {} - if tileState.lines.hasOwnProperty(line.id) - lineState = tileState.lines[line.id] - lineState.screenRow = screenRow - lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) - lineState.precedingBlockDecorations = precedingBlockDecorations - lineState.followingBlockDecorations = followingBlockDecorations - else - tileState.lines[line.id] = - screenRow: screenRow - lineText: line.lineText - tagCodes: line.tagCodes - decorationClasses: @lineDecorationClassesForRow(screenRow) - precedingBlockDecorations: precedingBlockDecorations - followingBlockDecorations: followingBlockDecorations - - for id, line of tileState.lines - delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) - return - - updateCursorsState: -> - return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth? - - @state.content.cursors = {} - for cursor in @model.cursorsForScreenRowRange(@startRow, @endRow - 1) when cursor.isVisible() - pixelRect = @pixelRectForScreenRange(cursor.getScreenRange()) - pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0 - @state.content.cursors[cursor.id] = pixelRect - return - - updateOverlaysState: -> - return unless @hasOverlayPositionRequirements() - - visibleDecorationIds = {} - - for decoration in @model.getOverlayDecorations() - continue unless decoration.getMarker().isValid() - - {item, position, class: klass, avoidOverflow} = decoration.getProperties() - if position is 'tail' - screenPosition = decoration.getMarker().getTailScreenPosition() - else - screenPosition = decoration.getMarker().getHeadScreenPosition() - - pixelPosition = @pixelPositionForScreenPosition(screenPosition) - - # Fixed positioning. - top = @boundingClientRect.top + pixelPosition.top + @lineHeight - left = @boundingClientRect.left + pixelPosition.left + @gutterWidth - - if overlayDimensions = @overlayDimensions[decoration.id] - {itemWidth, itemHeight, contentMargin} = overlayDimensions - - if avoidOverflow isnt false - rightDiff = left + itemWidth + contentMargin - @windowWidth - left -= rightDiff if rightDiff > 0 - - leftDiff = left + contentMargin - left -= leftDiff if leftDiff < 0 - - if top + itemHeight > @windowHeight and - top - (itemHeight + @lineHeight) >= 0 - top -= itemHeight + @lineHeight - - pixelPosition.top = top - pixelPosition.left = left - - overlayState = @state.content.overlays[decoration.id] ?= {item} - overlayState.pixelPosition = pixelPosition - overlayState.class = klass if klass? - visibleDecorationIds[decoration.id] = true - - for id of @state.content.overlays - delete @state.content.overlays[id] unless visibleDecorationIds[id] - - for id of @overlayDimensions - delete @overlayDimensions[id] unless visibleDecorationIds[id] - - return - - updateLineNumberGutterState: -> - @lineNumberGutter.maxLineNumberDigits = Math.max( - 2, - @model.getLineCount().toString().length - ) - - updateCommonGutterState: -> - @sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)" - @gutterBackgroundColor - else - @backgroundColor - - didAddGutter: (gutter) -> - gutterDisposables = new CompositeDisposable - gutterDisposables.add gutter.onDidChangeVisible => @emitDidUpdateState() - gutterDisposables.add gutter.onDidDestroy => - @disposables.remove(gutterDisposables) - gutterDisposables.dispose() - @emitDidUpdateState() - # It is not necessary to @updateCustomGutterDecorationState here. - # The destroyed gutter will be removed from the list of gutters in @state, - # and thus will be removed from the DOM. - @disposables.add(gutterDisposables) - @emitDidUpdateState() - - updateGutterOrderState: -> - @state.gutters = [] - if @model.isMini() - return - for gutter in @model.getGutters() - isVisible = @gutterIsVisible(gutter) - if gutter.name is 'line-number' - content = @lineNumberGutter - else - @customGutterDecorations[gutter.name] ?= {} - content = @customGutterDecorations[gutter.name] - @state.gutters.push({ - gutter, - visible: isVisible, - styles: @sharedGutterStyles, - content, - }) - - # Updates the decoration state for the gutter with the given gutterName. - # @customGutterDecorations is an {Object}, with the form: - # * gutterName : { - # decoration.id : { - # top: # of pixels from top - # height: # of pixels height of this decoration - # item (optional): HTMLElement - # class (optional): {String} class - # } - # } - updateCustomGutterDecorationState: -> - return unless @startRow? and @endRow? and @lineHeight? - - if @model.isMini() - # Mini editors have no gutter decorations. - # We clear instead of reassigning to preserve the reference. - @clearAllCustomGutterDecorations() - - for gutter in @model.getGutters() - gutterName = gutter.name - gutterDecorations = @customGutterDecorations[gutterName] - if gutterDecorations - # Clear the gutter decorations; they are rebuilt. - # We clear instead of reassigning to preserve the reference. - @clearDecorationsForCustomGutterName(gutterName) - else - @customGutterDecorations[gutterName] = {} - - continue unless @gutterIsVisible(gutter) - for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName] - top = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.start.row) - bottom = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) - @customGutterDecorations[gutterName][decorationId] = - top: top - height: bottom - top - item: properties.item - class: properties.class - - clearAllCustomGutterDecorations: -> - allGutterNames = Object.keys(@customGutterDecorations) - for gutterName in allGutterNames - @clearDecorationsForCustomGutterName(gutterName) - - clearDecorationsForCustomGutterName: (gutterName) -> - gutterDecorations = @customGutterDecorations[gutterName] - if gutterDecorations - allDecorationIds = Object.keys(gutterDecorations) - for decorationId in allDecorationIds - delete gutterDecorations[decorationId] - - gutterIsVisible: (gutterModel) -> - isVisible = gutterModel.isVisible() - if gutterModel.name is 'line-number' - isVisible = isVisible and @model.doesShowLineNumbers() - isVisible - - updateLineNumbersState: (tileState, screenRows) -> - tileState.lineNumbers ?= {} - visibleLineNumberIds = {} - - for screenRow in screenRows when @isRowRendered(screenRow) - line = @linesByScreenRow.get(screenRow) - continue unless line? - lineId = line.id - {row: bufferRow, column: bufferColumn} = @displayLayer.translateScreenPosition(Point(screenRow, 0)) - softWrapped = bufferColumn isnt 0 - foldable = not softWrapped and @model.isFoldableAtBufferRow(bufferRow) - decorationClasses = @lineNumberDecorationClassesForRow(screenRow) - blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight - if screenRow % @tileSize isnt 0 - blockDecorationsAfterPreviousScreenRowHeight = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - @lineHeight - @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow - 1) - blockDecorationsHeight += blockDecorationsAfterPreviousScreenRowHeight - - tileState.lineNumbers[lineId] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight} - visibleLineNumberIds[lineId] = true - - for id of tileState.lineNumbers - delete tileState.lineNumbers[id] unless visibleLineNumberIds[id] - - return - - updateStartRow: -> - return unless @scrollTop? and @lineHeight? - - @startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop)) - atom.assert( - Number.isFinite(@startRow), - 'Invalid start row', - (error) => - error.metadata = { - startRow: @startRow?.toString(), - scrollTop: @scrollTop?.toString(), - scrollHeight: @scrollHeight?.toString(), - clientHeight: @clientHeight?.toString(), - lineHeight: @lineHeight?.toString() - } - ) - - updateEndRow: -> - return unless @scrollTop? and @lineHeight? and @height? - - @endRow = Math.min( - @model.getApproximateScreenLineCount(), - @lineTopIndex.rowForPixelPosition(@scrollTop + @height + @lineHeight - 1) + 1 - ) - - updateRowsPerPage: -> - rowsPerPage = Math.floor(@getClientHeight() / @lineHeight) - if rowsPerPage isnt @rowsPerPage - @rowsPerPage = rowsPerPage - @model.setRowsPerPage(@rowsPerPage) - - updateScrollWidth: -> - return unless @contentWidth? and @clientWidth? - - scrollWidth = Math.max(@contentWidth, @clientWidth) - unless @scrollWidth is scrollWidth - @scrollWidth = scrollWidth - @updateScrollLeft(@scrollLeft) - - updateScrollHeight: -> - return unless @contentHeight? and @clientHeight? - - contentHeight = @contentHeight - if @model.getScrollPastEnd() - extraScrollHeight = @clientHeight - (@lineHeight * 3) - contentHeight += extraScrollHeight if extraScrollHeight > 0 - scrollHeight = Math.max(contentHeight, @height) - - unless @scrollHeight is scrollHeight - @scrollHeight = scrollHeight - @updateScrollTop(@scrollTop) - - updateVerticalDimensions: -> - if @lineHeight? - oldContentHeight = @contentHeight - @contentHeight = Math.round(@lineTopIndex.pixelPositionAfterBlocksForRow(@model.getApproximateScreenLineCount())) - - if @contentHeight isnt oldContentHeight - @updateHeight() - @updateScrollbarDimensions() - @updateScrollHeight() - - updateHorizontalDimensions: -> - if @baseCharacterWidth? - oldContentWidth = @contentWidth - rightmostPosition = @model.getApproximateRightmostScreenPosition() - @contentWidth = @pixelPositionForScreenPosition(rightmostPosition).left - @contentWidth += @scrollLeft - @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width - - if @contentWidth isnt oldContentWidth - @updateScrollbarDimensions() - @updateClientWidth() - @updateScrollWidth() - - updateClientHeight: -> - return unless @height? and @horizontalScrollbarHeight? - - clientHeight = @height - @horizontalScrollbarHeight - @model.setHeight(clientHeight, true) - - unless @clientHeight is clientHeight - @clientHeight = clientHeight - @updateScrollHeight() - @updateScrollTop(@scrollTop) - - updateClientWidth: -> - return unless @contentFrameWidth? and @verticalScrollbarWidth? - - if @model.getAutoWidth() - clientWidth = @contentWidth - else - clientWidth = @contentFrameWidth - @verticalScrollbarWidth - - @model.setWidth(clientWidth, true) unless @editorWidthInChars - - unless @clientWidth is clientWidth - @clientWidth = clientWidth - @updateScrollWidth() - @updateScrollLeft(@scrollLeft) - - updateScrollTop: (scrollTop) -> - scrollTop = @constrainScrollTop(scrollTop) - if scrollTop isnt @realScrollTop and not Number.isNaN(scrollTop) - @realScrollTop = scrollTop - @scrollTop = Math.round(scrollTop) - @model.setFirstVisibleScreenRow(Math.round(@scrollTop / @lineHeight), true) - - @updateStartRow() - @updateEndRow() - @didStartScrolling() - @emitter.emit 'did-change-scroll-top', @scrollTop - - constrainScrollTop: (scrollTop) -> - return scrollTop unless scrollTop? and @scrollHeight? and @clientHeight? - Math.max(0, Math.min(scrollTop, @scrollHeight - @clientHeight)) - - updateScrollLeft: (scrollLeft) -> - scrollLeft = @constrainScrollLeft(scrollLeft) - if scrollLeft isnt @realScrollLeft and not Number.isNaN(scrollLeft) - @realScrollLeft = scrollLeft - @scrollLeft = Math.round(scrollLeft) - @model.setFirstVisibleScreenColumn(Math.round(@scrollLeft / @baseCharacterWidth)) - - @emitter.emit 'did-change-scroll-left', @scrollLeft - - constrainScrollLeft: (scrollLeft) -> - return scrollLeft unless scrollLeft? and @scrollWidth? and @clientWidth? - Math.max(0, Math.min(scrollLeft, @scrollWidth - @clientWidth)) - - updateScrollbarDimensions: -> - return unless @contentFrameWidth? and @height? - return unless @measuredVerticalScrollbarWidth? and @measuredHorizontalScrollbarHeight? - return unless @contentWidth? and @contentHeight? - - if @model.getAutoWidth() - clientWidthWithVerticalScrollbar = @contentWidth + @measuredVerticalScrollbarWidth - else - clientWidthWithVerticalScrollbar = @contentFrameWidth - clientWidthWithoutVerticalScrollbar = clientWidthWithVerticalScrollbar - @measuredVerticalScrollbarWidth - clientHeightWithHorizontalScrollbar = @height - clientHeightWithoutHorizontalScrollbar = clientHeightWithHorizontalScrollbar - @measuredHorizontalScrollbarHeight - - horizontalScrollbarVisible = - not @model.isMini() and - (@contentWidth > clientWidthWithVerticalScrollbar or - @contentWidth > clientWidthWithoutVerticalScrollbar and @contentHeight > clientHeightWithHorizontalScrollbar) - - verticalScrollbarVisible = - not @model.isMini() and - (@contentHeight > clientHeightWithHorizontalScrollbar or - @contentHeight > clientHeightWithoutHorizontalScrollbar and @contentWidth > clientWidthWithVerticalScrollbar) - - horizontalScrollbarHeight = - if horizontalScrollbarVisible - @measuredHorizontalScrollbarHeight - else - 0 - - verticalScrollbarWidth = - if verticalScrollbarVisible - @measuredVerticalScrollbarWidth - else - 0 - - unless @horizontalScrollbarHeight is horizontalScrollbarHeight - @horizontalScrollbarHeight = horizontalScrollbarHeight - @updateClientHeight() - - unless @verticalScrollbarWidth is verticalScrollbarWidth - @verticalScrollbarWidth = verticalScrollbarWidth - @updateClientWidth() - - lineDecorationClassesForRow: (row) -> - return null if @model.isMini() - - decorationClasses = null - for id, properties of @lineDecorationsByScreenRow[row] - decorationClasses ?= [] - decorationClasses.push(properties.class) - decorationClasses - - lineNumberDecorationClassesForRow: (row) -> - return null if @model.isMini() - - decorationClasses = null - for id, properties of @lineNumberDecorationsByScreenRow[row] - decorationClasses ?= [] - decorationClasses.push(properties.class) - decorationClasses - - getCursorBlinkPeriod: -> @cursorBlinkPeriod - - getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay - - setFocused: (focused) -> - unless @focused is focused - @focused = focused - if @focused - @startBlinkingCursors() - else - @stopBlinkingCursors(false) - @emitDidUpdateState() - - setScrollTop: (scrollTop) -> - return unless scrollTop? - - @pendingScrollLogicalPosition = null - @pendingScrollTop = scrollTop - - @shouldUpdateDecorations = true - @emitDidUpdateState() - - getScrollTop: -> - @scrollTop - - getRealScrollTop: -> - @realScrollTop ? @scrollTop - - didStartScrolling: -> - if @stoppedScrollingTimeoutId? - clearTimeout(@stoppedScrollingTimeoutId) - @stoppedScrollingTimeoutId = null - @stoppedScrollingTimeoutId = setTimeout(@didStopScrolling.bind(this), @stoppedScrollingDelay) - - didStopScrolling: -> - if @mouseWheelScreenRow? - @mouseWheelScreenRow = null - @shouldUpdateDecorations = true - - @emitDidUpdateState() - - setScrollLeft: (scrollLeft) -> - return unless scrollLeft? - - @pendingScrollLogicalPosition = null - @pendingScrollLeft = scrollLeft - - @emitDidUpdateState() - - getScrollLeft: -> - @scrollLeft - - getRealScrollLeft: -> - @realScrollLeft ? @scrollLeft - - getClientHeight: -> - if @clientHeight - @clientHeight - else - @explicitHeight - @horizontalScrollbarHeight - - getClientWidth: -> - if @clientWidth - @clientWidth - else - @contentFrameWidth - @verticalScrollbarWidth - - getScrollBottom: -> @getScrollTop() + @getClientHeight() - setScrollBottom: (scrollBottom) -> - @setScrollTop(scrollBottom - @getClientHeight()) - @getScrollBottom() - - getScrollRight: -> @getScrollLeft() + @getClientWidth() - setScrollRight: (scrollRight) -> - @setScrollLeft(scrollRight - @getClientWidth()) - @getScrollRight() - - getScrollHeight: -> - @scrollHeight - - getScrollWidth: -> - @scrollWidth - - getMaxScrollTop: -> - scrollHeight = @getScrollHeight() - clientHeight = @getClientHeight() - return 0 unless scrollHeight? and clientHeight? - - scrollHeight - clientHeight - - setHorizontalScrollbarHeight: (horizontalScrollbarHeight) -> - unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight - @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight - @emitDidUpdateState() - - setVerticalScrollbarWidth: (verticalScrollbarWidth) -> - unless @measuredVerticalScrollbarWidth is verticalScrollbarWidth - @measuredVerticalScrollbarWidth = verticalScrollbarWidth - @emitDidUpdateState() - - setAutoHeight: (autoHeight) -> - unless @autoHeight is autoHeight - @autoHeight = autoHeight - @emitDidUpdateState() - - setExplicitHeight: (explicitHeight) -> - unless @explicitHeight is explicitHeight - @explicitHeight = explicitHeight - @updateHeight() - @shouldUpdateDecorations = true - @emitDidUpdateState() - - updateHeight: -> - height = @explicitHeight ? @contentHeight - unless @height is height - @height = height - @updateScrollbarDimensions() - @updateClientHeight() - @updateScrollHeight() - @updateEndRow() - - didChangeAutoWidth: -> - @emitDidUpdateState() - - setContentFrameWidth: (contentFrameWidth) -> - if @contentFrameWidth isnt contentFrameWidth or @editorWidthInChars? - @contentFrameWidth = contentFrameWidth - @editorWidthInChars = null - @updateScrollbarDimensions() - @updateClientWidth() - @invalidateAllBlockDecorationsDimensions = true - @shouldUpdateDecorations = true - @emitDidUpdateState() - - setBoundingClientRect: (boundingClientRect) -> - unless @clientRectsEqual(@boundingClientRect, boundingClientRect) - @boundingClientRect = boundingClientRect - @invalidateAllBlockDecorationsDimensions = true - @shouldUpdateDecorations = true - @emitDidUpdateState() - - clientRectsEqual: (clientRectA, clientRectB) -> - clientRectA? and clientRectB? and - clientRectA.top is clientRectB.top and - clientRectA.left is clientRectB.left and - clientRectA.width is clientRectB.width and - clientRectA.height is clientRectB.height - - setWindowSize: (width, height) -> - if @windowWidth isnt width or @windowHeight isnt height - @windowWidth = width - @windowHeight = height - @invalidateAllBlockDecorationsDimensions = true - @shouldUpdateDecorations = true - - @emitDidUpdateState() - - setBackgroundColor: (backgroundColor) -> - unless @backgroundColor is backgroundColor - @backgroundColor = backgroundColor - @emitDidUpdateState() - - setGutterBackgroundColor: (gutterBackgroundColor) -> - unless @gutterBackgroundColor is gutterBackgroundColor - @gutterBackgroundColor = gutterBackgroundColor - @emitDidUpdateState() - - setGutterWidth: (gutterWidth) -> - if @gutterWidth isnt gutterWidth - @gutterWidth = gutterWidth - @updateOverlaysState() - - getGutterWidth: -> - @gutterWidth - - setLineHeight: (lineHeight) -> - unless @lineHeight is lineHeight - @lineHeight = lineHeight - @model.setLineHeightInPixels(@lineHeight) - @lineTopIndex.setDefaultLineHeight(@lineHeight) - @restoreScrollTopIfNeeded() - @model.setLineHeightInPixels(lineHeight) - @shouldUpdateDecorations = true - @emitDidUpdateState() - - setMouseWheelScreenRow: (screenRow) -> - if @mouseWheelScreenRow isnt screenRow - @mouseWheelScreenRow = screenRow - @didStartScrolling() - - setBaseCharacterWidth: (baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> - unless @baseCharacterWidth is baseCharacterWidth and @doubleWidthCharWidth is doubleWidthCharWidth and @halfWidthCharWidth is halfWidthCharWidth and koreanCharWidth is @koreanCharWidth - @baseCharacterWidth = baseCharacterWidth - @doubleWidthCharWidth = doubleWidthCharWidth - @halfWidthCharWidth = halfWidthCharWidth - @koreanCharWidth = koreanCharWidth - @model.setDefaultCharWidth(baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) - @restoreScrollLeftIfNeeded() - @measurementsChanged() - - measurementsChanged: -> - @invalidateAllBlockDecorationsDimensions = true - @shouldUpdateDecorations = true - @emitDidUpdateState() - - hasPixelPositionRequirements: -> - @lineHeight? and @baseCharacterWidth? - - pixelPositionForScreenPosition: (screenPosition) -> - position = @linesYardstick.pixelPositionForScreenPosition(screenPosition) - position.top -= @getScrollTop() - position.left -= @getScrollLeft() - - position.top = Math.round(position.top) - position.left = Math.round(position.left) - - position - - hasPixelRectRequirements: -> - @hasPixelPositionRequirements() and @scrollWidth? - - hasOverlayPositionRequirements: -> - @hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight - - absolutePixelRectForScreenRange: (screenRange) -> - lineHeight = @model.getLineHeightInPixels() - - if screenRange.end.row > screenRange.start.row - top = @linesYardstick.pixelPositionForScreenPosition(screenRange.start).top - left = 0 - height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight - width = @getScrollWidth() - else - {top, left} = @linesYardstick.pixelPositionForScreenPosition(screenRange.start) - height = lineHeight - width = @linesYardstick.pixelPositionForScreenPosition(screenRange.end).left - left - - {top, left, width, height} - - pixelRectForScreenRange: (screenRange) -> - rect = @absolutePixelRectForScreenRange(screenRange) - rect.top -= @getScrollTop() - rect.left -= @getScrollLeft() - rect.top = Math.round(rect.top) - rect.left = Math.round(rect.left) - rect.width = Math.round(rect.width) - 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) - - updateBlockDecorations: -> - if @invalidateAllBlockDecorationsDimensions - for decoration in @model.getDecorations(type: 'block') - @invalidatedDimensionsByBlockDecoration.add(decoration) - @invalidateAllBlockDecorationsDimensions = false - - visibleDecorationsById = {} - visibleDecorationsByScreenRowAndId = {} - for markerId, decorations of @model.decorationsForScreenRowRange(@getStartTileRow(), @getEndTileRow() + @tileSize - 1) - for decoration in decorations when decoration.isType('block') - screenRow = decoration.getMarker().getHeadScreenPosition().row - if decoration.getProperties().position is "after" - @followingBlockDecorationsByScreenRowAndId[screenRow] ?= {} - @followingBlockDecorationsByScreenRowAndId[screenRow][decoration.id] = {screenRow, decoration} - else - @precedingBlockDecorationsByScreenRowAndId[screenRow] ?= {} - @precedingBlockDecorationsByScreenRowAndId[screenRow][decoration.id] = {screenRow, decoration} - visibleDecorationsById[decoration.id] = true - visibleDecorationsByScreenRowAndId[screenRow] ?= {} - visibleDecorationsByScreenRowAndId[screenRow][decoration.id] = true - - for screenRow, blockDecorations of @precedingBlockDecorationsByScreenRowAndId - if Number(screenRow) isnt @mouseWheelScreenRow - for id, blockDecoration of blockDecorations - unless visibleDecorationsByScreenRowAndId[screenRow]?[id] - delete @precedingBlockDecorationsByScreenRowAndId[screenRow][id] - - for screenRow, blockDecorations of @followingBlockDecorationsByScreenRowAndId - if Number(screenRow) isnt @mouseWheelScreenRow - for id, blockDecoration of blockDecorations - unless visibleDecorationsByScreenRowAndId[screenRow]?[id] - delete @followingBlockDecorationsByScreenRowAndId[screenRow][id] - - @state.content.offScreenBlockDecorations = {} - @invalidatedDimensionsByBlockDecoration.forEach (decoration) => - unless visibleDecorationsById[decoration.id] - @state.content.offScreenBlockDecorations[decoration.id] = decoration - - updateLineDecorations: -> - @lineDecorationsByScreenRow = {} - @lineNumberDecorationsByScreenRow = {} - @customGutterDecorationsByGutterName = {} - - for decorationId, decorationState of @decorations - {properties, bufferRange, screenRange, rangeIsReversed} = decorationState - if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number') - @addToLineDecorationCaches(decorationId, properties, bufferRange, screenRange, rangeIsReversed) - - else if Decoration.isType(properties, 'gutter') and properties.gutterName? - @customGutterDecorationsByGutterName[properties.gutterName] ?= {} - @customGutterDecorationsByGutterName[properties.gutterName][decorationId] = decorationState - - return - - updateHighlightDecorations: -> - @visibleHighlights = {} - - for decorationId, {properties, screenRange} of @decorations - if Decoration.isType(properties, 'highlight') - @updateHighlightState(decorationId, properties, screenRange) - - for tileId, tileState of @state.content.tiles - for id of tileState.highlights - delete tileState.highlights[id] unless @visibleHighlights[tileId]?[id]? - - return - - addToLineDecorationCaches: (decorationId, properties, bufferRange, screenRange, rangeIsReversed) -> - if screenRange.isEmpty() - return if properties.onlyNonEmpty - else - return if properties.onlyEmpty - omitLastRow = screenRange.end.column is 0 - - if rangeIsReversed - headScreenPosition = screenRange.start - else - headScreenPosition = screenRange.end - - if properties.class is 'folded' and Decoration.isType(properties, 'line-number') - screenRow = @model.screenRowForBufferRow(bufferRange.start.row) - @lineNumberDecorationsByScreenRow[screenRow] ?= {} - @lineNumberDecorationsByScreenRow[screenRow][decorationId] = properties - else - startRow = Math.max(screenRange.start.row, @getStartTileRow()) - endRow = Math.min(screenRange.end.row, @getEndTileRow() + @tileSize) - for row in [startRow..endRow] 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-number') - @lineNumberDecorationsByScreenRow[row] ?= {} - @lineNumberDecorationsByScreenRow[row][decorationId] = properties - - return - - intersectRangeWithTile: (range, tileStartRow) -> - intersectingStartRow = Math.max(tileStartRow, range.start.row) - intersectingEndRow = Math.min(tileStartRow + @tileSize - 1, range.end.row) - intersectingRange = new Range( - new Point(intersectingStartRow, 0), - new Point(intersectingEndRow, Infinity) - ) - - if intersectingStartRow is range.start.row - intersectingRange.start.column = range.start.column - - if intersectingEndRow is range.end.row - intersectingRange.end.column = range.end.column - - intersectingRange - - updateHighlightState: (decorationId, properties, screenRange) -> - return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements() - - @constrainRangeToVisibleRowRange(screenRange) - - return if screenRange.isEmpty() - - startTile = @tileForRow(screenRange.start.row) - endTile = @tileForRow(screenRange.end.row) - needsFlash = properties.flashCount? and @flashCountsByDecorationId[decorationId] isnt properties.flashCount - if needsFlash - @flashCountsByDecorationId[decorationId] = properties.flashCount - - for tileStartRow in [startTile..endTile] by @tileSize - rangeWithinTile = @intersectRangeWithTile(screenRange, tileStartRow) - - continue if rangeWithinTile.isEmpty() - - tileState = @state.content.tiles[tileStartRow] ?= {highlights: {}} - highlightState = tileState.highlights[decorationId] ?= {} - - highlightState.needsFlash = needsFlash - highlightState.flashCount = properties.flashCount - highlightState.flashClass = properties.flashClass - highlightState.flashDuration = properties.flashDuration - highlightState.class = properties.class - highlightState.deprecatedRegionClass = properties.deprecatedRegionClass - highlightState.regions = @buildHighlightRegions(rangeWithinTile) - - for region in highlightState.regions - @repositionRegionWithinTile(region, tileStartRow) - - @visibleHighlights[tileStartRow] ?= {} - @visibleHighlights[tileStartRow][decorationId] = true - - true - - constrainRangeToVisibleRowRange: (screenRange) -> - if screenRange.start.row < @startRow - screenRange.start.row = @startRow - screenRange.start.column = 0 - - if screenRange.end.row < @startRow - screenRange.end.row = @startRow - screenRange.end.column = 0 - - if screenRange.start.row >= @endRow - screenRange.start.row = @endRow - screenRange.start.column = 0 - - if screenRange.end.row >= @endRow - screenRange.end.row = @endRow - screenRange.end.column = 0 - - repositionRegionWithinTile: (region, tileStartRow) -> - region.top += @scrollTop - @lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow) - - buildHighlightRegions: (screenRange) -> - lineHeightInPixels = @lineHeight - startPixelPosition = @pixelPositionForScreenPosition(screenRange.start) - endPixelPosition = @pixelPositionForScreenPosition(screenRange.end) - startPixelPosition.left += @scrollLeft - endPixelPosition.left += @scrollLeft - spannedRows = screenRange.end.row - screenRange.start.row + 1 - - regions = [] - - if spannedRows is 1 - region = - top: startPixelPosition.top - height: lineHeightInPixels - left: startPixelPosition.left - - if screenRange.end.column is Infinity - region.right = 0 - else - region.width = endPixelPosition.left - startPixelPosition.left - - regions.push(region) - else - # First row, extending from selection start to the right side of screen - regions.push( - top: startPixelPosition.top - left: startPixelPosition.left - height: lineHeightInPixels - right: 0 - ) - - # Middle rows, extending from left side to right side of screen - if spannedRows > 2 - regions.push( - top: startPixelPosition.top + lineHeightInPixels - height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels - left: 0 - right: 0 - ) - - # Last row, extending from left side of screen to selection end - if screenRange.end.column > 0 - region = - top: endPixelPosition.top - height: lineHeightInPixels - left: 0 - - if screenRange.end.column is Infinity - region.right = 0 - else - region.width = endPixelPosition.left - - regions.push(region) - - regions - - setOverlayDimensions: (decorationId, itemWidth, itemHeight, contentMargin) -> - @overlayDimensions[decorationId] ?= {} - overlayState = @overlayDimensions[decorationId] - dimensionsAreEqual = overlayState.itemWidth is itemWidth and - overlayState.itemHeight is itemHeight and - overlayState.contentMargin is contentMargin - unless dimensionsAreEqual - overlayState.itemWidth = itemWidth - overlayState.itemHeight = itemHeight - overlayState.contentMargin = contentMargin - - @emitDidUpdateState() - - setBlockDecorationDimensions: (decoration, width, height) -> - return unless @observedBlockDecorations.has(decoration) - - @lineTopIndex.resizeBlock(decoration.id, height) - - @invalidatedDimensionsByBlockDecoration.delete(decoration) - @shouldUpdateDecorations = true - @emitDidUpdateState() - - invalidateBlockDecorationDimensions: (decoration) -> - @invalidatedDimensionsByBlockDecoration.add(decoration) - @shouldUpdateDecorations = true - @emitDidUpdateState() - - spliceBlockDecorationsInRange: (start, end, screenDelta) -> - return if screenDelta is 0 - - oldExtent = end - start - newExtent = end - start + screenDelta - invalidatedBlockDecorationIds = @lineTopIndex.splice(start, oldExtent, newExtent) - invalidatedBlockDecorationIds.forEach (id) => - decoration = @model.decorationForId(id) - newScreenPosition = decoration.getMarker().getHeadScreenPosition() - @lineTopIndex.moveBlock(id, newScreenPosition.row) - @invalidatedDimensionsByBlockDecoration.add(decoration) - - didAddBlockDecoration: (decoration) -> - return if not decoration.isType('block') or @observedBlockDecorations.has(decoration) - - didMoveDisposable = decoration.getMarker().bufferMarker.onDidChange (markerEvent) => - @didMoveBlockDecoration(decoration, markerEvent) - - didDestroyDisposable = decoration.onDidDestroy => - @disposables.remove(didMoveDisposable) - @disposables.remove(didDestroyDisposable) - didMoveDisposable.dispose() - didDestroyDisposable.dispose() - @didDestroyBlockDecoration(decoration) - - isAfter = decoration.getProperties().position is "after" - @lineTopIndex.insertBlock(decoration.id, decoration.getMarker().getHeadScreenPosition().row, 0, isAfter) - - @observedBlockDecorations.add(decoration) - @invalidateBlockDecorationDimensions(decoration) - @disposables.add(didMoveDisposable) - @disposables.add(didDestroyDisposable) - @shouldUpdateDecorations = true - @emitDidUpdateState() - - didMoveBlockDecoration: (decoration, markerEvent) -> - # Don't move blocks after a text change, because we already splice on buffer - # change. - return if markerEvent.textChanged - - @lineTopIndex.moveBlock(decoration.id, decoration.getMarker().getHeadScreenPosition().row) - @shouldUpdateDecorations = true - @emitDidUpdateState() - - didDestroyBlockDecoration: (decoration) -> - return unless @observedBlockDecorations.has(decoration) - - @lineTopIndex.removeBlock(decoration.id) - @observedBlockDecorations.delete(decoration) - @invalidatedDimensionsByBlockDecoration.delete(decoration) - @shouldUpdateDecorations = true - @emitDidUpdateState() - - observeCursor: (cursor) -> - didChangePositionDisposable = cursor.onDidChangePosition => - @pauseCursorBlinking() - - @emitDidUpdateState() - - didChangeVisibilityDisposable = cursor.onDidChangeVisibility => - - @emitDidUpdateState() - - didDestroyDisposable = cursor.onDidDestroy => - @disposables.remove(didChangePositionDisposable) - @disposables.remove(didChangeVisibilityDisposable) - @disposables.remove(didDestroyDisposable) - - @emitDidUpdateState() - - @disposables.add(didChangePositionDisposable) - @disposables.add(didChangeVisibilityDisposable) - @disposables.add(didDestroyDisposable) - - didAddCursor: (cursor) -> - @observeCursor(cursor) - @pauseCursorBlinking() - - @emitDidUpdateState() - - startBlinkingCursors: -> - unless @isCursorBlinking() - @state.content.cursorsVisible = true - @toggleCursorBlinkHandle = setInterval(@toggleCursorBlink.bind(this), @getCursorBlinkPeriod() / 2) - - isCursorBlinking: -> - @toggleCursorBlinkHandle? - - stopBlinkingCursors: (visible) -> - if @isCursorBlinking() - @state.content.cursorsVisible = visible - clearInterval(@toggleCursorBlinkHandle) - @toggleCursorBlinkHandle = null - - toggleCursorBlink: -> - @state.content.cursorsVisible = not @state.content.cursorsVisible - @emitDidUpdateState() - - pauseCursorBlinking: -> - @stopBlinkingCursors(true) - @startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay()) - @startBlinkingCursorsAfterDelay() - @emitDidUpdateState() - - requestAutoscroll: (position) -> - @pendingScrollLogicalPosition = position - @pendingScrollTop = null - @pendingScrollLeft = null - @shouldUpdateDecorations = true - @emitDidUpdateState() - - didChangeFirstVisibleScreenRow: (screenRow) -> - @setScrollTop(@lineTopIndex.pixelPositionAfterBlocksForRow(screenRow)) - - getVerticalScrollMarginInPixels: -> - Math.round(@model.getVerticalScrollMargin() * @lineHeight) - - getHorizontalScrollMarginInPixels: -> - Math.round(@model.getHorizontalScrollMargin() * @baseCharacterWidth) - - getVerticalScrollbarWidth: -> - @verticalScrollbarWidth - - getHorizontalScrollbarHeight: -> - @horizontalScrollbarHeight - - commitPendingLogicalScrollTopPosition: -> - return unless @pendingScrollLogicalPosition? - - {screenRange, options} = @pendingScrollLogicalPosition - - verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - - top = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.start.row) - bottom = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.end.row) + @lineHeight - - if options?.center - desiredScrollCenter = (top + bottom) / 2 - unless @getScrollTop() < desiredScrollCenter < @getScrollBottom() - desiredScrollTop = desiredScrollCenter - @getClientHeight() / 2 - desiredScrollBottom = desiredScrollCenter + @getClientHeight() / 2 - else - desiredScrollTop = top - verticalScrollMarginInPixels - desiredScrollBottom = bottom + verticalScrollMarginInPixels - - if options?.reversed ? true - if desiredScrollBottom > @getScrollBottom() - @updateScrollTop(desiredScrollBottom - @getClientHeight()) - if desiredScrollTop < @getScrollTop() - @updateScrollTop(desiredScrollTop) - else - if desiredScrollTop < @getScrollTop() - @updateScrollTop(desiredScrollTop) - if desiredScrollBottom > @getScrollBottom() - @updateScrollTop(desiredScrollBottom - @getClientHeight()) - - commitPendingLogicalScrollLeftPosition: -> - return unless @pendingScrollLogicalPosition? - - {screenRange, options} = @pendingScrollLogicalPosition - - horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels() - - {left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start)) - {left: right} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end)) - - left += @scrollLeft - right += @scrollLeft - - desiredScrollLeft = left - horizontalScrollMarginInPixels - desiredScrollRight = right + horizontalScrollMarginInPixels - - if options?.reversed ? true - if desiredScrollRight > @getScrollRight() - @updateScrollLeft(desiredScrollRight - @getClientWidth()) - if desiredScrollLeft < @getScrollLeft() - @updateScrollLeft(desiredScrollLeft) - else - if desiredScrollLeft < @getScrollLeft() - @updateScrollLeft(desiredScrollLeft) - if desiredScrollRight > @getScrollRight() - @updateScrollLeft(desiredScrollRight - @getClientWidth()) - - commitPendingScrollLeftPosition: -> - if @pendingScrollLeft? - @updateScrollLeft(@pendingScrollLeft) - @pendingScrollLeft = null - - commitPendingScrollTopPosition: -> - if @pendingScrollTop? - @updateScrollTop(@pendingScrollTop) - @pendingScrollTop = null - - clearPendingScrollPosition: -> - @pendingScrollLogicalPosition = null - @pendingScrollTop = null - @pendingScrollLeft = null - - canScrollLeftTo: (scrollLeft) -> - @scrollLeft isnt @constrainScrollLeft(scrollLeft) - - canScrollTopTo: (scrollTop) -> - @scrollTop isnt @constrainScrollTop(scrollTop) - - restoreScrollTopIfNeeded: -> - unless @scrollTop? - @updateScrollTop(@lineTopIndex.pixelPositionAfterBlocksForRow(@model.getFirstVisibleScreenRow())) - - restoreScrollLeftIfNeeded: -> - unless @scrollLeft? - @updateScrollLeft(@model.getFirstVisibleScreenColumn() * @baseCharacterWidth) - - onDidChangeScrollTop: (callback) -> - @emitter.on 'did-change-scroll-top', callback - - onDidChangeScrollLeft: (callback) -> - @emitter.on 'did-change-scroll-left', callback - - getVisibleRowRange: -> - [@startRow, @endRow] - - isRowRendered: (row) -> - @getStartTileRow() <= row < @getEndTileRow() + @tileSize - - isOpenTagCode: (tagCode) -> - @displayLayer.isOpenTagCode(tagCode) - - isCloseTagCode: (tagCode) -> - @displayLayer.isCloseTagCode(tagCode) - - tagForCode: (tagCode) -> - @displayLayer.tagForCode(tagCode) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index f1034b672..dae7e0698 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -12,7 +12,8 @@ Model = require './model' Selection = require './selection' TextMateScopeSelector = require('first-mate').ScopeSelector GutterContainer = require './gutter-container' -TextEditorElement = require './text-editor-element' +TextEditorComponent = null +TextEditorElement = null {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' ZERO_WIDTH_NBSP = '\ufeff' @@ -61,6 +62,20 @@ class TextEditor extends Model @setClipboard: (clipboard) -> @clipboard = clipboard + @setScheduler: (scheduler) -> + TextEditorComponent ?= require './text-editor-component' + TextEditorComponent.setScheduler(scheduler) + + @didUpdateStyles: -> + TextEditorComponent ?= require './text-editor-component' + TextEditorComponent.didUpdateStyles() + + @didUpdateScrollbarStyles: -> + TextEditorComponent ?= require './text-editor-component' + TextEditorComponent.didUpdateScrollbarStyles() + + @viewForItem: (item) -> item.element ? item + serializationVersion: 1 buffer: null @@ -89,6 +104,17 @@ class TextEditor extends Model Object.defineProperty @prototype, "element", get: -> @getElement() + Object.defineProperty @prototype, "editorElement", + get: -> + Grim.deprecate(""" + `TextEditor.prototype.editorElement` has always been private, but now + it is gone. Reading the `editorElement` property still returns a + reference to the editor element but this field will be removed in a + later version of Atom, so we recommend using the `element` property instead. + """) + + @getElement() + Object.defineProperty(@prototype, 'displayBuffer', get: -> Grim.deprecate(""" `TextEditor.prototype.displayBuffer` has always been private, but now @@ -128,7 +154,7 @@ class TextEditor extends Model super { - @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, + @softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength, @softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @editorWidthInChars, @@ -138,8 +164,6 @@ class TextEditor extends Model } = params @assert ?= (condition) -> condition - @firstVisibleScreenRow ?= 0 - @firstVisibleScreenColumn ?= 0 @emitter = new Emitter @disposables = new CompositeDisposable @cursors = [] @@ -198,7 +222,10 @@ class TextEditor extends Model @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true - @decorationManager = new DecorationManager(@displayLayer) + @decorationManager = new DecorationManager(this) + @decorateMarkerLayer(@selectionsMarkerLayer, type: 'cursor') + @decorateCursorLine() unless @isMini() + @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) for marker in @selectionsMarkerLayer.getMarkers() @@ -220,13 +247,23 @@ class TextEditor extends Model priority: 0 visible: lineNumberGutterVisible + decorateCursorLine: -> + @cursorLineDecorations = [ + @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line', class: 'cursor-line', onlyEmpty: true), + @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line'), + @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) + ] + doBackgroundWork: (deadline) => + previousLongestRow = @getApproximateLongestScreenRow() if @displayLayer.doBackgroundWork(deadline) - @presenter?.updateVerticalDimensions() @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) else @backgroundWorkHandle = null + if @getApproximateLongestScreenRow() isnt previousLongestRow + @component?.scheduleUpdate() + update: (params) -> displayLayerParams = {} @@ -292,6 +329,12 @@ class TextEditor extends Model displayLayerParams.invisibles = @getInvisibles() displayLayerParams.softWrapColumn = @getSoftWrapColumn() displayLayerParams.showIndentGuides = @doesShowIndentGuide() + if @mini + decoration.destroy() for decoration in @cursorLineDecorations + @cursorLineDecorations = null + else + @decorateCursorLine() + @component?.scheduleUpdate() when 'placeholderText' if value isnt @placeholderText @@ -314,7 +357,6 @@ class TextEditor extends Model when 'showLineNumbers' if value isnt @showLineNumbers @showLineNumbers = value - @presenter?.didChangeShowLineNumbers() when 'showInvisibles' if value isnt @showInvisibles @@ -339,22 +381,20 @@ class TextEditor extends Model when 'scrollPastEnd' if value isnt @scrollPastEnd @scrollPastEnd = value - @presenter?.didChangeScrollPastEnd() + @component?.scheduleUpdate() when 'autoHeight' if value isnt @autoHeight @autoHeight = value - @presenter?.setAutoHeight(@autoHeight) when 'autoWidth' if value isnt @autoWidth @autoWidth = value - @presenter?.didChangeAutoWidth() when 'showCursorOnSelection' if value isnt @showCursorOnSelection @showCursorOnSelection = value - cursor.setShowCursorOnSelection(value) for cursor in @getCursors() + @component?.scheduleUpdate() else if param isnt 'ref' and param isnt 'key' @@ -362,11 +402,14 @@ class TextEditor extends Model @displayLayer.reset(displayLayerParams) - if @editorElement? - @editorElement.views.getNextUpdatePromise() + if @component? + @component.getNextUpdatePromise() else Promise.resolve() + scheduleComponentUpdate: -> + @component?.scheduleUpdate() + serialize: -> tokenizedBufferState = @tokenizedBuffer.serialize() @@ -381,8 +424,8 @@ class TextEditor extends Model displayLayerId: @displayLayer.id selectionsMarkerLayerId: @selectionsMarkerLayer.id - firstVisibleScreenRow: @getFirstVisibleScreenRow() - firstVisibleScreenColumn: @getFirstVisibleScreenColumn() + initialScrollTopRow: @getScrollTopRow() + initialScrollLeftColumn: @getScrollLeftColumn() atomicSoftTabs: @displayLayer.atomicSoftTabs softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent @@ -413,14 +456,17 @@ class TextEditor extends Model @emitter.on 'did-terminate-pending-state', callback subscribeToDisplayLayer: -> - @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) @disposables.add @displayLayer.onDidChangeSync (e) => @mergeIntersectingSelections() + @component?.didChangeDisplayLayer(e) @emitter.emit 'did-change', e @disposables.add @displayLayer.onDidReset => @mergeIntersectingSelections() + @component?.didResetDisplayLayer() @emitter.emit 'did-change', {} + @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) + @disposables.add @selectionsMarkerLayer.onDidUpdate => @component?.didUpdateSelections() destroyed: -> @disposables.dispose() @@ -432,8 +478,9 @@ class TextEditor extends Model @gutterContainer.destroy() @emitter.emit 'did-destroy' @emitter.clear() - @editorElement = null - @presenter = null + @component?.element.component = null + @component = null + @lineNumberGutter.element = null ### Section: Event Subscription @@ -688,6 +735,11 @@ class TextEditor extends Model onDidRemoveDecoration: (callback) -> @decorationManager.onDidRemoveDecoration(callback) + # Called by DecorationManager when a decoration is added. + didAddDecoration: (decoration) -> + if decoration.isType('block') + @component?.didAddBlockDecoration(decoration) + # Extended: Calls your `callback` when the placeholder text is changed. # # * `callback` {Function} @@ -697,9 +749,6 @@ class TextEditor extends Model onDidChangePlaceholderText: (callback) -> @emitter.on 'did-change-placeholder-text', callback - onDidChangeFirstVisibleScreenRow: (callback, fromView) -> - @emitter.on 'did-change-first-visible-screen-row', callback - onDidChangeScrollTop: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") @@ -735,7 +784,8 @@ class TextEditor extends Model @buffer, selectionsMarkerLayer, softTabs, suppressCursorCreation: true, tabLength: @tokenizedBuffer.getTabLength(), - @firstVisibleScreenRow, @firstVisibleScreenColumn, + initialScrollTopRow: @getScrollTopRow(), + initialScrollLeftColumn: @getScrollLeftColumn(), @assert, displayLayer, grammar: @getGrammar(), @autoWidth, @autoHeight, @showCursorOnSelection }) @@ -749,9 +799,6 @@ class TextEditor extends Model isMini: -> @mini - setUpdatedSynchronously: (updatedSynchronously) -> - @decorationManager.setUpdatedSynchronously(updatedSynchronously) - onDidChangeMini: (callback) -> @emitter.on 'did-change-mini', callback @@ -971,29 +1018,28 @@ class TextEditor extends Model tokens = [] lineTextIndex = 0 currentTokenScopes = [] - {lineText, tagCodes} = @screenLineForScreenRow(screenRow) - for tagCode in tagCodes - if @displayLayer.isOpenTagCode(tagCode) - currentTokenScopes.push(@displayLayer.tagForCode(tagCode)) - else if @displayLayer.isCloseTagCode(tagCode) + {lineText, tags} = @screenLineForScreenRow(screenRow) + for tag in tags + if @displayLayer.isOpenTag(tag) + currentTokenScopes.push(@displayLayer.classNameForTag(tag)) + else if @displayLayer.isCloseTag(tag) currentTokenScopes.pop() else tokens.push({ - text: lineText.substr(lineTextIndex, tagCode) + text: lineText.substr(lineTextIndex, tag) scopes: currentTokenScopes.slice() }) - lineTextIndex += tagCode + lineTextIndex += tag tokens screenLineForScreenRow: (screenRow) -> - @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] + @displayLayer.getScreenLine(screenRow) bufferRowForScreenRow: (screenRow) -> @displayLayer.translateScreenPosition(Point(screenRow, 0)).row bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> - for screenRow in [startScreenRow..endScreenRow] - @bufferRowForScreenRow(screenRow) + @displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1) screenRowForBufferRow: (row) -> @displayLayer.translateBufferPosition(Point(row, 0)).row @@ -1751,20 +1797,32 @@ class TextEditor extends Model # * `block` Positions the view associated with the given item before or # after the row of the given `TextEditorMarker`, depending on the `position` # property. + # * `cursor` Renders a cursor at the head of the given marker. If multiple + # decorations are created for the same marker, their class strings and + # style objects are combined into a single cursor. You can use this + # decoration type to style existing cursors by passing in their markers + # or render artificial cursors that don't actually exist in the model + # by passing a marker that isn't actually associated with a cursor. # * `class` This CSS class will be applied to the decorated line number, # line, highlight, or overlay. + # * `style` An {Object} containing CSS style properties to apply to the + # relevant DOM node. Currently this only works with a `type` of `cursor`. # * `item` (optional) An {HTMLElement} or a model {Object} with a # corresponding view registered. Only applicable to the `gutter`, - # `overlay` and `block` types. + # `overlay` and `block` decoration types. # * `onlyHead` (optional) If `true`, the decoration will only be applied to # the head of the `DisplayMarker`. Only applicable to the `line` and - # `line-number` types. + # `line-number` decoration types. # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if # the associated `DisplayMarker` is empty. Only applicable to the `gutter`, - # `line`, and `line-number` types. + # `line`, and `line-number` decoration types. # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied # if the associated `DisplayMarker` is non-empty. Only applicable to the - # `gutter`, `line`, and `line-number` types. + # `gutter`, `line`, and `line-number` decoration types. + # * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied + # to the last row of a non-empty range, even if it ends at column 0. + # Defaults to `true`. Only applicable to the `gutter`, `line`, and + # `line-number` decoration types. # * `position` (optional) Only applicable to decorations of type `overlay` and `block`. # Controls where the view is positioned relative to the `TextEditorMarker`. # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and @@ -1852,12 +1910,6 @@ class TextEditor extends Model getOverlayDecorations: (propertyFilter) -> @decorationManager.getOverlayDecorations(propertyFilter) - decorationForId: (id) -> - @decorationManager.decorationForId(id) - - decorationsForMarkerId: (id) -> - @decorationManager.decorationsForMarkerId(id) - ### Section: Markers ### @@ -2096,9 +2148,9 @@ class TextEditor extends Model # # Returns the first matched {Cursor} or undefined getCursorAtScreenPosition: (position) -> - for cursor in @cursors - return cursor if cursor.getScreenPosition().isEqual(position) - undefined + if selection = @getSelectionAtScreenPosition(position) + if selection.getHeadScreenPosition().isEqual(position) + selection.cursor # Essential: Get the position of the most recently added cursor in screen # coordinates. @@ -2140,7 +2192,7 @@ class TextEditor extends Model # # Returns a {Cursor}. addCursorAtBufferPosition: (bufferPosition, options) -> - @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) + @selectionsMarkerLayer.markBufferPosition(bufferPosition, Object.assign({invalidate: 'never'}, options)) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor @@ -2287,14 +2339,12 @@ class TextEditor extends Model cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection) @cursors.push(cursor) @cursorsByMarkerId.set(marker.id, cursor) - @decorateMarker(marker, type: 'line-number', class: 'cursor-line') - @decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) - @decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true) cursor moveCursors: (fn) -> - fn(cursor) for cursor in @getCursors() - @mergeCursors() + @transact => + fn(cursor) for cursor in @getCursors() + @mergeCursors() cursorMoved: (event) -> @emitter.emit 'did-change-cursor-position', event @@ -2650,6 +2700,11 @@ class TextEditor extends Model @createLastSelectionIfNeeded() _.last(@selections) + getSelectionAtScreenPosition: (position) -> + markers = @selectionsMarkerLayer.findMarkers(containsScreenPosition: position) + if markers.length > 0 + @cursorsByMarkerId.get(markers[0].id).selection + # Extended: Get current {Selection}s. # # Returns: An {Array} of {Selection}s. @@ -2808,6 +2863,7 @@ class TextEditor extends Model # Called by the selection selectionRangeChanged: (event) -> + @component?.didChangeSelectionRange() @emitter.emit 'did-change-selection-range', event createLastSelectionIfNeeded: -> @@ -3321,7 +3377,7 @@ class TextEditor extends Model # # Returns a {Boolean}. isFoldedAtCursorRow: -> - @isFoldedAtScreenRow(@getCursorScreenPosition().row) + @isFoldedAtBufferRow(@getCursorBufferPosition().row) # Extended: Determine whether the given row in buffer coordinates is folded. # @@ -3329,7 +3385,11 @@ class TextEditor extends Model # # Returns a {Boolean}. isFoldedAtBufferRow: (bufferRow) -> - @displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0 + range = Range( + Point(bufferRow, 0), + Point(bufferRow, @buffer.lineLengthForRow(bufferRow)) + ) + @displayLayer.foldsIntersectingBufferRange(range).length > 0 # Extended: Determine whether the given row in screen coordinates is folded. # @@ -3379,6 +3439,9 @@ class TextEditor extends Model getGutters: -> @gutterContainer.getGutters() + getLineNumberGutter: -> + @lineNumberGutter + # Essential: Get the gutter with the given name. # # Returns a {Gutter}, or `null` if no gutter exists for the given name. @@ -3426,7 +3489,9 @@ class TextEditor extends Model @getElement().scrollToBottom() scrollToScreenRange: (screenRange, options = {}) -> + screenRange = @clipScreenRange(screenRange) if options.clip isnt false scrollEvent = {screenRange, options} + @component?.didRequestAutoscroll(scrollEvent) @emitter.emit "did-request-autoscroll", scrollEvent getHorizontalScrollbarHeight: -> @@ -3453,9 +3518,12 @@ class TextEditor extends Model # Returns the number of rows per page getRowsPerPage: -> - Math.max(@rowsPerPage ? 1, 1) - - setRowsPerPage: (@rowsPerPage) -> + if @component? + clientHeight = @component.getScrollContainerClientHeight() + lineHeight = @component.getLineHeight() + Math.max(1, Math.ceil(clientHeight / lineHeight)) + else + 1 ### Section: Config @@ -3483,7 +3551,11 @@ class TextEditor extends Model # Experimental: Does this editor allow scrolling past the last line? # # Returns a {Boolean}. - getScrollPastEnd: -> @scrollPastEnd + getScrollPastEnd: -> + if @getAutoHeight() + false + else + @scrollPastEnd # Experimental: How fast does the editor scroll in response to mouse wheel # movements? @@ -3543,7 +3615,17 @@ class TextEditor extends Model # Get the Element for the editor. getElement: -> - @editorElement ?= new TextEditorElement().initialize(this, atom) + if @component? + @component.element + else + TextEditorComponent ?= require('./text-editor-component') + TextEditorElement ?= require('./text-editor-element') + new TextEditorComponent({ + model: this, + updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, + @initialScrollTopRow, @initialScrollLeftColumn + }) + @component.element # Essential: Retrieves the greyed out placeholder of a mini editor. # @@ -3600,66 +3682,51 @@ class TextEditor extends Model @doubleWidthCharWidth = doubleWidthCharWidth @halfWidthCharWidth = halfWidthCharWidth @koreanCharWidth = koreanCharWidth - @displayLayer.reset({}) if @isSoftWrapped() and @getEditorWidthInChars()? + if @isSoftWrapped() + @displayLayer.reset({ + softWrapColumn: @getSoftWrapColumn() + }) defaultCharWidth - setHeight: (height, reentrant=false) -> - if reentrant - @height = height - else - Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") - @getElement().setHeight(height) + setHeight: (height) -> + 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.") - @height + @getElement().getHeight() getAutoHeight: -> @autoHeight ? true getAutoWidth: -> @autoWidth ? false - setWidth: (width, reentrant=false) -> - if reentrant - @update({width}) - @width - else - Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") - @getElement().setWidth(width) + setWidth: (width) -> + 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.") - @width + @getElement().getWidth() - # Experimental: Scroll the editor such that the given screen row is at the - # top of the visible area. - setFirstVisibleScreenRow: (screenRow, fromView) -> - unless fromView - maxScreenRow = @getScreenLineCount() - 1 - unless @scrollPastEnd - if @height? and @lineHeightInPixels? - maxScreenRow -= Math.floor(@height / @lineHeightInPixels) - screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0) + # Use setScrollTopRow instead of this method + setFirstVisibleScreenRow: (screenRow) -> + @setScrollTopRow(screenRow) - unless screenRow is @firstVisibleScreenRow - @firstVisibleScreenRow = screenRow - @emitter.emit 'did-change-first-visible-screen-row', screenRow unless fromView - - getFirstVisibleScreenRow: -> @firstVisibleScreenRow + getFirstVisibleScreenRow: -> + @getElement().component.getFirstVisibleRow() getLastVisibleScreenRow: -> - if @height? and @lineHeightInPixels? - Math.min(@firstVisibleScreenRow + Math.floor(@height / @lineHeightInPixels), @getScreenLineCount() - 1) - else - null + @getElement().component.getLastVisibleRow() getVisibleRowRange: -> - if lastVisibleScreenRow = @getLastVisibleScreenRow() - [@firstVisibleScreenRow, lastVisibleScreenRow] - else - null + [@getFirstVisibleScreenRow(), @getLastVisibleScreenRow()] - setFirstVisibleScreenColumn: (@firstVisibleScreenColumn) -> - getFirstVisibleScreenColumn: -> @firstVisibleScreenColumn + # Use setScrollLeftColumn instead of this method + setFirstVisibleScreenColumn: (column) -> + @setScrollLeftColumn(column) + + getFirstVisibleScreenColumn: -> + @getElement().component.getFirstVisibleColumn() getScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") @@ -3716,6 +3783,18 @@ class TextEditor extends Model @getElement().getMaxScrollTop() + getScrollTopRow: -> + @getElement().component.getScrollTopRow() + + setScrollTopRow: (scrollTopRow) -> + @getElement().component.setScrollTopRow(scrollTopRow) + + getScrollLeftColumn: -> + @getElement().component.getScrollLeftColumn() + + setScrollLeftColumn: (scrollLeftColumn) -> + @getElement().component.setScrollLeftColumn(scrollLeftColumn) + intersectsVisibleRowRange: (startRow, endRow) -> Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") diff --git a/src/tiled-component.coffee b/src/tiled-component.coffee deleted file mode 100644 index 37de27e9b..000000000 --- a/src/tiled-component.coffee +++ /dev/null @@ -1,51 +0,0 @@ -module.exports = -class TiledComponent - updateSync: (state) -> - @newState = @getNewState(state) - @oldState ?= @buildEmptyState() - - @beforeUpdateSync?(state) - - @removeTileNodes() if @shouldRecreateAllTilesOnUpdate?() - @updateTileNodes() - - @afterUpdateSync?(state) - - removeTileNodes: -> - @removeTileNode(tileRow) for tileRow of @oldState.tiles - return - - removeTileNode: (tileRow) -> - @componentsByTileId[tileRow].destroy() - delete @componentsByTileId[tileRow] - delete @oldState.tiles[tileRow] - - updateTileNodes: -> - @componentsByTileId ?= {} - - for tileRow of @oldState.tiles - unless @newState.tiles.hasOwnProperty(tileRow) - @removeTileNode(tileRow) - - for tileRow, tileState of @newState.tiles - if @oldState.tiles.hasOwnProperty(tileRow) - component = @componentsByTileId[tileRow] - else - component = @componentsByTileId[tileRow] = @buildComponentForTile(tileRow) - - @getTilesNode().appendChild(component.getDomNode()) - @oldState.tiles[tileRow] = Object.assign({}, tileState) - - component.updateSync(@newState) - - return - - getComponentForTile: (tileRow) -> - @componentsByTileId[tileRow] - - getComponents: -> - for _, component of @componentsByTileId - component - - getTiles: -> - @getComponents().map((component) -> component.getDomNode()) diff --git a/src/tokenized-buffer-iterator.js b/src/tokenized-buffer-iterator.js index 29d2fdf86..d22f97874 100644 --- a/src/tokenized-buffer-iterator.js +++ b/src/tokenized-buffer-iterator.js @@ -1,118 +1,104 @@ const {Point} = require('text-buffer') +const {fromFirstMateScopeId} = require('./first-mate-helpers') module.exports = class TokenizedBufferIterator { constructor (tokenizedBuffer) { this.tokenizedBuffer = tokenizedBuffer - this.openTags = null - this.closeTags = null - this.containingTags = null + this.openScopeIds = null + this.closeScopeIds = null } seek (position) { - this.openTags = [] - this.closeTags = [] + this.openScopeIds = [] + this.closeScopeIds = [] this.tagIndex = null const currentLine = this.tokenizedBuffer.tokenizedLineForRow(position.row) - this.currentTags = currentLine.tags - this.currentLineOpenTags = currentLine.openScopes + this.currentLineTags = currentLine.tags this.currentLineLength = currentLine.text.length - this.containingTags = this.currentLineOpenTags.map((id) => this.scopeForId(id)) + const containingScopeIds = currentLine.openScopes.map((id) => fromFirstMateScopeId(id)) let currentColumn = 0 - for (let [index, tag] of this.currentTags.entries()) { + for (let index = 0; index < this.currentLineTags.length; index++) { + const tag = this.currentLineTags[index] if (tag >= 0) { if (currentColumn >= position.column) { this.tagIndex = index break } else { currentColumn += tag - while (this.closeTags.length > 0) { - this.closeTags.shift() - this.containingTags.pop() + while (this.closeScopeIds.length > 0) { + this.closeScopeIds.shift() + containingScopeIds.pop() } - while (this.openTags.length > 0) { - const openTag = this.openTags.shift() - this.containingTags.push(openTag) + while (this.openScopeIds.length > 0) { + const openTag = this.openScopeIds.shift() + containingScopeIds.push(openTag) } } } else { - const scopeName = this.scopeForId(tag) - if (tag % 2 === 0) { - if (this.openTags.length > 0) { + const scopeId = fromFirstMateScopeId(tag) + if ((tag & 1) === 0) { + if (this.openScopeIds.length > 0) { if (currentColumn >= position.column) { this.tagIndex = index break } else { - while (this.closeTags.length > 0) { - this.closeTags.shift() - this.containingTags.pop() + while (this.closeScopeIds.length > 0) { + this.closeScopeIds.shift() + containingScopeIds.pop() } - while (this.openTags.length > 0) { - const openTag = this.openTags.shift() - this.containingTags.push(openTag) + while (this.openScopeIds.length > 0) { + const openTag = this.openScopeIds.shift() + containingScopeIds.push(openTag) } } } - this.closeTags.push(scopeName) + this.closeScopeIds.push(scopeId) } else { - this.openTags.push(scopeName) + this.openScopeIds.push(scopeId) } } } if (this.tagIndex == null) { - this.tagIndex = this.currentTags.length + this.tagIndex = this.currentLineTags.length } this.position = Point(position.row, Math.min(this.currentLineLength, currentColumn)) - return this.containingTags.slice() + return containingScopeIds } moveToSuccessor () { - for (let tag of this.closeTags) { // eslint-disable-line no-unused-vars - this.containingTags.pop() - } - for (let tag of this.openTags) { - this.containingTags.push(tag) - } - this.openTags = [] - this.closeTags = [] + this.openScopeIds = [] + this.closeScopeIds = [] while (true) { - if (this.tagIndex === this.currentTags.length) { + if (this.tagIndex === this.currentLineTags.length) { if (this.isAtTagBoundary()) { break - } else if (this.shouldMoveToNextLine) { - this.moveToNextLine() - this.openTags = this.currentLineOpenTags.map((id) => this.scopeForId(id)) - this.shouldMoveToNextLine = false - } else if (this.nextLineHasMismatchedContainingTags()) { - this.closeTags = this.containingTags.slice().reverse() - this.containingTags = [] - this.shouldMoveToNextLine = true } else if (!this.moveToNextLine()) { return false } } else { - const tag = this.currentTags[this.tagIndex] + const tag = this.currentLineTags[this.tagIndex] if (tag >= 0) { if (this.isAtTagBoundary()) { break } else { this.position = Point(this.position.row, Math.min( this.currentLineLength, - this.position.column + this.currentTags[this.tagIndex] + this.position.column + this.currentLineTags[this.tagIndex] )) } } else { - const scopeName = this.scopeForId(tag) - if (tag % 2 === 0) { - if (this.openTags.length > 0) { + const scopeId = fromFirstMateScopeId(tag) + if ((tag & 1) === 0) { + if (this.openScopeIds.length > 0) { break } else { - this.closeTags.push(scopeName) + this.closeScopeIds.push(scopeId) } } else { - this.openTags.push(scopeName) + this.openScopeIds.push(scopeId) } } this.tagIndex++ @@ -125,24 +111,12 @@ module.exports = class TokenizedBufferIterator { return this.position } - getCloseTags () { - return this.closeTags.slice() + getCloseScopeIds () { + return this.closeScopeIds.slice() } - getOpenTags () { - return this.openTags.slice() - } - - nextLineHasMismatchedContainingTags () { - const line = this.tokenizedBuffer.tokenizedLineForRow(this.position.row + 1) - if (line == null) { - return false - } else { - return ( - this.containingTags.length !== line.openScopes.length || - this.containingTags.some((tag, i) => tag !== this.scopeForId(line.openScopes[i])) - ) - } + getOpenScopeIds () { + return this.openScopeIds.slice() } moveToNextLine () { @@ -151,24 +125,14 @@ module.exports = class TokenizedBufferIterator { if (tokenizedLine == null) { return false } else { - this.currentTags = tokenizedLine.tags + this.currentLineTags = tokenizedLine.tags this.currentLineLength = tokenizedLine.text.length - this.currentLineOpenTags = tokenizedLine.openScopes this.tagIndex = 0 return true } } isAtTagBoundary () { - return this.closeTags.length > 0 || this.openTags.length > 0 - } - - scopeForId (id) { - const scope = this.tokenizedBuffer.grammar.scopeForId(id) - if (scope) { - return `syntax--${scope.replace(/\./g, '.syntax--')}` - } else { - return null - } + return this.closeScopeIds.length > 0 || this.openScopeIds.length > 0 } } diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 234f82be9..8fca6c06b 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -7,6 +7,9 @@ TokenIterator = require './token-iterator' ScopeDescriptor = require './scope-descriptor' TokenizedBufferIterator = require './tokenized-buffer-iterator' NullGrammar = require './null-grammar' +{toFirstMateScopeId} = require './first-mate-helpers' + +prefixedScopes = new Map() module.exports = class TokenizedBuffer extends Model @@ -46,6 +49,19 @@ class TokenizedBuffer extends Model buildIterator: -> new TokenizedBufferIterator(this) + classNameForScopeId: (id) -> + scope = @grammar.scopeForId(toFirstMateScopeId(id)) + if scope + prefixedScope = prefixedScopes.get(scope) + if prefixedScope + prefixedScope + else + prefixedScope = "syntax--#{scope.replace(/\./g, ' syntax--')}" + prefixedScopes.set(scope, prefixedScope) + prefixedScope + else + null + getInvalidatedRanges: -> [] @@ -252,7 +268,7 @@ class TokenizedBuffer extends Model buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> lineEnding = @buffer.lineEndingForRow(row) {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) - new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator}) + new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator, @grammar}) tokenizedLineForRow: (bufferRow) -> if 0 <= bufferRow <= @buffer.getLastRow() @@ -262,7 +278,7 @@ class TokenizedBuffer extends Model text = @buffer.lineForRow(bufferRow) lineEnding = @buffer.lineEndingForRow(bufferRow) tags = [@grammar.startIdForScope(@grammar.scopeName), text.length, @grammar.endIdForScope(@grammar.scopeName)] - @tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator}) + @tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator, @grammar}) tokenizedLinesForRows: (startRow, endRow) -> for row in [startRow..endRow] by 1 @@ -328,17 +344,16 @@ class TokenizedBuffer extends Model @indentLevelForLine(line) indentLevelForLine: (line) -> - if match = line.match(/^[\t ]+/) - indentLength = 0 - for character in match[0] - if character is '\t' - indentLength += @getTabLength() - (indentLength % @getTabLength()) - else - indentLength++ + indentLength = 0 + for char in line + if char is '\t' + indentLength += @getTabLength() - (indentLength % @getTabLength()) + else if char is ' ' + indentLength++ + else + break - indentLength / @getTabLength() - else - 0 + indentLength / @getTabLength() scopeDescriptorForPosition: (position) -> {row, column} = @buffer.clipPosition(Point.fromObject(position)) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index a65b3a793..5a22a297a 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -1,5 +1,5 @@ Token = require './token' -CommentScopeRegex = /(\b|\.)comment/ +CommentScopeRegex = /(\b|\.)comment/ idCounter = 1 @@ -10,9 +10,9 @@ class TokenizedLine return unless properties? - {@openScopes, @text, @tags, @ruleStack, @tokenIterator} = properties + {@openScopes, @text, @tags, @ruleStack, @tokenIterator, @grammar} = properties - getTokenIterator: -> @tokenIterator.reset(this, arguments...) + getTokenIterator: -> @tokenIterator.reset(this) Object.defineProperty @prototype, 'tokens', get: -> iterator = @getTokenIterator() @@ -48,17 +48,26 @@ class TokenizedLine return @isCommentLine if @isCommentLine? @isCommentLine = false - iterator = @getTokenIterator() - while iterator.next() - scopes = iterator.getScopes() - continue if scopes.length is 1 - for scope in scopes - if CommentScopeRegex.test(scope) - @isCommentLine = true - break - break + + for tag in @openScopes + if @isCommentOpenTag(tag) + @isCommentLine = true + return @isCommentLine + + for tag in @tags + if @isCommentOpenTag(tag) + @isCommentLine = true + return @isCommentLine + @isCommentLine + isCommentOpenTag: (tag) -> + if tag < 0 and (tag & 1) is 1 + scope = @grammar.scopeForId(tag) + if CommentScopeRegex.test(scope) + return true + false + tokenAtIndex: (index) -> @tokens[index] diff --git a/src/view-registry.coffee b/src/view-registry.coffee index b8ead9717..f300cc031 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -27,21 +27,13 @@ module.exports = class ViewRegistry animationFrameRequest: null documentReadInProgress: false - performDocumentPollAfterUpdate: false - debouncedPerformDocumentPoll: null - minimumPollInterval: 200 constructor: (@atomEnvironment) -> - @polling = false @clear() - initialize: -> - @observer = new MutationObserver(@requestDocumentPoll) - clear: -> @views = new WeakMap @providers = [] - @debouncedPerformDocumentPoll = _.throttle(@performDocumentPoll, @minimumPollInterval).bind(this) @clearDocumentRequests() # Essential: Add a provider that will be used to construct views in the @@ -175,16 +167,6 @@ class ViewRegistry new Disposable => @documentReaders = @documentReaders.filter (reader) -> reader isnt fn - pollDocument: (fn) -> - @startPollingDocument() if @documentPollers.length is 0 - @documentPollers.push(fn) - new Disposable => - @documentPollers = @documentPollers.filter (poller) -> poller isnt fn - @stopPollingDocument() if @documentPollers.length is 0 - - pollAfterNextUpdate: -> - @performDocumentPollAfterUpdate = true - getNextUpdatePromise: -> @nextUpdatePromise ?= new Promise (resolve) => @resolveNextUpdatePromise = resolve @@ -192,13 +174,11 @@ class ViewRegistry clearDocumentRequests: -> @documentReaders = [] @documentWriters = [] - @documentPollers = [] @nextUpdatePromise = null @resolveNextUpdatePromise = null if @animationFrameRequest? cancelAnimationFrame(@animationFrameRequest) @animationFrameRequest = null - @stopPollingDocument() requestDocumentUpdate: -> @animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate) @@ -213,32 +193,9 @@ class ViewRegistry @documentReadInProgress = true reader() while reader = @documentReaders.shift() - @performDocumentPoll() if @performDocumentPollAfterUpdate - @performDocumentPollAfterUpdate = false @documentReadInProgress = false # process updates requested as a result of reads writer() while writer = @documentWriters.shift() resolveNextUpdatePromise?() - - startPollingDocument: -> - window.addEventListener('resize', @requestDocumentPoll) - @observer.observe(document, {subtree: true, childList: true, attributes: true}) - @polling = true - - stopPollingDocument: -> - if @polling - window.removeEventListener('resize', @requestDocumentPoll) - @observer.disconnect() - @polling = false - - requestDocumentPoll: => - if @animationFrameRequest? - @performDocumentPollAfterUpdate = true - else - @debouncedPerformDocumentPoll() - - performDocumentPoll: -> - poller() for poller in @documentPollers - return diff --git a/src/workspace-element.js b/src/workspace-element.js index 7b1ca3a50..895809f1a 100644 --- a/src/workspace-element.js +++ b/src/workspace-element.js @@ -64,7 +64,6 @@ class WorkspaceElement extends HTMLElement { line-height: ${this.config.get('editor.lineHeight')}; }` this.styleManager.addStyleSheet(styleSheetSource, {sourcePath: 'global-text-editor-styles', priority: -1}) - this.viewRegistry.performDocumentPoll() } initialize (model, {config, project, styleManager, viewRegistry}) { diff --git a/static/atom.less b/static/atom.less index caa1e1c6b..14e7def8f 100644 --- a/static/atom.less +++ b/static/atom.less @@ -21,7 +21,7 @@ @import "docks"; @import "panes"; @import "syntax"; -@import "text-editor-light"; +@import "text-editor"; @import "title-bar"; @import "workspace-view"; diff --git a/static/cursors.less b/static/cursors.less index 0b54c6ea1..5cbfadef6 100644 --- a/static/cursors.less +++ b/static/cursors.less @@ -13,14 +13,14 @@ // Editors & when ( lightness(@syntax-background-color) < 50% ) { - .platform-darwin atom-text-editor:not([mini]) .editor-contents--private { + .platform-darwin atom-text-editor:not([mini]) { .cursor-white(); } } // Mini Editors & when ( lightness(@input-background-color) < 50% ) { - .platform-darwin atom-text-editor[mini] .editor-contents--private { + .platform-darwin atom-text-editor[mini] { .cursor-white(); } } diff --git a/static/index.html b/static/index.html index 39c7d80c1..386481bb5 100644 --- a/static/index.html +++ b/static/index.html @@ -1,7 +1,7 @@ - + diff --git a/static/index.js b/static/index.js index 064732470..7a7513683 100644 --- a/static/index.js +++ b/static/index.js @@ -42,21 +42,21 @@ if (process.platform === 'win32') { relativeFilePath = relativeFilePath.replace(/\\/g, '/') } - let cachedModule = snapshotResult.customRequire.cache[relativeFilePath] // eslint-disable-line no-undef + let cachedModule = snapshotResult.customRequire.cache[relativeFilePath] if (!cachedModule) { cachedModule = {exports: Module._load(module, this, false)} - snapshotResult.customRequire.cache[relativeFilePath] = cachedModule // eslint-disable-line no-undef + snapshotResult.customRequire.cache[relativeFilePath] = cachedModule } return cachedModule.exports } - snapshotResult.setGlobals(global, process, window, document, console, require) // eslint-disable-line no-undef + snapshotResult.setGlobals(global, process, window, document, console, require) } - const FileSystemBlobStore = useSnapshot ? snapshotResult.customRequire('../src/file-system-blob-store.js') : require('../src/file-system-blob-store') // eslint-disable-line no-undef + const FileSystemBlobStore = useSnapshot ? snapshotResult.customRequire('../src/file-system-blob-store.js') : require('../src/file-system-blob-store') blobStore = FileSystemBlobStore.load(path.join(process.env.ATOM_HOME, 'blob-store')) - const NativeCompileCache = useSnapshot ? snapshotResult.customRequire('../src/native-compile-cache.js') : require('../src/native-compile-cache') // eslint-disable-line no-undef + const NativeCompileCache = useSnapshot ? snapshotResult.customRequire('../src/native-compile-cache.js') : require('../src/native-compile-cache') NativeCompileCache.setCacheStore(blobStore) NativeCompileCache.setV8Version(process.versions.v8) NativeCompileCache.install() @@ -88,21 +88,21 @@ } function setupWindow () { - const CompileCache = useSnapshot ? snapshotResult.customRequire('../src/compile-cache.js') : require('../src/compile-cache') // eslint-disable-line no-undef + const CompileCache = useSnapshot ? snapshotResult.customRequire('../src/compile-cache.js') : require('../src/compile-cache') CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) CompileCache.install(process.resourcesPath, require) - const ModuleCache = useSnapshot ? snapshotResult.customRequire('../src/module-cache.js') : require('../src/module-cache') // eslint-disable-line no-undef + const ModuleCache = useSnapshot ? snapshotResult.customRequire('../src/module-cache.js') : require('../src/module-cache') ModuleCache.register(getWindowLoadSettings()) - const startCrashReporter = useSnapshot ? snapshotResult.customRequire('../src/crash-reporter-start.js') : require('../src/crash-reporter-start') // eslint-disable-line no-undef + const startCrashReporter = useSnapshot ? snapshotResult.customRequire('../src/crash-reporter-start.js') : require('../src/crash-reporter-start') startCrashReporter({_version: getWindowLoadSettings().appVersion}) - const CSON = useSnapshot ? snapshotResult.customRequire('../node_modules/season/lib/cson.js') : require('season') // eslint-disable-line no-undef + const CSON = useSnapshot ? snapshotResult.customRequire('../node_modules/season/lib/cson.js') : require('season') CSON.setCacheDir(path.join(CompileCache.getCacheDirectory(), 'cson')) const initScriptPath = path.relative(entryPointDirPath, getWindowLoadSettings().windowInitializationScript) - const initialize = useSnapshot ? snapshotResult.customRequire(initScriptPath) : require(initScriptPath) // eslint-disable-line no-undef + const initialize = useSnapshot ? snapshotResult.customRequire(initScriptPath) : require(initScriptPath) return initialize({blobStore: blobStore}).then(function () { electron.ipcRenderer.send('window-command', 'window:loaded') }) diff --git a/static/text-editor-light.less b/static/text-editor.less similarity index 65% rename from static/text-editor-light.less rename to static/text-editor.less index 5f159ce94..06518ac0f 100644 --- a/static/text-editor-light.less +++ b/static/text-editor.less @@ -5,54 +5,21 @@ atom-text-editor { display: flex; font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace; + cursor: text; - .editor--private, .editor-contents--private { - height: 100%; - width: 100%; - } - - .editor-contents--private { - width: 100%; - cursor: text; - display: flex; - -webkit-user-select: none; - position: relative; + .gutter-container { + width: min-content; + background-color: inherit; + cursor: default; } .gutter { overflow: hidden; z-index: 0; text-align: right; - cursor: default; min-width: 1em; box-sizing: border-box; - } - - .line-numbers { - position: relative; - } - - .line-number { - position: relative; - white-space: nowrap; - padding-left: .5em; - opacity: 0.6; - - &.cursor-line { - opacity: 1; - } - - .icon-right { - .octicon(chevron-down, 0.8em); - display: inline-block; - visibility: hidden; - opacity: .6; - padding: 0 .4em; - - &::before { - text-align: center; - } - } + background-color: inherit; } .gutter:hover { @@ -78,14 +45,33 @@ atom-text-editor { } } - .scroll-view { - position: relative; - z-index: 0; + .line-numbers { + width: max-content; + background-color: inherit; + } - overflow: hidden; - flex: 1; - min-width: 0; - min-height: 0; + .line-number { + width: min-content; + padding-left: .5em; + white-space: nowrap; + opacity: 0.6; + position: relative; + + .icon-right { + .octicon(chevron-down, 0.8em); + display: inline-block; + visibility: hidden; + opacity: .6; + padding: 0 .4em; + + &::before { + text-align: center; + } + } + } + + .lines { + background-color: inherit; } .highlight { @@ -94,19 +80,14 @@ atom-text-editor { } .highlight .region { - position: absolute; pointer-events: none; z-index: -1; } - .lines { - min-width: 100%; - position: relative; - z-index: 1; - } - .line { white-space: pre; + overflow: hidden; + contain: layout; &.cursor-line .fold-marker::after { opacity: 1; @@ -139,17 +120,6 @@ atom-text-editor { box-shadow: inset 1px 0; } - .hidden-input { - padding: 0; - border: 0; - position: absolute; - z-index: -1; - top: 0; - left: 0; - opacity: 0; - width: 1px; - } - .cursor { z-index: 4; pointer-events: none; @@ -166,43 +136,6 @@ atom-text-editor { .cursors.blink-off .cursor { opacity: 0; } - - .horizontal-scrollbar { - position: absolute; - left: 0; - right: 0; - bottom: 0; - - height: 15px; - overflow-x: auto; - overflow-y: hidden; - z-index: 3; - cursor: default; - - .scrollbar-content { - height: 15px; - } - } - - .vertical-scrollbar { - position: absolute; - top: 0; - right: 0; - bottom: 0; - - width: 15px; - overflow-x: hidden; - overflow-y: auto; - z-index: 3; - cursor: default; - } - - .scrollbar-corner { - position: absolute; - overflow: auto; - bottom: 0; - right: 0; - } } atom-text-editor[mini] {