diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8823bd9cc..d4e2254d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1,6 @@
See https://atom.io/releases
+
+## 1.3.0
+
+* The tree-view now sorts directory entries more naturally, in a locale-sensitive way.
+* Lines can now be moved up and down with multiple cursors.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c4dee3c17..8d215e1fe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ These are just guidelines, not rules, use your best judgment and feel free to pr
* [Atom and Packages](#atom-and-packages)
[How Can I Contribute?](#how-can-i-contribute)
- * [Submitting Issues](#submitting-issues)
+ * [Reporting Bugs](#reporting-bugs)
* [Your First Code Contribution](#your-first-code-contribution)
* [Pull Requests](#pull-requests)
@@ -71,30 +71,91 @@ For more information on how to work with Atom's official packages, see [Contribu
Also, because Atom is so extensible, it's possible that a feature you've become accustomed to in Atom or an issue you're encountering aren't coming from a bundled package at all, but rather a [community package](https://atom.io/packages) you've installed.
Each community package has its own repository too, and you should be able to find it in Settings > Packages for the packages you installed and contribute there.
-## How can I contribute?
+## How Can I Contribute?
-### Submitting Issues
+### Reporting Bugs
-* You can create an issue [here](https://github.com/atom/atom/issues/new), but
- before doing that please read the notes below on debugging and submitting issues,
- and include as many details as possible with your report.
-* Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging) for tips
- on debugging. You might be able to find the cause of the problem and fix
- things yourself.
-* Include the version of Atom you are using and the OS.
-* Include screenshots and animated GIFs whenever possible; they are immensely
- helpful.
-* Include the behavior you expected and other places you've seen that behavior
- such as Emacs, vi, Xcode, etc.
-* Check the dev tools (`alt-cmd-i`) for errors to include. If the dev tools
- are open _before_ the error is triggered, a full stack trace for the error
- will be logged. If you can reproduce the error, use this approach to get the
- full stack trace and include it in the issue.
-* On Mac, check Console.app for stack traces to include if reporting a crash.
-* Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)
- to see if a similar issue has already been submitted.
-* Please setup a [profile picture](https://help.github.com/articles/how-do-i-set-up-my-profile-picture)
- to make yourself recognizable and so we can all get to know each other better.
+This section guides you through submitting a bug report for Atom. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.
+
+Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). If you'd like, you can use [this template](#template-for-submitting-bug-reports) to structure the information.
+
+#### Before Submitting A Bug Report
+
+* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem [in the latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode), and if you can get the desired behavior by changing [Atom's or packages' config settings](https://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings).
+* **Check the [FAQs on the forum](https://discuss.atom.io/c/faq)** for a list of common questions and problems.
+* **Determine [which repository the problem should be reported in](#atom-and-packages)**.
+* **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)** to see if the problem has already been reported. If it has, add a comment to the existing issue instead of opening a new one.
+
+#### How Do I Submit A (Good) Bug Report?
+
+Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your bug is related to, create an issue on that repository and provide the following information.
+
+Explain the problem and include additional details to help maintainers reproduce the problem:
+
+* **Use a clear and descriptive title** for the issue to identify the problem.
+* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started Atom, e.g. which command exactly you used in the terminal, or how you started Atom otherwise. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you moved the cursor to the end of a line, explain if you used the mouse, or a keyboard shortcut or an Atom command, and if so which one?
+* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
+* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
+* **Explain which behavior you expected to see instead and why.**
+* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](http://www.cockos.com/licecap/) to record GIFs on OSX and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
+* **If you're reporting that Atom crashed**, include a crash report with a stack trace from the operating system. On OSX, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or put it in a [gist](https://gist.github.com/) and provide link to that gist.
+* **If the problem is related to performance**, include a [CPU profile capture and a screenshot](https://atom.io/docs/latest/hacking-atom-debugging#diagnose-performance-problems-with-the-dev-tools-cpu-profiler) with your report.
+* **If the Chrome's developer tools pane is shown without you triggering it**, that normally means that an exception was thrown. The Console tab will include an entry for the exception. Expand the exception so that the stack trace is visible, and provide the full exception and stack trace in a [code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines) and as a screenshot.
+* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
+
+Provide more context by answering these questions:
+
+* **Can you reproduce the problem in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode)?**
+* **Did the problem start happening recently** (e.g. after updating to a new version of Atom) or was this always a problem?
+* If the problem started happening recently, **can you reproduce the problem in an older version of Atom?** What's the most recent version in which the problem doesn't happen? You can download older versions of Atom from [the releases page](https://github.com/atom/atom/releases).
+* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
+* If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using?
+
+Include details about your configuration and environment:
+
+* **Which version of Atom are you using?** You can get the exact version by running `atom -v` in your terminal, or by starting Atom and running the `Application: About` command from the [Command Palette](https://github.com/atom/command-palette).
+* **What's the name and version of the OS you're using**?
+* **Are you running Atom in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest?
+* **Which [packages](#atom-and-packages) do you have installed?** You can get that list by running `apm list --installed`.
+* **Are you using [local configuration files](https://atom.io/docs/latest/using-atom-basic-customization)** `config.cson`, `keymap.cson`, `snippets.cson`, `styles.less` and `init.coffee` to customize Atom? If so, provide the contents of those files, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/).
+* **Are you using Atom with multiple monitors?** If so, can you reproduce the problem when you use a single monitor?
+* **Which keyboard layout are you using?** Are you using a US layout or some other layout?
+
+#### Template For Submitting Bug Reports
+
+ [Short description of problem here]
+
+ **Reproduction Steps:**
+
+ 1. [First Step]
+ 2. [Second Step]
+ 3. [Other Steps...]
+
+ **Expected behavior:**
+
+ [Describe expected behavior here]
+
+ **Observed behavior:**
+
+ [Describe observed behavior here]
+
+ **Screenshots and GIFs**
+
+ 
+
+ **Atom version:** [Enter Atom version here]
+ **OS and version:** [Enter OS name and version here]
+
+ **Installed packages:**
+
+ [List of installed packages here]
+
+ **Additional information:**
+
+ * Problem can be reproduced in safe mode: [Yes/No]
+ * Problem started happening recently, didn't happen in an older version of Atom: [Yes/No]
+ * Problem can be reliably reproduced, doesn't happen randomly: [Yes/No]
+ * Problem happens with all files and projects, not only some files or projects: [Yes/No]
### Your First Code Contribution
diff --git a/menus/darwin.cson b/menus/darwin.cson
index 6fff290e2..52b7a5bc8 100644
--- a/menus/darwin.cson
+++ b/menus/darwin.cson
@@ -19,7 +19,7 @@
{ type: 'separator' }
{ label: 'Install Shell Commands', command: 'window:install-shell-commands' }
{ type: 'separator' }
- { label: 'Services', submenu: [] }
+ { label: 'Services', role: 'services', submenu: [] }
{ type: 'separator' }
{ label: 'Hide Atom', command: 'application:hide' }
{ label: 'Hide Others', command: 'application:hide-other-applications' }
@@ -184,6 +184,7 @@
{
label: 'Window'
+ role: 'window'
submenu: [
{ label: 'Minimize', command: 'application:minimize' }
{ label: 'Zoom', command: 'application:zoom' }
@@ -194,6 +195,7 @@
{
label: 'Help'
+ role: 'help'
submenu: [
{ label: 'Terms of Use', command: 'application:open-terms-of-use' }
{ label: 'Documentation', command: 'application:open-documentation' }
diff --git a/package.json b/package.json
index 52fd8d91d..c29d36f91 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
- "electronVersion": "0.34.0",
+ "electronVersion": "0.34.3",
"dependencies": {
"async": "0.2.6",
"atom-keymap": "^6.1.0",
@@ -28,7 +28,7 @@
"fs-plus": "^2.8.0",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
- "git-utils": "^4",
+ "git-utils": "^4.0.7",
"grim": "1.5.0",
"jasmine-json": "~0.0",
"jasmine-tagged": "^1.1.4",
@@ -39,7 +39,7 @@
"normalize-package-data": "^2.0.0",
"nslog": "^3",
"oniguruma": "^5",
- "pathwatcher": "^6.2",
+ "pathwatcher": "~6.2",
"property-accessors": "^1.1.3",
"random-words": "0.0.1",
"resolve": "^1.1.6",
@@ -52,7 +52,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
- "text-buffer": "7.1.3",
+ "text-buffer": "^8.0.3",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -76,7 +76,7 @@
"autocomplete-css": "0.11.0",
"autocomplete-html": "0.7.2",
"autocomplete-plus": "2.23.0",
- "autocomplete-snippets": "1.7.1",
+ "autocomplete-snippets": "1.8.0",
"autoflow": "0.26.0",
"autosave": "0.23.0",
"background-tips": "0.26.0",
@@ -87,31 +87,31 @@
"dev-live-reload": "0.47.0",
"encoding-selector": "0.21.0",
"exception-reporting": "0.37.0",
- "find-and-replace": "0.190.0",
+ "find-and-replace": "0.191.0",
"fuzzy-finder": "0.93.0",
"git-diff": "0.57.0",
"go-to-line": "0.30.0",
"grammar-selector": "0.48.0",
- "image-view": "0.55.0",
+ "image-view": "0.56.0",
"incompatible-packages": "0.25.0",
"keybinding-resolver": "0.33.0",
"line-ending-selector": "0.3.0",
"link": "0.31.0",
- "markdown-preview": "0.156.0",
+ "markdown-preview": "0.156.1",
"metrics": "0.53.0",
"notifications": "0.61.0",
- "open-on-github": "0.39.0",
+ "open-on-github": "0.40.0",
"package-generator": "0.41.0",
"release-notes": "0.53.0",
- "settings-view": "0.231.0",
- "snippets": "0.101.0",
- "spell-check": "0.62.0",
+ "settings-view": "0.232.0",
+ "snippets": "1.0.1",
+ "spell-check": "0.63.0",
"status-bar": "0.80.0",
"styleguide": "0.45.0",
"symbols-view": "0.110.0",
"tabs": "0.88.0",
"timecop": "0.33.0",
- "tree-view": "0.196.0",
+ "tree-view": "0.198.0",
"update-package-dependencies": "0.10.0",
"welcome": "0.32.0",
"whitespace": "0.32.0",
@@ -127,19 +127,19 @@
"language-html": "0.42.0",
"language-hyperlink": "0.15.0",
"language-java": "0.16.1",
- "language-javascript": "0.98.0",
+ "language-javascript": "0.100.0",
"language-json": "0.17.1",
"language-less": "0.28.3",
- "language-make": "0.19.0",
+ "language-make": "0.20.0",
"language-mustache": "0.13.0",
"language-objective-c": "0.15.0",
- "language-perl": "0.30.0",
- "language-php": "0.32.0",
+ "language-perl": "0.31.0",
+ "language-php": "0.34.0",
"language-property-list": "0.8.0",
"language-python": "0.41.0",
- "language-ruby": "0.60.0",
+ "language-ruby": "0.61.0",
"language-ruby-on-rails": "0.24.0",
- "language-sass": "0.42.1",
+ "language-sass": "0.43.0",
"language-shellscript": "0.20.0",
"language-source": "0.9.0",
"language-sql": "0.19.0",
diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee
new file mode 100644
index 000000000..5f8e03ca3
--- /dev/null
+++ b/spec/async-spec-helpers.coffee
@@ -0,0 +1,28 @@
+exports.beforeEach = (fn) ->
+ global.beforeEach ->
+ result = fn()
+ if result instanceof Promise
+ waitsForPromise(-> result)
+
+exports.afterEach = (fn) ->
+ global.afterEach ->
+ result = fn()
+ if result instanceof Promise
+ waitsForPromise(-> result)
+
+['it', 'fit', 'ffit', 'fffit'].forEach (name) ->
+ exports[name] = (description, fn) ->
+ global[name] description, ->
+ result = fn()
+ if result instanceof Promise
+ waitsForPromise(-> result)
+
+waitsForPromise = (fn) ->
+ promise = fn()
+ waitsFor 'spec promise to resolve', 30000, (done) ->
+ promise.then(
+ done,
+ (error) ->
+ jasmine.getEnv().currentSpec.fail(error)
+ done()
+ )
diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee
index e12ac75c1..23f8e0e51 100644
--- a/spec/atom-environment-spec.coffee
+++ b/spec/atom-environment-spec.coffee
@@ -243,23 +243,6 @@ describe "AtomEnvironment", ->
atomEnvironment.destroy()
- describe "::destroy()", ->
- it "unsubscribes from all buffers", ->
- atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document})
-
- waitsForPromise ->
- atomEnvironment.workspace.open("sample.js")
-
- runs ->
- buffer = atomEnvironment.workspace.getActivePaneItem().buffer
- pane = atomEnvironment.workspace.getActivePane()
- pane.splitRight(copyActiveItem: true)
- expect(atomEnvironment.workspace.getTextEditors().length).toBe 2
-
- atomEnvironment.destroy()
-
- expect(buffer.getSubscriptionCount()).toBe 0
-
describe "::openLocations(locations) (called via IPC from browser process)", ->
beforeEach ->
spyOn(atom.workspace, 'open')
diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee
index 68dd9c754..a54c01198 100644
--- a/spec/display-buffer-spec.coffee
+++ b/spec/display-buffer-spec.coffee
@@ -418,11 +418,11 @@ describe "DisplayBuffer", ->
describe "when creating a fold where one already exists", ->
it "returns existing fold and does't create new fold", ->
fold = displayBuffer.createFold(0, 10)
- expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1
+ expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
newFold = displayBuffer.createFold(0, 10)
expect(newFold).toBe fold
- expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1
+ expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
describe "when a fold is created inside an existing folded region", ->
it "creates/destroys the fold, but does not trigger change event", ->
@@ -829,7 +829,6 @@ describe "DisplayBuffer", ->
it "unsubscribes all display buffer markers from their underlying buffer marker (regression)", ->
marker = displayBuffer.markBufferPosition([12, 2])
displayBuffer.destroy()
- expect(marker.bufferMarker.getSubscriptionCount()).toBe 0
expect( -> buffer.insert([12, 2], '\n')).not.toThrow()
describe "markers", ->
@@ -879,7 +878,7 @@ describe "DisplayBuffer", ->
[markerChangedHandler, marker] = []
beforeEach ->
- marker = displayBuffer.markScreenRange([[5, 4], [5, 10]], maintainHistory: true)
+ marker = displayBuffer.addMarkerLayer(maintainHistory: true).markScreenRange([[5, 4], [5, 10]])
marker.onDidChange markerChangedHandler = jasmine.createSpy("markerChangedHandler")
it "triggers the 'changed' event whenever the markers head's screen position changes in the buffer or on screen", ->
@@ -1016,7 +1015,7 @@ describe "DisplayBuffer", ->
expect(markerChangedHandler).not.toHaveBeenCalled()
it "updates markers before emitting buffer change events, but does not notify their observers until the change event", ->
- marker2 = displayBuffer.markBufferRange([[8, 1], [8, 1]], maintainHistory: true)
+ marker2 = displayBuffer.addMarkerLayer(maintainHistory: true).markBufferRange([[8, 1], [8, 1]])
marker2.onDidChange marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler")
displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange()
@@ -1237,11 +1236,6 @@ describe "DisplayBuffer", ->
decoration.destroy()
expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined()
- it "does not leak disposables", ->
- disposablesSize = displayBuffer.disposables.disposables.size
- decoration.destroy()
- expect(displayBuffer.disposables.disposables.size).toBe(disposablesSize - 1)
-
describe "when a decoration is updated via Decoration::update()", ->
it "emits an 'updated' event containing the new and old params", ->
decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
@@ -1249,7 +1243,7 @@ describe "DisplayBuffer", ->
{oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
expect(oldProperties).toEqual decorationProperties
- expect(newProperties).toEqual type: 'line-number', gutterName: 'line-number', class: 'two', id: decoration.id
+ expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
describe "::getDecorations(properties)", ->
it "returns decorations matching the given optional properties", ->
diff --git a/spec/fixtures/sample-with-many-folds.js b/spec/fixtures/sample-with-many-folds.js
new file mode 100644
index 000000000..a3c5b7acc
--- /dev/null
+++ b/spec/fixtures/sample-with-many-folds.js
@@ -0,0 +1,12 @@
+1;
+2;
+function f3() {
+ return 4;
+};
+6;
+7;
+function f8() {
+ return 9;
+};
+11;
+12;
diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee
index 88e730d44..391c63dc7 100644
--- a/spec/package-manager-spec.coffee
+++ b/spec/package-manager-spec.coffee
@@ -433,6 +433,13 @@ describe "PackageManager", ->
runs ->
expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0
+ describe "when setting core.packagesWithKeymapsDisabled", ->
+ it "ignores package names in the array that aren't loaded", ->
+ atom.packages.observePackagesWithKeymapsDisabled()
+
+ expect(-> atom.config.set("core.packagesWithKeymapsDisabled", ["package-does-not-exist"])).not.toThrow()
+ expect(-> atom.config.set("core.packagesWithKeymapsDisabled", [])).not.toThrow()
+
describe "when the package's keymaps are disabled and re-enabled after it is activated", ->
it "removes and re-adds the keymaps", ->
element1 = createTestElement('test-1')
diff --git a/spec/sample.js b/spec/sample.js
deleted file mode 100644
index 66dc9051d..000000000
--- a/spec/sample.js
+++ /dev/null
@@ -1 +0,0 @@
-undefined
\ No newline at end of file
diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee
deleted file mode 100644
index 91020f299..000000000
--- a/spec/text-editor-component-spec.coffee
+++ /dev/null
@@ -1,3685 +0,0 @@
-_ = require 'underscore-plus'
-{extend, flatten, toArray, last} = _
-
-TextEditorElement = require '../src/text-editor-element'
-nbsp = String.fromCharCode(160)
-
-describe "TextEditorComponent", ->
- [contentNode, editor, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = []
- [lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize, tileHeightInPixels] = []
-
- beforeEach ->
- tileSize = 3
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- runs ->
- spyOn(window, "setInterval").andCallFake window.fakeSetInterval
- spyOn(window, "clearInterval").andCallFake window.fakeClearInterval
-
- noAnimationFrame = -> throw new Error('No animation frame requested')
- nextAnimationFrame = noAnimationFrame
-
- spyOn(window, 'requestAnimationFrame').andCallFake (fn) ->
- nextAnimationFrame = ->
- nextAnimationFrame = noAnimationFrame
- fn()
-
- waitsForPromise ->
- atom.workspace.open('sample.js').then (o) -> editor = o
-
- runs ->
- contentNode = document.querySelector('#jasmine-content')
- contentNode.style.width = '1000px'
-
- wrapperNode = new TextEditorElement()
- wrapperNode.tileSize = tileSize
- wrapperNode.initialize(editor, atom)
- wrapperNode.setUpdatedSynchronously(false)
- jasmine.attachToDOM(wrapperNode)
-
- {component} = wrapperNode
- component.setFontFamily('monospace')
- component.setLineHeight(1.3)
- component.setFontSize(20)
-
- lineHeightInPixels = editor.getLineHeightInPixels()
- tileHeightInPixels = tileSize * lineHeightInPixels
- charWidth = editor.getDefaultCharWidth()
- componentNode = component.getDomNode()
- verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar')
- horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar')
-
- component.measureDimensions()
- nextAnimationFrame()
-
- # Mutating the DOM in the previous frame causes a document poll; clear it here
- waits 0
- runs -> nextAnimationFrame()
-
- afterEach ->
- contentNode.style.width = ''
-
- describe "async updates", ->
- it "handles corrupted state gracefully", ->
- # trigger state updates, e.g. presenter.updateLinesState
- editor.insertNewline()
-
- # simulate state corruption
- component.presenter.startRow = -1
- component.presenter.endRow = 9999
-
- expect(nextAnimationFrame).not.toThrow()
-
- it "doesn't update when an animation frame was requested but the component got destroyed before its delivery", ->
- editor.setText("You shouldn't see this update.")
- expect(nextAnimationFrame).not.toBe(noAnimationFrame)
-
- component.destroy()
- nextAnimationFrame()
-
- expect(component.lineNodeForScreenRow(0).textContent).not.toBe("You shouldn't see this update.")
-
- describe "line rendering", ->
- expectTileContainsRow = (tileNode, screenRow, {top}) ->
- lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']")
- tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
-
- expect(lineNode.offsetTop).toBe(top)
- if tokenizedLine.text is ""
- expect(lineNode.innerHTML).toBe(" ")
- else
- expect(lineNode.textContent).toBe(tokenizedLine.text)
-
- it "gives the lines container the same height as the wrapper node", ->
- linesNode = componentNode.querySelector(".lines")
-
- wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels)
-
- wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels)
-
- it "renders higher tiles in front of lower ones", ->
- wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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'))
- nextAnimationFrame()
-
- 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", ->
- wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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(tileSize)
- 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(tileSize)
- 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(tileSize)
- 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 = tileSize * lineHeightInPixels + 5
- verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
- nextAnimationFrame()
-
- 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(tileSize)
- 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(tileSize)
- 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(tileSize)
- 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", ->
- wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- editor.getBuffer().deleteRows(0, 1)
- nextAnimationFrame()
-
- 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')
- nextAnimationFrame()
-
- 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", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
- verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels
- verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
- nextAnimationFrame()
- buffer = editor.getBuffer()
-
- buffer.insert([0, 0], '\n\n')
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text
-
- buffer.delete([[0, 0], [3, 0]])
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text
-
- it "updates the top position of lines when the line height changes", ->
- initialLineHeightInPixels = editor.getLineHeightInPixels()
- component.setLineHeight(2)
- nextAnimationFrame()
-
- 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", ->
- initialLineHeightInPixels = editor.getLineHeightInPixels()
- component.setFontSize(10)
- nextAnimationFrame()
-
- newLineHeightInPixels = editor.getLineHeightInPixels()
- expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels
- expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels
-
- xit "updates the top position of lines when the font family changes", ->
- # Can't find a font that changes the line height, but we think one might exist
- linesComponent = component.refs.lines
- spyOn(linesComponent, 'measureLineHeightAndDefaultCharWidth').andCallFake -> editor.setLineHeightInPixels(10)
-
- initialLineHeightInPixels = editor.getLineHeightInPixels()
- component.setFontFamily('sans-serif')
- nextAnimationFrame()
-
- expect(linesComponent.measureLineHeightAndDefaultCharWidth).toHaveBeenCalled()
- 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 aren't enough lines to scroll vertically", ->
- editor.setText('')
- wrapperNode.style.height = '300px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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", ->
- gutterWidth = componentNode.querySelector('.gutter').offsetWidth
- scrollViewNode = componentNode.querySelector('.scroll-view')
- lineNodes = componentNode.querySelectorAll('.line')
-
- componentNode.style.width = gutterWidth + (30 * charWidth) + 'px'
- component.measureDimensions()
- nextAnimationFrame()
- expect(wrapperNode.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth
-
- # At the time of writing, using width: 100% to achieve the full-width
- # lines caused full-screen repaints after switching away from an editor
- # and back again Please ensure you don't cause a performance regression if
- # you change this behavior.
- editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth()
-
- for lineNode in lineNodes
- expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth)
-
- componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px'
- component.measureDimensions()
- nextAnimationFrame()
- scrollViewWidth = scrollViewNode.offsetWidth
-
- for lineNode in lineNodes
- expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth)
-
- it "renders an nbsp on empty lines when no line-ending character is defined", ->
- atom.config.set("editor.showInvisibles", false)
- expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp
-
- it "gives the lines and tiles divs the same background color as the editor to improve GPU performance", ->
- linesNode = componentNode.querySelector('.lines')
- backgroundColor = getComputedStyle(wrapperNode).backgroundColor
- expect(linesNode.style.backgroundColor).toBe backgroundColor
-
- for tileNode in component.tileNodesForLines()
- expect(tileNode.style.backgroundColor).toBe(backgroundColor)
-
- wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)'
- atom.views.performDocumentPoll()
-
- advanceClock(atom.views.documentPollingInterval)
- nextAnimationFrame()
- expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)'
- for tileNode in component.tileNodesForLines()
- expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)")
-
-
- it "applies .leading-whitespace for lines with leading spaces and/or tabs", ->
- editor.setText(' a')
- nextAnimationFrame()
-
- 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')
- nextAnimationFrame()
-
- 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", ->
- editor.setText(' ')
- nextAnimationFrame()
-
- 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')
- nextAnimationFrame()
-
- 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 ')
- nextAnimationFrame()
-
- 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')
- nextAnimationFrame()
-
- 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", ->
- wrapperNode.setContinuousReflow(true)
-
- oldLineNodes = componentNode.querySelectorAll(".line")
-
- advanceClock(10)
- expect(nextAnimationFrame).toBe(noAnimationFrame)
-
- advanceClock(component.presenter.minimumReflowInterval - 10)
- nextAnimationFrame()
-
- newLineNodes = componentNode.querySelectorAll(".line")
- expect(oldLineNodes).not.toEqual(newLineNodes)
-
- wrapperNode.setContinuousReflow(false)
- advanceClock(component.presenter.minimumReflowInterval)
- expect(nextAnimationFrame).toBe(noAnimationFrame)
-
- describe "when showInvisibles is enabled", ->
- invisibles = null
-
- beforeEach ->
- invisibles =
- eol: 'E'
- space: 'S'
- tab: 'T'
- cr: 'C'
-
- atom.config.set("editor.showInvisibles", true)
- atom.config.set("editor.invisibles", invisibles)
- nextAnimationFrame()
-
- it "re-renders the lines when the showInvisibles config option changes", ->
- editor.setText " a line with tabs\tand spaces \n"
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}"
-
- atom.config.set("editor.showInvisibles", false)
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces "
-
- atom.config.set("editor.showInvisibles", true)
- nextAnimationFrame()
- 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", ->
- editor.setText " a line with tabs\tand spaces \n"
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}"
-
- 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", ->
- editor.setText "var\n"
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).innerHTML).toBe "var#{invisibles.eol}"
-
- it "displays trailing carriage returns using a visible, non-empty value", ->
- editor.setText "a line that ends with a carriage return\r\n"
- nextAnimationFrame()
- 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", ->
- expect(component.lineNodeForScreenRow(10).textContent).toBe invisibles.eol
-
- it "renders an nbsp on empty lines when the line-ending character is an empty string", ->
- atom.config.set("editor.invisibles", eol: '')
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp
-
- it "renders an nbsp on empty lines when the line-ending character is false", ->
- atom.config.set("editor.invisibles", eol: false)
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp
-
- it "interleaves invisible line-ending characters with indent guides on empty lines", ->
- atom.config.set "editor.showIndentGuide", true
- nextAnimationFrame()
-
- editor.setTextInBufferRange([[10, 0], [11, 0]], "\r\n", normalizeLineEndings: false)
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE'
-
- editor.setTabLength(3)
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE '
-
- editor.setTabLength(1)
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE'
-
- editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ')
- editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ')
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE'
-
- describe "when soft wrapping is enabled", ->
- beforeEach ->
- editor.setText "a line that wraps \n"
- editor.setSoftWrapped(true)
- nextAnimationFrame()
- componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- it "doesn't show end of line invisibles at the end of wrapped lines", ->
- expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that "
- expect(component.lineNodeForScreenRow(1).textContent).toBe "wraps#{invisibles.space}#{invisibles.eol}"
-
- describe "when indent guides are enabled", ->
- beforeEach ->
- atom.config.set "editor.showIndentGuide", true
- nextAnimationFrame()
-
- it "adds an 'indent-guide' class to spans comprising the leading whitespace", ->
- 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
-
- 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", ->
- editor.getBuffer().insert([1, Infinity], '\n')
- nextAnimationFrame()
-
- 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", ->
- editor.getBuffer().insert([1, Infinity], '\n ')
- nextAnimationFrame()
-
- 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", ->
- atom.config.set 'editor.showInvisibles', true
- atom.config.set 'editor.invisibles', space: '-', eol: 'x'
- editor.getBuffer().insert([1, Infinity], '\n ')
- nextAnimationFrame()
-
- 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", ->
- editor.getBuffer().setText " hi "
- nextAnimationFrame()
-
- 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", ->
- editor.getBuffer().insert([12, 0], '\n')
- nextAnimationFrame()
- editor.getBuffer().insert([13, 0], ' ')
- nextAnimationFrame()
-
- 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", ->
- editor.getBuffer().insert([12, 2], '\n')
- nextAnimationFrame()
- editor.getBuffer().insert([12, 0], ' ')
- nextAnimationFrame()
-
- 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", ->
- beforeEach ->
- expect(atom.config.get("editor.showIndentGuide")).toBe false
-
- it "does not render indent guides on lines containing only whitespace", ->
- editor.getBuffer().insert([1, Infinity], '\n ')
- nextAnimationFrame()
-
- line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2))
- expect(line2LeafNodes.length).toBe 3
- expect(line2LeafNodes[0].textContent).toBe ' '
- expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe false
- expect(line2LeafNodes[1].textContent).toBe ' '
- expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe false
- expect(line2LeafNodes[2].textContent).toBe ' '
- expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe false
-
- describe "when the buffer contains null bytes", ->
- it "excludes the null byte from character measurement", ->
- editor.setText("a\0b")
- nextAnimationFrame()
- expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual 2 * charWidth
-
- describe "when there is a fold", ->
- it "renders a fold marker on the folded line", ->
- foldedLineNode = component.lineNodeForScreenRow(4)
- expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy()
-
- editor.foldBufferRow(4)
- nextAnimationFrame()
- foldedLineNode = component.lineNodeForScreenRow(4)
- expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy()
-
- editor.unfoldBufferRow(4)
- nextAnimationFrame()
- foldedLineNode = component.lineNodeForScreenRow(4)
- expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy()
-
- describe "gutter rendering", ->
- expectTileContainsRow = (tileNode, screenRow, {top, text}) ->
- 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", ->
- wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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'))
- nextAnimationFrame()
-
- 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", ->
- linesNode = componentNode.querySelector(".line-numbers")
-
- wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels)
-
- wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels)
-
- it "renders the currently-visible line numbers in a tiled fashion", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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 = tileSize * lineHeightInPixels + 5
- verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
- nextAnimationFrame()
-
- 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(tileSize)
- 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(tileSize)
- 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(tileSize)
- 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", ->
- editor.getBuffer().insert([0, 0], '\n\n')
- nextAnimationFrame()
-
- 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')
- nextAnimationFrame()
-
- 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", ->
- editor.setSoftWrapped(true)
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 30 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(componentNode.querySelectorAll('.line-number').length).toBe 9 + 1 # 3 line-numbers tiles + 1 dummy line
- 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", ->
- editor.getBuffer().setText([1..10].join('\n'))
- nextAnimationFrame()
- for screenRow in [0..8]
- expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}"
- expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10"
-
- gutterNode = componentNode.querySelector('.gutter')
- initialGutterWidth = gutterNode.offsetWidth
-
- # Removes padding when the max number of digits goes down
- editor.getBuffer().delete([[1, 0], [2, 0]])
- nextAnimationFrame()
- for screenRow in [0..8]
- expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}"
- expect(gutterNode.offsetWidth).toBeLessThan initialGutterWidth
-
- # Increases padding when the max number of digits goes up
- editor.getBuffer().insert([0, 0], '\n\n')
- nextAnimationFrame()
- for screenRow in [0..8]
- expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}"
- expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10"
- 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", ->
- wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px'
- component.measureDimensions()
- nextAnimationFrame()
- 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", ->
- gutterNode = componentNode.querySelector('.gutter')
- lineNumbersNode = gutterNode.querySelector('.line-numbers')
- {backgroundColor} = getComputedStyle(wrapperNode)
- expect(lineNumbersNode.style.backgroundColor).toBe backgroundColor
- for tileNode in component.tileNodesForLineNumbers()
- expect(tileNode.style.backgroundColor).toBe(backgroundColor)
-
- # favor gutter color if it's assigned
- gutterNode.style.backgroundColor = 'rgb(255, 0, 0)'
- atom.views.performDocumentPoll()
-
- nextAnimationFrame()
- expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)'
- for tileNode in 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", ->
- expect(component.gutterContainerComponent.getLineNumberGutterComponent()?).toBe true
-
- editor.setLineNumberGutterVisible(false)
- nextAnimationFrame()
-
- expect(componentNode.querySelector('.gutter').style.display).toBe 'none'
-
- atom.config.set("editor.showLineNumbers", false)
- nextAnimationFrame()
-
- expect(componentNode.querySelector('.gutter').style.display).toBe 'none'
-
- editor.setLineNumberGutterVisible(true)
- nextAnimationFrame()
-
- expect(componentNode.querySelector('.gutter').style.display).toBe 'none'
-
- atom.config.set("editor.showLineNumbers", true)
- nextAnimationFrame()
-
- expect(componentNode.querySelector('.gutter').style.display).toBe ''
- expect(component.lineNumberNodeForScreenRow(3)?).toBe true
-
- it "keeps rebuilding line numbers when continuous reflow is on", ->
- wrapperNode.setContinuousReflow(true)
-
- oldLineNodes = componentNode.querySelectorAll(".line-number")
-
- advanceClock(10)
- expect(nextAnimationFrame).toBe(noAnimationFrame)
-
- advanceClock(component.presenter.minimumReflowInterval - 10)
- nextAnimationFrame()
-
- newLineNodes = componentNode.querySelectorAll(".line-number")
- expect(oldLineNodes).not.toEqual(newLineNodes)
-
- wrapperNode.setContinuousReflow(false)
- advanceClock(component.presenter.minimumReflowInterval)
- expect(nextAnimationFrame).toBe(noAnimationFrame)
-
- describe "fold decorations", ->
- describe "rendering fold decorations", ->
- it "adds the foldable class to line numbers when the line is foldable", ->
- 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", ->
- editor.getBuffer().insert([0, 0], '\n')
- nextAnimationFrame()
- 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", ->
- expect(lineNumberHasClass(11, 'foldable')).toBe false
-
- editor.getBuffer().insert([11, 44], '\n fold me')
- nextAnimationFrame()
- expect(lineNumberHasClass(11, 'foldable')).toBe true
-
- editor.undo()
- nextAnimationFrame()
- expect(lineNumberHasClass(11, 'foldable')).toBe false
-
- it "adds, updates and removes the folded class on the correct line number componentNodes", ->
- editor.foldBufferRow(4)
- nextAnimationFrame()
- expect(lineNumberHasClass(4, 'folded')).toBe true
-
- editor.getBuffer().insert([0, 0], '\n')
- nextAnimationFrame()
- expect(lineNumberHasClass(4, 'folded')).toBe false
- expect(lineNumberHasClass(5, 'folded')).toBe true
-
- editor.unfoldBufferRow(5)
- nextAnimationFrame()
- expect(lineNumberHasClass(5, 'folded')).toBe false
-
- describe "when soft wrapping is enabled", ->
- beforeEach ->
- editor.setSoftWrapped(true)
- nextAnimationFrame()
- componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- it "doesn't add the foldable class for soft-wrapped lines", ->
- expect(lineNumberHasClass(0, 'foldable')).toBe true
- expect(lineNumberHasClass(1, 'foldable')).toBe false
-
- describe "mouse interactions with fold indicators", ->
- [gutterNode] = []
-
- buildClickEvent = (target) ->
- buildMouseEvent('click', {target})
-
- beforeEach ->
- gutterNode = componentNode.querySelector('.gutter')
-
- describe "when the component is destroyed", ->
- it "stops listening for folding events", ->
- nextAnimationFrame() unless nextAnimationFrame is noAnimationFrame # clear pending frame request if needed
-
- component.destroy()
-
- lineNumber = component.lineNumberNodeForScreenRow(1)
- target = lineNumber.querySelector('.icon-right')
- target.dispatchEvent(buildClickEvent(target))
-
- expect(nextAnimationFrame).toBe(noAnimationFrame)
-
- it "folds and unfolds the block represented by the fold indicator when clicked", ->
- expect(lineNumberHasClass(1, 'folded')).toBe false
-
- lineNumber = component.lineNumberNodeForScreenRow(1)
- target = lineNumber.querySelector('.icon-right')
- target.dispatchEvent(buildClickEvent(target))
- nextAnimationFrame()
- expect(lineNumberHasClass(1, 'folded')).toBe true
-
- lineNumber = component.lineNumberNodeForScreenRow(1)
- target = lineNumber.querySelector('.icon-right')
- target.dispatchEvent(buildClickEvent(target))
- nextAnimationFrame()
- expect(lineNumberHasClass(1, 'folded')).toBe false
-
- it "does not fold when the line number componentNode is clicked", ->
- nextAnimationFrame() unless nextAnimationFrame is noAnimationFrame # clear pending frame request if needed
-
- lineNumber = component.lineNumberNodeForScreenRow(1)
- lineNumber.dispatchEvent(buildClickEvent(lineNumber))
- expect(nextAnimationFrame).toBe noAnimationFrame
- expect(lineNumberHasClass(1, 'folded')).toBe false
-
- describe "cursor rendering", ->
- it "renders the currently visible cursors", ->
- cursor1 = editor.getLastCursor()
- cursor1.setScreenPosition([0, 5], autoscroll: false)
-
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 20 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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)"
-
- cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false)
- cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false)
- nextAnimationFrame()
-
- 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'))
- nextAnimationFrame()
- horizontalScrollbarNode.scrollLeft = 3.5 * charWidth
- horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
- nextAnimationFrame()
-
- 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)
- nextAnimationFrame()
- 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()
- nextAnimationFrame()
- 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", ->
- atom.config.set('editor.fontFamily', 'sans-serif')
- editor.setCursorScreenPosition([0, 16])
- nextAnimationFrame()
-
- cursor = componentNode.querySelector('.cursor')
- cursorRect = cursor.getBoundingClientRect()
-
- cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild
- range = document.createRange()
- range.setStart(cursorLocationTextNode, 0)
- range.setEnd(cursorLocationTextNode, 1)
- 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", ->
- atom.config.set('editor.fontFamily', 'sans-serif')
- editor.setText('he\u0301y') # e with an accent mark
- editor.setCursorBufferPosition([0, 3])
- nextAnimationFrame()
-
- cursor = componentNode.querySelector('.cursor')
- cursorRect = cursor.getBoundingClientRect()
-
- cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2]
-
- range = document.createRange()
- range.setStart(cursorLocationTextNode, 0)
- range.setEnd(cursorLocationTextNode, 1)
- rangeRect = range.getBoundingClientRect()
-
- expect(cursorRect.left).toBeCloseTo rangeRect.left, 0
- expect(cursorRect.width).toBeCloseTo rangeRect.width, 0
-
- it "positions cursors correctly after character widths are changed via a stylesheet change", ->
- atom.config.set('editor.fontFamily', 'sans-serif')
- editor.setCursorScreenPosition([0, 16])
- nextAnimationFrame()
-
- atom.styles.addStyleSheet """
- .function.js {
- font-weight: bold;
- }
- """, context: 'atom-text-editor'
- nextAnimationFrame() # update based on new measurements
-
- cursor = componentNode.querySelector('.cursor')
- cursorRect = cursor.getBoundingClientRect()
-
- cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild
- range = document.createRange()
- range.setStart(cursorLocationTextNode, 0)
- range.setEnd(cursorLocationTextNode, 1)
- 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", ->
- editor.setCursorScreenPosition([0, Infinity])
- nextAnimationFrame()
- 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", ->
- editor.setCursorScreenPosition([1, 0])
- nextAnimationFrame()
- cursorNode = componentNode.querySelector('.cursor')
- expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0
-
- it "blinks cursors when they aren't moving", ->
- cursorsNode = componentNode.querySelector('.cursors')
-
- wrapperNode.focus()
- nextAnimationFrame()
- expect(cursorsNode.classList.contains('blink-off')).toBe false
-
- advanceClock(component.cursorBlinkPeriod / 2)
- nextAnimationFrame()
- expect(cursorsNode.classList.contains('blink-off')).toBe true
-
- advanceClock(component.cursorBlinkPeriod / 2)
- nextAnimationFrame()
- expect(cursorsNode.classList.contains('blink-off')).toBe false
-
- # Stop blinking after moving the cursor
- editor.moveRight()
- nextAnimationFrame()
- expect(cursorsNode.classList.contains('blink-off')).toBe false
-
- advanceClock(component.cursorBlinkResumeDelay)
- advanceClock(component.cursorBlinkPeriod / 2)
- nextAnimationFrame()
- expect(cursorsNode.classList.contains('blink-off')).toBe true
-
- it "does not render cursors that are associated with non-empty selections", ->
- editor.setSelectedScreenRange([[0, 4], [4, 6]])
- editor.addCursorAtScreenPosition([6, 8])
- nextAnimationFrame()
-
- 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", ->
- editor.setCursorBufferPosition([1, 10])
- component.setLineHeight(2)
- nextAnimationFrame()
- 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", ->
- editor.setCursorBufferPosition([1, 10])
- component.setFontSize(10)
- nextAnimationFrame()
- 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", ->
- editor.setCursorBufferPosition([1, 10])
- component.setFontFamily('sans-serif')
- nextAnimationFrame()
- cursorNode = componentNode.querySelector('.cursor')
-
- {left} = wrapperNode.pixelPositionForScreenPosition([1, 10])
- expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(left)}px, #{editor.getLineHeightInPixels()}px)"
-
- describe "selection rendering", ->
- [scrollViewNode, scrollViewClientLeft] = []
-
- beforeEach ->
- scrollViewNode = componentNode.querySelector('.scroll-view')
- scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left
-
- it "renders 1 region for 1-line selections", ->
- # 1-line selection
- editor.setSelectedScreenRange([[1, 6], [1, 10]])
- nextAnimationFrame()
- regions = componentNode.querySelectorAll('.selection .region')
-
- expect(regions.length).toBe 1
- 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", ->
- editor.setSelectedScreenRange([[1, 6], [2, 10]])
- nextAnimationFrame()
- tileNode = component.tileNodesForLines()[0]
- regions = tileNode.querySelectorAll('.selection .region')
- expect(regions.length).toBe 2
-
- 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
-
- 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", ->
- editor.setSelectedScreenRange([[0, 6], [5, 10]])
- nextAnimationFrame()
-
- # Tile 0
- 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
-
- # Tile 3
- 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", ->
- editor.addSelectionForBufferRange([[2, 2], [2, 2]])
- nextAnimationFrame()
- 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", ->
- editor.setSelectedBufferRange([[1, 6], [1, 10]])
- component.setLineHeight(2)
- nextAnimationFrame()
- selectionNode = componentNode.querySelector('.region')
- expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels()
-
- it "updates selections when the font size changes", ->
- editor.setSelectedBufferRange([[1, 6], [1, 10]])
- component.setFontSize(10)
- nextAnimationFrame()
- 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", ->
- editor.setSelectedBufferRange([[1, 6], [1, 10]])
- component.setFontFamily('sans-serif')
- nextAnimationFrame()
- 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", ->
- editor.setSelectedBufferRange([[1, 6], [1, 10]], flash: true)
- nextAnimationFrame()
- selectionNode = componentNode.querySelector('.selection')
- expect(selectionNode.classList.contains('flash')).toBe true
-
- advanceClock editor.selectionFlashDuration
- expect(selectionNode.classList.contains('flash')).toBe false
-
- editor.setSelectedBufferRange([[1, 5], [1, 7]], flash: true)
- nextAnimationFrame()
- expect(selectionNode.classList.contains('flash')).toBe true
-
- describe "line decoration rendering", ->
- [marker, decoration, decorationParams] = []
-
- beforeEach ->
- marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true)
- decorationParams = {type: ['line-number', 'line'], class: 'a'}
- decoration = editor.decorateMarker(marker, decorationParams)
- nextAnimationFrame()
-
- it "applies line decoration classes to lines and line numbers", ->
- expect(lineAndLineNumberHaveClass(2, 'a')).toBe true
- expect(lineAndLineNumberHaveClass(3, 'a')).toBe true
-
- # Shrink editor vertically
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- # Add decorations that are out of range
- marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]])
- editor.decorateMarker(marker2, type: ['line-number', 'line'], class: 'b')
- nextAnimationFrame()
-
- # Scroll decorations into view
- verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels
- verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
- nextAnimationFrame()
- expect(lineAndLineNumberHaveClass(9, 'b')).toBe true
-
- # Fold a line to move the decorations
- editor.foldBufferRow(5)
- nextAnimationFrame()
- 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", ->
- editor.setText("a line that wraps, ok")
- editor.setSoftWrapped(true)
- componentNode.style.width = 16 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- marker.destroy()
- marker = editor.markBufferRange([[0, 0], [0, 2]])
- editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'b')
- nextAnimationFrame()
- expect(lineNumberHasClass(0, 'b')).toBe true
- expect(lineNumberHasClass(1, 'b')).toBe false
-
- marker.setBufferRange([[0, 0], [0, Infinity]])
- nextAnimationFrame()
- expect(lineNumberHasClass(0, 'b')).toBe true
- expect(lineNumberHasClass(1, 'b')).toBe true
-
- it "updates decorations when markers move", ->
- 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')
- nextAnimationFrame()
- 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]])
- nextAnimationFrame()
- 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", ->
- decoration.destroy()
- nextAnimationFrame()
- 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", ->
- editor.getBuffer().insert([3, 2], 'n')
- nextAnimationFrame()
- 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()
- nextAnimationFrame()
- 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", ->
- marker.destroy()
- nextAnimationFrame()
- 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", ->
- it "only applies the decoration's class to lines containing the marker's head", ->
- editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-head', onlyHead: true)
- nextAnimationFrame()
- 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", ->
- it "only applies the decoration when its marker is empty", ->
- editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-empty', onlyEmpty: true)
- nextAnimationFrame()
- expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false
- expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe false
-
- marker.clearTail()
- nextAnimationFrame()
- expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false
- expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe true
-
- describe "when the decoration's 'onlyNonEmpty' property is true", ->
- it "only applies the decoration when its marker is non-empty", ->
- editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-non-empty', onlyNonEmpty: true)
- nextAnimationFrame()
- expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe true
- expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe true
-
- marker.clearTail()
- nextAnimationFrame()
- expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe false
- expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe false
-
- describe "highlight decoration rendering", ->
- [marker, decoration, decorationParams, scrollViewClientLeft] = []
- beforeEach ->
- scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left
- marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true)
- decorationParams = {type: 'highlight', class: 'test-highlight'}
- decoration = editor.decorateMarker(marker, decorationParams)
- nextAnimationFrame()
-
- it "does not render highlights for off-screen lines until they come on-screen", ->
- wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside')
- editor.decorateMarker(marker, type: 'highlight', class: 'some-highlight')
- nextAnimationFrame()
-
- # Should not be rendering range containing the marker
- expect(component.presenter.endRow).toBeLessThan 9
-
- regions = componentNode.querySelectorAll('.some-highlight .region')
-
- # Nothing when outside the rendered row range
- expect(regions.length).toBe 0
-
- verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels
- verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
- nextAnimationFrame()
- expect(component.presenter.endRow).toBeGreaterThan(8)
-
- regions = componentNode.querySelectorAll('.some-highlight .region')
-
- expect(regions.length).toBe 1
- 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", ->
- regions = componentNode.querySelectorAll('.test-highlight .region')
- expect(regions.length).toBe 2
-
- it "removes highlights when a decoration is removed", ->
- decoration.destroy()
- nextAnimationFrame()
- regions = componentNode.querySelectorAll('.test-highlight .region')
- expect(regions.length).toBe 0
-
- it "does not render a highlight that is within a fold", ->
- editor.foldBufferRow(1)
- nextAnimationFrame()
- expect(componentNode.querySelectorAll('.test-highlight').length).toBe 0
-
- it "removes highlights when a decoration's marker is destroyed", ->
- marker.destroy()
- nextAnimationFrame()
- regions = componentNode.querySelectorAll('.test-highlight .region')
- expect(regions.length).toBe 0
-
- it "only renders highlights when a decoration's marker is valid", ->
- editor.getBuffer().insert([3, 2], 'n')
- nextAnimationFrame()
-
- expect(marker.isValid()).toBe false
- regions = componentNode.querySelectorAll('.test-highlight .region')
- expect(regions.length).toBe 0
-
- editor.getBuffer().undo()
- nextAnimationFrame()
-
- expect(marker.isValid()).toBe true
- regions = componentNode.querySelectorAll('.test-highlight .region')
- expect(regions.length).toBe 2
-
- it "allows multiple space-delimited decoration classes", ->
- decoration.setProperties(type: 'highlight', class: 'foo bar')
- nextAnimationFrame()
- expect(componentNode.querySelectorAll('.foo.bar').length).toBe 2
- decoration.setProperties(type: 'highlight', class: 'bar baz')
- nextAnimationFrame()
- expect(componentNode.querySelectorAll('.bar.baz').length).toBe 2
-
- it "renders classes on the regions directly if 'deprecatedRegionClass' option is defined", ->
- decoration = editor.decorateMarker(marker, type: 'highlight', class: 'test-highlight', deprecatedRegionClass: 'test-highlight-region')
- nextAnimationFrame()
-
- regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region')
- expect(regions.length).toBe 2
-
- describe "when flashing a decoration via Decoration::flash()", ->
- highlightNode = null
- beforeEach ->
- highlightNode = componentNode.querySelectorAll('.test-highlight')[1]
-
- it "adds and removes the flash class specified in ::flash", ->
- expect(highlightNode.classList.contains('flash-class')).toBe false
-
- decoration.flash('flash-class', 10)
- nextAnimationFrame()
- 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", ->
- it "removes the class from the decoration highlight before adding it for the second ::flash call", ->
- decoration.flash('flash-class', 10)
- nextAnimationFrame()
- expect(highlightNode.classList.contains('flash-class')).toBe true
- advanceClock(2)
-
- decoration.flash('flash-class', 10)
- nextAnimationFrame()
-
- # Removed for 1 frame to force CSS transition to restart
- expect(highlightNode.classList.contains('flash-class')).toBe false
-
- nextAnimationFrame()
- expect(highlightNode.classList.contains('flash-class')).toBe true
-
- advanceClock(10)
- expect(highlightNode.classList.contains('flash-class')).toBe false
-
- describe "when a decoration's marker moves", ->
- it "moves rendered highlights when the buffer is changed", ->
- regionStyle = componentNode.querySelector('.test-highlight .region').style
- originalTop = parseInt(regionStyle.top)
-
- expect(originalTop).toBe(2 * lineHeightInPixels)
-
- editor.getBuffer().insert([0, 0], '\n')
- nextAnimationFrame()
-
- regionStyle = componentNode.querySelector('.test-highlight .region').style
- newTop = parseInt(regionStyle.top)
-
- expect(newTop).toBe(0)
-
- it "moves rendered highlights when the marker is manually moved", ->
- regionStyle = componentNode.querySelector('.test-highlight .region').style
- expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels
-
- marker.setBufferRange([[5, 8], [5, 13]])
- nextAnimationFrame()
-
- regionStyle = componentNode.querySelector('.test-highlight .region').style
- expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels
-
- describe "when a decoration is updated via Decoration::update", ->
- it "renders the decoration's new params", ->
- expect(componentNode.querySelector('.test-highlight')).toBeTruthy()
-
- decoration.setProperties(type: 'highlight', class: 'new-test-highlight')
- nextAnimationFrame()
-
- expect(componentNode.querySelector('.test-highlight')).toBeFalsy()
- expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy()
-
- describe "overlay decoration rendering", ->
- [item, gutterWidth] = []
- beforeEach ->
- item = document.createElement('div')
- item.classList.add 'overlay-test'
- item.style.background = 'red'
- gutterWidth = componentNode.querySelector('.gutter').offsetWidth
-
- describe "when the marker is empty", ->
- it "renders an overlay decoration when added and removes the overlay when the decoration is destroyed", ->
- marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never')
- decoration = editor.decorateMarker(marker, {type: 'overlay', item})
- nextAnimationFrame()
-
- overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test')
- expect(overlay).toBe item
-
- decoration.destroy()
- nextAnimationFrame()
-
- 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", ->
- marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never')
- decoration = editor.decorateMarker(marker, {type: 'overlay', class: 'my-overlay', item})
- nextAnimationFrame()
-
- overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay')
- expect(overlay).not.toBe null
-
- child = overlay.querySelector('.overlay-test')
- expect(child).toBe item
-
- describe "when the marker is not empty", ->
- it "renders at the head of the marker by default", ->
- marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never')
- decoration = editor.decorateMarker(marker, {type: 'overlay', item})
- nextAnimationFrame()
- nextAnimationFrame()
-
- position = wrapperNode.pixelPositionForBufferPosition([2, 10])
-
- 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", ->
- [itemWidth, itemHeight, windowWidth, windowHeight] = []
- beforeEach ->
- 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'
-
- atom.setWindowDimensions({width: windowWidth, height: windowHeight})
-
- component.measureDimensions()
- component.measureWindowSize()
- nextAnimationFrame()
-
- afterEach ->
- atom.restoreWindowDimensions()
-
- # This spec should actually run on Linux as well, see TextEditorComponent#measureWindowSize for further information.
- it "slides horizontally left when near the right edge on #win32 and #darwin", ->
- marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], invalidate: 'never')
- decoration = editor.decorateMarker(marker, {type: 'overlay', item})
- nextAnimationFrame()
- nextAnimationFrame()
-
- position = wrapperNode.pixelPositionForBufferPosition([0, 26])
-
- 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'
-
- editor.insertText('a')
- nextAnimationFrame()
-
- expect(overlay.style.left).toBe windowWidth - itemWidth + 'px'
- expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
-
- editor.insertText('b')
- nextAnimationFrame()
-
- expect(overlay.style.left).toBe windowWidth - itemWidth + 'px'
- expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
-
- describe "hidden input field", ->
- it "renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused", ->
- editor.setVerticalScrollMargin(0)
- editor.setHorizontalScrollMargin(0)
-
- inputNode = componentNode.querySelector('.hidden-input')
- wrapperNode.style.height = 5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(editor.getCursorScreenPosition()).toEqual [0, 0]
- wrapperNode.setScrollTop(3 * lineHeightInPixels)
- wrapperNode.setScrollLeft(3 * charWidth)
- nextAnimationFrame()
-
- expect(inputNode.offsetTop).toBe 0
- expect(inputNode.offsetLeft).toBe 0
-
- # In bounds, not focused
- editor.setCursorBufferPosition([5, 4], autoscroll: false)
- nextAnimationFrame()
- expect(inputNode.offsetTop).toBe 0
- expect(inputNode.offsetLeft).toBe 0
-
- # In bounds and focused
- wrapperNode.focus() # updates via state change
- nextAnimationFrame()
- expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop()
- expect(inputNode.offsetLeft).toBeCloseTo (4 * charWidth) - wrapperNode.getScrollLeft(), 0
-
- # In bounds, not focused
- inputNode.blur() # updates via state change
- nextAnimationFrame()
- expect(inputNode.offsetTop).toBe 0
- expect(inputNode.offsetLeft).toBe 0
-
- # Out of bounds, not focused
- editor.setCursorBufferPosition([1, 2], autoscroll: false)
- nextAnimationFrame()
- expect(inputNode.offsetTop).toBe 0
- expect(inputNode.offsetLeft).toBe 0
-
- # Out of bounds, focused
- inputNode.focus() # updates via state change
- nextAnimationFrame()
- expect(inputNode.offsetTop).toBe 0
- expect(inputNode.offsetLeft).toBe 0
-
- describe "mouse interactions on the lines", ->
- linesNode = null
-
- beforeEach ->
- linesNode = componentNode.querySelector('.lines')
-
- describe "when the mouse is single-clicked above the first line", ->
- it "moves the cursor to the start of file buffer position", ->
- editor.setText('foo')
- editor.setCursorBufferPosition([0, 3])
- height = 4.5 * lineHeightInPixels
- wrapperNode.style.height = height + 'px'
- wrapperNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- coordinates = clientCoordinatesForScreenPosition([0, 2])
- coordinates.clientY = -1
- linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates))
- nextAnimationFrame()
- expect(editor.getCursorScreenPosition()).toEqual [0, 0]
-
- describe "when the mouse is single-clicked below the last line", ->
- it "moves the cursor to the end of file buffer position", ->
- editor.setText('foo')
- editor.setCursorBufferPosition([0, 0])
- height = 4.5 * lineHeightInPixels
- wrapperNode.style.height = height + 'px'
- wrapperNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- coordinates = clientCoordinatesForScreenPosition([0, 2])
- coordinates.clientY = height * 2
- linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates))
- nextAnimationFrame()
- expect(editor.getCursorScreenPosition()).toEqual [0, 3]
-
- describe "when a non-folded line is single-clicked", ->
- describe "when no modifier keys are held down", ->
- it "moves the cursor to the nearest screen position", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- wrapperNode.setScrollTop(3.5 * lineHeightInPixels)
- wrapperNode.setScrollLeft(2 * charWidth)
- nextAnimationFrame()
-
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8])))
- nextAnimationFrame()
- expect(editor.getCursorScreenPosition()).toEqual [4, 8]
-
- describe "when the shift key is held down", ->
- it "selects to the nearest screen position", ->
- editor.setCursorScreenPosition([3, 4])
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), shiftKey: true))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [5, 6]]
-
- describe "when the command key is held down", ->
- describe "the current cursor position and screen position do not match", ->
- it "adds a cursor at the nearest screen position", ->
- editor.setCursorScreenPosition([3, 4])
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), metaKey: true))
- nextAnimationFrame()
- 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", ->
- it "removes a cursor at the mouse screen position", ->
- editor.setCursorScreenPosition([3, 4])
- editor.addCursorAtScreenPosition([5, 2])
- editor.addCursorAtScreenPosition([7, 5])
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true))
- nextAnimationFrame()
- 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", ->
- it "neither adds a new cursor nor removes the current cursor", ->
- editor.setCursorScreenPosition([3, 4])
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]]]
-
- describe "when a non-folded line is double-clicked", ->
- describe "when no modifier keys are held down", ->
- it "selects the word containing the nearest screen position", ->
- 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", ->
- it "selects the word containing the newly-added cursor", ->
- 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", ->
- describe "when no modifier keys are held down", ->
- it "selects the line containing the nearest screen position", ->
- 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", ->
- it "selects the line containing the newly-added cursor", ->
- 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", ->
- it "selects to the nearest screen position until the mouse button is released", ->
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 1))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]]
-
- linesNode.dispatchEvent(buildMouseEvent('mouseup'))
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), which: 1))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]]
-
- it "autoscrolls when the cursor approaches the boundaries of the editor", ->
- wrapperNode.style.height = '100px'
- wrapperNode.style.width = '100px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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))
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollTop()).toBe(0)
- expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0)
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 100}, which: 1))
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBeGreaterThan(0)
-
- previousScrollTop = wrapperNode.getScrollTop()
- previousScrollLeft = wrapperNode.getScrollLeft()
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 50}, which: 1))
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollTop()).toBe(previousScrollTop)
- expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft)
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 10}, which: 1))
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop)
-
- it "stops selecting if the mouse is dragged into the dev tools", ->
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 0))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1))
- expect(nextAnimationFrame).toBe noAnimationFrame
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
-
- it "stops selecting before the buffer is modified during the drag", ->
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
-
- editor.insertText('x')
- nextAnimationFrame()
-
- expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]]
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1))
- expect(nextAnimationFrame).toBe noAnimationFrame
- 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))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [5, 4]]
-
- editor.delete()
- nextAnimationFrame()
-
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]]
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1))
- expect(nextAnimationFrame).toBe noAnimationFrame
- expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]]
-
- describe "when the command key is held down", ->
- it "adds a new selection and selects to the nearest screen position, then merges intersecting selections when the mouse button is released", ->
- 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))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [6, 8]]]
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), which: 1))
- nextAnimationFrame()
- 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", ->
- it "cleans up the handlers for window.mouseup and window.mousemove", ->
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
- nextAnimationFrame()
-
- spyOn(window, 'removeEventListener').andCallThrough()
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), which: 1))
- editor.destroy()
- nextAnimationFrame()
-
- call.args.pop() for call in window.removeEventListener.calls
- expect(window.removeEventListener).toHaveBeenCalledWith('mouseup')
- expect(window.removeEventListener).toHaveBeenCalledWith('mousemove')
-
- describe "when the mouse is double-clicked and dragged", ->
- it "expands the selection over the nearest word as the cursor moves", ->
- jasmine.attachToDOM(wrapperNode)
- wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]]
-
- maximalScrollTop = wrapperNode.getScrollTop()
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), which: 1))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [9, 4]]
- expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression)
-
- linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1))
-
- describe "when the mouse is triple-clicked and dragged", ->
- it "expands the selection over the nearest line as the cursor moves", ->
- jasmine.attachToDOM(wrapperNode)
- wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]]
-
- maximalScrollTop = wrapperNode.getScrollTop()
-
- linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), which: 1))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [8, 0]]
- expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression)
-
- linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1))
-
- describe "when a line is folded", ->
- beforeEach ->
- editor.foldBufferRow 4
- nextAnimationFrame()
-
- describe "when the folded line's fold-marker is clicked", ->
- it "unfolds the buffer row", ->
- target = component.lineNodeForScreenRow(4).querySelector '.fold-marker'
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {target}))
- expect(editor.isFoldedAtBufferRow 4).toBe false
-
- describe "when the horizontal scrollbar is interacted with", ->
- it "clicking on the scrollbar does not move the cursor", ->
- target = horizontalScrollbarNode
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {target}))
- expect(editor.getCursorScreenPosition()).toEqual [0, 0]
-
- describe "mouse interactions on the gutter", ->
- gutterNode = null
-
- beforeEach ->
- gutterNode = componentNode.querySelector('.gutter')
-
- describe "when the component is destroyed", ->
- it "stops listening for selection events", ->
- component.destroy()
-
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1)))
-
- expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [0, 0]]
-
- describe "when the gutter is clicked", ->
- it "selects the clicked row", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4)))
- expect(editor.getSelectedScreenRange()).toEqual [[4, 0], [5, 0]]
-
- describe "when the gutter is meta-clicked", ->
- it "creates a new selection for the clicked row", ->
- 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", ->
- beforeEach ->
- editor.setSelectedScreenRange([[3, 4], [4, 5]])
-
- describe "when the clicked row is before the current selection's tail", ->
- it "selects to the beginning of the clicked row", ->
- 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", ->
- it "selects to the beginning of the row following the clicked row", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), shiftKey: true))
- expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [7, 0]]
-
- describe "when the gutter is clicked and dragged", ->
- describe "when dragging downward", ->
- it "selects the rows between the start and end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
- nextAnimationFrame()
- gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6)))
- expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]]
-
- describe "when dragging upward", ->
- it "selects the rows between the start and end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6)))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2)))
- nextAnimationFrame()
- 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", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4)))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2)))
- nextAnimationFrame()
- expect(editor.getLastSelection().isReversed()).toBe true
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
- nextAnimationFrame()
- expect(editor.getLastSelection().isReversed()).toBe false
-
- it "autoscrolls when the cursor approaches the top or bottom of the editor", ->
- wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollTop()).toBe 0
-
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollTop()).toBeGreaterThan 0
- maxScrollTop = wrapperNode.getScrollTop()
-
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10)))
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe maxScrollTop
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7)))
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBeLessThan maxScrollTop
-
- it "stops selecting if a textInput event occurs during the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]]
-
- inputEvent = new Event('textInput')
- inputEvent.data = 'x'
- Object.defineProperty(inputEvent, 'target', get: -> componentNode.querySelector('.hidden-input'))
- componentNode.dispatchEvent(inputEvent)
- nextAnimationFrame()
-
- expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]]
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12)))
- expect(nextAnimationFrame).toBe noAnimationFrame
- expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]]
-
- describe "when the gutter is meta-clicked and dragged", ->
- beforeEach ->
- editor.setSelectedScreenRange([[3, 0], [3, 2]])
-
- describe "when dragging downward", ->
- it "selects the rows between the start and end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true))
- nextAnimationFrame()
- 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", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true))
- nextAnimationFrame()
- 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", ->
- it "selects the rows between the start and end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), metaKey: true))
- nextAnimationFrame()
- gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), metaKey: true))
- expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]]
-
- it "merges overlapping selections", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), metaKey: true))
- nextAnimationFrame()
- 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", ->
- describe "when the shift-click is below the existing selection's tail", ->
- describe "when dragging downward", ->
- it "selects the rows between the existing selection's tail and the end of the drag", ->
- editor.setSelectedScreenRange([[3, 4], [4, 5]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]]
-
- describe "when dragging upward", ->
- it "selects the rows between the end of the drag and the tail of the existing selection", ->
- editor.setSelectedScreenRange([[4, 4], [5, 5]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[4, 4], [6, 0]]
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]]
-
- describe "when the shift-click is above the existing selection's tail", ->
- describe "when dragging upward", ->
- it "selects the rows between the end of the drag and the tail of the existing selection", ->
- editor.setSelectedScreenRange([[4, 4], [5, 5]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]]
-
- describe "when dragging downward", ->
- it "selects the rows between the existing selection's tail and the end of the drag", ->
- editor.setSelectedScreenRange([[3, 4], [4, 5]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [3, 4]]
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]]
-
- describe "when soft wrap is enabled", ->
- beforeEach ->
- gutterNode = componentNode.querySelector('.gutter')
-
- editor.setSoftWrapped(true)
- nextAnimationFrame()
- componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- describe "when the gutter is clicked", ->
- it "selects the clicked buffer row", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1)))
- expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [2, 0]]
-
- describe "when the gutter is meta-clicked", ->
- it "creates a new selection for the clicked buffer row", ->
- 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", ->
- beforeEach ->
- editor.setSelectedScreenRange([[7, 4], [7, 6]])
-
- describe "when the clicked row is before the current selection's tail", ->
- it "selects to the beginning of the clicked buffer row", ->
- 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", ->
- it "selects to the beginning of the screen row following the clicked buffer row", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), shiftKey: true))
- expect(editor.getSelectedScreenRange()).toEqual [[7, 4], [16, 0]]
-
- describe "when the gutter is clicked and dragged", ->
- describe "when dragging downward", ->
- it "selects the buffer row containing the click, then screen rows until the end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1)))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
- nextAnimationFrame()
- gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6)))
- expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [6, 14]]
-
- describe "when dragging upward", ->
- it "selects the buffer row containing the click, then screen rows until the end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6)))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
- nextAnimationFrame()
- gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1)))
- expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [10, 0]]
-
- describe "when the gutter is meta-clicked and dragged", ->
- beforeEach ->
- editor.setSelectedScreenRange([[7, 4], [7, 6]])
-
- describe "when dragging downward", ->
- it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), metaKey: true))
- nextAnimationFrame()
- 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", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), metaKey: true))
- nextAnimationFrame()
- gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), metaKey: true))
- expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [7, 12]]]
-
- describe "when dragging upward", ->
- it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), metaKey: true))
- nextAnimationFrame()
- gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), metaKey: true))
- expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[11, 4], [19, 0]]]
-
- it "merges overlapping selections on mouseup", ->
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true))
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), metaKey: true))
- nextAnimationFrame()
- gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), metaKey: true))
- expect(editor.getSelectedScreenRanges()).toEqual [[[5, 0], [19, 0]]]
-
- describe "when the gutter is shift-clicked and dragged", ->
- describe "when the shift-click is below the existing selection's tail", ->
- describe "when dragging downward", ->
- it "selects the screen rows between the existing selection's tail and the end of the drag", ->
- editor.setSelectedScreenRange([[1, 4], [1, 7]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [11, 14]]
-
- describe "when dragging upward", ->
- it "selects the screen rows between the end of the drag and the tail of the existing selection", ->
- editor.setSelectedScreenRange([[1, 4], [1, 7]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [7, 12]]
-
- describe "when the shift-click is above the existing selection's tail", ->
- describe "when dragging upward", ->
- it "selects the screen rows between the end of the drag and the tail of the existing selection", ->
- editor.setSelectedScreenRange([[7, 4], [7, 6]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [7, 4]]
-
- describe "when dragging downward", ->
- it "selects the screen rows between the existing selection's tail and the end of the drag", ->
- editor.setSelectedScreenRange([[7, 4], [7, 6]])
- gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true))
-
- gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3)))
- nextAnimationFrame()
- expect(editor.getSelectedScreenRange()).toEqual [[3, 2], [7, 4]]
-
- describe "focus handling", ->
- inputNode = null
-
- beforeEach ->
- inputNode = componentNode.querySelector('.hidden-input')
-
- it "transfers focus to the hidden input", ->
- expect(document.activeElement).toBe document.body
- wrapperNode.focus()
- expect(document.activeElement).toBe wrapperNode
- expect(wrapperNode.shadowRoot.activeElement).toBe inputNode
-
- it "adds the 'is-focused' class to the editor when the hidden input is focused", ->
- expect(document.activeElement).toBe document.body
- inputNode.focus()
- nextAnimationFrame()
- expect(componentNode.classList.contains('is-focused')).toBe true
- expect(wrapperNode.classList.contains('is-focused')).toBe true
- inputNode.blur()
- nextAnimationFrame()
- expect(componentNode.classList.contains('is-focused')).toBe false
- expect(wrapperNode.classList.contains('is-focused')).toBe false
-
- describe "selection handling", ->
- cursor = null
-
- beforeEach ->
- cursor = editor.getLastCursor()
- cursor.setScreenPosition([0, 0])
-
- it "adds the 'has-selection' class to the editor when there is a selection", ->
- expect(componentNode.classList.contains('has-selection')).toBe false
-
- editor.selectDown()
- nextAnimationFrame()
- expect(componentNode.classList.contains('has-selection')).toBe true
-
- cursor.moveDown()
- nextAnimationFrame()
- expect(componentNode.classList.contains('has-selection')).toBe false
-
- describe "scrolling", ->
- it "updates the vertical scrollbar when the scrollTop is changed in the model", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(verticalScrollbarNode.scrollTop).toBe 0
-
- wrapperNode.setScrollTop(10)
- nextAnimationFrame()
- expect(verticalScrollbarNode.scrollTop).toBe 10
-
- it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", ->
- componentNode.style.width = 30 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- tilesNodes = component.tileNodesForLines()
-
- top = 0
- for tileNode in tilesNodes
- expect(tileNode.style['-webkit-transform']).toBe "translate3d(0px, #{top}px, 0px)"
- top += tileNode.offsetHeight
-
- expect(horizontalScrollbarNode.scrollLeft).toBe 0
-
- wrapperNode.setScrollLeft(100)
- nextAnimationFrame()
-
- top = 0
- for tileNode in 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", ->
- componentNode.style.width = 30 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollLeft()).toBe 0
- horizontalScrollbarNode.scrollLeft = 100
- horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollLeft()).toBe 100
-
- it "does not obscure the last line with the horizontal scrollbar", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- wrapperNode.setScrollBottom(wrapperNode.getScrollHeight())
- nextAnimationFrame()
- lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow())
- bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom
- topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top
- expect(bottomOfLastLine).toBe topOfHorizontalScrollbar
-
- # Scroll so there's no space below the last line when the horizontal scrollbar disappears
- wrapperNode.style.width = 100 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
- bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom
- bottomOfEditor = componentNode.getBoundingClientRect().bottom
- expect(bottomOfLastLine).toBe bottomOfEditor
-
- it "does not obscure the last character of the longest line with the vertical scrollbar", ->
- wrapperNode.style.height = 7 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- wrapperNode.setScrollLeft(Infinity)
- nextAnimationFrame()
-
- rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right
- leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left
- expect(Math.round(rightOfLongestLine)).toBeCloseTo leftOfVerticalScrollbar - 1, 0 # Leave 1 px so the cursor is visible on the end of the line
-
- it "only displays dummy scrollbars when scrollable in that direction", ->
- expect(verticalScrollbarNode.style.display).toBe 'none'
- expect(horizontalScrollbarNode.style.display).toBe 'none'
-
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = '1000px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(verticalScrollbarNode.style.display).toBe ''
- expect(horizontalScrollbarNode.style.display).toBe 'none'
-
- componentNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(verticalScrollbarNode.style.display).toBe ''
- expect(horizontalScrollbarNode.style.display).toBe ''
-
- wrapperNode.style.height = 20 * lineHeightInPixels + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- 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", ->
- wrapperNode.style.height = 4 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- atom.styles.addStyleSheet """
- ::-webkit-scrollbar {
- width: 8px;
- height: 8px;
- }
- """, context: 'atom-text-editor'
-
- nextAnimationFrame() # handle stylesheet change event
- nextAnimationFrame() # perform requested update
-
- 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", ->
- 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'
- component.measureDimensions()
- nextAnimationFrame()
- 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()
- nextAnimationFrame()
- 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'
- component.measureDimensions()
- nextAnimationFrame()
- 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", ->
- gutterNode = componentNode.querySelector('.gutter')
- componentNode.style.width = 10 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- expect(horizontalScrollbarNode.scrollWidth).toBe wrapperNode.getScrollWidth()
- expect(horizontalScrollbarNode.style.left).toBe '0px'
-
- describe "mousewheel events", ->
- beforeEach ->
- atom.config.set('editor.scrollSensitivity', 100)
-
- describe "updating scrollTop and scrollLeft", ->
- beforeEach ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 20 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- it "updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)", ->
- expect(verticalScrollbarNode.scrollTop).toBe 0
- expect(horizontalScrollbarNode.scrollLeft).toBe 0
-
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10))
- nextAnimationFrame()
- expect(verticalScrollbarNode.scrollTop).toBe 10
- expect(horizontalScrollbarNode.scrollLeft).toBe 0
-
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5))
- nextAnimationFrame()
- expect(verticalScrollbarNode.scrollTop).toBe 10
- expect(horizontalScrollbarNode.scrollLeft).toBe 15
-
- it "updates the scrollLeft or scrollTop according to the scroll sensitivity", ->
- atom.config.set('editor.scrollSensitivity', 50)
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10))
- nextAnimationFrame()
- expect(horizontalScrollbarNode.scrollLeft).toBe 0
-
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5))
- nextAnimationFrame()
- expect(verticalScrollbarNode.scrollTop).toBe 5
- expect(horizontalScrollbarNode.scrollLeft).toBe 7
-
- it "uses the previous scrollSensitivity when the value is not an int", ->
- atom.config.set('editor.scrollSensitivity', 'nope')
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10))
- nextAnimationFrame()
- expect(verticalScrollbarNode.scrollTop).toBe 10
-
- it "parses negative scrollSensitivity values at the minimum", ->
- atom.config.set('editor.scrollSensitivity', -50)
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10))
- nextAnimationFrame()
- expect(verticalScrollbarNode.scrollTop).toBe 1
-
- describe "when the mousewheel event's target is a line", ->
- it "keeps the line on the DOM if it is scrolled off-screen", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 20 * charWidth + 'px'
- component.measureDimensions()
-
- lineNode = componentNode.querySelector('.line')
- wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500)
- Object.defineProperty(wheelEvent, 'target', get: -> lineNode)
- componentNode.dispatchEvent(wheelEvent)
- nextAnimationFrame()
-
- expect(componentNode.contains(lineNode)).toBe true
-
- it "does not set the mouseWheelScreenRow if scrolling horizontally", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 20 * charWidth + 'px'
- component.measureDimensions()
-
- lineNode = componentNode.querySelector('.line')
- wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0)
- Object.defineProperty(wheelEvent, 'target', get: -> lineNode)
- componentNode.dispatchEvent(wheelEvent)
- nextAnimationFrame()
-
- expect(component.presenter.mouseWheelScreenRow).toBe null
-
- it "clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling", ->
- expect(wrapperNode.getScrollTop()).toBe 0
-
- lineNode = componentNode.querySelector('.line')
- wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 10)
- Object.defineProperty(wheelEvent, 'target', get: -> lineNode)
- componentNode.dispatchEvent(wheelEvent)
-
- expect(wrapperNode.getScrollTop()).toBe 0
-
- expect(component.presenter.mouseWheelScreenRow).toBe 0
- advanceClock(component.presenter.stoppedScrollingDelay)
- expect(component.presenter.mouseWheelScreenRow).toBe null
-
- it "does not preserve the line if it is on screen", ->
- expect(componentNode.querySelectorAll('.line-number').length).toBe 14 # dummy line
- lineNodes = componentNode.querySelectorAll('.line')
- expect(lineNodes.length).toBe 13
- lineNode = lineNodes[0]
-
- wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 100) # goes nowhere, we're already at scrollTop 0
- Object.defineProperty(wheelEvent, 'target', get: -> lineNode)
- componentNode.dispatchEvent(wheelEvent)
-
- expect(component.presenter.mouseWheelScreenRow).toBe 0
- editor.insertText("hello")
- expect(componentNode.querySelectorAll('.line-number').length).toBe 14 # dummy line
- expect(componentNode.querySelectorAll('.line').length).toBe 13
-
- describe "when the mousewheel event's target is a line number", ->
- it "keeps the line number on the DOM if it is scrolled off-screen", ->
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 20 * charWidth + 'px'
- component.measureDimensions()
-
- lineNumberNode = componentNode.querySelectorAll('.line-number')[1]
- wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500)
- Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode)
- componentNode.dispatchEvent(wheelEvent)
- nextAnimationFrame()
-
- expect(componentNode.contains(lineNumberNode)).toBe true
-
- it "only prevents the default action of the mousewheel event if it actually lead to scrolling", ->
- spyOn(WheelEvent::, 'preventDefault').andCallThrough()
-
- wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
- wrapperNode.style.width = 20 * charWidth + 'px'
- component.measureDimensions()
- nextAnimationFrame()
-
- # try to scroll past the top, which is impossible
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 50))
- expect(wrapperNode.getScrollTop()).toBe 0
- expect(WheelEvent::preventDefault).not.toHaveBeenCalled()
-
- # scroll to the bottom in one huge event
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000))
- nextAnimationFrame()
- maxScrollTop = wrapperNode.getScrollTop()
- expect(WheelEvent::preventDefault).toHaveBeenCalled()
- WheelEvent::preventDefault.reset()
-
- # try to scroll past the bottom, which is impossible
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -30))
- expect(wrapperNode.getScrollTop()).toBe maxScrollTop
- expect(WheelEvent::preventDefault).not.toHaveBeenCalled()
-
- # try to scroll past the left side, which is impossible
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 50, wheelDeltaY: 0))
- expect(wrapperNode.getScrollLeft()).toBe 0
- expect(WheelEvent::preventDefault).not.toHaveBeenCalled()
-
- # scroll all the way right
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0))
- nextAnimationFrame()
- maxScrollLeft = wrapperNode.getScrollLeft()
- expect(WheelEvent::preventDefault).toHaveBeenCalled()
- WheelEvent::preventDefault.reset()
-
- # try to scroll past the right side, which is impossible
- componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -30, wheelDeltaY: 0))
- expect(wrapperNode.getScrollLeft()).toBe maxScrollLeft
- expect(WheelEvent::preventDefault).not.toHaveBeenCalled()
-
- describe "input events", ->
- inputNode = null
-
- beforeEach ->
- inputNode = componentNode.querySelector('.hidden-input')
-
- buildTextInputEvent = ({data, target}) ->
- event = new Event('textInput')
- event.data = data
- Object.defineProperty(event, 'target', get: -> target)
- event
-
- it "inserts the newest character in the input's value into the buffer", ->
- componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode))
- nextAnimationFrame()
- expect(editor.lineTextForBufferRow(0)).toBe 'xvar quicksort = function () {'
-
- componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode))
- nextAnimationFrame()
- expect(editor.lineTextForBufferRow(0)).toBe 'xyvar quicksort = function () {'
-
- it "replaces the last character if the length of the input's value doesn't increase, as occurs with the accented character menu", ->
- componentNode.dispatchEvent(buildTextInputEvent(data: 'u', target: inputNode))
- nextAnimationFrame()
- expect(editor.lineTextForBufferRow(0)).toBe 'uvar quicksort = function () {'
-
- # simulate the accented character suggestion's selection of the previous character
- inputNode.setSelectionRange(0, 1)
- componentNode.dispatchEvent(buildTextInputEvent(data: 'ü', target: inputNode))
- nextAnimationFrame()
- expect(editor.lineTextForBufferRow(0)).toBe 'üvar quicksort = function () {'
-
- it "does not handle input events when input is disabled", ->
- nextAnimationFrame = noAnimationFrame # This spec is flaky on the build machine, so this.
- component.setInputEnabled(false)
- componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode))
- expect(nextAnimationFrame).toBe noAnimationFrame
- expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {'
-
- it "groups events that occur close together in time into single undo entries", ->
- currentTime = 0
- spyOn(Date, 'now').andCallFake -> currentTime
-
- atom.config.set('editor.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", ->
- inputNode = null
-
- buildIMECompositionEvent = (event, {data, target}={}) ->
- event = new Event(event)
- event.data = data
- Object.defineProperty(event, 'target', get: -> target)
- event
-
- beforeEach ->
- inputNode = inputNode = componentNode.querySelector('.hidden-input')
-
- describe "when nothing is selected", ->
- it "inserts the chosen completion", ->
- 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", ->
- 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", ->
- 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", ->
- beforeEach ->
- editor.setSelectedBufferRanges [[[0, 4], [0, 9]], [[0, 16], [0, 19]]] # select 'quick' and 'fun'
-
- it "inserts the chosen completion", ->
- 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", ->
- 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", ->
- describe "editor:consolidate-selections", ->
- it "consolidates selections on the editor model, aborting the key binding if there is only one selection", ->
- spyOn(editor, 'consolidateSelections').andCallThrough()
-
- 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 changing the font", ->
- it "measures the default char, the korean char, the double width char and the half width char widths", ->
- expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0)
-
- component.setFontSize(10)
- nextAnimationFrame()
-
- expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0)
- expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0)
- expect(editor.getDoubleWidthCharWidth()).toBe(10)
- expect(editor.getHalfWidthCharWidth()).toBe(5)
-
- describe "hiding and showing the editor", ->
- describe "when the editor is hidden when it is mounted", ->
- it "defers measurement and rendering until the editor becomes visible", ->
- wrapperNode.remove()
-
- hiddenParent = document.createElement('div')
- hiddenParent.style.display = 'none'
- contentNode.appendChild(hiddenParent)
-
- wrapperNode = new TextEditorElement()
- wrapperNode.tileSize = tileSize
- wrapperNode.initialize(editor, atom)
- hiddenParent.appendChild(wrapperNode)
-
- {component} = wrapperNode
- 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", ->
- it "does not attempt to measure the lineHeightInPixels until the editor becomes visible again", ->
- initialLineHeightInPixels = null
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- 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", ->
- it "does not attempt to measure the lineHeightInPixels or defaultCharWidth until the editor becomes visible again", ->
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- initialLineHeightInPixels = editor.getLineHeightInPixels()
- 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", ->
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- component.setFontSize(22)
- editor.getBuffer().insert([0, 0], 'a') # regression test against atom/atom#3318
-
- wrapperNode.style.display = ''
- component.checkForVisibilityChange()
-
- editor.setCursorBufferPosition([0, Infinity])
- nextAnimationFrame()
-
- cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left
- line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right
- expect(cursorLeft).toBeCloseTo line0Right, 0
-
- describe "when the fontFamily changes while the editor is hidden", ->
- it "does not attempt to measure the defaultCharWidth until the editor becomes visible again", ->
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- initialLineHeightInPixels = editor.getLineHeightInPixels()
- 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", ->
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- component.setFontFamily('serif')
-
- wrapperNode.style.display = ''
- component.checkForVisibilityChange()
-
- editor.setCursorBufferPosition([0, Infinity])
- nextAnimationFrame()
-
- cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left
- line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right
- expect(cursorLeft).toBeCloseTo line0Right, 0
-
- describe "when stylesheets change while the editor is hidden", ->
- afterEach ->
- atom.themes.removeStylesheet('test')
-
- it "does not re-measure character widths until the editor is shown again", ->
- atom.config.set('editor.fontFamily', 'sans-serif')
-
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- atom.themes.applyStylesheet 'test', """
- .function.js {
- font-weight: bold;
- }
- """
-
- wrapperNode.style.display = ''
- component.checkForVisibilityChange()
-
- editor.setCursorBufferPosition([0, Infinity])
- nextAnimationFrame()
-
- cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left
- line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right
- expect(cursorLeft).toBeCloseTo line0Right, 0
-
- describe "when lines are changed while the editor is hidden", ->
- xit "does not measure new characters until the editor is shown again", ->
- # TODO: This spec fails. Check if we need to keep it or not.
-
- editor.setText('')
-
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- editor.setText('var z = 1')
- editor.setCursorBufferPosition([0, Infinity])
- nextAnimationFrame()
-
- wrapperNode.style.display = 'none'
- component.checkForVisibilityChange()
-
- expect(componentNode.querySelector('.cursor').style['-webkit-transform']).toBe "translate(#{9 * charWidth}px, 0px)"
-
- describe "soft wrapping", ->
- beforeEach ->
- editor.setSoftWrapped(true)
- nextAnimationFrame()
-
- it "updates the wrap location when the editor is resized", ->
- newHeight = 4 * editor.getLineHeightInPixels() + "px"
- expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight
- wrapperNode.style.height = newHeight
-
- atom.views.performDocumentPoll()
- nextAnimationFrame()
- expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + model longest screen row
-
- gutterWidth = componentNode.querySelector('.gutter').offsetWidth
- componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
- atom.views.performDocumentPoll()
- nextAnimationFrame()
- expect(componentNode.querySelector('.line').textContent).toBe "var quicksort "
-
- it "accounts for the scroll view's padding when determining the wrap location", ->
- scrollViewNode = componentNode.querySelector('.scroll-view')
- scrollViewNode.style.paddingLeft = 20 + 'px'
- componentNode.style.width = 30 * charWidth + 'px'
-
- atom.views.performDocumentPoll()
- nextAnimationFrame()
-
- expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = "
-
- describe "default decorations", ->
- it "applies .cursor-line decorations for line numbers overlapping selections", ->
- editor.setCursorScreenPosition([4, 4])
- nextAnimationFrame()
- 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]])
- nextAnimationFrame()
- expect(lineNumberHasClass(3, 'cursor-line')).toBe true
- expect(lineNumberHasClass(4, 'cursor-line')).toBe true
-
- editor.setSelectedScreenRange([[3, 4], [4, 0]])
- nextAnimationFrame()
- 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", ->
- editor.setSelectedScreenRange([[3, 4], [5, 0]])
- nextAnimationFrame()
- 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", ->
- editor.setCursorScreenPosition([4, 4])
- nextAnimationFrame()
- 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]])
- nextAnimationFrame()
- 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", ->
- editor.setCursorScreenPosition([4, 4])
- nextAnimationFrame()
- expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe true
-
- editor.setSelectedScreenRange([[3, 4], [4, 4]])
- nextAnimationFrame()
- expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe false
-
- describe "height", ->
- describe "when the wrapper view has an explicit height", ->
- it "does not assign a height on the component node", ->
- wrapperNode.style.height = '200px'
- component.measureDimensions()
- nextAnimationFrame()
- expect(componentNode.style.height).toBe ''
-
- describe "when the wrapper view does not have an explicit height", ->
- it "assigns a height on the component node based on the editor's content", ->
- expect(wrapperNode.style.height).toBe ''
- expect(componentNode.style.height).toBe editor.getScreenLineCount() * lineHeightInPixels + 'px'
-
- describe "when the 'mini' property is true", ->
- beforeEach ->
- editor.setMini(true)
- nextAnimationFrame()
-
- it "does not render the gutter", ->
- expect(componentNode.querySelector('.gutter')).toBeNull()
-
- it "adds the 'mini' class to the wrapper view", ->
- expect(wrapperNode.classList.contains('mini')).toBe true
-
- it "does not have an opaque background on lines", ->
- expect(component.linesComponent.getDomNode().getAttribute('style')).not.toContain 'background-color'
-
- it "does not render invisible characters", ->
- atom.config.set('editor.invisibles', eol: 'E')
- atom.config.set('editor.showInvisibles', true)
- expect(component.lineNodeForScreenRow(0).textContent).toBe 'var quicksort = function () {'
-
- it "does not assign an explicit line-height on the editor contents", ->
- expect(componentNode.style.lineHeight).toBe ''
-
- it "does not apply cursor-line decorations", ->
- expect(component.lineNodeForScreenRow(0).classList.contains('cursor-line')).toBe false
-
- describe "when placholderText is specified", ->
- it "renders the placeholder text when the buffer is empty", ->
- editor.setPlaceholderText('Hello World')
- expect(componentNode.querySelector('.placeholder-text')).toBeNull()
- editor.setText('')
- nextAnimationFrame()
- expect(componentNode.querySelector('.placeholder-text').textContent).toBe "Hello World"
- editor.setText('hey')
- nextAnimationFrame()
- expect(componentNode.querySelector('.placeholder-text')).toBeNull()
-
- describe "grammar data attributes", ->
- it "adds and updates the grammar data attribute based on the current grammar", ->
- 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", ->
- it "adds and updates the encoding data attribute based on the current encoding", ->
- expect(wrapperNode.dataset.encoding).toBe 'utf8'
- editor.setEncoding('utf16le')
- expect(wrapperNode.dataset.encoding).toBe 'utf16le'
-
- describe "detaching and reattaching the editor (regression)", ->
- it "does not throw an exception", ->
- wrapperNode.remove()
- jasmine.attachToDOM(wrapperNode)
-
- atom.commands.dispatch(wrapperNode, 'core:move-right')
-
- expect(editor.getCursorBufferPosition()).toEqual [0, 1]
-
- describe 'scoped config settings', ->
- [coffeeEditor, coffeeComponent] = []
-
- beforeEach ->
- waitsForPromise ->
- atom.packages.activatePackage('language-coffee-script')
- waitsForPromise ->
- atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o
-
- afterEach: ->
- atom.packages.deactivatePackages()
- atom.packages.unloadPackages()
-
- describe 'soft wrap settings', ->
- beforeEach ->
- atom.config.set 'editor.softWrap', true, scopeSelector: '.source.coffee'
- atom.config.set 'editor.preferredLineLength', 17, scopeSelector: '.source.coffee'
- atom.config.set 'editor.softWrapAtPreferredLineLength', true, scopeSelector: '.source.coffee'
-
- editor.setDefaultCharWidth(1)
- editor.setEditorWidthInChars(20)
- coffeeEditor.setDefaultCharWidth(1)
- coffeeEditor.setEditorWidthInChars(20)
-
- it "wraps lines when editor.softWrap is true for a matching scope", ->
- expect(editor.lineTextForScreenRow(2)).toEqual ' if (items.length <= 1) return items;'
- expect(coffeeEditor.lineTextForScreenRow(3)).toEqual ' return items '
-
- it 'updates the wrapped lines when editor.preferredLineLength changes', ->
- atom.config.set 'editor.preferredLineLength', 20, scopeSelector: '.source.coffee'
- expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if '
-
- it 'updates the wrapped lines when editor.softWrapAtPreferredLineLength changes', ->
- atom.config.set 'editor.softWrapAtPreferredLineLength', false, scopeSelector: '.source.coffee'
- expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if '
-
- it 'updates the wrapped lines when editor.softWrap changes', ->
- atom.config.set 'editor.softWrap', false, scopeSelector: '.source.coffee'
- expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if items.length <= 1'
-
- atom.config.set 'editor.softWrap', true, scopeSelector: '.source.coffee'
- expect(coffeeEditor.lineTextForScreenRow(3)).toEqual ' return items '
-
- it 'updates the wrapped lines when the grammar changes', ->
- editor.setGrammar(coffeeEditor.getGrammar())
- expect(editor.isSoftWrapped()).toBe true
- expect(editor.lineTextForScreenRow(0)).toEqual 'var quicksort = '
-
- describe '::isSoftWrapped()', ->
- it 'returns the correct value based on the scoped settings', ->
- expect(editor.isSoftWrapped()).toBe false
- expect(coffeeEditor.isSoftWrapped()).toBe true
-
- describe 'invisibles settings', ->
- [jsInvisibles, coffeeInvisibles] = []
- beforeEach ->
- jsInvisibles =
- eol: 'J'
- space: 'A'
- tab: 'V'
- cr: 'A'
-
- coffeeInvisibles =
- eol: 'C'
- space: 'O'
- tab: 'F'
- cr: 'E'
-
- atom.config.set 'editor.showInvisibles', true, scopeSelector: '.source.js'
- atom.config.set 'editor.invisibles', jsInvisibles, scopeSelector: '.source.js'
-
- atom.config.set 'editor.showInvisibles', false, scopeSelector: '.source.coffee'
- atom.config.set 'editor.invisibles', coffeeInvisibles, scopeSelector: '.source.coffee'
-
- editor.setText " a line with tabs\tand spaces \n"
- nextAnimationFrame()
-
- it "renders the invisibles when editor.showInvisibles is true for a given grammar", ->
- expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}"
-
- it "does not render the invisibles when editor.showInvisibles is false for a given grammar", ->
- editor.setGrammar(coffeeEditor.getGrammar())
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces "
-
- it "re-renders the invisibles when the invisible settings change", ->
- jsGrammar = editor.getGrammar()
- editor.setGrammar(coffeeEditor.getGrammar())
- atom.config.set 'editor.showInvisibles', true, scopeSelector: '.source.coffee'
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).textContent).toBe "#{coffeeInvisibles.space}a line with tabs#{coffeeInvisibles.tab}and spaces#{coffeeInvisibles.space}#{coffeeInvisibles.eol}"
-
- newInvisibles =
- eol: 'N'
- space: 'E'
- tab: 'W'
- cr: 'I'
- atom.config.set 'editor.invisibles', newInvisibles, scopeSelector: '.source.coffee'
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).textContent).toBe "#{newInvisibles.space}a line with tabs#{newInvisibles.tab}and spaces#{newInvisibles.space}#{newInvisibles.eol}"
-
- editor.setGrammar(jsGrammar)
- nextAnimationFrame()
- expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}"
-
- describe 'editor.showIndentGuide', ->
- beforeEach ->
- atom.config.set 'editor.showIndentGuide', true, scopeSelector: '.source.js'
- atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.coffee'
- nextAnimationFrame()
-
- it "has an 'indent-guide' class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false", ->
- 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
-
- editor.setGrammar(coffeeEditor.getGrammar())
- nextAnimationFrame()
-
- line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1))
- expect(line1LeafNodes[0].textContent).toBe ' '
- expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false
- expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false
-
- it "removes the 'indent-guide' class when editor.showIndentGuide to false", ->
- 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
-
- atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.js'
- nextAnimationFrame()
-
- line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1))
- expect(line1LeafNodes[0].textContent).toBe ' '
- expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false
- expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false
-
- describe "autoscroll", ->
- beforeEach ->
- editor.setVerticalScrollMargin(2)
- editor.setHorizontalScrollMargin(2)
- component.setLineHeight("10px")
- component.setFontSize(17)
- component.measureDimensions()
- nextAnimationFrame()
-
- wrapperNode.setWidth(55)
- wrapperNode.setHeight(55)
- component.measureDimensions()
- nextAnimationFrame()
-
- component.presenter.setHorizontalScrollbarHeight(0)
- component.presenter.setVerticalScrollbarWidth(0)
- nextAnimationFrame()
-
- describe "when selecting buffer ranges", ->
- it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", ->
- expect(wrapperNode.getScrollTop()).toBe 0
-
- editor.setSelectedBufferRange([[5, 6], [6, 8]])
- nextAnimationFrame()
- 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]])
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- expect(wrapperNode.getScrollLeft()).toBe 0
-
- editor.setSelectedBufferRange([[6, 6], [6, 8]])
- nextAnimationFrame()
- expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10
- expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0
-
- describe "when adding selections for buffer ranges", ->
- it "autoscrolls to the added selection if needed", ->
- editor.addSelectionForBufferRange([[8, 10], [8, 15]])
- nextAnimationFrame()
-
- 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", ->
- it "autoscrolls to the selection", ->
- editor.setCursorScreenPosition([5, 6])
- nextAnimationFrame()
-
- wrapperNode.scrollToTop()
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
-
- editor.selectLinesContainingCursors()
- nextAnimationFrame()
- expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10
-
- describe "when inserting text", ->
- describe "when there are multiple empty selections on different lines", ->
- it "autoscrolls to the last cursor", ->
- editor.setCursorScreenPosition([1, 2], autoscroll: false)
- nextAnimationFrame()
-
- editor.addCursorAtScreenPosition([10, 4], autoscroll: false)
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.insertText('a')
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 75
-
- describe "when scrolled to cursor position", ->
- it "scrolls the last cursor into view, centering around the cursor if possible and the 'center' option isn't false", ->
- editor.setCursorScreenPosition([8, 8], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- expect(wrapperNode.getScrollLeft()).toBe 0
-
- editor.scrollToCursorPosition()
- nextAnimationFrame()
- 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", ->
- it "scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor", ->
- expect(wrapperNode.getScrollTop()).toBe 0
- expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10
-
- editor.setCursorScreenPosition([2, 0])
- nextAnimationFrame()
- expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10
-
- editor.moveDown()
- nextAnimationFrame()
- expect(wrapperNode.getScrollBottom()).toBe 6 * 10
-
- editor.moveDown()
- nextAnimationFrame()
- expect(wrapperNode.getScrollBottom()).toBe 7 * 10
-
- it "scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor", ->
- editor.setCursorScreenPosition([11, 0])
- nextAnimationFrame()
- wrapperNode.setScrollBottom(wrapperNode.getScrollHeight())
- nextAnimationFrame()
-
- editor.moveUp()
- nextAnimationFrame()
- expect(wrapperNode.getScrollBottom()).toBe wrapperNode.getScrollHeight()
-
- editor.moveUp()
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 7 * 10
-
- editor.moveUp()
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 6 * 10
-
- it "scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor", ->
- expect(wrapperNode.getScrollLeft()).toBe 0
- expect(wrapperNode.getScrollRight()).toBe 5.5 * 10
-
- editor.setCursorScreenPosition([0, 2])
- nextAnimationFrame()
- expect(wrapperNode.getScrollRight()).toBe 5.5 * 10
-
- editor.moveRight()
- nextAnimationFrame()
-
- margin = component.presenter.getHorizontalScrollMarginInPixels()
- right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin
- expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0
-
- editor.moveRight()
- nextAnimationFrame()
- 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", ->
- wrapperNode.setScrollRight(wrapperNode.getScrollWidth())
- nextAnimationFrame()
- expect(wrapperNode.getScrollRight()).toBe wrapperNode.getScrollWidth()
- editor.setCursorScreenPosition([6, 62], autoscroll: false)
- nextAnimationFrame()
-
- editor.moveLeft()
- nextAnimationFrame()
-
- margin = component.presenter.getHorizontalScrollMarginInPixels()
- left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin
- expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0
-
- editor.moveLeft()
- nextAnimationFrame()
- 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", ->
- editor.setCursorScreenPosition([13, Infinity])
- editor.insertNewline()
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollBottom()).toBe 14 * 10
- editor.insertNewline()
- nextAnimationFrame()
- expect(wrapperNode.getScrollBottom()).toBe 15 * 10
-
- it "autoscrolls to the cursor when it moves due to undo", ->
- editor.insertText('abc')
- wrapperNode.setScrollTop(Infinity)
- nextAnimationFrame()
-
- editor.undo()
- nextAnimationFrame()
-
- expect(wrapperNode.getScrollTop()).toBe 0
-
- it "doesn't scroll when the cursor moves into the visible area", ->
- editor.setCursorBufferPosition([0, 0])
- nextAnimationFrame()
-
- wrapperNode.setScrollTop(40)
- nextAnimationFrame()
-
- editor.setCursorBufferPosition([6, 0])
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 40
-
- it "honors the autoscroll option on cursor and selection manipulation methods", ->
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.addCursorAtScreenPosition([11, 11], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.addCursorAtBufferPosition([11, 11], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.setCursorScreenPosition([11, 11], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.setCursorBufferPosition([11, 11], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.clearSelections(autoscroll: false)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
-
- editor.addSelectionForScreenRange([[0, 0], [0, 4]])
- nextAnimationFrame()
-
- editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBeGreaterThan 0
- editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
- editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBeGreaterThan 0
- editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true)
- nextAnimationFrame()
- expect(wrapperNode.getScrollTop()).toBe 0
-
- describe "::getVisibleRowRange()", ->
- beforeEach ->
- wrapperNode.style.height = lineHeightInPixels * 8 + "px"
- component.measureDimensions()
- nextAnimationFrame()
-
- it "returns the first and the last visible rows", ->
- component.setScrollTop(0)
- nextAnimationFrame()
-
- expect(component.getVisibleRowRange()).toEqual [0, 9]
-
- it "ends at last buffer row even if there's more space available", ->
- wrapperNode.style.height = lineHeightInPixels * 13 + "px"
- component.measureDimensions()
- nextAnimationFrame()
-
- component.setScrollTop(60)
- nextAnimationFrame()
-
- expect(component.getVisibleRowRange()).toEqual [0, 13]
-
- describe "middle mouse paste on Linux", ->
- originalPlatform = null
-
- beforeEach ->
- originalPlatform = process.platform
- Object.defineProperty process, 'platform', value: 'linux'
-
- afterEach ->
- Object.defineProperty process, 'platform', value: originalPlatform
-
- it "pastes the previously selected text at the clicked location", ->
- jasmine.unspy(window, 'setTimeout')
- clipboardWrittenTo = false
- spyOn(require('ipc'), 'send').andCallFake (eventName, selectedText) ->
- if eventName is '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]])
-
- waitsFor ->
- clipboardWrittenTo
-
- runs ->
- 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'
-
- buildMouseEvent = (type, properties...) ->
- properties = extend({bubbles: true, cancelable: true}, properties...)
- properties.detail ?= 1
- event = new MouseEvent(type, properties)
- Object.defineProperty(event, 'which', get: -> properties.which) if properties.which?
- if properties.target?
- Object.defineProperty(event, 'target', get: -> properties.target)
- Object.defineProperty(event, 'srcObject', get: -> properties.target)
- event
-
- clientCoordinatesForScreenPosition = (screenPosition) ->
- positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition)
- scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect()
- clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft()
- clientY = scrollViewClientRect.top + positionOffset.top - wrapperNode.getScrollTop()
- {clientX, clientY}
-
- clientCoordinatesForScreenRowInGutter = (screenRow) ->
- positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, Infinity])
- gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect()
- clientX = gutterClientRect.left + positionOffset.left - wrapperNode.getScrollLeft()
- clientY = gutterClientRect.top + positionOffset.top - wrapperNode.getScrollTop()
- {clientX, clientY}
-
- lineAndLineNumberHaveClass = (screenRow, klass) ->
- lineHasClass(screenRow, klass) and lineNumberHasClass(screenRow, klass)
-
- lineNumberHasClass = (screenRow, klass) ->
- component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass)
-
- lineNumberForBufferRowHasClass = (bufferRow, klass) ->
- screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow)
- component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass)
-
- lineHasClass = (screenRow, klass) ->
- component.lineNodeForScreenRow(screenRow).classList.contains(klass)
-
- getLeafNodes = (node) ->
- if node.children.length > 0
- flatten(toArray(node.children).map(getLeafNodes))
- else
- [node]
diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js
new file mode 100644
index 000000000..609d20291
--- /dev/null
+++ b/spec/text-editor-component-spec.js
@@ -0,0 +1,4752 @@
+/** @babel */
+
+import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
+import TextEditorElement from '../src/text-editor-element'
+import _, {extend, flatten, last, toArray} from 'underscore-plus'
+
+const NBSP = String.fromCharCode(160)
+const TILE_SIZE = 3
+
+describe('TextEditorComponent', function () {
+ let charWidth, component, componentNode, contentNode, editor,
+ horizontalScrollbarNode, lineHeightInPixels, tileHeightInPixels,
+ verticalScrollbarNode, wrapperNode
+
+ beforeEach(async function () {
+ jasmine.useRealClock()
+
+ await atom.packages.activatePackage('language-javascript')
+ editor = await atom.workspace.open('sample.js')
+
+ 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()
+ await nextViewUpdatePromise()
+ })
+
+ afterEach(function () {
+ contentNode.style.width = ''
+ })
+
+ describe('async updates', function () {
+ it('handles corrupted state gracefully', async function () {
+ editor.insertNewline()
+ component.presenter.startRow = -1
+ component.presenter.endRow = 9999
+ await nextViewUpdatePromise() // assert an update does occur
+ })
+
+ it('does not update when an animation frame was requested but the component got destroyed before its delivery', async function () {
+ editor.setText('You should not see this update.')
+ component.destroy()
+
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(0).textContent).not.toBe('You should not see this update.')
+ })
+ })
+
+ describe('line rendering', async function () {
+ function expectTileContainsRow (tileNode, screenRow, {top}) {
+ let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]')
+ let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
+
+ expect(lineNode.offsetTop).toBe(top)
+ if (tokenizedLine.text === '') {
+ expect(lineNode.innerHTML).toBe(' ')
+ } else {
+ expect(lineNode.textContent).toBe(tokenizedLine.text)
+ }
+ }
+
+ it('gives the lines container the same height as the wrapper node', async function () {
+ let linesNode = componentNode.querySelector('.lines')
+ wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels)
+ wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+
+ await nextViewUpdatePromise()
+
+ expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels)
+ })
+
+ it('renders higher tiles in front of lower ones', async function () {
+ wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+
+ await nextViewUpdatePromise()
+
+ 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'))
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+
+ await nextViewUpdatePromise()
+
+ 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'))
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ editor.getBuffer().deleteRows(0, 1)
+
+ await nextViewUpdatePromise()
+
+ 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')
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+
+ await nextViewUpdatePromise()
+
+ verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels
+ verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
+
+ await nextViewUpdatePromise()
+
+ let buffer = editor.getBuffer()
+ buffer.insert([0, 0], '\n\n')
+
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
+ buffer.delete([[0, 0], [3, 0]])
+
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
+ })
+
+ it('updates the top position of lines when the line height changes', async function () {
+ let initialLineHeightInPixels = editor.getLineHeightInPixels()
+
+ component.setLineHeight(2)
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ let initialLineHeightInPixels = editor.getLineHeightInPixels()
+ component.setFontSize(10)
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setText('')
+ wrapperNode.style.height = '300px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ 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', async 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()
+
+ await nextViewUpdatePromise()
+
+ 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()
+
+ await nextViewUpdatePromise()
+
+ let scrollViewWidth = scrollViewNode.offsetWidth
+ for (let lineNode of lineNodes) {
+ expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth)
+ }
+ })
+
+ it('renders an nbsp on empty lines when no line-ending character is defined', function () {
+ atom.config.set('editor.showInvisibles', false)
+ expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
+ })
+
+ 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)'
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setText(' a')
+
+ await nextViewUpdatePromise()
+
+ 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')
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setText(' ')
+ await nextViewUpdatePromise()
+
+ 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')
+ await nextViewUpdatePromise()
+
+ 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 ')
+ await nextViewUpdatePromise()
+
+ 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')
+ await nextViewUpdatePromise()
+
+ 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.querySelector('.line')
+
+ waitsFor(function () {
+ return componentNode.querySelector('.line') !== oldLineNode
+ })
+ })
+
+ describe('when showInvisibles is enabled', function () {
+ const invisibles = {
+ eol: 'E',
+ space: 'S',
+ tab: 'T',
+ cr: 'C'
+ }
+
+ beforeEach(async function () {
+ atom.config.set('editor.showInvisibles', true)
+ atom.config.set('editor.invisibles', invisibles)
+ await nextViewUpdatePromise()
+ })
+
+ it('re-renders the lines when the showInvisibles config option changes', async function () {
+ editor.setText(' a line with tabs\tand spaces \n')
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol)
+
+ atom.config.set('editor.showInvisibles', false)
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ')
+
+ atom.config.set('editor.showInvisibles', true)
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setText(' a line with tabs\tand spaces \n')
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setText('let\n')
+ await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '')
+ })
+
+ it('displays trailing carriage returns using a visible, non-empty value', async function () {
+ editor.setText('a line that ends with a carriage return\r\n')
+ await nextViewUpdatePromise()
+ 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 an nbsp on empty lines when the line-ending character is an empty string', async function () {
+ atom.config.set('editor.invisibles', {
+ eol: ''
+ })
+ await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
+ })
+
+ it('renders an nbsp on empty lines when the line-ending character is false', async function () {
+ atom.config.set('editor.invisibles', {
+ eol: false
+ })
+ await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
+ })
+
+ it('interleaves invisible line-ending characters with indent guides on empty lines', async function () {
+ atom.config.set('editor.showIndentGuide', true)
+
+ await nextViewUpdatePromise()
+
+ editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', {
+ normalizeLineEndings: false
+ })
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
+ editor.setTabLength(3)
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ')
+ editor.setTabLength(1)
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
+ editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ')
+ editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ')
+ await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
+ })
+
+ describe('when soft wrapping is enabled', function () {
+ beforeEach(async function () {
+ editor.setText('a line that wraps \n')
+ editor.setSoftWrapped(true)
+ await nextViewUpdatePromise()
+
+ componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ })
+
+ it('does not show end of line invisibles at the end of wrapped lines', function () {
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ')
+ expect(component.lineNodeForScreenRow(1).textContent).toBe('wraps' + invisibles.space + invisibles.eol)
+ })
+ })
+ })
+
+ describe('when indent guides are enabled', function () {
+ beforeEach(async function () {
+ atom.config.set('editor.showIndentGuide', true)
+ await nextViewUpdatePromise()
+ })
+
+ 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', async function () {
+ editor.getBuffer().insert([1, Infinity], '\n')
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.getBuffer().insert([1, Infinity], '\n ')
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ atom.config.set('editor.showInvisibles', true)
+ atom.config.set('editor.invisibles', {
+ space: '-',
+ eol: 'x'
+ })
+ editor.getBuffer().insert([1, Infinity], '\n ')
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.getBuffer().setText(' hi ')
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.getBuffer().insert([12, 0], '\n')
+ await nextViewUpdatePromise()
+
+ editor.getBuffer().insert([13, 0], ' ')
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.getBuffer().insert([12, 2], '\n')
+
+ await nextViewUpdatePromise()
+
+ editor.getBuffer().insert([12, 0], ' ')
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.getBuffer().insert([1, Infinity], '\n ')
+
+ await nextViewUpdatePromise()
+
+ let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2))
+ expect(line2LeafNodes.length).toBe(3)
+ expect(line2LeafNodes[0].textContent).toBe(' ')
+ expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(false)
+ expect(line2LeafNodes[1].textContent).toBe(' ')
+ expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(false)
+ expect(line2LeafNodes[2].textContent).toBe(' ')
+ expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false)
+ })
+ })
+
+ describe('when the buffer contains null bytes', function () {
+ it('excludes the null byte from character measurement', async function () {
+ editor.setText('a\0b')
+ await nextViewUpdatePromise()
+ 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', async function () {
+ let foldedLineNode = component.lineNodeForScreenRow(4)
+ expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy()
+ editor.foldBufferRow(4)
+
+ await nextViewUpdatePromise()
+
+ foldedLineNode = component.lineNodeForScreenRow(4)
+ expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy()
+ editor.unfoldBufferRow(4)
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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'))
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ let linesNode = componentNode.querySelector('.line-numbers')
+ wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+
+ await nextViewUpdatePromise()
+
+ expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels)
+ wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+
+ await nextViewUpdatePromise()
+
+ expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels)
+ })
+
+ it('renders the currently-visible line numbers in a tiled fashion', async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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'))
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.getBuffer().insert([0, 0], '\n\n')
+ await nextViewUpdatePromise()
+
+ 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')
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setSoftWrapped(true)
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 30 * charWidth + 'px'
+ component.measureDimensions()
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.getBuffer().setText([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].join('\n'))
+ await nextViewUpdatePromise()
+
+ for (let screenRow = 0; screenRow <= 8; ++screenRow) {
+ expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1))
+ }
+ expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10')
+ let gutterNode = componentNode.querySelector('.gutter')
+ let initialGutterWidth = gutterNode.offsetWidth
+ editor.getBuffer().delete([[1, 0], [2, 0]])
+
+ await nextViewUpdatePromise()
+
+ for (let screenRow = 0; screenRow <= 8; ++screenRow) {
+ expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + (screenRow + 1))
+ }
+ expect(gutterNode.offsetWidth).toBeLessThan(initialGutterWidth)
+ editor.getBuffer().insert([0, 0], '\n\n')
+
+ await nextViewUpdatePromise()
+
+ for (let screenRow = 0; screenRow <= 8; ++screenRow) {
+ expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1))
+ }
+ expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10')
+ 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', async function () {
+ wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ 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', async 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()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ expect(component.gutterContainerComponent.getLineNumberGutterComponent() != null).toBe(true)
+ editor.setLineNumberGutterVisible(false)
+ await nextViewUpdatePromise()
+
+ expect(componentNode.querySelector('.gutter').style.display).toBe('none')
+ atom.config.set('editor.showLineNumbers', false)
+ await nextViewUpdatePromise()
+
+ expect(componentNode.querySelector('.gutter').style.display).toBe('none')
+ editor.setLineNumberGutterVisible(true)
+ await nextViewUpdatePromise()
+
+ expect(componentNode.querySelector('.gutter').style.display).toBe('none')
+ atom.config.set('editor.showLineNumbers', true)
+ await nextViewUpdatePromise()
+
+ 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]
+
+ waitsFor(function () {
+ return componentNode.querySelectorAll('.line-number')[1] !== oldLineNode
+ })
+ })
+
+ 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', async function () {
+ editor.getBuffer().insert([0, 0], '\n')
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ expect(lineNumberHasClass(11, 'foldable')).toBe(false)
+ editor.getBuffer().insert([11, 44], '\n fold me')
+ await nextViewUpdatePromise()
+ expect(lineNumberHasClass(11, 'foldable')).toBe(true)
+ editor.undo()
+ await nextViewUpdatePromise()
+ expect(lineNumberHasClass(11, 'foldable')).toBe(false)
+ })
+
+ it('adds, updates and removes the folded class on the correct line number componentNodes', async function () {
+ editor.foldBufferRow(4)
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(4, 'folded')).toBe(true)
+
+ editor.getBuffer().insert([0, 0], '\n')
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(4, 'folded')).toBe(false)
+ expect(lineNumberHasClass(5, 'folded')).toBe(true)
+
+ editor.unfoldBufferRow(5)
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(5, 'folded')).toBe(false)
+ })
+
+ describe('when soft wrapping is enabled', function () {
+ beforeEach(async function () {
+ editor.setSoftWrapped(true)
+ await nextViewUpdatePromise()
+ componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ })
+
+ it('does not add the foldable class for soft-wrapped lines', function () {
+ expect(lineNumberHasClass(0, 'foldable')).toBe(true)
+ expect(lineNumberHasClass(1, 'foldable')).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')
+ return target.dispatchEvent(buildClickEvent(target))
+ })
+ })
+
+ it('folds and unfolds the block represented by the fold indicator when clicked', async function () {
+ expect(lineNumberHasClass(1, 'folded')).toBe(false)
+
+ let lineNumber = component.lineNumberNodeForScreenRow(1)
+ let target = lineNumber.querySelector('.icon-right')
+
+ target.dispatchEvent(buildClickEvent(target))
+
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(1, 'folded')).toBe(true)
+ lineNumber = component.lineNumberNodeForScreenRow(1)
+ target = lineNumber.querySelector('.icon-right')
+ target.dispatchEvent(buildClickEvent(target))
+
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(1, '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', async function () {
+ let cursor1 = editor.getLastCursor()
+ cursor1.setScreenPosition([0, 5], {
+ autoscroll: false
+ })
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 20 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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
+ })
+ await nextViewUpdatePromise()
+
+ 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
+ horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
+ await nextViewUpdatePromise()
+
+ horizontalScrollbarNode.scrollLeft = 3.5 * charWidth
+ horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
+ await nextViewUpdatePromise()
+
+ 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
+ })
+ await nextViewUpdatePromise()
+
+ 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()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ atom.config.set('editor.fontFamily', 'sans-serif')
+ editor.setCursorScreenPosition([0, 16])
+ await nextViewUpdatePromise()
+
+ let cursor = componentNode.querySelector('.cursor')
+ let cursorRect = cursor.getBoundingClientRect()
+ let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.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', async function () {
+ atom.config.set('editor.fontFamily', 'sans-serif')
+ editor.setText('he\u0301y')
+ editor.setCursorBufferPosition([0, 3])
+ await nextViewUpdatePromise()
+
+ let cursor = componentNode.querySelector('.cursor')
+ let cursorRect = cursor.getBoundingClientRect()
+ let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2]
+ 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('positions cursors correctly after character widths are changed via a stylesheet change', async function () {
+ atom.config.set('editor.fontFamily', 'sans-serif')
+ editor.setCursorScreenPosition([0, 16])
+ await nextViewUpdatePromise()
+
+ atom.styles.addStyleSheet('.function.js {\n font-weight: bold;\n}', {
+ context: 'atom-text-editor'
+ })
+ await nextViewUpdatePromise()
+
+ let cursor = componentNode.querySelector('.cursor')
+ let cursorRect = cursor.getBoundingClientRect()
+ let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.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', async function () {
+ editor.setCursorScreenPosition([0, Infinity])
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setCursorScreenPosition([1, 0])
+ await nextViewUpdatePromise()
+ 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()
+ await nextViewUpdatePromise()
+ expect(cursorsNode.classList.contains('blink-off')).toBe(false)
+ await conditionPromise(function () {
+ return cursorsNode.classList.contains('blink-off')
+ })
+ await conditionPromise(function () {
+ return !cursorsNode.classList.contains('blink-off')
+ })
+ editor.moveRight()
+ await nextViewUpdatePromise()
+ expect(cursorsNode.classList.contains('blink-off')).toBe(false)
+ await conditionPromise(function () {
+ return cursorsNode.classList.contains('blink-off')
+ })
+ })
+
+ it('does not render cursors that are associated with non-empty selections', async function () {
+ editor.setSelectedScreenRange([[0, 4], [4, 6]])
+ editor.addCursorAtScreenPosition([6, 8])
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setCursorBufferPosition([1, 10])
+ component.setLineHeight(2)
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setCursorBufferPosition([1, 10])
+ component.setFontSize(10)
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setCursorBufferPosition([1, 10])
+ component.setFontFamily('sans-serif')
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setSelectedScreenRange([[1, 6], [1, 10]])
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setSelectedScreenRange([[1, 6], [2, 10]])
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setSelectedScreenRange([[0, 6], [5, 10]])
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.addSelectionForBufferRange([[2, 2], [2, 2]])
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setSelectedBufferRange([[1, 6], [1, 10]])
+ component.setLineHeight(2)
+ await nextViewUpdatePromise()
+ let selectionNode = componentNode.querySelector('.region')
+ expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels())
+ })
+
+ it('updates selections when the font size changes', async function () {
+ editor.setSelectedBufferRange([[1, 6], [1, 10]])
+ component.setFontSize(10)
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setSelectedBufferRange([[1, 6], [1, 10]])
+ component.setFontFamily('sans-serif')
+
+ await nextViewUpdatePromise()
+
+ 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
+ })
+ await nextViewUpdatePromise()
+
+ let selectionNode = componentNode.querySelector('.selection')
+ expect(selectionNode.classList.contains('flash')).toBe(true)
+
+ await conditionPromise(function () {
+ return !selectionNode.classList.contains('flash')
+ })
+
+ editor.setSelectedBufferRange([[1, 5], [1, 7]], {
+ flash: true
+ })
+ await nextViewUpdatePromise()
+
+ expect(selectionNode.classList.contains('flash')).toBe(true)
+ })
+ })
+
+ describe('line decoration rendering', 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)
+ await nextViewUpdatePromise()
+ })
+
+ 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'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]])
+ editor.decorateMarker(marker2, {
+ type: ['line-number', 'line'],
+ 'class': 'b'
+ })
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels
+ verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
+ await nextViewUpdatePromise()
+
+ expect(lineAndLineNumberHaveClass(9, 'b')).toBe(true)
+
+ editor.foldBufferRow(5)
+ await nextViewUpdatePromise()
+
+ 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()
+
+ await nextViewUpdatePromise()
+ marker.destroy()
+ marker = editor.markBufferRange([[0, 0], [0, 2]])
+ editor.decorateMarker(marker, {
+ type: ['line-number', 'line'],
+ 'class': 'b'
+ })
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(0, 'b')).toBe(true)
+ expect(lineNumberHasClass(1, 'b')).toBe(false)
+ marker.setBufferRange([[0, 0], [0, Infinity]])
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+ 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)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+ 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', 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)
+ await nextViewUpdatePromise()
+ 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)
+ await nextViewUpdatePromise()
+
+ expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false)
+ expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(false)
+
+ marker.clearTail()
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+
+ expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(true)
+ expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(true)
+
+ marker.clearTail()
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(false)
+ expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(false)
+ })
+ })
+ })
+
+ 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)
+ await nextViewUpdatePromise()
+ })
+
+ it('does not render highlights for off-screen lines until they come on-screen', async function () {
+ wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], {
+ invalidate: 'inside'
+ })
+ editor.decorateMarker(marker, {
+ type: 'highlight',
+ 'class': 'some-highlight'
+ })
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ 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'))
+ await nextViewUpdatePromise()
+
+ 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', async 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)
+ await nextViewUpdatePromise()
+ 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)
+ await nextViewUpdatePromise()
+ 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)
+ await nextViewUpdatePromise()
+ 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)
+ await nextViewUpdatePromise()
+
+ expect(marker.isValid()).toBe(false)
+ let regions = componentNode.querySelectorAll('.test-highlight .region')
+ expect(regions.length).toBe(0)
+ editor.getBuffer().undo()
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+ expect(componentNode.querySelectorAll('.foo.bar').length).toBe(2)
+ decoration.setProperties({
+ type: 'highlight',
+ 'class': 'bar baz'
+ })
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+ 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)
+ await nextViewUpdatePromise()
+ 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(async 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)
+ await nextViewUpdatePromise()
+
+ expect(highlightNode.classList.contains('flash-class')).toBe(true)
+ await conditionPromise(function () {
+ return !highlightNode.classList.contains('flash-class')
+ })
+ })
+
+ 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)
+ await nextViewUpdatePromise()
+ expect(highlightNode.classList.contains('flash-class')).toBe(true)
+
+ decoration.flash('flash-class', 500)
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ expect(highlightNode.classList.contains('flash-class')).toBe(false)
+
+ await conditionPromise(function () {
+ return highlightNode.classList.contains('flash-class')
+ })
+ })
+ })
+ })
+
+ 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)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+ 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.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
+ invalidate: 'never'
+ })
+ let decoration = editor.decorateMarker(marker, {
+ type: 'overlay',
+ item: item
+ })
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ let overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test')
+ expect(overlay).toBe(item)
+
+ decoration.destroy()
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ 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.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
+ invalidate: 'never'
+ })
+ let decoration = editor.decorateMarker(marker, {
+ type: 'overlay',
+ 'class': 'my-overlay',
+ item: item
+ })
+
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ 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.displayBuffer.markBufferRange([[2, 5], [2, 10]], {
+ invalidate: 'never'
+ })
+ let decoration = editor.decorateMarker(marker, {
+ type: 'overlay',
+ item: item
+ })
+
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ 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'
+ atom.setWindowDimensions({
+ width: windowWidth,
+ height: windowHeight
+ })
+ component.measureDimensions()
+ component.measureWindowSize()
+ await nextViewUpdatePromise()
+ })
+
+ afterEach(function () {
+ atom.restoreWindowDimensions()
+ })
+
+ it('slides horizontally left when near the right edge on #win32 and #darwin', async function () {
+ let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], {
+ invalidate: 'never'
+ })
+ let decoration = editor.decorateMarker(marker, {
+ type: 'overlay',
+ item: item
+ })
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ let position = wrapperNode.pixelPositionForBufferPosition([0, 26])
+ 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')
+
+ editor.insertText('a')
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ expect(overlay.style.left).toBe(windowWidth - itemWidth + 'px')
+ expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px')
+
+ editor.insertText('b')
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ expect(overlay.style.left).toBe(windowWidth - 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'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ expect(editor.getCursorScreenPosition()).toEqual([0, 0])
+
+ wrapperNode.setScrollTop(3 * lineHeightInPixels)
+ wrapperNode.setScrollLeft(3 * charWidth)
+ await nextViewUpdatePromise()
+
+ expect(inputNode.offsetTop).toBe(0)
+ expect(inputNode.offsetLeft).toBe(0)
+
+ editor.setCursorBufferPosition([5, 4], {
+ autoscroll: false
+ })
+ await decorationsUpdatedPromise(editor)
+ await nextViewUpdatePromise()
+
+ expect(inputNode.offsetTop).toBe(0)
+ expect(inputNode.offsetLeft).toBe(0)
+
+ wrapperNode.focus()
+ await nextViewUpdatePromise()
+
+ expect(inputNode.offsetTop).toBe((5 * lineHeightInPixels) - wrapperNode.getScrollTop())
+ expect(inputNode.offsetLeft).toBeCloseTo((4 * charWidth) - wrapperNode.getScrollLeft(), 0)
+
+ inputNode.blur()
+ await nextViewUpdatePromise()
+
+ expect(inputNode.offsetTop).toBe(0)
+ expect(inputNode.offsetLeft).toBe(0)
+
+ editor.setCursorBufferPosition([1, 2], {
+ autoscroll: false
+ })
+ await nextViewUpdatePromise()
+
+ expect(inputNode.offsetTop).toBe(0)
+ expect(inputNode.offsetLeft).toBe(0)
+
+ inputNode.focus()
+ await nextViewUpdatePromise()
+
+ 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', async 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'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ let coordinates = clientCoordinatesForScreenPosition([0, 2])
+ coordinates.clientY = -1
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates))
+
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setText('foo')
+ editor.setCursorBufferPosition([0, 0])
+ let height = 4.5 * lineHeightInPixels
+ wrapperNode.style.height = height + 'px'
+ wrapperNode.style.width = 10 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ let coordinates = clientCoordinatesForScreenPosition([0, 2])
+ coordinates.clientY = height * 2
+
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates))
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 10 * charWidth + 'px'
+ component.measureDimensions()
+ wrapperNode.setScrollTop(3.5 * lineHeightInPixels)
+ wrapperNode.setScrollLeft(2 * charWidth)
+ await nextViewUpdatePromise()
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8])))
+ await nextViewUpdatePromise()
+ expect(editor.getCursorScreenPosition()).toEqual([4, 8])
+ })
+ })
+
+ describe('when the shift key is held down', function () {
+ it('selects to the nearest screen position', async function () {
+ editor.setCursorScreenPosition([3, 4])
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), {
+ shiftKey: true
+ }))
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setCursorScreenPosition([3, 4])
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), {
+ metaKey: true
+ }))
+ await nextViewUpdatePromise()
+ 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', async function () {
+ it('removes a cursor at the mouse screen position', async function () {
+ editor.setCursorScreenPosition([3, 4])
+ editor.addCursorAtScreenPosition([5, 2])
+ editor.addCursorAtScreenPosition([7, 5])
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), {
+ metaKey: true
+ }))
+ await nextViewUpdatePromise()
+ 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', async function () {
+ it('neither adds a new cursor nor removes the current cursor', async function () {
+ editor.setCursorScreenPosition([3, 4])
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), {
+ metaKey: true
+ }))
+ await nextViewUpdatePromise()
+ 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', async function () {
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), {
+ which: 1
+ }))
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]])
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]])
+ linesNode.dispatchEvent(buildMouseEvent('mouseup'))
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]])
+ })
+
+ it('autoscrolls when the cursor approaches the boundaries of the editor', async function () {
+ wrapperNode.style.height = '100px'
+ wrapperNode.style.width = '100px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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) {
+ await nextAnimationFramePromise()
+ }
+
+ 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) {
+ await nextAnimationFramePromise()
+ }
+
+ 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) {
+ await nextAnimationFramePromise()
+ }
+
+ 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) {
+ await nextAnimationFramePromise()
+ }
+
+ expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop)
+ })
+
+ it('stops selecting if the mouse is dragged into the dev tools', async function () {
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), {
+ which: 1
+ }))
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]])
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), {
+ which: 0
+ }))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]])
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]])
+ })
+
+ it('stops selecting before the buffer is modified during the drag', async function () {
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), {
+ which: 1
+ }))
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]])
+
+ editor.insertText('x')
+ await nextAnimationFramePromise()
+
+ 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
+ }))
+ await nextAnimationFramePromise()
+
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [5, 4]])
+
+ editor.delete()
+ await nextAnimationFramePromise()
+
+ 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', async 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
+ }))
+ await nextAnimationFramePromise()
+
+ expect(editor.getSelectedScreenRanges()).toEqual([[[4, 4], [4, 9]], [[2, 4], [6, 8]]])
+
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+
+ 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', async function () {
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), {
+ which: 1
+ }))
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+
+ spyOn(window, 'removeEventListener').andCallThrough()
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), {
+ which: 1
+ }))
+
+ editor.destroy()
+ await nextAnimationFramePromise()
+
+ 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', async function () {
+ jasmine.attachToDOM(wrapperNode)
+ wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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
+ }))
+ await nextAnimationFramePromise()
+
+ expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [12, 2]])
+ let maximalScrollTop = wrapperNode.getScrollTop()
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+
+ 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', async function () {
+ jasmine.attachToDOM(wrapperNode)
+ wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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
+ }))
+ await nextAnimationFramePromise()
+
+ expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [12, 2]])
+ let maximalScrollTop = wrapperNode.getScrollTop()
+ linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), {
+ which: 1
+ }))
+ await nextAnimationFramePromise()
+
+ 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 line is folded', function () {
+ beforeEach(async function () {
+ editor.foldBufferRow(4)
+ await nextViewUpdatePromise()
+ })
+
+ describe('when the folded line\'s fold-marker is clicked', function () {
+ it('unfolds the buffer row', function () {
+ let target = component.lineNodeForScreenRow(4).querySelector('.fold-marker')
+ linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {
+ target: target
+ }))
+ expect(editor.isFoldedAtBufferRow(4)).toBe(false)
+ })
+ })
+ })
+
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6)))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4)))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2)))
+ await nextAnimationFramePromise()
+ expect(editor.getLastSelection().isReversed()).toBe(true)
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
+ await nextAnimationFramePromise()
+ expect(editor.getLastSelection().isReversed()).toBe(false)
+ })
+
+ it('autoscrolls when the cursor approaches the top or bottom of the editor', async function () {
+ wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
+ await nextAnimationFramePromise()
+
+ expect(wrapperNode.getScrollTop()).toBeGreaterThan(0)
+ let maxScrollTop = wrapperNode.getScrollTop()
+
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10)))
+ await nextAnimationFramePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(maxScrollTop)
+
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7)))
+ await nextAnimationFramePromise()
+
+ expect(wrapperNode.getScrollTop()).toBeLessThan(maxScrollTop)
+ })
+
+ it('stops selecting if a textInput event occurs during the drag', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
+ await nextAnimationFramePromise()
+
+ 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)
+ await nextAnimationFramePromise()
+
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+
+ gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), {
+ metaKey: true
+ }))
+ expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [7, 0]]])
+ })
+
+ it('merges overlapping selections', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+
+ 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', async function () {
+ editor.setSelectedScreenRange([[3, 4], [4, 5]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ editor.setSelectedScreenRange([[4, 4], [5, 5]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5)))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[4, 4], [6, 0]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ editor.setSelectedScreenRange([[4, 4], [5, 5]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ editor.setSelectedScreenRange([[3, 4], [4, 5]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2)))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [3, 4]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [9, 0]])
+ })
+ })
+ })
+ })
+
+ describe('when soft wrap is enabled', function () {
+ beforeEach(async function () {
+ gutterNode = componentNode.querySelector('.gutter')
+ editor.setSoftWrapped(true)
+ await nextViewUpdatePromise()
+ componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ })
+
+ 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], [16, 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1)))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6)))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+ gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), {
+ metaKey: true
+ }))
+ expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [19, 0]]])
+ })
+
+ it('merges overlapping selections on mouseup', async function () {
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), {
+ metaKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), {
+ metaKey: true
+ }))
+ await nextAnimationFramePromise()
+ gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), {
+ metaKey: true
+ }))
+ expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [19, 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', async function () {
+ editor.setSelectedScreenRange([[1, 4], [1, 7]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11)))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 14]])
+ })
+ })
+
+ describe('when dragging upward', function () {
+ it('selects the screen rows between the end of the drag and the tail of the existing selection', async function () {
+ editor.setSelectedScreenRange([[1, 4], [1, 7]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ editor.setSelectedScreenRange([[7, 4], [7, 6]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
+ await nextAnimationFramePromise()
+ 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', async function () {
+ editor.setSelectedScreenRange([[7, 4], [7, 6]])
+ gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), {
+ shiftKey: true
+ }))
+ gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3)))
+ await nextAnimationFramePromise()
+ expect(editor.getSelectedScreenRange()).toEqual([[3, 2], [7, 4]])
+ })
+ })
+ })
+ })
+ })
+ })
+
+ describe('focus handling', async 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(wrapperNode)
+ expect(wrapperNode.shadowRoot.activeElement).toBe(inputNode)
+ })
+
+ it('adds the "is-focused" class to the editor when the hidden input is focused', async function () {
+ expect(document.activeElement).toBe(document.body)
+ inputNode.focus()
+ await nextViewUpdatePromise()
+
+ expect(componentNode.classList.contains('is-focused')).toBe(true)
+ expect(wrapperNode.classList.contains('is-focused')).toBe(true)
+ inputNode.blur()
+ await nextViewUpdatePromise()
+
+ expect(componentNode.classList.contains('is-focused')).toBe(false)
+ expect(wrapperNode.classList.contains('is-focused')).toBe(false)
+ })
+ })
+
+ describe('selection handling', function () {
+ let cursor
+
+ beforeEach(async function () {
+ editor.setCursorScreenPosition([0, 0])
+ await nextViewUpdatePromise()
+ })
+
+ it('adds the "has-selection" class to the editor when there is a selection', async function () {
+ expect(componentNode.classList.contains('has-selection')).toBe(false)
+ editor.selectDown()
+ await nextViewUpdatePromise()
+ expect(componentNode.classList.contains('has-selection')).toBe(true)
+ editor.moveDown()
+ await nextViewUpdatePromise()
+ expect(componentNode.classList.contains('has-selection')).toBe(false)
+ })
+ })
+
+ describe('scrolling', function () {
+ it('updates the vertical scrollbar when the scrollTop is changed in the model', async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ expect(verticalScrollbarNode.scrollTop).toBe(0)
+ wrapperNode.setScrollTop(10)
+ await nextViewUpdatePromise()
+ expect(verticalScrollbarNode.scrollTop).toBe(10)
+ })
+
+ it('updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model', async function () {
+ componentNode.style.width = 30 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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)
+
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ componentNode.style.width = 30 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ expect(wrapperNode.getScrollLeft()).toBe(0)
+ horizontalScrollbarNode.scrollLeft = 100
+ horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
+ await nextViewUpdatePromise()
+ expect(wrapperNode.getScrollLeft()).toBe(100)
+ })
+
+ it('does not obscure the last line with the horizontal scrollbar', async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 10 * charWidth + 'px'
+ component.measureDimensions()
+ wrapperNode.setScrollBottom(wrapperNode.getScrollHeight())
+ await nextViewUpdatePromise()
+
+ 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()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.style.height = 7 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 10 * charWidth + 'px'
+ component.measureDimensions()
+ wrapperNode.setScrollLeft(Infinity)
+
+ await nextViewUpdatePromise()
+ 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', async function () {
+ expect(verticalScrollbarNode.style.display).toBe('none')
+ expect(horizontalScrollbarNode.style.display).toBe('none')
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = '1000px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ expect(verticalScrollbarNode.style.display).toBe('')
+ expect(horizontalScrollbarNode.style.display).toBe('none')
+ componentNode.style.width = 10 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ expect(verticalScrollbarNode.style.display).toBe('')
+ expect(horizontalScrollbarNode.style.display).toBe('')
+ wrapperNode.style.height = 20 * lineHeightInPixels + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.style.height = 4 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 10 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ atom.styles.addStyleSheet('::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}', {
+ context: 'atom-text-editor'
+ })
+
+ await nextAnimationFramePromise()
+ await nextAnimationFramePromise()
+
+ 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', async 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'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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()
+ await nextViewUpdatePromise()
+
+ 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'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ let gutterNode = componentNode.querySelector('.gutter')
+ componentNode.style.width = 10 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ expect(horizontalScrollbarNode.scrollWidth).toBe(wrapperNode.getScrollWidth())
+ expect(horizontalScrollbarNode.style.left).toBe('0px')
+ })
+ })
+
+ describe('mousewheel events', function () {
+ beforeEach(function () {
+ atom.config.set('editor.scrollSensitivity', 100)
+ })
+
+ describe('updating scrollTop and scrollLeft', function () {
+ beforeEach(async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 20 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ })
+
+ it('updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)', async function () {
+ expect(verticalScrollbarNode.scrollTop).toBe(0)
+ expect(horizontalScrollbarNode.scrollLeft).toBe(0)
+ componentNode.dispatchEvent(new WheelEvent('mousewheel', {
+ wheelDeltaX: -5,
+ wheelDeltaY: -10
+ }))
+ await nextAnimationFramePromise()
+
+ expect(verticalScrollbarNode.scrollTop).toBe(10)
+ expect(horizontalScrollbarNode.scrollLeft).toBe(0)
+ componentNode.dispatchEvent(new WheelEvent('mousewheel', {
+ wheelDeltaX: -15,
+ wheelDeltaY: -5
+ }))
+ await nextAnimationFramePromise()
+
+ expect(verticalScrollbarNode.scrollTop).toBe(10)
+ expect(horizontalScrollbarNode.scrollLeft).toBe(15)
+ })
+
+ it('updates the scrollLeft or scrollTop according to the scroll sensitivity', async function () {
+ atom.config.set('editor.scrollSensitivity', 50)
+ componentNode.dispatchEvent(new WheelEvent('mousewheel', {
+ wheelDeltaX: -5,
+ wheelDeltaY: -10
+ }))
+ await nextAnimationFramePromise()
+
+ expect(horizontalScrollbarNode.scrollLeft).toBe(0)
+ componentNode.dispatchEvent(new WheelEvent('mousewheel', {
+ wheelDeltaX: -15,
+ wheelDeltaY: -5
+ }))
+ await nextAnimationFramePromise()
+
+ expect(verticalScrollbarNode.scrollTop).toBe(5)
+ expect(horizontalScrollbarNode.scrollLeft).toBe(7)
+ })
+
+ it('uses the previous scrollSensitivity when the value is not an int', async function () {
+ atom.config.set('editor.scrollSensitivity', 'nope')
+ componentNode.dispatchEvent(new WheelEvent('mousewheel', {
+ wheelDeltaX: 0,
+ wheelDeltaY: -10
+ }))
+ await nextAnimationFramePromise()
+ expect(verticalScrollbarNode.scrollTop).toBe(10)
+ })
+
+ it('parses negative scrollSensitivity values at the minimum', async function () {
+ atom.config.set('editor.scrollSensitivity', -50)
+ componentNode.dispatchEvent(new WheelEvent('mousewheel', {
+ wheelDeltaX: 0,
+ wheelDeltaY: -10
+ }))
+ await nextAnimationFramePromise()
+ expect(verticalScrollbarNode.scrollTop).toBe(1)
+ })
+ })
+
+ describe('when the mousewheel event\'s target is a line', function () {
+ it('keeps the line on the DOM if it is scrolled off-screen', async function () {
+ component.presenter.stoppedScrollingDelay = 3000 // account for slower build machines
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 20 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextViewUpdatePromise()
+
+ expect(componentNode.contains(lineNode)).toBe(true)
+ })
+
+ it('does not set the mouseWheelScreenRow if scrolling horizontally', async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 20 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextAnimationFramePromise()
+
+ 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)
+
+ await conditionPromise(function () {
+ return component.presenter.mouseWheelScreenRow == null
+ })
+ })
+
+ 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', async function () {
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 20 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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)
+ await nextAnimationFramePromise()
+
+ expect(componentNode.contains(lineNumberNode)).toBe(true)
+ })
+ })
+
+ it('only prevents the default action of the mousewheel event if it actually lead to scrolling', async function () {
+ spyOn(WheelEvent.prototype, 'preventDefault').andCallThrough()
+ wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
+ wrapperNode.style.width = 20 * charWidth + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ 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
+ }))
+ await nextAnimationFramePromise()
+
+ 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
+ }))
+ await nextAnimationFramePromise()
+
+ 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
+ }
+
+ let inputNode
+
+ beforeEach(function () {
+ inputNode = componentNode.querySelector('.hidden-input')
+ })
+
+ it('inserts the newest character in the input\'s value into the buffer', async function () {
+ componentNode.dispatchEvent(buildTextInputEvent({
+ data: 'x',
+ target: inputNode
+ }))
+ await nextViewUpdatePromise()
+
+ 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 the length of the input\'s value does not increase, as occurs with the accented character menu', async function () {
+ componentNode.dispatchEvent(buildTextInputEvent({
+ data: 'u',
+ target: inputNode
+ }))
+ await nextViewUpdatePromise()
+
+ expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {')
+ inputNode.setSelectionRange(0, 1)
+ componentNode.dispatchEvent(buildTextInputEvent({
+ data: 'ü',
+ target: inputNode
+ }))
+ await nextViewUpdatePromise()
+
+ expect(editor.lineTextForBufferRow(0)).toBe('üvar quicksort = function () {')
+ })
+
+ it('does not handle input events when input is disabled', async function () {
+ component.setInputEnabled(false)
+ componentNode.dispatchEvent(buildTextInputEvent({
+ data: 'x',
+ target: inputNode
+ }))
+ expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {')
+ await nextAnimationFramePromise()
+ 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
+ })
+ atom.config.set('editor.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 changing the font', async function () {
+ it('measures the default char, the korean char, the double width char and the half width char widths', async function () {
+ expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0)
+ component.setFontSize(10)
+ await nextViewUpdatePromise()
+ expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0)
+ expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0)
+ expect(editor.getDoubleWidthCharWidth()).toBe(10)
+ expect(editor.getHalfWidthCharWidth()).toBe(5)
+ })
+ })
+
+ 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', async 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])
+ await nextViewUpdatePromise()
+ 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', async function () {
+ wrapperNode.style.display = 'none'
+ component.checkForVisibilityChange()
+ component.setFontFamily('serif')
+ wrapperNode.style.display = ''
+ component.checkForVisibilityChange()
+ editor.setCursorBufferPosition([0, Infinity])
+ await nextViewUpdatePromise()
+ 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', async function () {
+ atom.config.set('editor.fontFamily', 'sans-serif')
+ wrapperNode.style.display = 'none'
+ component.checkForVisibilityChange()
+ atom.themes.applyStylesheet('test', '.function.js {\n font-weight: bold;\n}')
+ wrapperNode.style.display = ''
+ component.checkForVisibilityChange()
+ editor.setCursorBufferPosition([0, Infinity])
+ await nextViewUpdatePromise()
+ 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(async function () {
+ editor.setSoftWrapped(true)
+ await nextViewUpdatePromise()
+ })
+
+ it('updates the wrap location when the editor is resized', async function () {
+ let newHeight = 4 * editor.getLineHeightInPixels() + 'px'
+ expect(parseInt(newHeight)).toBeLessThan(wrapperNode.offsetHeight)
+ wrapperNode.style.height = newHeight
+ await nextViewUpdatePromise()
+
+ expect(componentNode.querySelectorAll('.line')).toHaveLength(7)
+ let gutterWidth = componentNode.querySelector('.gutter').offsetWidth
+ componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
+ atom.views.performDocumentPoll()
+ await nextViewUpdatePromise()
+ expect(componentNode.querySelector('.line').textContent).toBe('var quicksort ')
+ })
+
+ it('accounts for the scroll view\'s padding when determining the wrap location', async function () {
+ let scrollViewNode = componentNode.querySelector('.scroll-view')
+ scrollViewNode.style.paddingLeft = 20 + 'px'
+ componentNode.style.width = 30 * charWidth + 'px'
+ atom.views.performDocumentPoll()
+ await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = ')
+ })
+ })
+
+ describe('default decorations', function () {
+ it('applies .cursor-line decorations for line numbers overlapping selections', async function () {
+ editor.setCursorScreenPosition([4, 4])
+ await nextViewUpdatePromise()
+
+ 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]])
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(3, 'cursor-line')).toBe(true)
+ expect(lineNumberHasClass(4, 'cursor-line')).toBe(true)
+ editor.setSelectedScreenRange([[3, 4], [4, 0]])
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setSelectedScreenRange([[3, 4], [5, 0]])
+ await nextViewUpdatePromise()
+ 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', async function () {
+ editor.setCursorScreenPosition([4, 4])
+ await nextViewUpdatePromise()
+
+ 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]])
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setCursorScreenPosition([4, 4])
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(true)
+ editor.setSelectedScreenRange([[3, 4], [4, 4]])
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(false)
+ })
+ })
+
+ describe('height', function () {
+ describe('when the wrapper view has an explicit height', function () {
+ it('does not assign a height on the component node', async function () {
+ wrapperNode.style.height = '200px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ 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('when the "mini" property is true', function () {
+ beforeEach(async function () {
+ editor.setMini(true)
+ await nextViewUpdatePromise()
+ })
+
+ 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 () {
+ atom.config.set('editor.invisibles', {
+ eol: 'E'
+ })
+ atom.config.set('editor.showInvisibles', true)
+ 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', async function () {
+ editor.setPlaceholderText('Hello World')
+ expect(componentNode.querySelector('.placeholder-text')).toBeNull()
+ editor.setText('')
+ await nextViewUpdatePromise()
+
+ expect(componentNode.querySelector('.placeholder-text').textContent).toBe('Hello World')
+ editor.setText('hey')
+ await nextViewUpdatePromise()
+
+ 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('scoped config settings', function () {
+ let coffeeComponent, coffeeEditor
+
+ beforeEach(async function () {
+ await atom.packages.activatePackage('language-coffee-script')
+ coffeeEditor = await atom.workspace.open('coffee.coffee', {autoIndent: false})
+ })
+
+ afterEach(function () {
+ atom.packages.deactivatePackages()
+ atom.packages.unloadPackages()
+ })
+
+ describe('soft wrap settings', function () {
+ beforeEach(function () {
+ atom.config.set('editor.softWrap', true, {
+ scopeSelector: '.source.coffee'
+ })
+ atom.config.set('editor.preferredLineLength', 17, {
+ scopeSelector: '.source.coffee'
+ })
+ atom.config.set('editor.softWrapAtPreferredLineLength', true, {
+ scopeSelector: '.source.coffee'
+ })
+ editor.setDefaultCharWidth(1)
+ editor.setEditorWidthInChars(20)
+ coffeeEditor.setDefaultCharWidth(1)
+ coffeeEditor.setEditorWidthInChars(20)
+ })
+
+ it('wraps lines when editor.softWrap is true for a matching scope', function () {
+ expect(editor.lineTextForScreenRow(2)).toEqual(' if (items.length <= 1) return items;')
+ expect(coffeeEditor.lineTextForScreenRow(3)).toEqual(' return items ')
+ })
+
+ it('updates the wrapped lines when editor.preferredLineLength changes', function () {
+ atom.config.set('editor.preferredLineLength', 20, {
+ scopeSelector: '.source.coffee'
+ })
+ expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if ')
+ })
+
+ it('updates the wrapped lines when editor.softWrapAtPreferredLineLength changes', function () {
+ atom.config.set('editor.softWrapAtPreferredLineLength', false, {
+ scopeSelector: '.source.coffee'
+ })
+ expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if ')
+ })
+
+ it('updates the wrapped lines when editor.softWrap changes', function () {
+ atom.config.set('editor.softWrap', false, {
+ scopeSelector: '.source.coffee'
+ })
+ expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if items.length <= 1')
+ atom.config.set('editor.softWrap', true, {
+ scopeSelector: '.source.coffee'
+ })
+ expect(coffeeEditor.lineTextForScreenRow(3)).toEqual(' return items ')
+ })
+
+ it('updates the wrapped lines when the grammar changes', function () {
+ editor.setGrammar(coffeeEditor.getGrammar())
+ expect(editor.isSoftWrapped()).toBe(true)
+ expect(editor.lineTextForScreenRow(0)).toEqual('var quicksort = ')
+ })
+
+ describe('::isSoftWrapped()', function () {
+ it('returns the correct value based on the scoped settings', function () {
+ expect(editor.isSoftWrapped()).toBe(false)
+ expect(coffeeEditor.isSoftWrapped()).toBe(true)
+ })
+ })
+ })
+
+ describe('invisibles settings', function () {
+ const jsInvisibles = {
+ eol: 'J',
+ space: 'A',
+ tab: 'V',
+ cr: 'A'
+ }
+ const coffeeInvisibles = {
+ eol: 'C',
+ space: 'O',
+ tab: 'F',
+ cr: 'E'
+ }
+
+ beforeEach(async function () {
+ atom.config.set('editor.showInvisibles', true, {
+ scopeSelector: '.source.js'
+ })
+ atom.config.set('editor.invisibles', jsInvisibles, {
+ scopeSelector: '.source.js'
+ })
+ atom.config.set('editor.showInvisibles', false, {
+ scopeSelector: '.source.coffee'
+ })
+ atom.config.set('editor.invisibles', coffeeInvisibles, {
+ scopeSelector: '.source.coffee'
+ })
+ editor.setText(' a line with tabs\tand spaces \n')
+ await nextViewUpdatePromise()
+ })
+
+ it('renders the invisibles when editor.showInvisibles is true for a given grammar', function () {
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('' + jsInvisibles.space + 'a line with tabs' + jsInvisibles.tab + 'and spaces' + jsInvisibles.space + jsInvisibles.eol)
+ })
+
+ it('does not render the invisibles when editor.showInvisibles is false for a given grammar', async function () {
+ editor.setGrammar(coffeeEditor.getGrammar())
+ await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ')
+ })
+
+ it('re-renders the invisibles when the invisible settings change', async function () {
+ let jsGrammar = editor.getGrammar()
+ editor.setGrammar(coffeeEditor.getGrammar())
+ atom.config.set('editor.showInvisibles', true, {
+ scopeSelector: '.source.coffee'
+ })
+ await nextViewUpdatePromise()
+
+ let newInvisibles = {
+ eol: 'N',
+ space: 'E',
+ tab: 'W',
+ cr: 'I'
+ }
+
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('' + coffeeInvisibles.space + 'a line with tabs' + coffeeInvisibles.tab + 'and spaces' + coffeeInvisibles.space + coffeeInvisibles.eol)
+ atom.config.set('editor.invisibles', newInvisibles, {
+ scopeSelector: '.source.coffee'
+ })
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('' + newInvisibles.space + 'a line with tabs' + newInvisibles.tab + 'and spaces' + newInvisibles.space + newInvisibles.eol)
+ editor.setGrammar(jsGrammar)
+ await nextViewUpdatePromise()
+
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('' + jsInvisibles.space + 'a line with tabs' + jsInvisibles.tab + 'and spaces' + jsInvisibles.space + jsInvisibles.eol)
+ })
+ })
+
+ describe('editor.showIndentGuide', function () {
+ beforeEach(async function () {
+ atom.config.set('editor.showIndentGuide', true, {
+ scopeSelector: '.source.js'
+ })
+ atom.config.set('editor.showIndentGuide', false, {
+ scopeSelector: '.source.coffee'
+ })
+ await nextViewUpdatePromise()
+ })
+
+ it('has an "indent-guide" class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false', async 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)
+ editor.setGrammar(coffeeEditor.getGrammar())
+ await nextViewUpdatePromise()
+
+ line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1))
+ expect(line1LeafNodes[0].textContent).toBe(' ')
+ expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(false)
+ expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false)
+ })
+
+ it('removes the "indent-guide" class when editor.showIndentGuide to false', async 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)
+ atom.config.set('editor.showIndentGuide', false, {
+ scopeSelector: '.source.js'
+ })
+ await nextViewUpdatePromise()
+
+ line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1))
+ expect(line1LeafNodes[0].textContent).toBe(' ')
+ expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(false)
+ expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false)
+ })
+ })
+ })
+
+ describe('autoscroll', function () {
+ beforeEach(async function () {
+ editor.setVerticalScrollMargin(2)
+ editor.setHorizontalScrollMargin(2)
+ component.setLineHeight('10px')
+ component.setFontSize(17)
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ wrapperNode.setWidth(55)
+ wrapperNode.setHeight(55)
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ component.presenter.setHorizontalScrollbarHeight(0)
+ component.presenter.setVerticalScrollbarWidth(0)
+ await nextViewUpdatePromise()
+ })
+
+ describe('when selecting buffer ranges', function () {
+ it('autoscrolls the selection if it is last unless the "autoscroll" option is false', async function () {
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.setSelectedBufferRange([[5, 6], [6, 8]])
+ await nextViewUpdatePromise()
+
+ 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]])
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ expect(wrapperNode.getScrollLeft()).toBe(0)
+ editor.setSelectedBufferRange([[6, 6], [6, 8]])
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.addSelectionForBufferRange([[8, 10], [8, 15]])
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setCursorScreenPosition([5, 6])
+ await nextViewUpdatePromise()
+
+ wrapperNode.scrollToTop()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.selectLinesContainingCursors()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setCursorScreenPosition([1, 2], {
+ autoscroll: false
+ })
+ await nextViewUpdatePromise()
+
+ editor.addCursorAtScreenPosition([10, 4], {
+ autoscroll: false
+ })
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.insertText('a')
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setCursorScreenPosition([8, 8], {
+ autoscroll: false
+ })
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ expect(wrapperNode.getScrollLeft()).toBe(0)
+ editor.scrollToCursorPosition()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10)
+ editor.setCursorScreenPosition([2, 0])
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10)
+ editor.moveDown()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollBottom()).toBe(6 * 10)
+ editor.moveDown()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollBottom()).toBe(7 * 10)
+ })
+
+ it('scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor', async function () {
+ editor.setCursorScreenPosition([11, 0])
+ await nextViewUpdatePromise()
+
+ wrapperNode.setScrollBottom(wrapperNode.getScrollHeight())
+ await nextViewUpdatePromise()
+
+ editor.moveUp()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollBottom()).toBe(wrapperNode.getScrollHeight())
+ editor.moveUp()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(7 * 10)
+ editor.moveUp()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(6 * 10)
+ })
+
+ it('scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor', async function () {
+ expect(wrapperNode.getScrollLeft()).toBe(0)
+ expect(wrapperNode.getScrollRight()).toBe(5.5 * 10)
+ editor.setCursorScreenPosition([0, 2])
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollRight()).toBe(5.5 * 10)
+ editor.moveRight()
+ await nextViewUpdatePromise()
+
+ let margin = component.presenter.getHorizontalScrollMarginInPixels()
+ let right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin
+ expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0)
+ editor.moveRight()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ wrapperNode.setScrollRight(wrapperNode.getScrollWidth())
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollRight()).toBe(wrapperNode.getScrollWidth())
+ editor.setCursorScreenPosition([6, 62], {
+ autoscroll: false
+ })
+ await nextViewUpdatePromise()
+
+ editor.moveLeft()
+ await nextViewUpdatePromise()
+
+ let margin = component.presenter.getHorizontalScrollMarginInPixels()
+ let left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin
+ expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0)
+ editor.moveLeft()
+ await nextViewUpdatePromise()
+
+ 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', async function () {
+ editor.setCursorScreenPosition([13, Infinity])
+ editor.insertNewline()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollBottom()).toBe(14 * 10)
+ editor.insertNewline()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollBottom()).toBe(15 * 10)
+ })
+
+ it('autoscrolls to the cursor when it moves due to undo', async function () {
+ editor.insertText('abc')
+ wrapperNode.setScrollTop(Infinity)
+ await nextViewUpdatePromise()
+
+ editor.undo()
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ })
+
+ it('does not scroll when the cursor moves into the visible area', async function () {
+ editor.setCursorBufferPosition([0, 0])
+ await nextViewUpdatePromise()
+
+ wrapperNode.setScrollTop(40)
+ await nextViewUpdatePromise()
+
+ editor.setCursorBufferPosition([6, 0])
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(40)
+ })
+
+ it('honors the autoscroll option on cursor and selection manipulation methods', async function () {
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.addCursorAtScreenPosition([11, 11], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.addCursorAtBufferPosition([11, 11], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.setCursorScreenPosition([11, 11], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.setCursorBufferPosition([11, 11], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.addSelectionForBufferRange([[11, 11], [11, 11]], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.addSelectionForScreenRange([[11, 11], [11, 12]], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.setSelectedBufferRange([[11, 0], [11, 1]], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.setSelectedScreenRange([[11, 0], [11, 6]], {autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.clearSelections({autoscroll: false})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.addSelectionForScreenRange([[0, 0], [0, 4]])
+ await nextViewUpdatePromise()
+
+ editor.getCursors()[0].setScreenPosition([11, 11], {autoscroll: true})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBeGreaterThan(0)
+ editor.getCursors()[0].setBufferPosition([0, 0], {autoscroll: true})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], {autoscroll: true})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBeGreaterThan(0)
+ editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], {autoscroll: true})
+ await nextViewUpdatePromise()
+
+ expect(wrapperNode.getScrollTop()).toBe(0)
+ })
+ })
+ })
+
+ describe('::getVisibleRowRange()', function () {
+ beforeEach(async function () {
+ wrapperNode.style.height = lineHeightInPixels * 8 + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ })
+
+ it('returns the first and the last visible rows', async function () {
+ component.setScrollTop(0)
+ await nextViewUpdatePromise()
+ expect(component.getVisibleRowRange()).toEqual([0, 9])
+ })
+
+ it('ends at last buffer row even if there\'s more space available', async function () {
+ wrapperNode.style.height = lineHeightInPixels * 13 + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+
+ component.setScrollTop(60)
+ await nextViewUpdatePromise()
+
+ expect(component.getVisibleRowRange()).toEqual([0, 13])
+ })
+ })
+
+ 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('ipc'), '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]])
+
+ await conditionPromise(function () {
+ return clipboardWrittenTo
+ })
+
+ 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.displayBuffer.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(function () {
+ if (condition()) {
+ window.clearInterval(interval)
+ window.clearTimeout(timeout)
+ resolve()
+ }
+ }, 100)
+ let timeout = window.setTimeout(function () {
+ window.clearInterval(interval)
+ reject(timeoutError)
+ }, 5000)
+ })
+ }
+
+ function timeoutPromise (timeout) {
+ return new Promise(function (resolve) {
+ window.setTimeout(resolve, timeout)
+ })
+ }
+
+ function nextAnimationFramePromise () {
+ return new Promise(function (resolve) {
+ window.requestAnimationFrame(resolve)
+ })
+ }
+
+ function nextViewUpdatePromise () {
+ let timeoutError = new Error('Timed out waiting on a view update.')
+ Error.captureStackTrace(timeoutError, nextViewUpdatePromise)
+
+ return new Promise(function (resolve, reject) {
+ let nextUpdatePromise = atom.views.getNextUpdatePromise()
+ nextUpdatePromise.then(function (ts) {
+ window.clearTimeout(timeout)
+ resolve(ts)
+ })
+ let timeout = window.setTimeout(function () {
+ timeoutError.message += ' Frame pending? ' + atom.views.animationFrameRequest + ' Same next update promise pending? ' + (nextUpdatePromise === atom.views.nextUpdatePromise)
+ reject(timeoutError)
+ }, 30000)
+ })
+ }
+
+ function decorationsUpdatedPromise(editor) {
+ return new Promise(function (resolve) {
+ let disposable = editor.onDidUpdateDecorations(function () {
+ disposable.dispose()
+ resolve()
+ })
+ })
+ }
+})
diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee
index ae9dfb6a6..54899a1d6 100644
--- a/spec/text-editor-presenter-spec.coffee
+++ b/spec/text-editor-presenter-spec.coffee
@@ -69,6 +69,13 @@ describe "TextEditorPresenter", ->
expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn)
+ waitsForStateToUpdate = (presenter, fn) ->
+ waitsFor "presenter state to update", 1000, (done) ->
+ fn?()
+ disposable = presenter.onDidUpdateState ->
+ disposable.dispose()
+ process.nextTick(done)
+
tiledContentContract = (stateFn) ->
it "contains states for tiles that are visible on screen", ->
presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2)
@@ -1162,55 +1169,62 @@ describe "TextEditorPresenter", ->
describe ".decorationClasses", ->
it "adds decoration classes to the relevant line state objects, both initially and when decorations change", ->
- marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
+ marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a')
presenter = buildPresenter()
- marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
+ marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
- 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
+ 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()
- expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
- 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.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()
- expectStateUpdate presenter, -> editor.undo()
- 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.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()
- expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
- 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, -> 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()
- expectStateUpdate presenter, -> decoration1.destroy()
- 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, -> 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()
- expectStateUpdate presenter, -> marker2.destroy()
- 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()
+ 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()
@@ -1221,11 +1235,12 @@ describe "TextEditorPresenter", ->
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
- expectStateUpdate presenter, -> marker.clearTail()
+ waitsForStateToUpdate presenter, -> marker.clearTail()
- expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
- expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
- expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
+ 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()
@@ -1236,40 +1251,49 @@ describe "TextEditorPresenter", ->
expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
- expectStateUpdate presenter, -> marker.clearTail()
+ waitsForStateToUpdate presenter, -> marker.clearTail()
- expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
+ runs ->
+ expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "honors the 'onlyHead' option on line decorations", ->
presenter = buildPresenter()
- marker = editor.markBufferRange([[4, 0], [6, 2]])
- decoration = editor.decorateMarker(marker, type: 'line', class: 'a', onlyHead: true)
+ waitsForStateToUpdate presenter, ->
+ marker = editor.markBufferRange([[4, 0], [6, 2]])
+ editor.decorateMarker(marker, type: 'line', class: 'a', onlyHead: true)
- expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
- expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
- expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
+ 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()
- marker = editor.markBufferRange([[4, 0], [6, 0]])
- decoration = editor.decorateMarker(marker, type: 'line', class: 'a')
+ waitsForStateToUpdate presenter, ->
+ marker = editor.markBufferRange([[4, 0], [6, 0]])
+ editor.decorateMarker(marker, type: 'line', class: 'a')
- expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a']
- expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a']
- expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
+ 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)
- marker = editor.markBufferRange([[0, 0], [0, 0]])
- decoration = editor.decorateMarker(marker, type: 'line', class: 'a')
- expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull()
- expectStateUpdate presenter, -> editor.setMini(false)
- expect(lineStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'a']
+ waitsForStateToUpdate presenter, ->
+ marker = editor.markBufferRange([[0, 0], [0, 0]])
+ decoration = editor.decorateMarker(marker, type: 'line', class: 'a')
- expectStateUpdate presenter, -> editor.setMini(true)
- expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull()
+ 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")
@@ -1283,9 +1307,12 @@ describe "TextEditorPresenter", ->
expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
- marker.setBufferRange([[0, 0], [0, Infinity]])
- expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
- expect(lineStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
+ 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) ->
@@ -1766,41 +1793,51 @@ describe "TextEditorPresenter", ->
expectUndefinedStateForSelection(presenter, 1)
# moving into view
- expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
- expectValues stateForSelectionInTile(presenter, 1, 2), {
- regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
- }
+ 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
- expectStateUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false)
- expectUndefinedStateForSelection(presenter, 1)
+ waitsForStateToUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false)
+ runs ->
+ expectUndefinedStateForSelection(presenter, 1)
# becoming non-empty
- expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
- expectValues stateForSelectionInTile(presenter, 1, 2), {
- regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
- }
+ 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}]
+ }
# moving out of view
- expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
- expectUndefinedStateForSelection(presenter, 1)
+ waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
+ runs ->
+ expectUndefinedStateForSelection(presenter, 1)
# adding
- expectStateUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
- expectValues stateForSelectionInTile(presenter, 2, 0), {
- regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}]
- }
+ waitsForStateToUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
+ runs ->
+ expectValues stateForSelectionInTile(presenter, 2, 0), {
+ regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}]
+ }
# moving added selection
- expectStateUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
- expectValues stateForSelectionInTile(presenter, 2, 0), {
- regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}]
- }
+ waitsForStateToUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
- # destroying
- destroyedSelection = editor.getSelections()[2]
- expectStateUpdate presenter, -> destroyedSelection.destroy()
- expectUndefinedStateForHighlight(presenter, destroyedSelection.decoration)
+ destroyedSelection = null
+ runs ->
+ expectValues stateForSelectionInTile(presenter, 2, 0), {
+ regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}]
+ }
+
+ # destroying
+ destroyedSelection = editor.getSelections()[2]
+
+ waitsForStateToUpdate presenter, -> destroyedSelection.destroy()
+ runs ->
+ expectUndefinedStateForHighlight(presenter, destroyedSelection.decoration)
it "updates when highlight decorations' properties are updated", ->
marker = editor.markBufferPosition([2, 2])
@@ -1810,44 +1847,45 @@ describe "TextEditorPresenter", ->
expectUndefinedStateForHighlight(presenter, highlight)
- expectStateUpdate presenter, ->
+ waitsForStateToUpdate presenter, ->
marker.setBufferRange([[2, 2], [2, 4]])
highlight.setProperties(class: 'b', type: 'highlight')
- expectValues stateForHighlightInTile(presenter, highlight, 2), {class: 'b'}
+ 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 = editor.decorateMarker(marker, type: 'highlight', class: 'a')
- expectStateUpdate presenter, ->
+ waitsForStateToUpdate presenter, ->
marker.setBufferRange([[2, 2], [5, 2]])
highlight.flash('b', 500)
+ runs ->
+ expectValues stateForHighlightInTile(presenter, highlight, 2), {
+ flashClass: 'b'
+ flashDuration: 500
+ flashCount: 1
+ }
+ expectValues stateForHighlightInTile(presenter, highlight, 4), {
+ flashClass: 'b'
+ flashDuration: 500
+ flashCount: 1
+ }
- expectValues stateForHighlightInTile(presenter, highlight, 2), {
- flashClass: 'b'
- flashDuration: 500
- flashCount: 1
- }
- expectValues stateForHighlightInTile(presenter, highlight, 4), {
- flashClass: 'b'
- flashDuration: 500
- flashCount: 1
- }
-
- expectStateUpdate presenter, -> highlight.flash('c', 600)
-
- expectValues stateForHighlightInTile(presenter, highlight, 2), {
- flashClass: 'c'
- flashDuration: 600
- flashCount: 2
- }
- expectValues stateForHighlightInTile(presenter, highlight, 4), {
- flashClass: 'c'
- flashDuration: 600
- flashCount: 2
- }
+ waitsForStateToUpdate presenter, -> highlight.flash('c', 600)
+ runs ->
+ expectValues stateForHighlightInTile(presenter, highlight, 2), {
+ flashClass: 'c'
+ flashDuration: 600
+ flashCount: 2
+ }
+ expectValues stateForHighlightInTile(presenter, highlight, 4), {
+ flashClass: 'c'
+ flashDuration: 600
+ flashCount: 2
+ }
describe ".overlays", ->
[item] = []
@@ -1855,7 +1893,7 @@ describe "TextEditorPresenter", ->
presenter.getState().content.overlays[decoration.id]
it "contains state for overlay decorations both initially and when their markers move", ->
- marker = editor.markBufferPosition([2, 13], invalidate: 'touch', maintainHistory: true)
+ marker = editor.addMarkerLayer(maintainHistory: true).markBufferPosition([2, 13], invalidate: 'touch')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
presenter = buildPresenter(explicitHeight: 30, scrollTop: 20)
@@ -1866,40 +1904,47 @@ describe "TextEditorPresenter", ->
}
# Change range
- expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]])
- expectValues stateForOverlay(presenter, decoration), {
- item: item
- pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10}
- }
+ 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
- expectStateUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x')
- expect(stateForOverlay(presenter, decoration)).toBeUndefined()
+ # Valid -> invalid
+ waitsForStateToUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x')
+ runs ->
+ expect(stateForOverlay(presenter, decoration)).toBeUndefined()
- # Invalid -> valid
- expectStateUpdate presenter, -> editor.undo()
- expectValues stateForOverlay(presenter, decoration), {
- item: item
- pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10}
- }
+ # 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
- expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true)
- expectValues stateForOverlay(presenter, decoration), {
- item: item
- pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10}
- }
+ 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
- decoration.destroy()
- expect(stateForOverlay(presenter, decoration)).toBeUndefined()
+ waitsForStateToUpdate presenter, -> decoration.destroy()
+ runs ->
+ expect(stateForOverlay(presenter, decoration)).toBeUndefined()
# Add
- decoration2 = editor.decorateMarker(marker, {type: 'overlay', item})
- expectValues stateForOverlay(presenter, decoration2), {
- item: item
- pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10}
- }
+ 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
@@ -2343,11 +2388,11 @@ describe "TextEditorPresenter", ->
describe ".decorationClasses", ->
it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", ->
- marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
+ marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a')
- presenter = buildPresenter()
- marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true)
+ 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']
@@ -2355,85 +2400,92 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b']
expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull()
- expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x')
- 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.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()
- expectStateUpdate presenter, -> editor.undo()
- 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.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()
- expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]])
- 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, -> 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()
- expectStateUpdate presenter, -> decoration1.destroy()
- 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, -> 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()
- expectStateUpdate presenter, -> marker2.destroy()
- 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()
+ 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", ->
- presenter = buildPresenter()
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()
- expectStateUpdate presenter, -> marker.clearTail()
+ waitsForStateToUpdate presenter, -> marker.clearTail()
- expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull()
- expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull()
- expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a']
+ 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", ->
- presenter = buildPresenter()
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']
- expectStateUpdate presenter, -> marker.clearTail()
+ waitsForStateToUpdate presenter, -> marker.clearTail()
- expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
+ runs ->
+ expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull()
it "honors the 'onlyHead' option on line-number decorations", ->
- presenter = buildPresenter()
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", ->
- presenter = buildPresenter()
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']
@@ -2465,9 +2517,10 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
- marker.setBufferRange([[0, 0], [0, Infinity]])
- expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
- expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
+ waitsForStateToUpdate presenter, -> marker.setBufferRange([[0, 0], [0, Infinity]])
+ runs ->
+ expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
+ expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
describe ".foldable", ->
it "marks line numbers at the start of a foldable region as foldable", ->
@@ -2600,14 +2653,15 @@ describe "TextEditorPresenter", ->
it "updates when a decoration's marker is modified", ->
# This update will move decoration1 out of view.
- expectStateUpdate presenter, ->
+ waitsForStateToUpdate presenter, ->
newRange = new Range([13, 0], [14, 0])
marker1.setBufferRange(newRange)
- decorationState = getContentForGutterWithName(presenter, 'test-gutter')
- expect(decorationState[decoration1.id]).toBeUndefined()
- expect(decorationState[decoration2.id].top).toBeDefined()
- expect(decorationState[decoration3.id]).toBeUndefined()
+ 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", ->
@@ -2619,12 +2673,14 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter'
class: 'test-class'
item: newItem
- expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
- decorationState = getContentForGutterWithName(presenter, 'test-gutter')
- expect(decorationState[decoration1.id].item).toBe newItem
- expect(decorationState[decoration2.id].item).toBe decorationItem
- expect(decorationState[decoration3.id]).toBeUndefined()
+ 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.
@@ -2633,12 +2689,13 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter'
class: 'new-test-class'
item: decorationItem
- expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
+ waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams)
- 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()
+ 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.
@@ -2647,12 +2704,13 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter' # This is an invalid/meaningless option here, but it shouldn't matter.
class: 'test-class'
item: decorationItem
- expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
+ waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams)
- decorationState = getContentForGutterWithName(presenter, 'test-gutter')
- expect(decorationState[decoration1.id]).toBeUndefined()
- expect(decorationState[decoration2.id].top).toBeDefined()
- expect(decorationState[decoration3.id]).toBeUndefined()
+ 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,
@@ -2662,24 +2720,25 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter-2'
class: 'new-test-class'
item: decorationItem
- expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams)
+ waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams)
- decorationState = getContentForGutterWithName(presenter, 'test-gutter')
- expect(decorationState[decoration1.id]).toBeUndefined()
- expect(decorationState[decoration2.id].top).toBeDefined()
- expect(decorationState[decoration3.id]).toBeUndefined()
+ 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()
+ # 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)
@@ -2714,13 +2773,17 @@ describe "TextEditorPresenter", ->
class: 'test-class'
marker4 = editor.markBufferRange([[0, 0], [1, 0]])
decoration4 = editor.decorateMarker(marker4, decorationParams)
- 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()
+ waitsForStateToUpdate presenter
+
+ 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 =
diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee
index b0c40d588..05e454da3 100644
--- a/spec/text-editor-spec.coffee
+++ b/spec/text-editor-spec.coffee
@@ -2123,6 +2123,692 @@ describe "TextEditor", ->
expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges()
describe "buffer manipulation", ->
+ describe ".moveLineUp", ->
+ it "moves the line under the cursor up", ->
+ editor.setCursorBufferPosition([1, 0])
+ editor.moveLineUp()
+ expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe " var sort = function(items) {"
+ expect(editor.indentationForBufferRow(0)).toBe 1
+ expect(editor.indentationForBufferRow(1)).toBe 0
+
+ it "updates the line's indentation when the editor.autoIndent setting is true", ->
+ atom.config.set('editor.autoIndent', true)
+ editor.setCursorBufferPosition([1, 0])
+ editor.moveLineUp()
+ expect(editor.indentationForBufferRow(0)).toBe 0
+ expect(editor.indentationForBufferRow(1)).toBe 0
+
+ describe "when there is a single selection", ->
+ describe "when the selection spans a single line", ->
+ describe "when there is no fold in the preceeding row", ->
+ it "moves the line to the preceding row", ->
+ expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ editor.setSelectedBufferRange([[3, 2], [3, 9]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 9]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;"
+
+ describe "when the cursor is at the beginning of a fold", ->
+ it "moves the line to the previous row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
+ expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]]
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]]
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeFalsy()
+
+
+ describe "when the preceding row consists of folded code", ->
+ it "moves the line above the folded row and preseveres the correct folds", ->
+ expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(9)).toBe " };"
+
+ editor.createFold(4, 7)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.setSelectedBufferRange([[8, 0], [8, 4]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [4, 4]]
+ expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {"
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ describe "when the selection spans multiple lines", ->
+ it "moves the lines spanned by the selection to the preceding row", ->
+ expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.setSelectedBufferRange([[3, 2], [4, 9]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(4)).toBe " if (items.length <= 1) return items;"
+
+ describe "when the selection's end intersects a fold", ->
+ it "moves the lines to the previous row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
+
+ expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(7)).toBe " if (items.length <= 1) return items;"
+
+ expect(editor.isFoldedAtBufferRow(2)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeFalsy()
+
+ describe "when the selection's start intersects a fold", ->
+ it "moves the lines to the previous row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [7, 9]]
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(7)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(8)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+
+ describe "when the selection spans multiple lines, but ends at column 0", ->
+ it "does not move the last line of the selection", ->
+ expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.setSelectedBufferRange([[3, 2], [4, 0]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 0]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ describe "when the preceeding row is a folded row", ->
+ it "moves the lines spanned by the selection to the preceeding row, but preserves the folded code", ->
+ expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(9)).toBe " };"
+
+ editor.createFold(4, 7)
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.setSelectedBufferRange([[8, 0], [9, 2]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 2]]
+ expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(5)).toBe " };"
+ expect(editor.lineTextForBufferRow(6)).toBe " while(items.length > 0) {"
+ expect(editor.isFoldedAtBufferRow(5)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(10)).toBeFalsy()
+
+ describe "when there are multiple selections", ->
+ describe "when all the selections span different lines", ->
+ describe "when there is no folds", ->
+ it "moves all lines that are spanned by a selection to the preceding row", ->
+ editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]
+ expect(editor.lineTextForBufferRow(0)).toBe " var sort = function(items) {"
+ expect(editor.lineTextForBufferRow(1)).toBe "var quicksort = function () {"
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(4)).toBe " current = items.shift();"
+ expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {"
+
+ describe "when one selection intersects a fold", ->
+ it "moves the lines to the previous row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRanges([
+ [[2, 2], [2, 9]],
+ [[4, 2], [4, 9]]
+ ], preserveFolds: true)
+
+ expect(editor.isFoldedAtBufferRow(2)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRanges()).toEqual([
+ [[1, 2], [1, 9]],
+ [[3, 2], [3, 9]]
+ ])
+
+ expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {"
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ expect(editor.isFoldedAtBufferRow(1)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(2)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ describe "when there is a fold", ->
+ it "moves all lines that spanned by a selection to preceding row, preserving all folds", ->
+ editor.createFold(4, 7)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.setSelectedBufferRanges([[[8, 0], [8, 3]], [[11, 0], [11, 5]]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[4, 0], [4, 3]], [[10, 0], [10, 5]]]
+ expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));"
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ describe 'when there are many folds', ->
+ beforeEach ->
+ waitsForPromise ->
+ atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o
+
+ describe 'and many selections intersects folded rows', ->
+ it 'moves and preserves all the folds', ->
+ editor.createFold(2, 4)
+ editor.createFold(7, 9)
+
+ editor.setSelectedBufferRanges([
+ [[1, 0], [5, 4]],
+ [[7, 0], [7, 4]]
+ ], preserveFolds: true)
+
+ editor.moveLineUp()
+
+ expect(editor.lineTextForBufferRow(1)).toEqual "function f3() {"
+ expect(editor.lineTextForBufferRow(4)).toEqual "6;"
+ expect(editor.lineTextForBufferRow(5)).toEqual "1;"
+ expect(editor.lineTextForBufferRow(6)).toEqual "function f8() {"
+ expect(editor.lineTextForBufferRow(9)).toEqual "7;"
+
+ expect(editor.isFoldedAtBufferRow(1)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(2)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeFalsy()
+
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ describe "when some of the selections span the same lines", ->
+ it "moves lines that contain multiple selections correctly", ->
+ editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]])
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [2, 9]], [[2, 12], [2, 13]]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ describe "when one of the selections spans line 0", ->
+ it "doesn't move any lines, since line 0 can't move", ->
+ editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]])
+
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]
+ expect(buffer.isModified()).toBe false
+
+ describe "when one of the selections spans the last line, and it is empty", ->
+ it "doesn't move any lines, since the last line can't move", ->
+ buffer.append('\n')
+ editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]])
+
+ editor.moveLineUp()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]
+
+ describe ".moveLineDown", ->
+ it "moves the line under the cursor down", ->
+ editor.setCursorBufferPosition([0, 0])
+ editor.moveLineDown()
+ expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe "var quicksort = function () {"
+ expect(editor.indentationForBufferRow(0)).toBe 1
+ expect(editor.indentationForBufferRow(1)).toBe 0
+
+ it "updates the line's indentation when the editor.autoIndent setting is true", ->
+ atom.config.set('editor.autoIndent', true)
+ editor.setCursorBufferPosition([0, 0])
+ editor.moveLineDown()
+ expect(editor.indentationForBufferRow(0)).toBe 1
+ expect(editor.indentationForBufferRow(1)).toBe 2
+
+ describe "when there is a single selection", ->
+ describe "when the selection spans a single line", ->
+ describe "when there is no fold in the following row", ->
+ it "moves the line to the following row", ->
+ expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ editor.setSelectedBufferRange([[2, 2], [2, 9]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;"
+
+ describe "when the cursor is at the beginning of a fold", ->
+ it "moves the line to the following row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [5, 9]]
+ expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {"
+
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ describe "when the following row is a folded row", ->
+ it "moves the line below the folded row and preserves the fold", ->
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.setSelectedBufferRange([[3, 0], [3, 4]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[7, 0], [7, 4]]
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeFalsy()
+
+
+ expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ describe "when the selection spans multiple lines", ->
+ it "moves the lines spanned by the selection to the following row", ->
+ expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.setSelectedBufferRange([[2, 2], [3, 9]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 9]]
+ expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ describe "when the selection spans multiple lines, but ends at column 0", ->
+ it "does not move the last line of the selection", ->
+ expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.setSelectedBufferRange([[2, 2], [3, 0]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 0]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ describe "when the selection's end intersects a fold", ->
+ it "moves the lines to the following row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
+
+ expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [5, 9]]
+ expect(editor.lineTextForBufferRow(3)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {"
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ describe "when the selection's start intersects a fold", ->
+ it "moves the lines to the following row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [9, 9]]
+ expect(editor.lineTextForBufferRow(4)).toBe " };"
+ expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(10)).toBeFalsy()
+
+ describe "when the following row is a folded row", ->
+ it "moves the lines spanned by the selection to the following row, but preserves the folded code", ->
+ expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ editor.createFold(4, 7)
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.setSelectedBufferRange([[2, 0], [3, 2]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRange()).toEqual [[6, 0], [7, 2]]
+ expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {"
+ expect(editor.isFoldedAtBufferRow(1)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(2)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeFalsy()
+ expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;"
+
+ describe "when there are multiple selections", ->
+ describe "when all the selections span different lines", ->
+ describe "when there is no folds", ->
+ it "moves all lines that are spanned by a selection to the following row", ->
+ editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]]]
+ expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {"
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(5)).toBe " current < pivot ? left.push(current) : right.push(current);"
+ expect(editor.lineTextForBufferRow(6)).toBe " current = items.shift();"
+
+ describe 'when there are many folds', ->
+ beforeEach ->
+ waitsForPromise ->
+ atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o
+
+ describe 'and many selections intersects folded rows', ->
+ it 'moves and preserves all the folds', ->
+ editor.createFold(2, 4)
+ editor.createFold(7, 9)
+
+ editor.setSelectedBufferRanges([
+ [[2, 0], [2, 4]],
+ [[6, 0], [10, 4]]
+ ], preserveFolds: true)
+
+ editor.moveLineDown()
+
+ expect(editor.lineTextForBufferRow(2)).toEqual "6;"
+ expect(editor.lineTextForBufferRow(3)).toEqual "function f3() {"
+ expect(editor.lineTextForBufferRow(6)).toEqual "12;"
+ expect(editor.lineTextForBufferRow(7)).toEqual "7;"
+ expect(editor.lineTextForBufferRow(8)).toEqual "function f8() {"
+ expect(editor.lineTextForBufferRow(11)).toEqual "11;"
+
+ expect(editor.isFoldedAtBufferRow(2)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(10)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(11)).toBeFalsy()
+
+ describe "when there is a fold below one of the selected row", ->
+ it "moves all lines spanned by a selection to the following row, preserving the fold", ->
+ editor.createFold(4, 7)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.setSelectedBufferRanges([[[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]]]
+ expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {"
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeFalsy()
+ expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+
+ describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", ->
+ it "moves all the lines below the fold, preserving the fold", ->
+ editor.createFold(4, 7)
+
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+ editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [7, 4]], [[6, 2], [6, 6]]]
+ expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {"
+ expect(editor.isFoldedAtBufferRow(2)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeFalsy()
+ expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+
+ describe "when one selection intersects a fold", ->
+ it "moves the lines to the previous row without breaking the fold", ->
+ expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
+
+ editor.createFold(4, 7)
+ editor.setSelectedBufferRanges([
+ [[2, 2], [2, 9]],
+ [[4, 2], [4, 9]]
+ ], preserveFolds: true)
+
+ expect(editor.isFoldedAtBufferRow(2)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRanges()).toEqual([
+ [[5, 2], [5, 9]]
+ [[3, 2], [3, 9]],
+ ])
+
+ expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;"
+ expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+
+ expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {"
+ expect(editor.lineTextForBufferRow(9)).toBe " };"
+
+ expect(editor.isFoldedAtBufferRow(2)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(4)).toBeFalsy()
+ expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(8)).toBeTruthy()
+ expect(editor.isFoldedAtBufferRow(9)).toBeFalsy()
+
+ describe "when some of the selections span the same lines", ->
+ it "moves lines that contain multiple selections correctly", ->
+ editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]])
+ editor.moveLineDown()
+
+ expect(editor.getSelectedBufferRanges()).toEqual [[[4, 12], [4, 13]], [[4, 2], [4, 9]]]
+ expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {"
+
+ describe "when the selections are above a wrapped line", ->
+ beforeEach ->
+ editor.setSoftWrapped(true)
+ editor.setEditorWidthInChars(80)
+ editor.setText("""
+ 1
+ 2
+ 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.
+ 3
+ 4
+ """)
+
+ it 'moves the lines past the soft wrapped line', ->
+ editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]])
+
+ editor.moveLineDown()
+
+ expect(editor.lineTextForBufferRow(0)).not.toBe "2"
+ expect(editor.lineTextForBufferRow(1)).toBe "1"
+ expect(editor.lineTextForBufferRow(2)).toBe "2"
+
describe ".insertText(text)", ->
describe "when there is a single selection", ->
beforeEach ->
@@ -3903,7 +4589,10 @@ describe "TextEditor", ->
expect(buffer.getLineCount()).toBe(count - 1)
describe "when the line being deleted preceeds a fold, and the command is undone", ->
- it "restores the line and preserves the fold", ->
+ # 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", ->
editor.setCursorBufferPosition([4])
editor.foldCurrentRow()
expect(editor.isFoldedAtScreenRow(4)).toBeTruthy()
@@ -4371,11 +5060,12 @@ describe "TextEditor", ->
expect(coffeeEditor.lineTextForBufferRow(2)).toBe ""
describe ".destroy()", ->
- it "destroys all markers associated with the edit session", ->
- editor.foldAll()
- expect(buffer.getMarkerCount()).toBeGreaterThan 0
+ it "destroys marker layers associated with the text editor", ->
+ selectionsMarkerLayerId = editor.selectionsMarkerLayer.id
+ foldsMarkerLayerId = editor.displayBuffer.foldsMarkerLayer.id
editor.destroy()
- expect(buffer.getMarkerCount()).toBe 0
+ expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined()
+ expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined()
it "notifies ::onDidDestroy observers when the editor is destroyed", ->
destroyObserverCalled = false
@@ -4492,36 +5182,6 @@ describe "TextEditor", ->
"""
expect(editor.getSelectedBufferRange()).toEqual [[13, 0], [14, 2]]
- describe ".moveLineUp()", ->
- it "moves the line under the cursor up", ->
- editor.setCursorBufferPosition([1, 0])
- editor.moveLineUp()
- expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe " var sort = function(items) {"
- expect(editor.indentationForBufferRow(0)).toBe 1
- expect(editor.indentationForBufferRow(1)).toBe 0
-
- it "updates the line's indentation when the editor.autoIndent setting is true", ->
- atom.config.set('editor.autoIndent', true)
- editor.setCursorBufferPosition([1, 0])
- editor.moveLineUp()
- expect(editor.indentationForBufferRow(0)).toBe 0
- expect(editor.indentationForBufferRow(1)).toBe 0
-
- describe ".moveLineDown()", ->
- it "moves the line under the cursor down", ->
- editor.setCursorBufferPosition([0, 0])
- editor.moveLineDown()
- expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe "var quicksort = function () {"
- expect(editor.indentationForBufferRow(0)).toBe 1
- expect(editor.indentationForBufferRow(1)).toBe 0
-
- it "updates the line's indentation when the editor.autoIndent setting is true", ->
- atom.config.set('editor.autoIndent', true)
- editor.setCursorBufferPosition([0, 0])
- editor.moveLineDown()
- expect(editor.indentationForBufferRow(0)).toBe 1
- expect(editor.indentationForBufferRow(1)).toBe 2
-
describe ".shouldPromptToSave()", ->
it "returns false when an edit session's buffer is in use by more than one session", ->
jasmine.unspy(editor, 'shouldPromptToSave')
@@ -4911,101 +5571,189 @@ describe "TextEditor", ->
it "does not allow a custom gutter with the 'line-number' name.", ->
expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow()
- describe '::decorateMarker', ->
- [marker] = []
+ describe '::decorateMarker', ->
+ [marker] = []
- beforeEach ->
- marker = editor.markBufferRange([[1, 0], [1, 0]])
+ beforeEach ->
+ marker = editor.markBufferRange([[1, 0], [1, 0]])
- it 'reflects an added decoration when one of its custom gutters is decorated.', ->
- gutter = editor.addGutter {'name': 'custom-gutter'}
- decoration = gutter.decorateMarker marker, {class: 'custom-class'}
- gutterDecorations = editor.getDecorations
- type: 'gutter'
- gutterName: 'custom-gutter'
- class: 'custom-class'
- expect(gutterDecorations.length).toBe 1
- expect(gutterDecorations[0]).toBe decoration
+ it 'reflects an added decoration when one of its custom gutters is decorated.', ->
+ gutter = editor.addGutter {'name': 'custom-gutter'}
+ decoration = gutter.decorateMarker marker, {class: 'custom-class'}
+ gutterDecorations = editor.getDecorations
+ type: 'gutter'
+ gutterName: 'custom-gutter'
+ class: 'custom-class'
+ expect(gutterDecorations.length).toBe 1
+ expect(gutterDecorations[0]).toBe decoration
- it 'reflects an added decoration when its line-number gutter is decorated.', ->
- decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'}
- gutterDecorations = editor.getDecorations
- type: 'line-number'
- gutterName: 'line-number'
- class: 'test-class'
- expect(gutterDecorations.length).toBe 1
- expect(gutterDecorations[0]).toBe decoration
+ it 'reflects an added decoration when its line-number gutter is decorated.', ->
+ decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'}
+ gutterDecorations = editor.getDecorations
+ type: 'line-number'
+ gutterName: 'line-number'
+ class: 'test-class'
+ expect(gutterDecorations.length).toBe 1
+ expect(gutterDecorations[0]).toBe decoration
- describe '::observeGutters', ->
- [payloads, callback] = []
+ describe '::observeGutters', ->
+ [payloads, callback] = []
- beforeEach ->
- payloads = []
- callback = (payload) ->
- payloads.push(payload)
+ beforeEach ->
+ payloads = []
+ callback = (payload) ->
+ payloads.push(payload)
- it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', ->
- lineNumberGutter = editor.gutterWithName('line-number')
- editor.observeGutters(callback)
- expect(payloads).toEqual [lineNumberGutter]
- gutter1 = editor.addGutter({name: 'test-gutter-1'})
- expect(payloads).toEqual [lineNumberGutter, gutter1]
- gutter2 = editor.addGutter({name: 'test-gutter-2'})
- expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2]
+ it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', ->
+ lineNumberGutter = editor.gutterWithName('line-number')
+ editor.observeGutters(callback)
+ expect(payloads).toEqual [lineNumberGutter]
+ gutter1 = editor.addGutter({name: 'test-gutter-1'})
+ expect(payloads).toEqual [lineNumberGutter, gutter1]
+ gutter2 = editor.addGutter({name: 'test-gutter-2'})
+ expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2]
- it 'does not call the callback when a gutter is removed.', ->
- gutter = editor.addGutter({name: 'test-gutter'})
- editor.observeGutters(callback)
- payloads = []
- gutter.destroy()
- expect(payloads).toEqual []
+ it 'does not call the callback when a gutter is removed.', ->
+ gutter = editor.addGutter({name: 'test-gutter'})
+ editor.observeGutters(callback)
+ payloads = []
+ gutter.destroy()
+ expect(payloads).toEqual []
- it 'does not call the callback after the subscription has been disposed.', ->
- subscription = editor.observeGutters(callback)
- payloads = []
- subscription.dispose()
- editor.addGutter({name: 'test-gutter'})
- expect(payloads).toEqual []
+ it 'does not call the callback after the subscription has been disposed.', ->
+ subscription = editor.observeGutters(callback)
+ payloads = []
+ subscription.dispose()
+ editor.addGutter({name: 'test-gutter'})
+ expect(payloads).toEqual []
- describe '::onDidAddGutter', ->
- [payloads, callback] = []
+ describe '::onDidAddGutter', ->
+ [payloads, callback] = []
- beforeEach ->
- payloads = []
- callback = (payload) ->
- payloads.push(payload)
+ beforeEach ->
+ payloads = []
+ callback = (payload) ->
+ payloads.push(payload)
- it 'calls the callback with each newly-added gutter, but not with existing gutters.', ->
- editor.onDidAddGutter(callback)
- expect(payloads).toEqual []
- gutter = editor.addGutter({name: 'test-gutter'})
- expect(payloads).toEqual [gutter]
+ it 'calls the callback with each newly-added gutter, but not with existing gutters.', ->
+ editor.onDidAddGutter(callback)
+ expect(payloads).toEqual []
+ gutter = editor.addGutter({name: 'test-gutter'})
+ expect(payloads).toEqual [gutter]
- it 'does not call the callback after the subscription has been disposed.', ->
- subscription = editor.onDidAddGutter(callback)
- payloads = []
- subscription.dispose()
- editor.addGutter({name: 'test-gutter'})
- expect(payloads).toEqual []
+ it 'does not call the callback after the subscription has been disposed.', ->
+ subscription = editor.onDidAddGutter(callback)
+ payloads = []
+ subscription.dispose()
+ editor.addGutter({name: 'test-gutter'})
+ expect(payloads).toEqual []
- describe '::onDidRemoveGutter', ->
- [payloads, callback] = []
+ describe '::onDidRemoveGutter', ->
+ [payloads, callback] = []
- beforeEach ->
- payloads = []
- callback = (payload) ->
- payloads.push(payload)
+ beforeEach ->
+ payloads = []
+ callback = (payload) ->
+ payloads.push(payload)
- it 'calls the callback when a gutter is removed.', ->
- gutter = editor.addGutter({name: 'test-gutter'})
- editor.onDidRemoveGutter(callback)
- expect(payloads).toEqual []
- gutter.destroy()
- expect(payloads).toEqual ['test-gutter']
+ it 'calls the callback when a gutter is removed.', ->
+ gutter = editor.addGutter({name: 'test-gutter'})
+ editor.onDidRemoveGutter(callback)
+ expect(payloads).toEqual []
+ gutter.destroy()
+ expect(payloads).toEqual ['test-gutter']
- it 'does not call the callback after the subscription has been disposed.', ->
- gutter = editor.addGutter({name: 'test-gutter'})
- subscription = editor.onDidRemoveGutter(callback)
- subscription.dispose()
- gutter.destroy()
- expect(payloads).toEqual []
+ it 'does not call the callback after the subscription has been disposed.', ->
+ gutter = editor.addGutter({name: 'test-gutter'})
+ subscription = editor.onDidRemoveGutter(callback)
+ subscription.dispose()
+ gutter.destroy()
+ expect(payloads).toEqual []
+
+ describe "decorations", ->
+ describe "::decorateMarker", ->
+ it "includes the decoration in the object returned from ::decorationsStateForScreenRowRange", ->
+ marker = editor.markBufferRange([[2, 4], [6, 8]])
+ decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo')
+ expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual {
+ properties: {type: 'highlight', class: 'foo'}
+ screenRange: marker.getScreenRange(),
+ rangeIsReversed: false
+ }
+
+ describe "::decorateMarkerLayer", ->
+ it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange", ->
+ layer1 = editor.getBuffer().addMarkerLayer()
+ marker1 = layer1.markRange([[2, 4], [6, 8]])
+ marker2 = layer1.markRange([[11, 0], [11, 12]])
+ layer2 = editor.getBuffer().addMarkerLayer()
+ marker3 = layer2.markRange([[8, 0], [9, 0]])
+
+ layer1Decoration1 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'foo')
+ layer1Decoration2 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'bar')
+ layer2Decoration = editor.decorateMarkerLayer(layer2, type: 'highlight', class: 'baz')
+
+ decorationState = editor.decorationsStateForScreenRowRange(0, 13)
+
+ expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'foo'},
+ screenRange: marker1.getRange(),
+ rangeIsReversed: false
+ }
+ expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'foo'},
+ screenRange: marker2.getRange(),
+ rangeIsReversed: false
+ }
+ expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'bar'},
+ screenRange: marker1.getRange(),
+ rangeIsReversed: false
+ }
+ expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'bar'},
+ screenRange: marker2.getRange(),
+ rangeIsReversed: false
+ }
+ expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'baz'},
+ screenRange: marker3.getRange(),
+ rangeIsReversed: false
+ }
+
+ layer1Decoration1.destroy()
+
+ decorationState = editor.decorationsStateForScreenRowRange(0, 12)
+ expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toBeUndefined()
+ expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toBeUndefined()
+ expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'bar'},
+ screenRange: marker1.getRange(),
+ rangeIsReversed: false
+ }
+ expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'bar'},
+ screenRange: marker2.getRange(),
+ rangeIsReversed: false
+ }
+ expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'baz'},
+ screenRange: marker3.getRange(),
+ rangeIsReversed: false
+ }
+
+ layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'})
+ decorationState = editor.decorationsStateForScreenRowRange(0, 12)
+ expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'quux'},
+ screenRange: marker1.getRange(),
+ rangeIsReversed: false
+ }
+
+ layer1Decoration2.setPropertiesForMarker(marker1, null)
+ decorationState = editor.decorationsStateForScreenRowRange(0, 12)
+ expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
+ properties: {type: 'highlight', class: 'bar'},
+ screenRange: marker1.getRange(),
+ rangeIsReversed: false
+ }
diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee
index e87125195..9864acff0 100644
--- a/spec/tokenized-buffer-spec.coffee
+++ b/spec/tokenized-buffer-spec.coffee
@@ -202,12 +202,12 @@ describe "TokenizedBuffer", ->
# previous line 3 should be combined with input to form line 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js'])
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
# lines below deleted regions should be shifted upward
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
- expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
- expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -254,7 +254,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
# previous line 3 is pushed down to become line 5
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee
index fcddd325a..a2b4965a5 100644
--- a/spec/view-registry-spec.coffee
+++ b/spec/view-registry-spec.coffee
@@ -209,3 +209,21 @@ describe "ViewRegistry", ->
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/src/browser/atom-application.coffee b/src/browser/atom-application.coffee
index b68ee2c73..8bb44349e 100644
--- a/src/browser/atom-application.coffee
+++ b/src/browser/atom-application.coffee
@@ -496,7 +496,7 @@ class AtomApplication
# :specPath - The directory to load specs from.
# :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
# and ~/.atom/dev/packages, defaults to false.
- runTests: ({headless, devMode, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout}) ->
+ runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
@@ -523,6 +523,7 @@ class AtomApplication
legacyTestRunnerPath = @resolveLegacyTestRunnerPath()
testRunnerPath = @resolveTestRunnerPath(testPaths[0])
+ devMode = true
isSpec = true
safeMode ?= false
new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode})
diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee
index a346f5c77..c507b634c 100644
--- a/src/browser/atom-window.coffee
+++ b/src/browser/atom-window.coffee
@@ -28,7 +28,6 @@ class AtomWindow
title: 'Atom'
'web-preferences':
'direct-write': true
- 'subpixel-font-scaling': true
if @isSpec
options['web-preferences']['page-visibility'] = true
diff --git a/src/cursor.coffee b/src/cursor.coffee
index 40cde4aca..0f87c2760 100644
--- a/src/cursor.coffee
+++ b/src/cursor.coffee
@@ -7,7 +7,7 @@ Model = require './model'
# where text can be inserted.
#
# Cursors belong to {TextEditor}s and have some metadata attached in the form
-# of a {Marker}.
+# of a {TextEditorMarker}.
module.exports =
class Cursor extends Model
screenPosition: null
@@ -127,7 +127,7 @@ class Cursor extends Model
Section: Cursor Position Details
###
- # Public: Returns the underlying {Marker} for the cursor.
+ # Public: Returns the underlying {TextEditorMarker} for the cursor.
# Useful with overlay {Decoration}s.
getMarker: -> @marker
diff --git a/src/decoration.coffee b/src/decoration.coffee
index 154900ce5..f57d234d1 100644
--- a/src/decoration.coffee
+++ b/src/decoration.coffee
@@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
decorationParams.gutterName = 'line-number'
decorationParams
-# Essential: Represents a decoration that follows a {Marker}. A decoration is
+# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is
# basically a visual representation of a marker. It allows you to add CSS
# classes to line numbers in the gutter, lines, and add selection-line regions
# around marked ranges of text.
@@ -25,7 +25,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
# ```
#
-# Best practice for destroying the decoration is by destroying the {Marker}.
+# Best practice for destroying the decoration is by destroying the {TextEditorMarker}.
#
# ```coffee
# marker.destroy()
@@ -67,20 +67,19 @@ class Decoration
@emitter = new Emitter
@id = nextId()
@setProperties properties
- @properties.id = @id
- @flashQueue = null
@destroyed = false
@markerDestroyDisposable = @marker.onDidDestroy => @destroy()
# Essential: Destroy this marker.
#
- # If you own the marker, you should use {Marker::destroy} which will destroy
+ # If you own the marker, you should use {TextEditorMarker::destroy} which will destroy
# this decoration.
destroy: ->
return if @destroyed
@markerDestroyDisposable.dispose()
@markerDestroyDisposable = null
@destroyed = true
+ @displayBuffer.didDestroyDecoration(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
@@ -150,9 +149,9 @@ class Decoration
return if @destroyed
oldProperties = @properties
@properties = translateDecorationParamsOldToNew(newProperties)
- @properties.id = @id
if newProperties.type?
@displayBuffer.decorationDidChangeType(this)
+ @displayBuffer.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
###
@@ -165,15 +164,10 @@ class Decoration
return false if @properties[key] isnt value
true
- onDidFlash: (callback) ->
- @emitter.on 'did-flash', callback
-
flash: (klass, duration=500) ->
- flashObject = {class: klass, duration}
- @flashQueue ?= []
- @flashQueue.push(flashObject)
+ @properties.flashCount ?= 0
+ @properties.flashCount++
+ @properties.flashClass = klass
+ @properties.flashDuration = duration
+ @displayBuffer.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-flash'
-
- consumeNextFlash: ->
- return @flashQueue.shift() if @flashQueue?.length > 0
- null
diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee
index 3da9b8ea5..f5a7bd853 100644
--- a/src/display-buffer.coffee
+++ b/src/display-buffer.coffee
@@ -7,7 +7,8 @@ Fold = require './fold'
Model = require './model'
Token = require './token'
Decoration = require './decoration'
-Marker = require './marker'
+LayerDecoration = require './layer-decoration'
+TextEditorMarkerLayer = require './text-editor-marker-layer'
class BufferToScreenConversionError extends Error
constructor: (@message, @metadata) ->
@@ -25,9 +26,12 @@ class DisplayBuffer extends Model
defaultCharWidth: null
height: null
width: null
+ didUpdateDecorationsEventScheduled: false
+ updatedSynchronously: false
@deserialize: (state, atomEnvironment) ->
state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
+ state.foldsMarkerLayer = state.tokenizedBuffer.buffer.getMarkerLayer(state.foldsMarkerLayerId)
state.config = atomEnvironment.config
state.assert = atomEnvironment.assert
state.grammarRegistry = atomEnvironment.grammars
@@ -38,8 +42,8 @@ class DisplayBuffer extends Model
super
{
- tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles,
- @largeFileMode, @config, @assert, @grammarRegistry, @packageManager
+ tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer,
+ ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager
} = params
@emitter = new Emitter
@@ -51,17 +55,22 @@ class DisplayBuffer extends Model
})
@buffer = @tokenizedBuffer.buffer
@charWidthsByScope = {}
- @markers = {}
+ @defaultMarkerLayer = new TextEditorMarkerLayer(this, @buffer.getDefaultMarkerLayer(), true)
+ @customMarkerLayersById = {}
@foldsByMarkerId = {}
@decorationsById = {}
@decorationsByMarkerId = {}
@overlayDecorationsById = {}
+ @layerDecorationsByMarkerLayerId = {}
+ @decorationCountsByLayerId = {}
+ @layerUpdateDisposablesByLayerId = {}
+
@disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
@disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange
- @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated
- @disposables.add @buffer.onDidUpdateMarkers => @emitter.emit 'did-update-markers'
- @foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id})
- folds = (new Fold(this, marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()))
+ @disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker
+
+ @foldsMarkerLayer ?= @buffer.addMarkerLayer()
+ folds = (new Fold(this, marker) for marker in @foldsMarkerLayer.getMarkers())
@updateAllScreenLines()
@decorateFold(fold) for fold in folds
@@ -107,17 +116,15 @@ class DisplayBuffer extends Model
editorWidthInChars: @editorWidthInChars
tokenizedBuffer: @tokenizedBuffer.serialize()
largeFileMode: @largeFileMode
+ foldsMarkerLayerId: @foldsMarkerLayer.id
copy: ->
- newDisplayBuffer = new DisplayBuffer({
+ foldsMarkerLayer = @foldsMarkerLayer.copy()
+ new DisplayBuffer({
@buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert,
- @grammarRegistry, @packageManager
+ @grammarRegistry, @packageManager, foldsMarkerLayer
})
- for marker in @findMarkers(displayBufferId: @id)
- marker.copy(displayBufferId: newDisplayBuffer.id)
- newDisplayBuffer
-
updateAllScreenLines: ->
@maxLineLength = 0
@screenLines = []
@@ -158,6 +165,9 @@ class DisplayBuffer extends Model
onDidUpdateMarkers: (callback) ->
@emitter.on 'did-update-markers', callback
+ onDidUpdateDecorations: (callback) ->
+ @emitter.on 'did-update-decorations', callback
+
emitDidChange: (eventProperties, refreshMarkers=true) ->
@emitter.emit 'did-change', eventProperties
if refreshMarkers
@@ -177,6 +187,8 @@ class DisplayBuffer extends Model
# visible - A {Boolean} indicating of the tokenized buffer is shown
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
+ setUpdatedSynchronously: (@updatedSynchronously) ->
+
getVerticalScrollMargin: ->
maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2)
Math.min(@verticalScrollMargin, maxScrollMargin)
@@ -386,10 +398,14 @@ class DisplayBuffer extends Model
# Returns the new {Fold}.
createFold: (startRow, endRow) ->
unless @largeFileMode
- foldMarker =
- @findFoldMarker({startRow, endRow}) ?
- @buffer.markRange([[startRow, 0], [endRow, Infinity]], @getFoldMarkerAttributes())
- @foldForMarker(foldMarker)
+ if foldMarker = @findFoldMarker({startRow, endRow})
+ @foldForMarker(foldMarker)
+ else
+ foldMarker = @foldsMarkerLayer.markRange([[startRow, 0], [endRow, Infinity]])
+ fold = new Fold(this, foldMarker)
+ fold.updateDisplayBuffer()
+ @decorateFold(fold)
+ fold
isFoldedAtBufferRow: (bufferRow) ->
@largestFoldContainingBufferRow(bufferRow)?
@@ -769,52 +785,68 @@ class DisplayBuffer extends Model
decorationsByMarkerId[marker.id] = decorations
decorationsByMarkerId
+ decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
+ decorationsState = {}
+
+ for layerId of @decorationCountsByLayerId
+ layer = @getMarkerLayer(layerId)
+
+ for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
+ screenRange = marker.getScreenRange()
+ rangeIsReversed = marker.isReversed()
+
+ if decorations = @decorationsByMarkerId[marker.id]
+ for decoration in decorations
+ decorationsState[decoration.id] = {
+ properties: decoration.properties
+ screenRange, rangeIsReversed
+ }
+
+ if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
+ for layerDecoration in layerDecorations
+ decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
+ properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
+ screenRange, rangeIsReversed
+ }
+
+ decorationsState
+
decorateMarker: (marker, decorationParams) ->
- marker = @getMarker(marker.id)
+ marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id)
decoration = new Decoration(marker, this, decorationParams)
- decorationDestroyedDisposable = decoration.onDidDestroy =>
- @removeDecoration(decoration)
- @disposables.remove(decorationDestroyedDisposable)
- @disposables.add(decorationDestroyedDisposable)
@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
- removeDecoration: (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]
+ decorateMarkerLayer: (markerLayer, decorationParams) ->
+ decoration = new LayerDecoration(markerLayer, this, decorationParams)
+ @layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
+ @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
+ @observeDecoratedLayer(markerLayer)
+ @scheduleUpdateDecorationsEvent()
+ decoration
decorationsForMarkerId: (markerId) ->
@decorationsByMarkerId[markerId]
- # Retrieves a {Marker} based on its id.
+ # Retrieves a {TextEditorMarker} based on its id.
#
# id - A {Number} representing a marker id
#
- # Returns the {Marker} (if it exists).
+ # Returns the {TextEditorMarker} (if it exists).
getMarker: (id) ->
- unless marker = @markers[id]
- if bufferMarker = @buffer.getMarker(id)
- marker = new Marker({bufferMarker, displayBuffer: this})
- @markers[id] = marker
- marker
+ @defaultMarkerLayer.getMarker(id)
# Retrieves the active markers in the buffer.
#
- # Returns an {Array} of existing {Marker}s.
+ # Returns an {Array} of existing {TextEditorMarker}s.
getMarkers: ->
- @buffer.getMarkers().map ({id}) => @getMarker(id)
+ @defaultMarkerLayer.getMarkers()
getMarkerCount: ->
@buffer.getMarkerCount()
@@ -822,54 +854,46 @@ class DisplayBuffer extends Model
# Public: Constructs a new marker at the given screen range.
#
# range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {Marker} constructor
+ # options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
- markScreenRange: (args...) ->
- bufferRange = @bufferRangeForScreenRange(args.shift())
- @markBufferRange(bufferRange, args...)
+ markScreenRange: (screenRange, options) ->
+ @defaultMarkerLayer.markScreenRange(screenRange, options)
# Public: Constructs a new marker at the given buffer range.
#
# range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {Marker} constructor
+ # options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
- markBufferRange: (range, options) ->
- @getMarker(@buffer.markRange(range, options).id)
+ markBufferRange: (bufferRange, options) ->
+ @defaultMarkerLayer.markBufferRange(bufferRange, options)
# Public: Constructs a new marker at the given screen position.
#
# range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {Marker} constructor
+ # options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markScreenPosition: (screenPosition, options) ->
- @markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options)
+ @defaultMarkerLayer.markScreenPosition(screenPosition, options)
# Public: Constructs a new marker at the given buffer position.
#
# range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {Marker} constructor
+ # options - Options to pass to the {TextEditorMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markBufferPosition: (bufferPosition, options) ->
- @getMarker(@buffer.markPosition(bufferPosition, options).id)
-
- # Public: Removes the marker with the given id.
- #
- # id - The {Number} of the ID to remove
- destroyMarker: (id) ->
- @buffer.destroyMarker(id)
- delete @markers[id]
+ @defaultMarkerLayer.markBufferPosition(bufferPosition, options)
# Finds the first marker satisfying the given attributes
#
# Refer to {DisplayBuffer::findMarkers} for details.
#
- # Returns a {Marker} or null
+ # Returns a {TextEditorMarker} or null
findMarker: (params) ->
- @findMarkers(params)[0]
+ @defaultMarkerLayer.findMarkers(params)[0]
# Public: Find all markers satisfying a set of parameters.
#
@@ -888,69 +912,36 @@ class DisplayBuffer extends Model
# :containedInBufferRange - A {Range} or range-compatible {Array}. Only
# returns markers contained within this range.
#
- # Returns an {Array} of {Marker}s
+ # Returns an {Array} of {TextEditorMarker}s
findMarkers: (params) ->
- params = @translateToBufferMarkerParams(params)
- @buffer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id)
+ @defaultMarkerLayer.findMarkers(params)
- translateToBufferMarkerParams: (params) ->
- bufferMarkerParams = {}
- for key, value of params
- switch key
- when 'startBufferRow'
- key = 'startRow'
- when 'endBufferRow'
- key = 'endRow'
- when 'startScreenRow'
- key = 'startRow'
- value = @bufferRowForScreenRow(value)
- when 'endScreenRow'
- key = 'endRow'
- value = @bufferRowForScreenRow(value)
- when 'intersectsBufferRowRange'
- key = 'intersectsRowRange'
- when 'intersectsScreenRowRange'
- key = 'intersectsRowRange'
- [startRow, endRow] = value
- value = [@bufferRowForScreenRow(startRow), @bufferRowForScreenRow(endRow)]
- when 'containsBufferRange'
- key = 'containsRange'
- when 'containsBufferPosition'
- key = 'containsPosition'
- when 'containedInBufferRange'
- key = 'containedInRange'
- when 'containedInScreenRange'
- key = 'containedInRange'
- value = @bufferRangeForScreenRange(value)
- when 'intersectsBufferRange'
- key = 'intersectsRange'
- when 'intersectsScreenRange'
- key = 'intersectsRange'
- value = @bufferRangeForScreenRange(value)
- bufferMarkerParams[key] = value
+ addMarkerLayer: (options) ->
+ bufferLayer = @buffer.addMarkerLayer(options)
+ @getMarkerLayer(bufferLayer.id)
- bufferMarkerParams
+ getMarkerLayer: (id) ->
+ if layer = @customMarkerLayersById[id]
+ layer
+ else if bufferLayer = @buffer.getMarkerLayer(id)
+ @customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer)
- findFoldMarker: (attributes) ->
- @findFoldMarkers(attributes)[0]
+ getDefaultMarkerLayer: -> @defaultMarkerLayer
- findFoldMarkers: (attributes) ->
- @buffer.findMarkers(@getFoldMarkerAttributes(attributes))
+ findFoldMarker: (params) ->
+ @findFoldMarkers(params)[0]
- getFoldMarkerAttributes: (attributes) ->
- if attributes
- _.extend(attributes, @foldMarkerAttributes)
- else
- @foldMarkerAttributes
+ findFoldMarkers: (params) ->
+ @foldsMarkerLayer.findMarkers(params)
refreshMarkerScreenPositions: ->
- for marker in @getMarkers()
- marker.notifyObservers(textChanged: false)
+ @defaultMarkerLayer.refreshMarkerScreenPositions()
+ layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById
return
destroyed: ->
- fold.destroy() for markerId, fold of @foldsByMarkerId
- marker.disposables.dispose() for id, marker of @markers
+ @defaultMarkerLayer.destroy()
+ @foldsMarkerLayer.destroy()
@scopedConfigSubscriptions.dispose()
@disposables.dispose()
@tokenizedBuffer.destroy()
@@ -1072,17 +1063,23 @@ class DisplayBuffer extends Model
@longestScreenRow = screenRow
@maxLineLength = length
- handleBufferMarkerCreated: (textBufferMarker) =>
- if textBufferMarker.matchesParams(@getFoldMarkerAttributes())
- fold = new Fold(this, textBufferMarker)
- fold.updateDisplayBuffer()
- @decorateFold(fold)
-
+ didCreateDefaultLayerMarker: (textBufferMarker) =>
if marker = @getMarker(textBufferMarker.id)
# The marker might have been removed in some other handler called before
# this one. Only emit when the marker still exists.
@emitter.emit 'did-create-marker', marker
+ scheduleUpdateDecorationsEvent: ->
+ if @updatedSynchronously
+ @emitter.emit 'did-update-decorations'
+ return
+
+ unless @didUpdateDecorationsEventScheduled
+ @didUpdateDecorationsEventScheduled = true
+ process.nextTick =>
+ @didUpdateDecorationsEventScheduled = false
+ @emitter.emit 'did-update-decorations'
+
decorateFold: (fold) ->
@decorateMarker(fold.marker, type: 'line-number', class: 'folded')
@@ -1095,6 +1092,42 @@ class DisplayBuffer extends Model
else
delete @overlayDecorationsById[decoration.id]
+ didDestroyDecoration: (decoration) ->
+ {marker} = decoration
+ return unless decorations = @decorationsByMarkerId[marker.id]
+ index = decorations.indexOf(decoration)
+
+ if index > -1
+ decorations.splice(index, 1)
+ delete @decorationsById[decoration.id]
+ @emitter.emit 'did-remove-decoration', decoration
+ delete @decorationsByMarkerId[marker.id] if decorations.length is 0
+ delete @overlayDecorationsById[decoration.id]
+ @unobserveDecoratedLayer(marker.layer)
+ @scheduleUpdateDecorationsEvent()
+
+ didDestroyLayerDecoration: (decoration) ->
+ {markerLayer} = decoration
+ return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
+ index = decorations.indexOf(decoration)
+
+ if index > -1
+ decorations.splice(index, 1)
+ delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
+ @unobserveDecoratedLayer(markerLayer)
+ @scheduleUpdateDecorationsEvent()
+
+ observeDecoratedLayer: (layer) ->
+ @decorationCountsByLayerId[layer.id] ?= 0
+ if ++@decorationCountsByLayerId[layer.id] is 1
+ @layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
+
+ unobserveDecoratedLayer: (layer) ->
+ if --@decorationCountsByLayerId[layer.id] is 0
+ @layerUpdateDisposablesByLayerId[layer.id].dispose()
+ delete @decorationCountsByLayerId[layer.id]
+ delete @layerUpdateDisposablesByLayerId[layer.id]
+
checkScreenLinesInvariant: ->
return if @isSoftWrapped()
return if _.size(@foldsByMarkerId) > 0
diff --git a/src/gutter.coffee b/src/gutter.coffee
index 8418823bf..f59fa7b6e 100644
--- a/src/gutter.coffee
+++ b/src/gutter.coffee
@@ -71,13 +71,13 @@ class Gutter
isVisible: ->
@visible
- # Essential: Add a decoration that tracks a {Marker}. When the marker moves,
+ # Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
#
# ## Arguments
#
- # * `marker` A {Marker} you want this decoration to follow.
+ # * `marker` A {TextEditorMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration. It is passed
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
# all options documented there.
diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee
index f33cca09d..72a071fb6 100644
--- a/src/initialize-test-window.coffee
+++ b/src/initialize-test-window.coffee
@@ -57,6 +57,12 @@ module.exports = ({blobStore}) ->
document.title = "Spec Suite"
+ # Avoid throttling of test window by playing silence
+ context = new AudioContext()
+ source = context.createBufferSource()
+ source.connect(context.destination)
+ source.start(0)
+
testRunner = require(testRunnerPath)
legacyTestRunner = require(legacyTestRunnerPath)
buildDefaultApplicationDelegate = -> new ApplicationDelegate()
diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee
new file mode 100644
index 000000000..1f76140a3
--- /dev/null
+++ b/src/layer-decoration.coffee
@@ -0,0 +1,61 @@
+_ = require 'underscore-plus'
+
+idCounter = 0
+nextId = -> idCounter++
+
+# Essential: Represents a decoration that applies to every marker on a given
+# layer. Created via {TextEditor::decorateMarkerLayer}.
+module.exports =
+class LayerDecoration
+ constructor: (@markerLayer, @displayBuffer, @properties) ->
+ @id = nextId()
+ @destroyed = false
+ @markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
+ @overridePropertiesByMarkerId = {}
+
+ # Essential: Destroys the decoration.
+ destroy: ->
+ return if @destroyed
+ @markerLayerDestroyedDisposable.dispose()
+ @markerLayerDestroyedDisposable = null
+ @destroyed = true
+ @displayBuffer.didDestroyLayerDecoration(this)
+
+ # Essential: Determine whether this decoration is destroyed.
+ #
+ # Returns a {Boolean}.
+ isDestroyed: -> @destroyed
+
+ getId: -> @id
+
+ getMarkerLayer: -> @markerLayer
+
+ # Essential: Get this decoration's properties.
+ #
+ # Returns an {Object}.
+ getProperties: ->
+ @properties
+
+ # Essential: Set this decoration's properties.
+ #
+ # * `newProperties` See {TextEditor::decorateMarker} for more information on
+ # the properties. The `type` of `gutter` and `overlay` are not supported on
+ # layer decorations.
+ setProperties: (newProperties) ->
+ return if @destroyed
+ @properties = newProperties
+ @displayBuffer.scheduleUpdateDecorationsEvent()
+
+ # Essential: Override the decoration properties for a specific marker.
+ #
+ # * `marker` The {TextEditorMarker} or {Marker} for which to override
+ # properties.
+ # * `properties` An {Object} containing properties to apply to this marker.
+ # Pass `null` to clear the override.
+ setPropertiesForMarker: (marker, properties) ->
+ return if @destroyed
+ if properties?
+ @overridePropertiesByMarkerId[marker.id] = properties
+ else
+ delete @overridePropertiesByMarkerId[marker.id]
+ @displayBuffer.scheduleUpdateDecorationsEvent()
diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee
index aa346200c..8ab10c048 100644
--- a/src/menu-helpers.coffee
+++ b/src/menu-helpers.coffee
@@ -46,7 +46,7 @@ normalizeLabel = (label) ->
label.replace(/\&/g, '')
cloneMenuItem = (item) ->
- item = _.pick(item, 'type', 'label', 'enabled', 'visible', 'command', 'submenu', 'commandDetail')
+ item = _.pick(item, 'type', 'label', 'enabled', 'visible', 'command', 'submenu', 'commandDetail', 'role')
if item.submenu?
item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem)
item
diff --git a/src/package-manager.coffee b/src/package-manager.coffee
index 5c0df4b70..de63502aa 100644
--- a/src/package-manager.coffee
+++ b/src/package-manager.coffee
@@ -336,8 +336,10 @@ class PackageManager
keymapsToEnable = _.difference(oldValue, newValue)
keymapsToDisable = _.difference(newValue, oldValue)
- @getLoadedPackage(packageName).deactivateKeymaps() for packageName in keymapsToDisable when not @isPackageDisabled(packageName)
- @getLoadedPackage(packageName).activateKeymaps() for packageName in keymapsToEnable when not @isPackageDisabled(packageName)
+ for packageName in keymapsToDisable when not @isPackageDisabled(packageName)
+ @getLoadedPackage(packageName)?.deactivateKeymaps()
+ for packageName in keymapsToEnable when not @isPackageDisabled(packageName)
+ @getLoadedPackage(packageName)?.activateKeymaps()
null
loadPackages: ->
@@ -419,7 +421,7 @@ class PackageManager
@config.transact =>
for pack in packages
promise = @activatePackage(pack.name)
- promises.push(promise) unless pack.hasActivationCommands()
+ promises.push(promise) unless pack.activationShouldBeDeferred()
return
@observeDisabledPackages()
@observePackagesWithKeymapsDisabled()
diff --git a/src/pane.coffee b/src/pane.coffee
index 92be02575..9886c735e 100644
--- a/src/pane.coffee
+++ b/src/pane.coffee
@@ -722,7 +722,7 @@ class Pane extends Model
@notificationManager.addWarning("Unable to save file: #{error.message}")
else if error.code is 'EACCES'
addWarningWithPath('Unable to save file: Permission denied')
- else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST']
+ else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST', 'ELOOP']
addWarningWithPath('Unable to save file', detail: error.message)
else if error.code is 'EROFS'
addWarningWithPath('Unable to save file: Read-only file system')
diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee
index dc0abbe60..430b0c0fd 100644
--- a/src/text-editor-component.coffee
+++ b/src/text-editor-component.coffee
@@ -216,7 +216,7 @@ class TextEditorComponent
@updatesPaused = false
if @updateRequestedWhilePaused and @canUpdate()
@updateRequestedWhilePaused = false
- @updateSync()
+ @requestUpdate()
getTopmostDOMNode: ->
@hostElement
diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee
index 55e23d2da..1a55eb002 100644
--- a/src/text-editor-element.coffee
+++ b/src/text-editor-element.coffee
@@ -103,6 +103,7 @@ class TextEditorElement extends HTMLElement
return if model.isDestroyed()
@model = model
+ @model.setUpdatedSynchronously(@isUpdatedSynchronously())
@initializeContent()
@mountComponent()
@addGrammarScopeAttribute()
@@ -194,7 +195,9 @@ class TextEditorElement extends HTMLElement
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
- setUpdatedSynchronously: (@updatedSynchronously) -> @updatedSynchronously
+ setUpdatedSynchronously: (@updatedSynchronously) ->
+ @model?.setUpdatedSynchronously(@updatedSynchronously)
+ @updatedSynchronously
isUpdatedSynchronously: -> @updatedSynchronously
diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee
new file mode 100644
index 000000000..e99ad7323
--- /dev/null
+++ b/src/text-editor-marker-layer.coffee
@@ -0,0 +1,192 @@
+TextEditorMarker = require './text-editor-marker'
+
+# Public: *Experimental:* A container for a related set of markers at the
+# {TextEditor} level. Wraps an underlying {MarkerLayer} on the editor's
+# {TextBuffer}.
+#
+# This API is experimental and subject to change on any release.
+module.exports =
+class TextEditorMarkerLayer
+ constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) ->
+ @id = @bufferMarkerLayer.id
+ @markersById = {}
+
+ ###
+ Section: Lifecycle
+ ###
+
+ # Essential: Destroy this layer.
+ destroy: ->
+ if @isDefaultLayer
+ marker.destroy() for id, marker of @markersById
+ else
+ @bufferMarkerLayer.destroy()
+
+ ###
+ Section: Querying
+ ###
+
+ # Essential: Get an existing marker by its id.
+ #
+ # Returns a {TextEditorMarker}.
+ getMarker: (id) ->
+ if editorMarker = @markersById[id]
+ editorMarker
+ else if bufferMarker = @bufferMarkerLayer.getMarker(id)
+ @markersById[id] = new TextEditorMarker(this, bufferMarker)
+
+ # Essential: Get all markers in the layer.
+ #
+ # Returns an {Array} of {TextEditorMarker}s.
+ getMarkers: ->
+ @bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id)
+
+ # Public: Get the number of markers in the marker layer.
+ #
+ # Returns a {Number}.
+ getMarkerCount: ->
+ @bufferMarkerLayer.getMarkerCount()
+
+ # Public: Find markers in the layer conforming to the given parameters.
+ #
+ # See the documentation for {TextEditor::findMarkers}.
+ findMarkers: (params) ->
+ params = @translateToBufferMarkerParams(params)
+ @bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id)
+
+ ###
+ Section: Marker creation
+ ###
+
+ # Essential: Create a marker on this layer with the given range in buffer
+ # coordinates.
+ #
+ # See the documentation for {TextEditor::markBufferRange}
+ markBufferRange: (bufferRange, options) ->
+ @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id)
+
+ # Essential: Create a marker on this layer with the given range in screen
+ # coordinates.
+ #
+ # See the documentation for {TextEditor::markScreenRange}
+ markScreenRange: (screenRange, options) ->
+ bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange)
+ @markBufferRange(bufferRange, options)
+
+ # Public: Create a marker on this layer with the given buffer position and no
+ # tail.
+ #
+ # See the documentation for {TextEditor::markBufferPosition}
+ markBufferPosition: (bufferPosition, options) ->
+ @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id)
+
+ # Public: Create a marker on this layer with the given screen position and no
+ # tail.
+ #
+ # See the documentation for {TextEditor::markScreenPosition}
+ markScreenPosition: (screenPosition, options) ->
+ bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition)
+ @markBufferPosition(bufferPosition, options)
+
+ ###
+ Section: Event Subscription
+ ###
+
+ # Public: Subscribe to be notified asynchronously whenever markers are
+ # created, updated, or destroyed on this layer. *Prefer this method for
+ # optimal performance when interacting with layers that could contain large
+ # numbers of markers.*
+ #
+ # * `callback` A {Function} that will be called with no arguments when changes
+ # occur on this layer.
+ #
+ # Subscribers are notified once, asynchronously when any number of changes
+ # occur in a given tick of the event loop. You should re-query the layer
+ # to determine the state of markers in which you're interested in. It may
+ # be counter-intuitive, but this is much more efficient than subscribing to
+ # events on individual markers, which are expensive to deliver.
+ #
+ # Returns a {Disposable}.
+ onDidUpdate: (callback) ->
+ @bufferMarkerLayer.onDidUpdate(callback)
+
+ # Public: Subscribe to be notified synchronously whenever markers are created
+ # on this layer. *Avoid this method for optimal performance when interacting
+ # with layers that could contain large numbers of markers.*
+ #
+ # * `callback` A {Function} that will be called with a {TextEditorMarker}
+ # whenever a new marker is created.
+ #
+ # You should prefer {onDidUpdate} when synchronous notifications aren't
+ # absolutely necessary.
+ #
+ # Returns a {Disposable}.
+ onDidCreateMarker: (callback) ->
+ @bufferMarkerLayer.onDidCreateMarker (bufferMarker) =>
+ callback(@getMarker(bufferMarker.id))
+
+ # Public: Subscribe to be notified synchronously when this layer is destroyed.
+ #
+ # Returns a {Disposable}.
+ onDidDestroy: (callback) ->
+ @bufferMarkerLayer.onDidDestroy(callback)
+
+ ###
+ Section: Private
+ ###
+
+ refreshMarkerScreenPositions: ->
+ for marker in @getMarkers()
+ marker.notifyObservers(textChanged: false)
+ return
+
+ didDestroyMarker: (marker) ->
+ delete @markersById[marker.id]
+
+ translateToBufferMarkerParams: (params) ->
+ bufferMarkerParams = {}
+ for key, value of params
+ switch key
+ when 'startBufferPosition'
+ key = 'startPosition'
+ when 'endBufferPosition'
+ key = 'endPosition'
+ when 'startScreenPosition'
+ key = 'startPosition'
+ value = @displayBuffer.bufferPositionForScreenPosition(value)
+ when 'endScreenPosition'
+ key = 'endPosition'
+ value = @displayBuffer.bufferPositionForScreenPosition(value)
+ when 'startBufferRow'
+ key = 'startRow'
+ when 'endBufferRow'
+ key = 'endRow'
+ when 'startScreenRow'
+ key = 'startRow'
+ value = @displayBuffer.bufferRowForScreenRow(value)
+ when 'endScreenRow'
+ key = 'endRow'
+ value = @displayBuffer.bufferRowForScreenRow(value)
+ when 'intersectsBufferRowRange'
+ key = 'intersectsRowRange'
+ when 'intersectsScreenRowRange'
+ key = 'intersectsRowRange'
+ [startRow, endRow] = value
+ value = [@displayBuffer.bufferRowForScreenRow(startRow), @displayBuffer.bufferRowForScreenRow(endRow)]
+ when 'containsBufferRange'
+ key = 'containsRange'
+ when 'containsBufferPosition'
+ key = 'containsPosition'
+ when 'containedInBufferRange'
+ key = 'containedInRange'
+ when 'containedInScreenRange'
+ key = 'containedInRange'
+ value = @displayBuffer.bufferRangeForScreenRange(value)
+ when 'intersectsBufferRange'
+ key = 'intersectsRange'
+ when 'intersectsScreenRange'
+ key = 'intersectsRange'
+ value = @displayBuffer.bufferRangeForScreenRange(value)
+ bufferMarkerParams[key] = value
+
+ bufferMarkerParams
diff --git a/src/marker.coffee b/src/text-editor-marker.coffee
similarity index 93%
rename from src/marker.coffee
rename to src/text-editor-marker.coffee
index 16f644027..df84700ee 100644
--- a/src/marker.coffee
+++ b/src/text-editor-marker.coffee
@@ -6,7 +6,7 @@ _ = require 'underscore-plus'
# targets, misspelled words, and anything else that needs to track a logical
# location in the buffer over time.
#
-# ### Marker Creation
+# ### TextEditorMarker Creation
#
# Use {TextEditor::markBufferRange} rather than creating Markers directly.
#
@@ -40,7 +40,7 @@ _ = require 'underscore-plus'
#
# See {TextEditor::markBufferRange} for usage.
module.exports =
-class Marker
+class TextEditorMarker
bufferMarkerSubscription: null
oldHeadBufferPosition: null
oldHeadScreenPosition: null
@@ -53,7 +53,8 @@ class Marker
Section: Construction and Destruction
###
- constructor: ({@bufferMarker, @displayBuffer}) ->
+ constructor: (@layer, @bufferMarker) ->
+ {@displayBuffer} = @layer
@emitter = new Emitter
@disposables = new CompositeDisposable
@id = @bufferMarker.id
@@ -66,7 +67,7 @@ class Marker
@bufferMarker.destroy()
@disposables.dispose()
- # Essential: Creates and returns a new {Marker} with the same properties as
+ # Essential: Creates and returns a new {TextEditorMarker} with the same properties as
# this marker.
#
# {Selection} markers (markers with a custom property `type: "selection"`)
@@ -79,9 +80,9 @@ class Marker
# marker. The new marker's properties are computed by extending this marker's
# properties with `properties`.
#
- # Returns a {Marker}.
+ # Returns a {TextEditorMarker}.
copy: (properties) ->
- @displayBuffer.getMarker(@bufferMarker.copy(properties).id)
+ @layer.getMarker(@bufferMarker.copy(properties).id)
###
Section: Event Subscription
@@ -129,7 +130,7 @@ class Marker
@emitter.on 'did-destroy', callback
###
- Section: Marker Details
+ Section: TextEditorMarker Details
###
# Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be
@@ -140,7 +141,7 @@ class Marker
# Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker
# can be invalid without being destroyed, in which case undoing the invalidating
# operation would restore the marker. Once a marker is destroyed by calling
- # {Marker::destroy}, no undo/redo operation can ever bring it back.
+ # {TextEditorMarker::destroy}, no undo/redo operation can ever bring it back.
isDestroyed: ->
@bufferMarker.isDestroyed()
@@ -169,7 +170,7 @@ class Marker
@bufferMarker.setProperties(properties)
matchesProperties: (attributes) ->
- attributes = @displayBuffer.translateToBufferMarkerParams(attributes)
+ attributes = @layer.translateToBufferMarkerParams(attributes)
@bufferMarker.matchesParams(attributes)
###
@@ -179,14 +180,14 @@ class Marker
# Essential: Returns a {Boolean} indicating whether this marker is equivalent to
# another marker, meaning they have the same range and options.
#
- # * `other` {Marker} other marker
+ # * `other` {TextEditorMarker} other marker
isEqual: (other) ->
return false unless other instanceof @constructor
@bufferMarker.isEqual(other.bufferMarker)
# Essential: Compares this marker to another based on their ranges.
#
- # * `other` {Marker}
+ # * `other` {TextEditorMarker}
#
# Returns a {Number}
compare: (other) ->
@@ -225,28 +226,28 @@ class Marker
@setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options)
# Essential: Retrieves the buffer position of the marker's start. This will always be
- # less than or equal to the result of {Marker::getEndBufferPosition}.
+ # less than or equal to the result of {TextEditorMarker::getEndBufferPosition}.
#
# Returns a {Point}.
getStartBufferPosition: ->
@bufferMarker.getStartPosition()
# Essential: Retrieves the screen position of the marker's start. This will always be
- # less than or equal to the result of {Marker::getEndScreenPosition}.
+ # less than or equal to the result of {TextEditorMarker::getEndScreenPosition}.
#
# Returns a {Point}.
getStartScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true)
# Essential: Retrieves the buffer position of the marker's end. This will always be
- # greater than or equal to the result of {Marker::getStartBufferPosition}.
+ # greater than or equal to the result of {TextEditorMarker::getStartBufferPosition}.
#
# Returns a {Point}.
getEndBufferPosition: ->
@bufferMarker.getEndPosition()
# Essential: Retrieves the screen position of the marker's end. This will always be
- # greater than or equal to the result of {Marker::getStartScreenPosition}.
+ # greater than or equal to the result of {TextEditorMarker::getStartScreenPosition}.
#
# Returns a {Point}.
getEndScreenPosition: ->
@@ -330,10 +331,10 @@ class Marker
# Returns a {String} representation of the marker
inspect: ->
- "Marker(id: #{@id}, bufferRange: #{@getBufferRange()})"
+ "TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})"
destroyed: ->
- delete @displayBuffer.markers[@id]
+ @layer.didDestroyMarker(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee
index 3b679b319..339280bf0 100644
--- a/src/text-editor-presenter.coffee
+++ b/src/text-editor-presenter.coffee
@@ -25,10 +25,9 @@ class TextEditorPresenter
@emitter = new Emitter
@visibleHighlights = {}
@characterWidthsByScope = {}
- @rangesByDecorationId = {}
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
- @customGutterDecorationsByGutterNameAndScreenRow = {}
+ @customGutterDecorationsByGutterName = {}
@screenRowsToMeasure = []
@transferMeasurementsToModel()
@transferMeasurementsFromModel()
@@ -46,6 +45,9 @@ class TextEditorPresenter
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) ->
@@ -181,7 +183,7 @@ class TextEditorPresenter
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
- @disposables.add @model.onDidUpdateMarkers =>
+ @disposables.add @model.onDidUpdateDecorations =>
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateDecorations = true
@@ -210,11 +212,9 @@ class TextEditorPresenter
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
- @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
@disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
@disposables.add @model.onDidRequestAutoscroll(@requestAutoscroll.bind(this))
@disposables.add @model.onDidChangeFirstVisibleScreenRow(@didChangeFirstVisibleScreenRow.bind(this))
- @observeDecoration(decoration) for decoration in @model.getDecorations()
@observeCursor(cursor) for cursor in @model.getCursors()
@disposables.add @model.onDidAddGutter(@didAddGutter.bind(this))
return
@@ -623,16 +623,14 @@ class TextEditorPresenter
@clearDecorationsForCustomGutterName(gutterName)
else
@customGutterDecorations[gutterName] = {}
- continue if not @gutterIsVisible(gutter)
- relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
- relevantDecorations.forEach (decoration) =>
- decorationRange = decoration.getMarker().getScreenRange()
- @customGutterDecorations[gutterName][decoration.id] =
- top: @lineHeight * decorationRange.start.row
- height: @lineHeight * decorationRange.getRowCount()
- item: decoration.getProperties().item
- class: decoration.getProperties().class
+ continue unless @gutterIsVisible(gutter)
+ for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName]
+ @customGutterDecorations[gutterName][decorationId] =
+ top: @lineHeight * screenRange.start.row
+ height: @lineHeight * screenRange.getRowCount()
+ item: properties.item
+ class: properties.class
clearAllCustomGutterDecorations: ->
allGutterNames = Object.keys(@customGutterDecorations)
@@ -845,32 +843,20 @@ class TextEditorPresenter
return null if @model.isMini()
decorationClasses = null
- for id, decoration of @lineDecorationsByScreenRow[row]
+ for id, properties of @lineDecorationsByScreenRow[row]
decorationClasses ?= []
- decorationClasses.push(decoration.getProperties().class)
+ decorationClasses.push(properties.class)
decorationClasses
lineNumberDecorationClassesForRow: (row) ->
return null if @model.isMini()
decorationClasses = null
- for id, decoration of @lineNumberDecorationsByScreenRow[row]
+ for id, properties of @lineNumberDecorationsByScreenRow[row]
decorationClasses ?= []
- decorationClasses.push(decoration.getProperties().class)
+ decorationClasses.push(properties.class)
decorationClasses
- # Returns a {Set} of {Decoration}s on the given custom gutter from startRow to endRow (inclusive).
- customGutterDecorationsInRange: (gutterName, startRow, endRow) ->
- decorations = new Set
-
- return decorations if @model.isMini() or gutterName is 'line-number' or
- not @customGutterDecorationsByGutterNameAndScreenRow[gutterName]
-
- for screenRow in [@startRow..@endRow - 1]
- for id, decoration of @customGutterDecorationsByGutterNameAndScreenRow[gutterName][screenRow]
- decorations.add(decoration)
- decorations
-
getCursorBlinkPeriod: -> @cursorBlinkPeriod
getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay
@@ -1182,93 +1168,32 @@ class TextEditorPresenter
rect
- observeDecoration: (decoration) ->
- decorationDisposables = new CompositeDisposable
- if decoration.isType('highlight')
- decorationDisposables.add decoration.onDidFlash =>
- @shouldUpdateDecorations = true
- @emitDidUpdateState()
-
- decorationDisposables.add decoration.onDidChangeProperties (event) =>
- @decorationPropertiesDidChange(decoration, event)
- decorationDisposables.add decoration.onDidDestroy =>
- @disposables.remove(decorationDisposables)
- decorationDisposables.dispose()
- @didDestroyDecoration(decoration)
- @disposables.add(decorationDisposables)
-
- decorationPropertiesDidChange: (decoration, {oldProperties}) ->
- @shouldUpdateDecorations = true
- if decoration.isType('line') or decoration.isType('gutter')
- if decoration.isType('line') or Decoration.isType(oldProperties, 'line')
- @shouldUpdateLinesState = true
- if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number')
- @shouldUpdateLineNumbersState = true
- if (decoration.isType('gutter') and not decoration.isType('line-number')) or
- (Decoration.isType(oldProperties, 'gutter') and not Decoration.isType(oldProperties, 'line-number'))
- @shouldUpdateCustomGutterDecorationState = true
- else if decoration.isType('overlay')
- @shouldUpdateOverlaysState = true
- @emitDidUpdateState()
-
- didDestroyDecoration: (decoration) ->
- @shouldUpdateDecorations = true
- if decoration.isType('line') or decoration.isType('gutter')
- @shouldUpdateLinesState = true if decoration.isType('line')
- if decoration.isType('line-number')
- @shouldUpdateLineNumbersState = true
- else if decoration.isType('gutter')
- @shouldUpdateCustomGutterDecorationState = true
- if decoration.isType('overlay')
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- didAddDecoration: (decoration) ->
- @observeDecoration(decoration)
-
- if decoration.isType('line') or decoration.isType('gutter')
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true if decoration.isType('line')
- if decoration.isType('line-number')
- @shouldUpdateLineNumbersState = true
- else if decoration.isType('gutter')
- @shouldUpdateCustomGutterDecorationState = true
- else if decoration.isType('highlight')
- @shouldUpdateDecorations = true
- else if decoration.isType('overlay')
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
fetchDecorations: ->
- @decorations = []
-
return unless 0 <= @startRow <= @endRow <= Infinity
-
- for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1)
- range = @model.getMarker(markerId).getScreenRange()
- for decoration in decorations
- @decorations.push({decoration, range})
+ @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1)
updateLineDecorations: ->
- @rangesByDecorationId = {}
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
- @customGutterDecorationsByGutterNameAndScreenRow = {}
+ @customGutterDecorationsByGutterName = {}
- for {decoration, range} in @decorations
- if decoration.isType('line') or decoration.isType('gutter')
- @addToLineDecorationCaches(decoration, range)
+ for decorationId, decorationState of @decorations
+ {properties, screenRange, rangeIsReversed} = decorationState
+ if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number')
+ @addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed)
+
+ else if Decoration.isType(properties, 'gutter') and properties.gutterName?
+ @customGutterDecorationsByGutterName[properties.gutterName] ?= {}
+ @customGutterDecorationsByGutterName[properties.gutterName][decorationId] = decorationState
return
updateHighlightDecorations: ->
@visibleHighlights = {}
- for {decoration, range} in @decorations
- if decoration.isType('highlight')
- @updateHighlightState(decoration, range)
+ for decorationId, {properties, screenRange} of @decorations
+ if Decoration.isType(properties, 'highlight')
+ @updateHighlightState(decorationId, properties, screenRange)
for tileId, tileState of @state.content.tiles
for id, highlight of tileState.highlights
@@ -1276,50 +1201,29 @@ class TextEditorPresenter
return
- removeFromLineDecorationCaches: (decoration) ->
- @removePropertiesFromLineDecorationCaches(decoration.id, decoration.getProperties())
-
- removePropertiesFromLineDecorationCaches: (decorationId, decorationProperties) ->
- if range = @rangesByDecorationId[decorationId]
- delete @rangesByDecorationId[decorationId]
-
- gutterName = decorationProperties.gutterName
- for row in [range.start.row..range.end.row] by 1
- delete @lineDecorationsByScreenRow[row]?[decorationId]
- delete @lineNumberDecorationsByScreenRow[row]?[decorationId]
- delete @customGutterDecorationsByGutterNameAndScreenRow[gutterName]?[row]?[decorationId] if gutterName
- return
-
- addToLineDecorationCaches: (decoration, range) ->
- marker = decoration.getMarker()
- properties = decoration.getProperties()
-
- return unless marker.isValid()
-
- if range.isEmpty()
+ addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) ->
+ if screenRange.isEmpty()
return if properties.onlyNonEmpty
else
return if properties.onlyEmpty
- omitLastRow = range.end.column is 0
+ omitLastRow = screenRange.end.column is 0
- @rangesByDecorationId[decoration.id] = range
+ if rangeIsReversed
+ headPosition = screenRange.start
+ else
+ headPosition = screenRange.end
- for row in [range.start.row..range.end.row] by 1
- continue if properties.onlyHead and row isnt marker.getHeadScreenPosition().row
- continue if omitLastRow and row is range.end.row
+ for row in [screenRange.start.row..screenRange.end.row] by 1
+ continue if properties.onlyHead and row isnt headPosition.row
+ continue if omitLastRow and row is screenRange.end.row
- if decoration.isType('line')
+ if Decoration.isType(properties, 'line')
@lineDecorationsByScreenRow[row] ?= {}
- @lineDecorationsByScreenRow[row][decoration.id] = decoration
+ @lineDecorationsByScreenRow[row][decorationId] = properties
- if decoration.isType('line-number')
+ if Decoration.isType(properties, 'line-number')
@lineNumberDecorationsByScreenRow[row] ?= {}
- @lineNumberDecorationsByScreenRow[row][decoration.id] = decoration
- else if decoration.isType('gutter')
- gutterName = decoration.getProperties().gutterName
- @customGutterDecorationsByGutterNameAndScreenRow[gutterName] ?= {}
- @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row] ?= {}
- @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row][decoration.id] = decoration
+ @lineNumberDecorationsByScreenRow[row][decorationId] = properties
return
@@ -1339,46 +1243,34 @@ class TextEditorPresenter
intersectingRange
- updateHighlightState: (decoration, range) ->
+ updateHighlightState: (decorationId, properties, screenRange) ->
return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements()
- properties = decoration.getProperties()
- marker = decoration.getMarker()
+ return if screenRange.isEmpty()
- if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(@startRow, @endRow - 1)
- return
+ if screenRange.start.row < @startRow
+ screenRange.start.row = @startRow
+ screenRange.start.column = 0
+ if screenRange.end.row >= @endRow
+ screenRange.end.row = @endRow
+ screenRange.end.column = 0
- 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 if screenRange.isEmpty()
- return if range.isEmpty()
-
- flash = decoration.consumeNextFlash()
-
- startTile = @tileForRow(range.start.row)
- endTile = @tileForRow(range.end.row)
+ startTile = @tileForRow(screenRange.start.row)
+ endTile = @tileForRow(screenRange.end.row)
for tileStartRow in [startTile..endTile] by @tileSize
- rangeWithinTile = @intersectRangeWithTile(range, tileStartRow)
+ rangeWithinTile = @intersectRangeWithTile(screenRange, tileStartRow)
continue if rangeWithinTile.isEmpty()
tileState = @state.content.tiles[tileStartRow] ?= {highlights: {}}
- highlightState = tileState.highlights[decoration.id] ?= {
- flashCount: 0
- flashDuration: null
- flashClass: null
- }
-
- if flash?
- highlightState.flashCount++
- highlightState.flashClass = flash.class
- highlightState.flashDuration = flash.duration
+ highlightState = tileState.highlights[decorationId] ?= {}
+ highlightState.flashCount = properties.flashCount
+ highlightState.flashClass = properties.flashClass
+ highlightState.flashDuration = properties.flashDuration
highlightState.class = properties.class
highlightState.deprecatedRegionClass = properties.deprecatedRegionClass
highlightState.regions = @buildHighlightRegions(rangeWithinTile)
@@ -1387,7 +1279,7 @@ class TextEditorPresenter
@repositionRegionWithinTile(region, tileStartRow)
@visibleHighlights[tileStartRow] ?= {}
- @visibleHighlights[tileStartRow][decoration.id] = true
+ @visibleHighlights[tileStartRow][decorationId] = true
true
diff --git a/src/text-editor.coffee b/src/text-editor.coffee
index 27376c7f2..a419429cc 100644
--- a/src/text-editor.coffee
+++ b/src/text-editor.coffee
@@ -72,6 +72,7 @@ class TextEditor extends Model
throw error
state.displayBuffer = displayBuffer
+ state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId)
state.config = atomEnvironment.config
state.notificationManager = atomEnvironment.notifications
state.packageManager = atomEnvironment.packages
@@ -87,10 +88,11 @@ class TextEditor extends Model
super
{
- @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength,
- softWrapped, @displayBuffer, buffer, suppressCursorCreation, @mini, @placeholderText,
- lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager,
- @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
+ @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine,initialColumn, tabLength,
+ softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
+ @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
+ @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
+ @project, @assert, @applicationDelegate
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
@@ -115,8 +117,9 @@ class TextEditor extends Model
@config, @assert, @grammarRegistry, @packageManager
})
@buffer = @displayBuffer.buffer
+ @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true)
- for marker in @findMarkers(@getSelectionMarkerAttributes())
+ for marker in @selectionsMarkerLayer.getMarkers()
marker.setProperties(preserveFolds: true)
@addSelection(marker)
@@ -146,6 +149,7 @@ class TextEditor extends Model
firstVisibleScreenRow: @getFirstVisibleScreenRow()
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
displayBuffer: @displayBuffer.serialize()
+ selectionsMarkerLayerId: @selectionsMarkerLayer.id
subscribeToBuffer: ->
@buffer.retain()
@@ -161,9 +165,9 @@ class TextEditor extends Model
@preserveCursorPositionOnBufferReload()
subscribeToDisplayBuffer: ->
- @disposables.add @displayBuffer.onDidCreateMarker @handleMarkerCreated
- @disposables.add @displayBuffer.onDidChangeGrammar => @handleGrammarChange()
- @disposables.add @displayBuffer.onDidTokenize => @handleTokenization()
+ @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
+ @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
+ @disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this)
@disposables.add @displayBuffer.onDidChange (e) =>
@mergeIntersectingSelections()
@emitter.emit 'did-change', e
@@ -177,6 +181,7 @@ class TextEditor extends Model
@disposables.dispose()
@tabTypeSubscription.dispose()
selection.destroy() for selection in @selections.slice()
+ @selectionsMarkerLayer.destroy()
@buffer.release()
@displayBuffer.destroy()
@languageMode.destroy()
@@ -471,6 +476,9 @@ class TextEditor extends Model
onDidUpdateMarkers: (callback) ->
@displayBuffer.onDidUpdateMarkers(callback)
+ onDidUpdateDecorations: (callback) ->
+ @displayBuffer.onDidUpdateDecorations(callback)
+
# Essential: Retrieves the current {TextBuffer}.
getBuffer: -> @buffer
@@ -480,14 +488,13 @@ class TextEditor extends Model
# Create an {TextEditor} with its initial state based on this object
copy: ->
displayBuffer = @displayBuffer.copy()
+ selectionsMarkerLayer = displayBuffer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
softTabs = @getSoftTabs()
newEditor = new TextEditor({
- @buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true,
- @config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
- @grammarRegistry, @project, @assert, @applicationDelegate
+ @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs,
+ suppressCursorCreation: true, @config, @notificationManager, @packageManager,
+ @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
})
- for marker in @findMarkers(editorId: @id)
- marker.copy(editorId: newEditor.id, preserveFolds: true)
newEditor
# Controls visibility based on the given {Boolean}.
@@ -502,6 +509,9 @@ class TextEditor extends Model
isMini: -> @mini
+ setUpdatedSynchronously: (updatedSynchronously) ->
+ @displayBuffer.setUpdatedSynchronously(updatedSynchronously)
+
onDidChangeMini: (callback) ->
@emitter.on 'did-change-mini', callback
@@ -869,116 +879,177 @@ class TextEditor extends Model
@transact groupingInterval, =>
fn(selection, index) for selection, index in @getSelectionsOrderedByBufferPosition()
- # Move lines intersection the most recent selection up by one row in screen
- # coordinates.
+ # Move lines intersecting the most recent selection or multiple selections
+ # up by one row in screen coordinates.
moveLineUp: ->
- selection = @getSelectedBufferRange()
- return if selection.start.row is 0
- lastRow = @buffer.getLastRow()
- return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is ''
+ selections = @getSelectedBufferRanges()
+ selections.sort (a, b) -> a.compare(b)
+
+ if selections[0].start.row is 0
+ return
+
+ if selections[selections.length - 1].start.row is @getLastBufferRow() and @buffer.getLastLine() is ''
+ return
@transact =>
- foldedRows = []
- rows = [selection.start.row..selection.end.row]
- if selection.start.row isnt selection.end.row and selection.end.column is 0
- rows.pop() unless @isFoldedAtBufferRow(selection.end.row)
+ newSelectionRanges = []
- # Move line around the fold that is directly above the selection
- precedingScreenRow = @screenPositionForBufferPosition([selection.start.row]).translate([-1])
- precedingBufferRow = @bufferPositionForScreenPosition(precedingScreenRow).row
- if fold = @largestFoldContainingBufferRow(precedingBufferRow)
- insertDelta = fold.getBufferRange().getRowCount()
- else
- insertDelta = 1
+ while selections.length > 0
+ # Find selections spanning a contiguous set of lines
+ selection = selections.shift()
+ selectionsToMove = [selection]
- for row in rows
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(row)
- bufferRange = fold.getBufferRange()
- startRow = bufferRange.start.row
- endRow = bufferRange.end.row
- foldedRows.push(startRow - insertDelta)
+ while selection.end.row is selections[0]?.start.row
+ selectionsToMove.push(selections[0])
+ selection.end.row = selections[0].end.row
+ selections.shift()
+
+ # Compute the range spanned by all these selections...
+ linesRangeStart = [selection.start.row, 0]
+ if selection.end.row > selection.start.row and selection.end.column is 0
+ # Don't move the last line of a multi-line selection if the selection ends at column 0
+ linesRange = new Range(linesRangeStart, selection.end)
else
- startRow = row
- endRow = row
+ linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0])
- insertPosition = Point.fromObject([startRow - insertDelta])
- endPosition = Point.min([endRow + 1], @buffer.getEndPosition())
- lines = @buffer.getTextInRange([[startRow], endPosition])
- if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row)
- lines = "#{lines}\n"
+ # If there's a fold containing either the starting row or the end row
+ # of the selection then the whole fold needs to be moved and restored.
+ # The initial fold range is stored and will be translated once the
+ # insert delta is know.
+ selectionFoldRanges = []
+ foldAtSelectionStart =
+ @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
+ foldAtSelectionEnd =
+ @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
+ if fold = foldAtSelectionStart ? foldAtSelectionEnd
+ selectionFoldRanges.push range = fold.getBufferRange()
+ newEndRow = range.end.row + 1
+ linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
+ fold.destroy()
- @buffer.deleteRows(startRow, endRow)
+ # If selected line range is preceded by a fold, one line above on screen
+ # could be multiple lines in the buffer.
+ precedingScreenRow = @screenRowForBufferRow(linesRange.start.row) - 1
+ precedingBufferRow = @bufferRowForScreenRow(precedingScreenRow)
+ insertDelta = linesRange.start.row - precedingBufferRow
+
+ # Any folds in the text that is moved will need to be re-created.
+ # It includes the folds that were intersecting with the selection.
+ rangesToRefold = selectionFoldRanges.concat(
+ @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
+ range = fold.getBufferRange()
+ fold.destroy()
+ range
+ ).map (range) -> range.translate([-insertDelta, 0])
# Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row)
- @unfoldBufferRow(insertPosition.row)
- foldedRows.push(insertPosition.row + endRow - startRow + fold.getBufferRange().getRowCount())
+ if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow)
+ rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0]))
+ fold.destroy()
- @buffer.insert(insertPosition, lines)
+ # Delete lines spanned by selection and insert them on the preceding buffer row
+ lines = @buffer.getTextInRange(linesRange)
+ lines += @buffer.lineEndingForRow(linesRange.end.row - 1) unless lines[lines.length - 1] is '\n'
+ @buffer.delete(linesRange)
+ @buffer.insert([precedingBufferRow, 0], lines)
- # Restore folds that existed before the lines were moved
- for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
- @foldBufferRow(foldedRow)
+ # Restore folds that existed before the lines were moved
+ for rangeToRefold in rangesToRefold
+ @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row)
- @setSelectedBufferRange(selection.translate([-insertDelta]), preserveFolds: true, autoscroll: true)
+ for selection in selectionsToMove
+ newSelectionRanges.push(selection.translate([-insertDelta, 0]))
+
+ @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true})
@autoIndentSelectedRows() if @shouldAutoIndent()
+ @scrollToBufferPosition([newSelectionRanges[0].start.row, 0])
- # Move lines intersecting the most recent selection down by one row in screen
- # coordinates.
+ # Move lines intersecting the most recent selection or muiltiple selections
+ # down by one row in screen coordinates.
moveLineDown: ->
- selection = @getSelectedBufferRange()
- lastRow = @buffer.getLastRow()
- return if selection.end.row is lastRow
- return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is ''
+ selections = @getSelectedBufferRanges()
+ selections.sort (a, b) -> a.compare(b)
+ selections = selections.reverse()
@transact =>
- foldedRows = []
- rows = [selection.end.row..selection.start.row]
- if selection.start.row isnt selection.end.row and selection.end.column is 0
- rows.shift() unless @isFoldedAtBufferRow(selection.end.row)
+ @consolidateSelections()
+ newSelectionRanges = []
- # Move line around the fold that is directly below the selection
- followingScreenRow = @screenPositionForBufferPosition([selection.end.row]).translate([1])
- followingBufferRow = @bufferPositionForScreenPosition(followingScreenRow).row
- if fold = @largestFoldContainingBufferRow(followingBufferRow)
- insertDelta = fold.getBufferRange().getRowCount()
- else
- insertDelta = 1
+ while selections.length > 0
+ # Find selections spanning a contiguous set of lines
+ selection = selections.shift()
+ selectionsToMove = [selection]
- for row in rows
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(row)
- bufferRange = fold.getBufferRange()
- startRow = bufferRange.start.row
- endRow = bufferRange.end.row
- foldedRows.push(endRow + insertDelta)
+ # if the current selection start row matches the next selections' end row - make them one selection
+ while selection.start.row is selections[0]?.end.row
+ selectionsToMove.push(selections[0])
+ selection.start.row = selections[0].start.row
+ selections.shift()
+
+ # Compute the range spanned by all these selections...
+ linesRangeStart = [selection.start.row, 0]
+ if selection.end.row > selection.start.row and selection.end.column is 0
+ # Don't move the last line of a multi-line selection if the selection ends at column 0
+ linesRange = new Range(linesRangeStart, selection.end)
else
- startRow = row
- endRow = row
+ linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0])
- if endRow + 1 is lastRow
- endPosition = [endRow, @buffer.lineLengthForRow(endRow)]
- else
- endPosition = [endRow + 1]
- lines = @buffer.getTextInRange([[startRow], endPosition])
- @buffer.deleteRows(startRow, endRow)
+ # If there's a fold containing either the starting row or the end row
+ # of the selection then the whole fold needs to be moved and restored.
+ # The initial fold range is stored and will be translated once the
+ # insert delta is know.
+ selectionFoldRanges = []
+ foldAtSelectionStart =
+ @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
+ foldAtSelectionEnd =
+ @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
+ if fold = foldAtSelectionStart ? foldAtSelectionEnd
+ selectionFoldRanges.push range = fold.getBufferRange()
+ newEndRow = range.end.row + 1
+ linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
+ fold.destroy()
- insertPosition = Point.min([startRow + insertDelta], @buffer.getEndPosition())
- if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0
+ # If selected line range is followed by a fold, one line below on screen
+ # could be multiple lines in the buffer. But at the same time, if the
+ # next buffer row is wrapped, one line in the buffer can represent many
+ # screen rows.
+ followingScreenRow = @displayBuffer.lastScreenRowForBufferRow(linesRange.end.row) + 1
+ followingBufferRow = @bufferRowForScreenRow(followingScreenRow)
+ insertDelta = followingBufferRow - linesRange.end.row
+
+ # Any folds in the text that is moved will need to be re-created.
+ # It includes the folds that were intersecting with the selection.
+ rangesToRefold = selectionFoldRanges.concat(
+ @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
+ range = fold.getBufferRange()
+ fold.destroy()
+ range
+ ).map (range) -> range.translate([insertDelta, 0])
+
+ # Make sure the inserted text doesn't go into an existing fold
+ if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow)
+ rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0]))
+ fold.destroy()
+
+ # Delete lines spanned by selection and insert them on the following correct buffer row
+ insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0)
+ lines = @buffer.getTextInRange(linesRange)
+ if linesRange.end.row is @buffer.getLastRow()
lines = "\n#{lines}"
- # Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row)
- @unfoldBufferRow(insertPosition.row)
- foldedRows.push(insertPosition.row + fold.getBufferRange().getRowCount())
-
+ @buffer.delete(linesRange)
@buffer.insert(insertPosition, lines)
- # Restore folds that existed before the lines were moved
- for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
- @foldBufferRow(foldedRow)
+ # Restore folds that existed before the lines were moved
+ for rangeToRefold in rangesToRefold
+ @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row)
- @setSelectedBufferRange(selection.translate([insertDelta]), preserveFolds: true, autoscroll: true)
+ for selection in selectionsToMove
+ newSelectionRanges.push(selection.translate([insertDelta, 0]))
+
+ @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true})
@autoIndentSelectedRows() if @shouldAutoIndent()
+ @scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0])
# Duplicate the most recent cursor's current line.
duplicateLines: ->
@@ -1335,9 +1406,9 @@ class TextEditor extends Model
Section: Decorations
###
- # Essential: Adds a decoration that tracks a {Marker}. When the marker moves,
- # is invalidated, or is destroyed, the decoration will be updated to reflect
- # the marker's state.
+ # Essential: Add a decoration that tracks a {TextEditorMarker}. When the
+ # marker moves, is invalidated, or is destroyed, the decoration will be
+ # updated to reflect the marker's state.
#
# The following are the supported decorations types:
#
@@ -1356,28 +1427,28 @@ class TextEditor extends Model
#
# ```
# * __overlay__: Positions the view associated with the given item at the head
- # or tail of the given `Marker`.
- # * __gutter__: A decoration that tracks a {Marker} in a {Gutter}. Gutter
+ # or tail of the given `TextEditorMarker`.
+ # * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter
# decorations are created by calling {Gutter::decorateMarker} on the
# desired `Gutter` instance.
#
# ## Arguments
#
- # * `marker` A {Marker} you want this decoration to follow.
+ # * `marker` A {TextEditorMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration e.g.
# `{type: 'line-number', class: 'linter-error'}`
# * `type` There are several supported decoration types. The behavior of the
# types are as follows:
# * `line` Adds the given `class` to the lines overlapping the rows
- # spanned by the `Marker`.
+ # spanned by the `TextEditorMarker`.
# * `line-number` Adds the given `class` to the line numbers overlapping
- # the rows spanned by the `Marker`.
+ # the rows spanned by the `TextEditorMarker`.
# * `highlight` Creates a `.highlight` div with the nested class with up
- # to 3 nested regions that fill the area spanned by the `Marker`.
+ # to 3 nested regions that fill the area spanned by the `TextEditorMarker`.
# * `overlay` Positions the view associated with the given item at the
- # head or tail of the given `Marker`, depending on the `position`
+ # head or tail of the given `TextEditorMarker`, depending on the `position`
# property.
- # * `gutter` Tracks a {Marker} in a {Gutter}. Created by calling
+ # * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling
# {Gutter::decorateMarker} on the desired `Gutter` instance.
# * `class` This CSS class will be applied to the decorated line number,
# line, highlight, or overlay.
@@ -1385,35 +1456,53 @@ class TextEditor extends Model
# corresponding view registered. Only applicable to the `gutter` and
# `overlay` types.
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
- # the head of the `Marker`. Only applicable to the `line` and
+ # the head of the `TextEditorMarker`. Only applicable to the `line` and
# `line-number` types.
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
- # the associated `Marker` is empty. Only applicable to the `gutter`,
+ # the associated `TextEditorMarker` is empty. Only applicable to the `gutter`,
# `line`, and `line-number` types.
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
- # if the associated `Marker` is non-empty. Only applicable to the
+ # if the associated `TextEditorMarker` is non-empty. Only applicable to the
# `gutter`, `line`, and `line-number` types.
# * `position` (optional) Only applicable to decorations of type `overlay`,
- # controls where the overlay view is positioned relative to the `Marker`.
+ # controls where the overlay view is positioned relative to the `TextEditorMarker`.
# Values can be `'head'` (the default), or `'tail'`.
#
# Returns a {Decoration} object
decorateMarker: (marker, decorationParams) ->
@displayBuffer.decorateMarker(marker, decorationParams)
- # Essential: Get all the decorations within a screen row range.
+ # Essential: *Experimental:* Add a decoration to every marker in the given
+ # marker layer. Can be used to decorate a large number of markers without
+ # having to create and manage many individual decorations.
+ #
+ # * `markerLayer` A {TextEditorMarkerLayer} or {MarkerLayer} to decorate.
+ # * `decorationParams` The same parameters that are passed to
+ # {decorateMarker}, except the `type` cannot be `overlay` or `gutter`.
+ #
+ # This API is experimental and subject to change on any release.
+ #
+ # Returns a {LayerDecoration}.
+ decorateMarkerLayer: (markerLayer, decorationParams) ->
+ @displayBuffer.decorateMarkerLayer(markerLayer, decorationParams)
+
+ # Deprecated: Get all the decorations within a screen row range on the default
+ # layer.
#
# * `startScreenRow` the {Number} beginning screen row
# * `endScreenRow` the {Number} end screen row (inclusive)
#
# Returns an {Object} of decorations in the form
# `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}`
- # where the keys are {Marker} IDs, and the values are an array of decoration
+ # where the keys are {TextEditorMarker} IDs, and the values are an array of decoration
# params objects attached to the marker.
# Returns an empty object when no decorations are found
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
@displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow)
+ decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
+ @displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
+
# Extended: Get all decorations.
#
# * `propertyFilter` (optional) An {Object} containing key value pairs that
@@ -1469,10 +1558,10 @@ class TextEditor extends Model
Section: Markers
###
- # Essential: Create a marker with the given range in buffer coordinates. This
- # marker will maintain its logical location as the buffer is changed, so if
- # you mark a particular word, the marker will remain over that word even if
- # the word's location in the buffer changes.
+ # Essential: Create a marker on the default marker layer with the given range
+ # in buffer coordinates. This marker will maintain its logical location as the
+ # buffer is changed, so if you mark a particular word, the marker will remain
+ # over that word even if the word's location in the buffer changes.
#
# * `range` A {Range} or range-compatible {Array}
# * `properties` A hash of key-value pairs to associate with the marker. There
@@ -1500,14 +1589,14 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {Marker}.
+ # Returns a {TextEditorMarker}.
markBufferRange: (args...) ->
@displayBuffer.markBufferRange(args...)
- # Essential: Create a marker with the given range in screen coordinates. This
- # marker will maintain its logical location as the buffer is changed, so if
- # you mark a particular word, the marker will remain over that word even if
- # the word's location in the buffer changes.
+ # Essential: Create a marker on the default marker layer with the given range
+ # in screen coordinates. This marker will maintain its logical location as the
+ # buffer is changed, so if you mark a particular word, the marker will remain
+ # over that word even if the word's location in the buffer changes.
#
# * `range` A {Range} or range-compatible {Array}
# * `properties` A hash of key-value pairs to associate with the marker. There
@@ -1535,29 +1624,32 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {Marker}.
+ # Returns a {TextEditorMarker}.
markScreenRange: (args...) ->
@displayBuffer.markScreenRange(args...)
- # Essential: Mark the given position in buffer coordinates.
+ # Essential: Mark the given position in buffer coordinates on the default
+ # marker layer.
#
# * `position` A {Point} or {Array} of `[row, column]`.
# * `options` (optional) See {TextBuffer::markRange}.
#
- # Returns a {Marker}.
+ # Returns a {TextEditorMarker}.
markBufferPosition: (args...) ->
@displayBuffer.markBufferPosition(args...)
- # Essential: Mark the given position in screen coordinates.
+ # Essential: Mark the given position in screen coordinates on the default
+ # marker layer.
#
# * `position` A {Point} or {Array} of `[row, column]`.
# * `options` (optional) See {TextBuffer::markRange}.
#
- # Returns a {Marker}.
+ # Returns a {TextEditorMarker}.
markScreenPosition: (args...) ->
@displayBuffer.markScreenPosition(args...)
- # Essential: Find all {Marker}s that match the given properties.
+ # Essential: Find all {TextEditorMarker}s on the default marker layer that
+ # match the given properties.
#
# This method finds markers based on the given properties. Markers can be
# associated with custom properties that will be compared with basic equality.
@@ -1579,44 +1671,60 @@ class TextEditor extends Model
findMarkers: (properties) ->
@displayBuffer.findMarkers(properties)
- # Extended: Observe changes in the set of markers that intersect a particular
- # region of the editor.
- #
- # * `callback` A {Function} to call whenever one or more {Marker}s appears,
- # disappears, or moves within the given region.
- # * `event` An {Object} with the following keys:
- # * `insert` A {Set} containing the ids of all markers that appeared
- # in the range.
- # * `update` A {Set} containing the ids of all markers that moved within
- # the region.
- # * `remove` A {Set} containing the ids of all markers that disappeared
- # from the region.
- #
- # Returns a {MarkerObservationWindow}, which allows you to specify the region
- # of interest by calling {MarkerObservationWindow::setBufferRange} or
- # {MarkerObservationWindow::setScreenRange}.
- observeMarkers: (callback) ->
- @displayBuffer.observeMarkers(callback)
-
- # Extended: Get the {Marker} for the given marker id.
+ # Extended: Get the {TextEditorMarker} on the default layer for the given
+ # marker id.
#
# * `id` {Number} id of the marker
getMarker: (id) ->
@displayBuffer.getMarker(id)
- # Extended: Get all {Marker}s. Consider using {::findMarkers}
+ # Extended: Get all {TextEditorMarker}s on the default marker layer. Consider
+ # using {::findMarkers}
getMarkers: ->
@displayBuffer.getMarkers()
- # Extended: Get the number of markers in this editor's buffer.
+ # Extended: Get the number of markers in the default marker layer.
#
# Returns a {Number}.
getMarkerCount: ->
@buffer.getMarkerCount()
- # {Delegates to: DisplayBuffer.destroyMarker}
- destroyMarker: (args...) ->
- @displayBuffer.destroyMarker(args...)
+ destroyMarker: (id) ->
+ @getMarker(id)?.destroy()
+
+ # Extended: *Experimental:* Create a marker layer to group related markers.
+ #
+ # * `options` An {Object} containing the following keys:
+ # * `maintainHistory` A {Boolean} indicating whether marker state should be
+ # restored on undo/redo. Defaults to `false`.
+ #
+ # This API is experimental and subject to change on any release.
+ #
+ # Returns a {TextEditorMarkerLayer}.
+ addMarkerLayer: (options) ->
+ @displayBuffer.addMarkerLayer(options)
+
+ # Public: *Experimental:* Get a {TextEditorMarkerLayer} by id.
+ #
+ # * `id` The id of the marker layer to retrieve.
+ #
+ # This API is experimental and subject to change on any release.
+ #
+ # Returns a {MarkerLayer} or `undefined` if no layer exists with the given
+ # id.
+ getMarkerLayer: (id) ->
+ @displayBuffer.getMarkerLayer(id)
+
+ # Public: *Experimental:* Get the default {TextEditorMarkerLayer}.
+ #
+ # All marker APIs not tied to an explicit layer interact with this default
+ # layer.
+ #
+ # This API is experimental and subject to change on any release.
+ #
+ # Returns a {TextEditorMarkerLayer}.
+ getDefaultMarkerLayer: ->
+ @displayBuffer.getDefaultMarkerLayer()
###
Section: Cursors
@@ -1686,7 +1794,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtBufferPosition: (bufferPosition, options) ->
- @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
+ @selectionsMarkerLayer.markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1696,7 +1804,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtScreenPosition: (screenPosition, options) ->
- @markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
+ @selectionsMarkerLayer.markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1821,7 +1929,7 @@ class TextEditor extends Model
getCursorsOrderedByBufferPosition: ->
@getCursors().sort (a, b) -> a.compare(b)
- # Add a cursor based on the given {Marker}.
+ # Add a cursor based on the given {TextEditorMarker}.
addCursor: (marker) ->
cursor = new Cursor(editor: this, marker: marker, config: @config)
@cursors.push(cursor)
@@ -1974,7 +2082,7 @@ class TextEditor extends Model
#
# Returns the added {Selection}.
addSelectionForBufferRange: (bufferRange, options={}) ->
- @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
+ @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
@getLastSelection().autoscroll() unless options.autoscroll is false
@getLastSelection()
@@ -1987,7 +2095,7 @@ class TextEditor extends Model
#
# Returns the added {Selection}.
addSelectionForScreenRange: (screenRange, options={}) ->
- @markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options))
+ @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options))
@getLastSelection().autoscroll() unless options.autoscroll is false
@getLastSelection()
@@ -2170,7 +2278,7 @@ class TextEditor extends Model
# Extended: Select the range of the given marker if it is valid.
#
- # * `marker` A {Marker}
+ # * `marker` A {TextEditorMarker}
#
# Returns the selected {Range} or `undefined` if the marker is invalid.
selectMarker: (marker) ->
@@ -2296,9 +2404,9 @@ class TextEditor extends Model
_.reduce(tail, reducer, [head])
return result if fn?
- # Add a {Selection} based on the given {Marker}.
+ # Add a {Selection} based on the given {TextEditorMarker}.
#
- # * `marker` The {Marker} to highlight
+ # * `marker` The {TextEditorMarker} to highlight
# * `options` (optional) An {Object} that pertains to the {Selection} constructor.
#
# Returns the new {Selection}.
@@ -3006,10 +3114,6 @@ class TextEditor extends Model
@subscribeToTabTypeConfig()
@emitter.emit 'did-change-grammar', @getGrammar()
- handleMarkerCreated: (marker) =>
- if marker.matchesProperties(@getSelectionMarkerAttributes())
- @addSelection(marker)
-
###
Section: TextEditor Rendering
###
@@ -3038,7 +3142,7 @@ class TextEditor extends Model
@viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition)
getSelectionMarkerAttributes: ->
- {type: 'selection', editorId: @id, invalidate: 'never', maintainHistory: true}
+ {type: 'selection', invalidate: 'never'}
getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)
diff --git a/src/view-registry.coffee b/src/view-registry.coffee
index 3a46aa87a..0f07600ae 100644
--- a/src/view-registry.coffee
+++ b/src/view-registry.coffee
@@ -43,7 +43,7 @@ _ = require 'underscore-plus'
# ```
module.exports =
class ViewRegistry
- documentUpdateRequested: false
+ animationFrameRequest: null
documentReadInProgress: false
performDocumentPollAfterUpdate: false
debouncedPerformDocumentPoll: null
@@ -195,20 +195,30 @@ class ViewRegistry
pollAfterNextUpdate: ->
@performDocumentPollAfterUpdate = true
+ getNextUpdatePromise: ->
+ @nextUpdatePromise ?= new Promise (resolve) =>
+ @resolveNextUpdatePromise = resolve
+
clearDocumentRequests: ->
@documentReaders = []
@documentWriters = []
@documentPollers = []
- @documentUpdateRequested = false
+ @nextUpdatePromise = null
+ @resolveNextUpdatePromise = null
+ if @animationFrameRequest?
+ cancelAnimationFrame(@animationFrameRequest)
+ @animationFrameRequest = null
@stopPollingDocument()
requestDocumentUpdate: ->
- unless @documentUpdateRequested
- @documentUpdateRequested = true
- requestAnimationFrame(@performDocumentUpdate)
+ @animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate)
performDocumentUpdate: =>
- @documentUpdateRequested = false
+ resolveNextUpdatePromise = @resolveNextUpdatePromise
+ @animationFrameRequest = null
+ @nextUpdatePromise = null
+ @resolveNextUpdatePromise = null
+
writer() while writer = @documentWriters.shift()
@documentReadInProgress = true
@@ -220,6 +230,8 @@ class ViewRegistry
# 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})
@@ -229,7 +241,7 @@ class ViewRegistry
@observer.disconnect()
requestDocumentPoll: =>
- if @documentUpdateRequested
+ if @animationFrameRequest?
@performDocumentPollAfterUpdate = true
else
@debouncedPerformDocumentPoll()
diff --git a/src/workspace.coffee b/src/workspace.coffee
index 80ef47c21..65a0a27fb 100644
--- a/src/workspace.coffee
+++ b/src/workspace.coffee
@@ -468,7 +468,7 @@ class Workspace extends Model
when 'EACCES'
@notificationManager.addWarning("Permission denied '#{error.path}'")
return Promise.resolve()
- when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL'
+ when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL', 'EMFILE'
@notificationManager.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message)
return Promise.resolve()
else