Merge branch 'master' into mb-synchronous-scroll-position

Conflicts:
	src/text-editor-presenter.coffee
	src/text-editor.coffee
This commit is contained in:
Max Brunsfeld
2015-11-10 16:23:39 -08:00
35 changed files with 6906 additions and 4619 deletions

View File

@@ -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.

View File

@@ -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**
![Screenshots and GIFs which follow reproduction steps to demonstrate the problem](url)
**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

View File

@@ -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' }

View File

@@ -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",

View File

@@ -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()
)

View File

@@ -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')

View File

@@ -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", ->

12
spec/fixtures/sample-with-many-folds.js vendored Normal file
View File

@@ -0,0 +1,12 @@
1;
2;
function f3() {
return 4;
};
6;
7;
function f8() {
return 9;
};
11;
12;

View File

@@ -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')

View File

@@ -1 +0,0 @@
undefined

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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 =

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -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()

View File

@@ -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})

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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')

View File

@@ -216,7 +216,7 @@ class TextEditorComponent
@updatesPaused = false
if @updateRequestedWhilePaused and @canUpdate()
@updateRequestedWhilePaused = false
@updateSync()
@requestUpdate()
getTopmostDOMNode: ->
@hostElement

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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
# </div>
# ```
# * __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)

View File

@@ -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()

View File

@@ -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