Merge remote-tracking branch 'master' into wl-update-language-javascript

# Conflicts:
#	package.json
This commit is contained in:
Wliu
2016-05-04 15:51:11 -04:00
83 changed files with 2704 additions and 6365 deletions

View File

@@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
"atom-package-manager": "1.9.2"
"atom-package-manager": "1.10.0"
}
}

View File

@@ -57,7 +57,7 @@ module.exports = (grunt) ->
homeDir = process.env.USERPROFILE
contentsDir = shellAppDir
appDir = path.join(shellAppDir, 'resources', 'app')
installDir ?= path.join(process.env.ProgramFiles, appName)
installDir ?= path.join(process.env.LOCALAPPDATA, appName, 'app-dev')
killCommand = 'taskkill /F /IM atom.exe'
else if process.platform is 'darwin'
homeDir = process.env.HOME
@@ -298,6 +298,7 @@ module.exports = (grunt) ->
unless process.platform is 'linux' or grunt.option('no-install')
defaultTasks.push 'install'
grunt.registerTask('default', defaultTasks)
grunt.registerTask('build-and-sign', ['download-electron', 'download-electron-chromedriver', 'build', 'set-version', 'generate-asar', 'codesign:app', 'install'])
getDefaultChannelAndReleaseBranch = (version) ->
if version.match(/dev/) or isBuildingPR()

View File

@@ -54,9 +54,9 @@ module.exports = (grunt) ->
# so that it doesn't becomes larger than it needs to be.
ignoredPaths = [
path.join('git-utils', 'deps')
path.join('nodegit', 'vendor')
path.join('nodegit', 'node_modules', 'node-pre-gyp')
path.join('nodegit', 'node_modules', '.bin')
path.join('ohnogit', 'node_modules', 'nodegit', 'vendor')
path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', 'node-pre-gyp')
path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', '.bin')
path.join('oniguruma', 'deps')
path.join('less', 'dist')
path.join('bootstrap', 'docs')
@@ -122,9 +122,9 @@ module.exports = (grunt) ->
# Ignore *.cc and *.h files from native modules
ignoredPaths.push "#{_.escapeRegExp(path.join('ctags', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('git-utils', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'generate') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'include') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'generate') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'include') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('keytar', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('nslog', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('oniguruma', 'src') + path.sep)}.*\\.(cc|h)*"
@@ -133,7 +133,6 @@ module.exports = (grunt) ->
ignoredPaths.push "#{_.escapeRegExp(path.join('scrollbar-style', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('spellchecker', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('cached-run-in-this-context', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('marker-index', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('keyboard-layout', 'src') + path.sep)}.*\\.(cc|h|mm)*"
# Ignore build files

View File

@@ -16,10 +16,22 @@ module.exports = (grunt) ->
{description} = grunt.config.get('atom.metadata')
if process.platform is 'win32'
runas ?= require 'runas'
copyFolder = path.resolve 'script', 'copy-folder.cmd'
if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0
grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}")
done = @async()
fs.access(installDir, fs.W_OK, (err) ->
adminRequired = true if err
if adminRequired
grunt.log.ok("User does not have write access to #{installDir}, elevating to admin")
runas ?= require 'runas'
copyFolder = path.resolve 'script', 'copy-folder.cmd'
if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: adminRequired) isnt 0
grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}")
else
grunt.log.ok("Installed into #{installDir}")
done()
)
else if process.platform is 'darwin'
rm installDir
mkdir path.dirname(installDir)

View File

@@ -34,7 +34,7 @@ git clone https://github.com/atom/atom/
cd atom
script/build
```
This will create the Atom application in the `out\Atom` folder as well as copy it to a folder named `Atom` within `Program Files`.
This will create the Atom application in the `out\Atom` folder as well as copy it to a subfolder of your user profile (e.g. `c:\Users\Bob`) called `AppData\Local\atom\app-dev`.
### `script/build` Options
* `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional):

View File

@@ -18,4 +18,4 @@
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Atom Flight Manual:
# https://atom.io/docs/latest/using-atom-basic-customization#cson
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson

View File

@@ -27,6 +27,7 @@
'ctrl-n': 'application:new-file'
'ctrl-s': 'core:save'
'ctrl-S': 'core:save-as'
'ctrl-f4': 'core:close'
'ctrl-w': 'core:close'
'ctrl-z': 'core:undo'
'ctrl-y': 'core:redo'

View File

@@ -12,7 +12,7 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
"electronVersion": "0.36.8",
"electronVersion": "0.36.12",
"dependencies": {
"async": "0.2.6",
"atom-keymap": "6.3.2",
@@ -36,10 +36,10 @@
"key-path-helpers": "^0.4.0",
"less-cache": "0.23",
"line-top-index": "0.2.0",
"marked": "^0.3.4",
"nodegit": "0.12.2",
"marked": "^0.3.5",
"normalize-package-data": "^2.0.0",
"nslog": "^3",
"ohnogit": "0.0.11",
"oniguruma": "^5",
"pathwatcher": "~6.2",
"property-accessors": "^1.1.3",
@@ -54,7 +54,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "8.4.6",
"text-buffer": "9.0.0",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -72,24 +72,24 @@
"one-light-syntax": "1.2.0",
"solarized-dark-syntax": "1.0.2",
"solarized-light-syntax": "1.0.2",
"about": "1.5.0",
"about": "1.5.2",
"archive-view": "0.61.1",
"autocomplete-atom-api": "0.10.0",
"autocomplete-css": "0.11.1",
"autocomplete-html": "0.7.2",
"autocomplete-plus": "2.29.2",
"autocomplete-plus": "2.31.0",
"autocomplete-snippets": "1.10.0",
"autoflow": "0.27.0",
"autosave": "0.23.1",
"background-tips": "0.26.0",
"bookmarks": "0.39.0",
"bookmarks": "0.41.0",
"bracket-matcher": "0.82.0",
"command-palette": "0.38.0",
"deprecation-cop": "0.54.1",
"dev-live-reload": "0.47.0",
"encoding-selector": "0.21.0",
"encoding-selector": "0.22.0",
"exception-reporting": "0.38.1",
"fuzzy-finder": "1.0.4",
"fuzzy-finder": "1.0.5",
"git-diff": "1.0.1",
"find-and-replace": "0.198.0",
"go-to-line": "0.30.0",
@@ -97,41 +97,41 @@
"image-view": "0.57.0",
"incompatible-packages": "0.26.1",
"keybinding-resolver": "0.35.0",
"line-ending-selector": "0.4.1",
"line-ending-selector": "0.5.0",
"link": "0.31.1",
"markdown-preview": "0.158.0",
"metrics": "0.53.1",
"notifications": "0.63.2",
"open-on-github": "1.1.0",
"package-generator": "1.0.0",
"settings-view": "0.235.1",
"settings-view": "0.236.0",
"snippets": "1.0.2",
"spell-check": "0.67.1",
"status-bar": "1.2.3",
"status-bar": "1.2.6",
"styleguide": "0.45.2",
"symbols-view": "0.112.0",
"tabs": "0.92.2",
"symbols-view": "0.113.0",
"tabs": "0.93.2",
"timecop": "0.33.1",
"tree-view": "0.206.0",
"tree-view": "0.206.2",
"update-package-dependencies": "0.10.0",
"welcome": "0.34.0",
"whitespace": "0.32.2",
"wrap-guide": "0.38.1",
"language-c": "0.51.3",
"language-c": "0.51.4",
"language-clojure": "0.20.0",
"language-coffee-script": "0.46.1",
"language-coffee-script": "0.47.0",
"language-csharp": "0.12.1",
"language-css": "0.36.1",
"language-gfm": "0.86.0",
"language-git": "0.12.1",
"language-git": "0.13.0",
"language-go": "0.42.0",
"language-html": "0.44.1",
"language-hyperlink": "0.16.0",
"language-java": "0.17.0",
"language-java": "0.18.0",
"language-javascript": "0.119.0",
"language-json": "0.18.0",
"language-less": "0.29.3",
"language-make": "0.21.1",
"language-make": "0.22.0",
"language-mustache": "0.13.0",
"language-objective-c": "0.15.1",
"language-perl": "0.34.0",
@@ -140,15 +140,15 @@
"language-python": "0.43.1",
"language-ruby": "0.68.5",
"language-ruby-on-rails": "0.25.0",
"language-sass": "0.46.0",
"language-shellscript": "0.21.1",
"language-sass": "0.49.0",
"language-shellscript": "0.22.0",
"language-source": "0.9.0",
"language-sql": "0.21.0",
"language-text": "0.7.1",
"language-todo": "0.27.0",
"language-toml": "0.18.0",
"language-xml": "0.34.5",
"language-yaml": "0.25.2"
"language-yaml": "0.26.0"
},
"private": true,
"scripts": {

View File

@@ -2,6 +2,7 @@
SET EXPECT_OUTPUT=
SET WAIT=
SET PSARGS=%*
FOR %%a IN (%*) DO (
IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES
@@ -25,7 +26,8 @@ FOR %%a IN (%*) DO (
IF "%EXPECT_OUTPUT%"=="YES" (
SET ELECTRON_ENABLE_LOGGING=YES
IF "%WAIT%"=="YES" (
powershell -noexit "%~dp0\..\..\atom.exe" --pid=$pid %* ; wait-event
powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event"
exit 0
) ELSE (
"%~dp0\..\..\atom.exe" %*
)

View File

@@ -1,2 +1,5 @@
#!/bin/sh
$(dirname "$0")/atom.cmd "$@"
pushd $(dirname "$0") > /dev/null
ATOMCMD=""$(pwd -W)"/atom.cmd"
popd > /dev/null
cmd.exe //c "$ATOMCMD" "$@"

View File

@@ -28,8 +28,10 @@ describe "AtomEnvironment", ->
atom.setSize(originalSize.width, originalSize.height)
it 'sets the size of the window, and can retrieve the size just set', ->
atom.setSize(100, 400)
expect(atom.getSize()).toEqual width: 100, height: 400
newWidth = originalSize.width + 12
newHeight = originalSize.height + 23
atom.setSize(newWidth, newHeight)
expect(atom.getSize()).toEqual width: newWidth, height: newHeight
describe ".isReleasedVersion()", ->
it "returns false if the version is a SHA and true otherwise", ->
@@ -206,12 +208,15 @@ describe "AtomEnvironment", ->
waitsForPromise ->
atom.loadState().then (state) -> expect(state).toEqual(serializedState)
it "saves state on keydown, mousedown, and when the editor window unloads", ->
it "saves state when the CPU is idle after a keydown or mousedown event", ->
spyOn(atom, 'saveState')
idleCallbacks = []
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
keydown = new KeyboardEvent('keydown')
atom.document.dispatchEvent(keydown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
@@ -219,17 +224,33 @@ describe "AtomEnvironment", ->
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atom.saveState.reset()
it "saves state immediately when unloading the editor window, ignoring pending and successive mousedown/keydown events", ->
spyOn(atom, 'saveState')
idleCallbacks = []
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
atom.unloadEditorWindow()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: true})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: false})
atom.saveState.reset()
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).not.toHaveBeenCalled()
atom.saveState.reset()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).not.toHaveBeenCalled()
it "serializes the project state with all the options supplied in saveState", ->
spyOn(atom.project, 'serialize').andReturn({foo: 42})
@@ -365,7 +386,7 @@ describe "AtomEnvironment", ->
updateAvailableHandler = jasmine.createSpy("update-available-handler")
subscription = atom.onUpdateAvailable updateAvailableHandler
autoUpdater = require('remote').require('auto-updater')
autoUpdater = require('electron').remote.require('auto-updater')
autoUpdater.emit 'update-downloaded', null, "notes", "version"
waitsFor ->

View File

@@ -1,5 +1,6 @@
ChildProcess = require 'child_process'
path = require 'path'
fs = require 'fs-plus'
BufferedProcess = require '../src/buffered-process'
describe "BufferedProcess", ->
@@ -15,20 +16,20 @@ describe "BufferedProcess", ->
describe "when there is an error handler specified", ->
describe "when an error event is emitted by the process", ->
it "calls the error handler and does not throw an exception", ->
process = new BufferedProcess
command: 'bad-command-nope'
bufferedProcess = new BufferedProcess
command: 'bad-command-nope1'
args: ['nothing']
options: {}
options: {shell: false}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
process.onWillThrowError(errorSpy)
bufferedProcess.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
runs ->
expect(window.onerror).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT'
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope1 ENOENT'
describe "when an error is thrown spawning the process", ->
it "calls the error handler and does not throw an exception", ->
@@ -37,13 +38,13 @@ describe "BufferedProcess", ->
error.code = 'EAGAIN'
throw error
process = new BufferedProcess
bufferedProcess = new BufferedProcess
command: 'ls'
args: []
options: {}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
process.onWillThrowError(errorSpy)
bufferedProcess.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
@@ -53,56 +54,24 @@ describe "BufferedProcess", ->
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'Something is really wrong'
describe "when there is not an error handler specified", ->
it "calls the error handler and does not throw an exception", ->
process = new BufferedProcess
command: 'bad-command-nope'
it "does throw an exception", ->
new BufferedProcess
command: 'bad-command-nope2'
args: ['nothing']
options: {}
options: {shell: false}
waitsFor -> window.onerror.callCount > 0
runs ->
expect(window.onerror).toHaveBeenCalled()
expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope`'
expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope2`'
expect(window.onerror.mostRecentCall.args[4].name).toBe 'BufferedProcessError'
describe "on Windows", ->
originalPlatform = null
beforeEach ->
# Prevent any commands from actually running and affecting the host
originalSpawn = ChildProcess.spawn
spyOn(ChildProcess, 'spawn').andCallFake ->
# Just spawn something that won't actually modify the host
if originalPlatform is 'win32'
originalSpawn('dir')
else
originalSpawn('ls')
originalPlatform = process.platform
Object.defineProperty process, 'platform', value: 'win32'
afterEach ->
Object.defineProperty process, 'platform', value: originalPlatform
describe "when the explorer command is spawned on Windows", ->
it "doesn't quote arguments of the form /root,C...", ->
new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']})
expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"'
it "spawns the command using a cmd.exe wrapper", ->
new BufferedProcess({command: 'dir'})
expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe'
expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'
expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d'
expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c'
expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"'
it "calls the specified stdout, stderr, and exit callbacks", ->
stdout = ''
stderr = ''
exitCallback = jasmine.createSpy('exit callback')
process = new BufferedProcess
new BufferedProcess
command: atom.packages.getApmPath()
args: ['-h']
options: {}
@@ -116,29 +85,51 @@ describe "BufferedProcess", ->
expect(stderr).toContain 'apm - Atom Package Manager'
expect(stdout).toEqual ''
it "calls the specified stdout callback only with whole lines", ->
it "calls the specified stdout callback with whole lines", ->
exitCallback = jasmine.createSpy('exit callback')
baseContent = "There are dozens of us! Dozens! It's as Ann as the nose on Plain's face. Can you believe that the only reason the club is going under is because it's in a terrifying neighborhood? She calls it a Mayonegg. Waiting for the Emmys. BTW did you know won 6 Emmys and was still canceled early by Fox? COME ON. I'll buy you a hundred George Michaels that you can teach to drive! Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'"
content = (baseContent for _ in [1..200]).join('\n')
loremPath = require.resolve("./fixtures/lorem.txt")
content = fs.readFileSync(loremPath).toString()
baseContent = content.split('\n')
stdout = ''
endLength = 10
outputAlwaysEndsWithStew = true
process = new BufferedProcess
command: '/bin/echo'
args: [content]
allLinesEndWithNewline = true
new BufferedProcess
command: if process.platform is 'win32' then 'type' else 'cat'
args: [loremPath]
options: {}
stdout: (lines) ->
endsWithNewline = (lines.charAt lines.length - 1) is '\n'
if not endsWithNewline then allLinesEndWithNewline = false
stdout += lines
end = baseContent.substr(baseContent.length - endLength, endLength)
lineEndsWithStew = lines.substr(lines.length - endLength, endLength) is end
expect(lineEndsWithStew).toBeTrue
outputAlwaysEndsWithStew = outputAlwaysEndsWithStew and lineEndsWithStew
exit: exitCallback
waitsFor -> exitCallback.callCount is 1
runs ->
expect(outputAlwaysEndsWithStew).toBeTrue
expect(stdout).toBe content += '\n'
expect(allLinesEndWithNewline).toBeTrue
expect(stdout).toBe content
describe "on Windows", ->
originalPlatform = null
beforeEach ->
# Prevent any commands from actually running and affecting the host
originalSpawn = ChildProcess.spawn
spyOn(ChildProcess, 'spawn')
originalPlatform = process.platform
Object.defineProperty process, 'platform', value: 'win32'
afterEach ->
Object.defineProperty process, 'platform', value: originalPlatform
describe "when the explorer command is spawned on Windows", ->
it "doesn't quote arguments of the form /root,C...", ->
new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']})
expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"'
it "spawns the command using a cmd.exe wrapper when options.shell is undefined", ->
new BufferedProcess({command: 'dir'})
expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe'
expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'
expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d'
expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c'
expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"'

View File

@@ -74,6 +74,13 @@ describe "CommandRegistry", ->
grandchild.dispatchEvent(new CustomEvent('command', bubbles: true))
expect(calls).toEqual ['.foo.bar', '.bar', '.foo']
it "orders inline listeners by reverse registration order", ->
calls = []
registry.add child, 'command', -> calls.push('child1')
registry.add child, 'command', -> calls.push('child2')
child.dispatchEvent(new CustomEvent('command', bubbles: true))
expect(calls).toEqual ['child2', 'child1']
it "stops bubbling through ancestors when .stopPropagation() is called on the event", ->
calls = []

View File

@@ -0,0 +1,85 @@
DecorationManager = require '../src/decoration-manager'
_ = require 'underscore-plus'
describe "DecorationManager", ->
[decorationManager, buffer, defaultMarkerLayer] = []
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
displayLayer = buffer.addDisplayLayer()
defaultMarkerLayer = displayLayer.addMarkerLayer()
decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer)
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
afterEach ->
decorationManager.destroy()
buffer.release()
describe "decorations", ->
[marker, decoration, decorationProperties] = []
beforeEach ->
marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]])
decorationProperties = {type: 'line-number', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
it "can add decorations associated with markers and remove them", ->
expect(decoration).toBeDefined()
expect(decoration.getProperties()).toBe decorationProperties
expect(decorationManager.decorationForId(decoration.id)).toBe decoration
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
decoration.destroy()
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
it "will not fail if the decoration is removed twice", ->
decoration.destroy()
decoration.destroy()
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
it "does not allow destroyed markers to be decorated", ->
marker.destroy()
expect(->
decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
).toThrow("Cannot decorate a destroyed marker")
expect(decorationManager.getOverlayDecorations()).toEqual []
describe "when a decoration is updated via Decoration::update()", ->
it "emits an 'updated' event containing the new and old params", ->
decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
decoration.setProperties type: 'line-number', class: 'two'
{oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
expect(oldProperties).toEqual decorationProperties
expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
describe "::getDecorations(properties)", ->
it "returns decorations matching the given optional properties", ->
expect(decorationManager.getDecorations()).toEqual [decoration]
expect(decorationManager.getDecorations(class: 'two').length).toEqual 0
expect(decorationManager.getDecorations(class: 'one').length).toEqual 1
describe "::decorateMarker", ->
describe "when decorating gutters", ->
[marker] = []
beforeEach ->
marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]])
it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
decorationProperties = {type: 'line-number', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
expect(decoration.isType('line-number')).toBe true
expect(decoration.isType('gutter')).toBe true
expect(decoration.getProperties().gutterName).toBe 'line-number'
expect(decoration.getProperties().class).toBe 'one'
it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
expect(decoration.isType('gutter')).toBe true
expect(decoration.isType('line-number')).toBe false
expect(decoration.getProperties().gutterName).toBe 'test-gutter'
expect(decoration.getProperties().class).toBe 'one'

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
{Point} = require 'text-buffer'
{isPairedCharacter} = require '../src/text-utils'
module.exports =
class FakeLinesYardstick
constructor: (@model, @lineTopIndex) ->
{@displayLayer} = @model
@characterWidthsByScope = {}
getScopedCharacterWidth: (scopeNames, char) ->
@@ -24,31 +26,38 @@ class FakeLinesYardstick
targetRow = screenPosition.row
targetColumn = screenPosition.column
baseCharacterWidth = @model.getDefaultCharWidth()
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
left = 0
column = 0
iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator()
while iterator.next()
characterWidths = @getScopedCharacterWidths(iterator.getScopes())
scopes = []
startIndex = 0
{tagCodes, lineText} = @model.screenLineForScreenRow(targetRow)
for tagCode in tagCodes
if @displayLayer.isOpenTagCode(tagCode)
scopes.push(@displayLayer.tagForCode(tagCode))
else if @displayLayer.isCloseTagCode(tagCode)
scopes.splice(scopes.lastIndexOf(@displayLayer.tagForCode(tagCode)), 1)
else
text = lineText.substr(startIndex, tagCode)
startIndex += tagCode
characterWidths = @getScopedCharacterWidths(scopes)
valueIndex = 0
text = iterator.getText()
while valueIndex < text.length
if iterator.isPairedCharacter()
char = text
charLength = 2
valueIndex += 2
else
char = text[valueIndex]
charLength = 1
valueIndex++
valueIndex = 0
while valueIndex < text.length
if isPairedCharacter(text, valueIndex)
char = text[valueIndex...valueIndex + 2]
charLength = 2
valueIndex += 2
else
char = text[valueIndex]
charLength = 1
valueIndex++
break if column is targetColumn
break if column is targetColumn
left += characterWidths[char] ? baseCharacterWidth unless char is '\0'
column += charLength
left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0'
column += charLength
{top, left}

View File

@@ -5,6 +5,12 @@
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[branch "master"]
remote = origin
merge = refs/heads/master
[remote "origin"]
url = git@github.com:atom/some-repo-i-guess.git
fetch = +refs/heads/*:refs/remotes/origin/*
[submodule "jstips"]
url = https://github.com/loverajoel/jstips
[submodule "You-Dont-Need-jQuery"]

View File

@@ -0,0 +1 @@
d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5

3
spec/fixtures/lorem.txt vendored Normal file
View File

@@ -0,0 +1,3 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ultricies nulla id nibh aliquam, vitae euismod ipsum scelerisque. Vestibulum vulputate facilisis nisi, eu rhoncus turpis pretium ut. Curabitur facilisis urna in diam efficitur, vel maximus tellus consectetur. Suspendisse pulvinar felis sed metus tristique, a posuere dui suscipit. Ut vehicula, tellus ac blandit consequat, libero dui hendrerit elit, non pretium metus odio sed dolor. Vivamus quis volutpat ipsum. In convallis magna nec nunc tristique malesuada. Sed sed hendrerit lacus. Etiam arcu dui, consequat vel neque vitae, iaculis egestas justo. Donec lacinia odio nulla, condimentum porta erat accumsan at. Nunc vulputate nulla vel nunc fermentum egestas.
Duis ultricies libero elit, nec facilisis mi rhoncus ornare. Aliquam aliquet libero vitae arcu porttitor mattis. Vestibulum ultricies consectetur arcu, non gravida magna eleifend vel. Phasellus varius mattis ultricies. Vestibulum placerat lacus non consectetur fringilla. Duis congue, arcu iaculis vehicula hendrerit, purus odio faucibus ipsum, et fermentum massa tellus euismod nulla. Vivamus pellentesque blandit massa, sit amet hendrerit turpis congue eu. Suspendisse diam dui, vestibulum nec semper varius, maximus eu nunc. Vivamus facilisis pulvinar viverra. Praesent luctus lectus id est porttitor volutpat. Suspendisse est augue, mattis a tincidunt id, condimentum in turpis. Curabitur at erat commodo orci interdum tincidunt. Sed sodales elit odio, a placerat ipsum luctus nec. Sed maximus, justo ut pharetra pellentesque, orci mi faucibus enim, quis viverra arcu dui sed nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent quis velit libero.
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus a rutrum tortor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce bibendum odio et neque vestibulum rutrum. Vestibulum commodo, nibh non sodales lobortis, dui ex consectetur leo, a finibus libero lectus ac diam. Etiam dui nunc, bibendum a tempor vel, vestibulum lacinia neque. Mauris consectetur odio sit amet maximus pretium. Sed rutrum nunc at ante ullamcorper fermentum. Proin at quam a mauris pellentesque viverra. Nunc pretium pulvinar ipsum. Vestibulum eu nibh ut ex gravida tempus. Praesent ut elit ut ligula tristique dapibus ut sit amet leo. Proin non molestie erat.

View File

@@ -1,3 +1,3 @@
#!/usr/bin/ruby
puts "America fuck yeah!"
puts "Atom fixture test"

View File

@@ -3,7 +3,6 @@
import fs from 'fs-plus'
import path from 'path'
import temp from 'temp'
import Git from 'nodegit'
import {it, beforeEach, afterEach} from './async-spec-helpers'
@@ -47,7 +46,7 @@ describe('GitRepositoryAsync', () => {
let threw = false
try {
await repo.repoPromise
await repo.getRepo()
} catch (e) {
threw = true
}
@@ -56,6 +55,14 @@ describe('GitRepositoryAsync', () => {
})
})
describe('openedPath', () => {
it('is the path passed to .open', () => {
const workingDirPath = copyRepository()
repo = GitRepositoryAsync.open(workingDirPath)
expect(repo.openedPath).toBe(workingDirPath)
})
})
describe('.getRepo()', () => {
beforeEach(() => {
const workingDirectory = copySubmoduleRepository()
@@ -64,19 +71,19 @@ describe('GitRepositoryAsync', () => {
})
it('returns the repository when not given a path', async () => {
const nodeGitRepo1 = await repo.repoPromise
const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo()
expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir())
})
it('returns the repository when given a non-submodule path', async () => {
const nodeGitRepo1 = await repo.repoPromise
const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo('README')
expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir())
})
it('returns the submodule repository when given a submodule path', async () => {
const nodeGitRepo1 = await repo.repoPromise
const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo('jstips')
expect(nodeGitRepo1.workdir()).not.toBe(nodeGitRepo2.workdir())
@@ -103,7 +110,7 @@ describe('GitRepositoryAsync', () => {
it('returns the repository path for a repository path', async () => {
repo = openFixture('master.git')
const repoPath = await repo.getPath()
expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git'))
expect(repoPath).toEqualPath(path.join(__dirname, 'fixtures', 'git', 'master.git'))
})
})
@@ -303,7 +310,7 @@ describe('GitRepositoryAsync', () => {
await repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe(1)
const status = Git.Status.STATUS.WT_MODIFIED
const status = GitRepositoryAsync.Git.Status.STATUS.WT_MODIFIED
expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status})
fs.writeFileSync(filePath, 'abc')
@@ -878,4 +885,34 @@ describe('GitRepositoryAsync', () => {
})
})
})
describe('.getOriginURL()', () => {
beforeEach(() => {
const workingDirectory = copyRepository('repo-with-submodules')
repo = GitRepositoryAsync.open(workingDirectory)
})
it('returns the origin URL', async () => {
const url = await repo.getOriginURL()
expect(url).toBe('git@github.com:atom/some-repo-i-guess.git')
})
})
describe('.getUpstreamBranch()', () => {
it('returns null when there is no upstream branch', async () => {
const workingDirectory = copyRepository()
repo = GitRepositoryAsync.open(workingDirectory)
const upstream = await repo.getUpstreamBranch()
expect(upstream).toBe(null)
})
it('returns the upstream branch', async () => {
const workingDirectory = copyRepository('repo-with-submodules')
repo = GitRepositoryAsync.open(workingDirectory)
const upstream = await repo.getUpstreamBranch()
expect(upstream).toBe('refs/remotes/origin/master')
})
})
})

View File

@@ -33,7 +33,7 @@ describe "GitRepository", ->
waitsForPromise ->
repo.async.getPath().then(onSuccess)
runs ->
expect(onSuccess.mostRecentCall.args[0]).toBe(repoPath)
expect(onSuccess.mostRecentCall.args[0]).toEqualPath(repoPath)
describe "new GitRepository(path)", ->
it "throws an exception when no repository is found", ->
@@ -289,6 +289,16 @@ describe "GitRepository", ->
expect(repo.isStatusModified(status)).toBe true
expect(repo.isStatusNew(status)).toBe false
it 'caches statuses that were looked up synchronously', ->
originalContent = 'undefined'
fs.writeFileSync(modifiedPath, 'making this path modified')
repo.getPathStatus('file.txt')
fs.writeFileSync(modifiedPath, originalContent)
waitsForPromise -> repo.refreshStatus()
runs ->
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy()
describe "buffer events", ->
[editor] = []

View File

@@ -334,66 +334,56 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 12]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(1).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 9]
fold2.destroy()
fold3 = editor.tokenizedLineForScreenRow(4).fold
expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [4, 7]
[fold1, fold2, fold3] = languageMode.unfoldAll()
expect([fold1.start.row, fold1.end.row]).toEqual [0, 12]
expect([fold2.start.row, fold2.end.row]).toEqual [1, 9]
expect([fold3.start.row, fold3.end.row]).toEqual [4, 7]
describe ".foldBufferRow(bufferRow)", ->
describe "when bufferRow can be folded", ->
it "creates a fold based on the syntactic region starting at the given row", ->
languageMode.foldBufferRow(1)
fold = editor.tokenizedLineForScreenRow(1).fold
expect(fold.getStartRow()).toBe 1
expect(fold.getEndRow()).toBe 9
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when bufferRow can't be folded", ->
it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", ->
languageMode.foldBufferRow(8)
fold = editor.tokenizedLineForScreenRow(1).fold
expect(fold.getStartRow()).toBe 1
expect(fold.getEndRow()).toBe 9
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when the bufferRow is already folded", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
languageMode.foldBufferRow(2)
expect(editor.tokenizedLineForScreenRow(1).fold).toBeDefined()
expect(editor.tokenizedLineForScreenRow(0).fold).not.toBeDefined()
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
expect(editor.isFoldedAtBufferRow(1)).toBe(true)
languageMode.foldBufferRow(1)
expect(editor.tokenizedLineForScreenRow(0).fold).toBeDefined()
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
describe "when the bufferRow is in a multi-line comment", ->
it "searches upward and downward for surrounding comment lines and folds them as a single fold", ->
buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment")
languageMode.foldBufferRow(1)
fold = editor.tokenizedLineForScreenRow(1).fold
expect(fold.getStartRow()).toBe 1
expect(fold.getEndRow()).toBe 3
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [1, 3]
describe "when the bufferRow is a single-line comment", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
buffer.insert([1, 0], " //this is a single line comment\n")
languageMode.foldBufferRow(1)
fold = editor.tokenizedLineForScreenRow(0).fold
expect(fold.getStartRow()).toBe 0
expect(fold.getEndRow()).toBe 13
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [0, 13]
describe ".foldAllAtIndentLevel(indentLevel)", ->
it "folds blocks of text at the given indentation level", ->
languageMode.foldAllAtIndentLevel(0)
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 0
languageMode.foldAllAtIndentLevel(1)
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {"
expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 4
languageMode.foldAllAtIndentLevel(2)
@@ -429,59 +419,35 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(1).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 4]
fold3 = editor.tokenizedLineForScreenRow(2).fold.destroy()
fold4 = editor.tokenizedLineForScreenRow(3).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8]
fold5 = editor.tokenizedLineForScreenRow(6).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [11, 16]
fold5.destroy()
fold6 = editor.tokenizedLineForScreenRow(13).fold
expect([fold6.getStartRow(), fold6.getEndRow()]).toEqual [21, 22]
fold6.destroy()
folds = languageMode.unfoldAll()
expect(folds.length).toBe 8
expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4]
expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27]
expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8]
expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16]
expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20]
expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22]
expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25]
describe ".foldAllAtIndentLevel()", ->
it "folds every foldable range at a given indentLevel", ->
languageMode.foldAllAtIndentLevel(2)
fold1 = editor.tokenizedLineForScreenRow(6).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [6, 8]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(11).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 16]
fold2.destroy()
fold3 = editor.tokenizedLineForScreenRow(17).fold
expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [17, 20]
fold3.destroy()
fold4 = editor.tokenizedLineForScreenRow(21).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [21, 22]
fold4.destroy()
fold5 = editor.tokenizedLineForScreenRow(24).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [24, 25]
fold5.destroy()
folds = languageMode.unfoldAll()
expect(folds.length).toBe 5
expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8]
expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16]
expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20]
expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22]
expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25]
it "does not fold anything but the indentLevel", ->
languageMode.foldAllAtIndentLevel(0)
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(5).fold
expect(fold2).toBeFalsy()
folds = languageMode.unfoldAll()
expect(folds.length).toBe 1
expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
describe ".isFoldableAtBufferRow(bufferRow)", ->
it "returns true if the line starts a multi-line comment", ->

View File

@@ -19,36 +19,45 @@ describe "LinesYardstick", ->
screenRowsToMeasure = []
buildLineNode = (screenRow) ->
tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
iterator = tokenizedLine.getTokenIterator()
startIndex = 0
scopes = []
screenLine = editor.screenLineForScreenRow(screenRow)
lineNode = document.createElement("div")
lineNode.style.whiteSpace = "pre"
while iterator.next()
span = document.createElement("span")
span.className = iterator.getScopes().join(' ').replace(/\.+/g, ' ')
span.textContent = iterator.getText()
lineNode.appendChild(span)
for tagCode in screenLine.tagCodes when tagCode isnt 0
if editor.displayLayer.isCloseTagCode(tagCode)
scopes.pop()
else if editor.displayLayer.isOpenTagCode(tagCode)
scopes.push(editor.displayLayer.tagForCode(tagCode))
else
text = screenLine.lineText.substr(startIndex, tagCode)
startIndex += tagCode
span = document.createElement("span")
span.className = scopes.join(' ').replace(/\.+/g, ' ')
span.textContent = text
lineNode.appendChild(span)
jasmine.attachToDOM(lineNode)
createdLineNodes.push(lineNode)
lineNode
mockLineNodesProvider =
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
buildLineNode(screenRow)
lineNodesById: {}
lineIdForScreenRow: (screenRow) ->
editor.screenLineForScreenRow(screenRow).id
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow)
lineNodeForScreenRow: (screenRow) ->
@lineNodesById[@lineIdForScreenRow(screenRow)] ?= buildLineNode(screenRow)
textNodesForScreenRow: (screenRow) ->
lineNode = @lineNodeForScreenRow(screenRow)
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT)
textNodes = []
while textNode = iterator.nextNode()
textNodes.push(textNode)
textNodes.push(textNode) while textNode = iterator.nextNode()
textNodes
editor.setLineHeightInPixels(14)
lineTopIndex = new LineTopIndex({
defaultLineHeight: editor.getLineHeightInPixels()
})
lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()})
linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars)
afterEach ->
@@ -69,9 +78,9 @@ describe "LinesYardstick", ->
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0))).toEqual({left: 0, top: 0})
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1))).toEqual({left: 7, top: 0})
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 37.78125, top: 0})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43.171875, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72.171875, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 38, top: 0})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.859375, top: 28})
it "reuses already computed pixel positions unless it is invalidated", ->
@@ -82,9 +91,9 @@ describe "LinesYardstick", ->
}
"""
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
atom.styles.addStyleSheet """
* {
@@ -92,9 +101,9 @@ describe "LinesYardstick", ->
}
"""
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
linesYardstick.invalidateCache()
@@ -102,23 +111,6 @@ describe "LinesYardstick", ->
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70})
it "correctly handles RTL characters", ->
atom.styles.addStyleSheet """
* {
font-size: 14px;
font-family: monospace;
}
"""
editor.setText("السلام عليكم")
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0)).left).toBe 0
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1)).left).toBe 8
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 2)).left).toBe 16
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5)).left).toBe 33
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 7)).left).toBe 50
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 9)).left).toBe 67
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 11)).left).toBe 84
it "doesn't report a width greater than 0 when the character to measure is at the beginning of a text node", ->
# This spec documents what seems to be a bug in Chromium, because we'd
# expect that Range(0, 0).getBoundingClientRect().width to always be zero.
@@ -163,9 +155,38 @@ describe "LinesYardstick", ->
expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14])
expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3])
expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9])
expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 99.9})).toEqual([5, 14])
expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 224.2365234375})).toEqual([5, 29])
expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 225})).toEqual([5, 30])
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 99.9})).toEqual([5, 14])
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29])
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30])
expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33])
it "overshoots to the nearest character when text nodes are not spatially contiguous", ->
atom.styles.addStyleSheet """
* {
font-size: 12px;
font-family: monospace;
}
"""
buildLineNode = (screenRow) ->
lineNode = document.createElement("div")
lineNode.style.whiteSpace = "pre"
lineNode.innerHTML = '<span>foo</span><span style="margin-left: 40px">bar</span>'
jasmine.attachToDOM(lineNode)
createdLineNodes.push(lineNode)
lineNode
editor.setText("foobar")
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6])
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6])
it "clips pixel positions above buffer start", ->
expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0]
@@ -178,3 +199,7 @@ describe "LinesYardstick", ->
expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2]
expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2]
expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0]
it "clips negative horizontal pixel positions", ->
expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: -10)).toEqual [0, 0]
expect(linesYardstick.screenPositionForPixelPosition(top: 1 * 14, left: -10)).toEqual [1, 0]

View File

@@ -8,13 +8,13 @@ describe 'ModuleCache', ->
beforeEach ->
spyOn(Module, '_findPath').andCallThrough()
it 'resolves atom shell module paths without hitting the filesystem', ->
it 'resolves Electron module paths without hitting the filesystem', ->
builtins = ModuleCache.cache.builtins
expect(Object.keys(builtins).length).toBeGreaterThan 0
for builtinName, builtinPath of builtins
expect(require.resolve(builtinName)).toBe builtinPath
expect(fs.isFileSync(require.resolve(builtinName)))
expect(fs.isFileSync(require.resolve(builtinName))).toBeTruthy()
expect(Module._findPath.callCount).toBe 0

View File

@@ -917,6 +917,82 @@ describe "Pane", ->
expect(item1.save).not.toHaveBeenCalled()
expect(pane.isDestroyed()).toBe false
describe "when item fails to save", ->
[pane, item1, item2] = []
beforeEach ->
pane = new Pane({items: [new Item("A"), new Item("B")], applicationDelegate: atom.applicationDelegate, config: atom.config})
[item1, item2] = pane.getItems()
item1.shouldPromptToSave = -> true
item1.getURI = -> "/test/path"
item1.save = jasmine.createSpy("save").andCallFake ->
error = new Error("EACCES, permission denied '/test/path'")
error.path = '/test/path'
error.code = 'EACCES'
throw error
it "does not destroy the pane if save fails and user clicks cancel", ->
confirmations = 0
confirm.andCallFake ->
confirmations++
if confirmations is 1
return 0 # click save
else
return 1 # click cancel
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
expect(item1.save).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe false
it "does destroy the pane if the user saves the file under a new name", ->
item1.saveAs = jasmine.createSpy("saveAs").andReturn(true)
confirmations = 0
confirm.andCallFake ->
confirmations++
return 0 # save and then save as
showSaveDialog.andReturn("new/path")
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe true
it "asks again if the saveAs also fails", ->
item1.saveAs = jasmine.createSpy("saveAs").andCallFake ->
error = new Error("EACCES, permission denied '/test/path'")
error.path = '/test/path'
error.code = 'EACCES'
throw error
confirmations = 0
confirm.andCallFake ->
confirmations++
if confirmations < 3
return 0 # save, save as, save as
return 2 # don't save
showSaveDialog.andReturn("new/path")
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(3)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe true
describe "::destroy()", ->
[container, pane1, pane2] = []

View File

@@ -87,7 +87,7 @@ describe "Project", ->
runs ->
bufferA = atom.project.getBuffers()[0]
layerA = bufferA.addMarkerLayer(maintainHistory: true)
layerA = bufferA.addMarkerLayer(persistent: true)
markerA = layerA.markPosition([0, 3])
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
@@ -526,7 +526,7 @@ describe "Project", ->
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true
expect(atom.project.relativizePath(inputPath)).toEqual [
atom.project.getPaths()[1],
'somewhere/something.txt'
path.join('somewhere', 'something.txt')
]
describe ".contains(path)", ->

View File

@@ -17,7 +17,7 @@ describe "TextEditor", ->
buffer = new TextBuffer
editor = atom.workspace.buildTextEditor({buffer})
editor.setEditorWidthInChars(80)
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
steps = []
times 30, ->
@@ -33,8 +33,8 @@ describe "TextEditor", ->
logLines()
throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", )
actualScreenLine = editor.tokenizedLineForScreenRow(screenRow)
unless actualScreenLine.text is referenceScreenLine.text
actualScreenLine = editor.lineTextForScreenRow(screenRow)
unless actualScreenLine is referenceScreenLine
logLines()
throw new Error("Invalid line text at screen row #{screenRow}")
@@ -84,7 +84,8 @@ describe "TextEditor", ->
referenceEditor.setEditorWidthInChars(80)
referenceEditor.setText(editor.getText())
referenceEditor.setSoftWrapped(editor.isSoftWrapped())
screenLines = referenceEditor.tokenizedLinesForScreenRows(0, referenceEditor.getLastScreenRow())
screenLines = [0..referenceEditor.getLastScreenRow()].map (row) => referenceEditor.lineTextForScreenRow(row)
bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow())
{screenLines, bufferRows}

View File

@@ -1,66 +0,0 @@
/** @babel */
import ResourcePool from '../src/resource-pool'
import {it} from './async-spec-helpers'
describe('ResourcePool', () => {
let queue
beforeEach(() => {
queue = new ResourcePool([{}])
})
describe('.enqueue', () => {
it('calls the enqueued function', async () => {
let called = false
await queue.enqueue(() => {
called = true
return Promise.resolve()
})
expect(called).toBe(true)
})
it('forwards values from the inner promise', async () => {
const result = await queue.enqueue(() => Promise.resolve(42))
expect(result).toBe(42)
})
it('forwards errors from the inner promise', async () => {
let threw = false
try {
await queue.enqueue(() => Promise.reject(new Error('down with the sickness')))
} catch (e) {
threw = true
}
expect(threw).toBe(true)
})
it('continues to dequeue work after a promise has been rejected', async () => {
try {
await queue.enqueue(() => Promise.reject(new Error('down with the sickness')))
} catch (e) {}
const result = await queue.enqueue(() => Promise.resolve(42))
expect(result).toBe(42)
})
it('queues up work', async () => {
let resolve = null
queue.enqueue(() => {
return new Promise((resolve_, reject) => {
resolve = resolve_
})
})
expect(queue.getQueueDepth()).toBe(0)
queue.enqueue(() => new Promise((resolve, reject) => {}))
expect(queue.getQueueDepth()).toBe(1)
resolve()
waitsFor(() => queue.getQueueDepth() === 0)
})
})
})

View File

@@ -101,3 +101,22 @@ describe "Selection", ->
selection.setBufferRange [[2, 0], [3, 0]]
selection.insertText("\r\n", autoIndent: true)
expect(buffer.lineForRow(2)).toBe " "
describe ".fold()", ->
it "folds the buffer range spanned by the selection", ->
selection.setBufferRange([[0, 3], [1, 6]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {"
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
it "doesn't create a fold when the selection is empty", ->
selection.setBufferRange([[0, 3], [0, 3]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
expect(editor.isFoldedAtBufferRow(0)).toBe(false)

View File

@@ -172,8 +172,8 @@ jasmine.useRealClock = ->
addCustomMatchers = (spec) ->
spec.addMatchers
toBeInstanceOf: (expected) ->
notText = if @isNot then " not" else ""
this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class"
beOrNotBe = if @isNot then "not be" else "be"
this.message = => "Expected #{jasmine.pp(@actual)} to #{beOrNotBe} instance of #{expected.name} class"
@actual instanceof expected
toHaveLength: (expected) ->
@@ -181,32 +181,38 @@ addCustomMatchers = (spec) ->
this.message = => "Expected object #{@actual} has no length method"
false
else
notText = if @isNot then " not" else ""
this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}"
haveOrNotHave = if @isNot then "not have" else "have"
this.message = => "Expected object with length #{@actual.length} to #{haveOrNotHave} length #{expected}"
@actual.length is expected
toExistOnDisk: (expected) ->
notText = this.isNot and " not" or ""
@message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
toOrNotTo = this.isNot and "not to" or "to"
@message = -> return "Expected path '#{@actual}' #{toOrNotTo} exist."
fs.existsSync(@actual)
toHaveFocus: ->
notText = this.isNot and " not" or ""
toOrNotTo = this.isNot and "not to" or "to"
if not document.hasFocus()
console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner."
@message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus."
@message = -> return "Expected element '#{@actual}' or its descendants #{toOrNotTo} have focus."
element = @actual
element = element.get(0) if element.jquery
element is document.activeElement or element.contains(document.activeElement)
toShow: ->
notText = if @isNot then " not" else ""
toOrNotTo = this.isNot and "not to" or "to"
element = @actual
element = element.get(0) if element.jquery
@message = -> return "Expected element '#{element}' or its descendants#{notText} to show."
@message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show."
element.style.display in ['block', 'inline-block', 'static', 'fixed']
toEqualPath: (expected) ->
actualPath = path.normalize(@actual)
expectedPath = path.normalize(expected)
@message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'."
actualPath is expectedPath
window.waitsForPromise = (args...) ->
label = null
if args.length > 1

View File

@@ -69,13 +69,12 @@ describe('TextEditorComponent', function () {
describe('line rendering', async function () {
function expectTileContainsRow (tileNode, screenRow, {top}) {
let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]')
let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
let text = editor.lineTextForScreenRow(screenRow)
expect(lineNode.offsetTop).toBe(top)
if (tokenizedLine.text === '') {
expect(lineNode.innerHTML).toBe('&nbsp;')
if (text === '') {
expect(lineNode.textContent).toBe(' ')
} else {
expect(lineNode.textContent).toBe(tokenizedLine.text)
expect(lineNode.textContent).toBe(text)
}
}
@@ -294,12 +293,12 @@ describe('TextEditorComponent', function () {
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3))
buffer.delete([[0, 0], [3, 0]])
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3))
})
it('updates the top position of lines when the line height changes', async function () {
@@ -361,9 +360,9 @@ describe('TextEditorComponent', function () {
}
})
it('renders an nbsp on empty lines when no line-ending character is defined', function () {
it('renders an placeholder space on empty lines when no line-ending character is defined', function () {
atom.config.set('editor.showInvisibles', false)
expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('gives the lines and tiles divs the same background color as the editor to improve GPU performance', async function () {
@@ -429,13 +428,14 @@ describe('TextEditorComponent', function () {
expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false)
})
it('keeps rebuilding lines when continuous reflow is on', function () {
it('keeps rebuilding lines when continuous reflow is on', async function () {
wrapperNode.setContinuousReflow(true)
let oldLineNode = componentNode.querySelector('.line')
let oldLineNode = componentNode.querySelectorAll('.line')[1]
waitsFor(function () {
return componentNode.querySelector('.line') !== oldLineNode
})
while (true) {
await nextViewUpdatePromise()
if (componentNode.querySelectorAll('.line')[1] !== oldLineNode) break
}
})
describe('when showInvisibles is enabled', function () {
@@ -484,7 +484,7 @@ describe('TextEditorComponent', function () {
it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () {
editor.setText('let\n')
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(0).innerHTML).toBe('<span class="source js"><span class="storage type var js">let</span></span><span class="invisible-character">' + invisibles.eol + '</span>')
expect(component.lineNodeForScreenRow(0).innerHTML).toBe('<span class="source js"><span class="storage type var js">let</span><span class="invisible-character eol">' + invisibles.eol + '</span></span>')
})
it('displays trailing carriage returns using a visible, non-empty value', async function () {
@@ -497,20 +497,20 @@ describe('TextEditorComponent', function () {
expect(component.lineNodeForScreenRow(10).textContent).toBe(invisibles.eol)
})
it('renders an nbsp on empty lines when the line-ending character is an empty string', async function () {
it('renders a placeholder space on empty lines when the line-ending character is an empty string', async function () {
atom.config.set('editor.invisibles', {
eol: ''
})
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('renders an nbsp on empty lines when the line-ending character is false', async function () {
it('renders an placeholder space on empty lines when the line-ending character is false', async function () {
atom.config.set('editor.invisibles', {
eol: false
})
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('interleaves invisible line-ending characters with indent guides on empty lines', async function () {
@@ -518,24 +518,25 @@ describe('TextEditorComponent', function () {
await nextViewUpdatePromise()
editor.setTabLength(2)
editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', {
normalizeLineEndings: false
})
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span><span class="invisible-character">E</span></span>')
editor.setTabLength(3)
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span><span class="invisible-character">E</span> </span>')
editor.setTabLength(1)
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span></span><span class="indent-guide"><span class="invisible-character">E</span></span>')
editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ')
editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ')
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span></span><span class="invisible-character">E</span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
})
describe('when soft wrapping is enabled', function () {
@@ -550,8 +551,8 @@ describe('TextEditorComponent', function () {
})
it('does not show end of line invisibles at the end of wrapped lines', function () {
expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ')
expect(component.lineNodeForScreenRow(1).textContent).toBe('wraps' + invisibles.space + invisibles.eol)
expect(component.lineNodeForScreenRow(0).textContent).toBe('a line ')
expect(component.lineNodeForScreenRow(1).textContent).toBe('that wraps' + invisibles.space + invisibles.eol)
})
})
})
@@ -986,13 +987,14 @@ describe('TextEditorComponent', function () {
expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true)
})
it('keeps rebuilding line numbers when continuous reflow is on', function () {
it('keeps rebuilding line numbers when continuous reflow is on', async function () {
wrapperNode.setContinuousReflow(true)
let oldLineNode = componentNode.querySelectorAll('.line-number')[1]
waitsFor(function () {
return componentNode.querySelectorAll('.line-number')[1] !== oldLineNode
})
while (true) {
await nextViewUpdatePromise()
if (componentNode.querySelectorAll('.line-number')[1] !== oldLineNode) break
}
})
describe('fold decorations', function () {
@@ -1051,7 +1053,7 @@ describe('TextEditorComponent', function () {
beforeEach(async function () {
editor.setSoftWrapped(true)
await nextViewUpdatePromise()
componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
component.measureDimensions()
await nextViewUpdatePromise()
})
@@ -1060,6 +1062,14 @@ describe('TextEditorComponent', function () {
expect(lineNumberHasClass(0, 'foldable')).toBe(true)
expect(lineNumberHasClass(1, 'foldable')).toBe(false)
})
it('does not add the folded class for soft-wrapped lines that contain a fold', async function () {
editor.foldBufferRange([[3, 19], [3, 21]])
await nextViewUpdatePromise()
expect(lineNumberHasClass(11, 'folded')).toBe(true)
expect(lineNumberHasClass(12, 'folded')).toBe(false)
})
})
})
@@ -1082,7 +1092,7 @@ describe('TextEditorComponent', function () {
component.destroy()
lineNumber = component.lineNumberNodeForScreenRow(1)
target = lineNumber.querySelector('.icon-right')
return target.dispatchEvent(buildClickEvent(target))
target.dispatchEvent(buildClickEvent(target))
})
})
@@ -1106,6 +1116,37 @@ describe('TextEditorComponent', function () {
expect(lineNumberHasClass(1, 'folded')).toBe(false)
})
it('unfolds all the free-form folds intersecting the buffer row when clicked', async function () {
expect(lineNumberHasClass(3, 'foldable')).toBe(false)
editor.foldBufferRange([[3, 4], [5, 4]])
editor.foldBufferRange([[5, 5], [8, 10]])
await nextViewUpdatePromise()
expect(lineNumberHasClass(3, 'folded')).toBe(true)
expect(lineNumberHasClass(5, 'folded')).toBe(false)
let lineNumber = component.lineNumberNodeForScreenRow(3)
let target = lineNumber.querySelector('.icon-right')
target.dispatchEvent(buildClickEvent(target))
await nextViewUpdatePromise()
expect(lineNumberHasClass(3, 'folded')).toBe(false)
expect(lineNumberHasClass(5, 'folded')).toBe(true)
editor.setSoftWrapped(true)
componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
component.measureDimensions()
await nextViewUpdatePromise()
editor.foldBufferRange([[3, 19], [3, 21]]) // fold starting on a soft-wrapped portion of the line
await nextViewUpdatePromise()
expect(lineNumberHasClass(11, 'folded')).toBe(true)
lineNumber = component.lineNumberNodeForScreenRow(11)
target = lineNumber.querySelector('.icon-right')
target.dispatchEvent(buildClickEvent(target))
await nextViewUpdatePromise()
expect(lineNumberHasClass(11, 'folded')).toBe(false)
})
it('does not fold when the line number componentNode is clicked', function () {
let lineNumber = component.lineNumberNodeForScreenRow(1)
lineNumber.dispatchEvent(buildClickEvent(lineNumber))
@@ -1200,7 +1241,7 @@ describe('TextEditorComponent', function () {
let cursor = componentNode.querySelector('.cursor')
let cursorRect = cursor.getBoundingClientRect()
let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2]
let range = document.createRange()
let range = document.createRange(cursorLocationTextNode)
range.setStart(cursorLocationTextNode, 0)
range.setEnd(cursorLocationTextNode, 1)
let rangeRect = range.getBoundingClientRect()
@@ -1208,6 +1249,17 @@ describe('TextEditorComponent', function () {
expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0)
})
it('positions cursors after the fold-marker when a fold ends the line', async function () {
editor.foldBufferRow(0)
await nextViewUpdatePromise()
editor.setCursorScreenPosition([0, 30])
await nextViewUpdatePromise()
let cursorRect = componentNode.querySelector('.cursor').getBoundingClientRect()
let foldMarkerRect = componentNode.querySelector('.fold-marker').getBoundingClientRect()
expect(cursorRect.left).toBeCloseTo(foldMarkerRect.right, 0)
})
it('positions cursors correctly after character widths are changed via a stylesheet change', async function () {
atom.config.set('editor.fontFamily', 'sans-serif')
editor.setCursorScreenPosition([0, 16])
@@ -1475,7 +1527,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]])
let marker2 = editor.markBufferRange([[9, 0], [9, 0]])
editor.decorateMarker(marker2, {
type: ['line-number', 'line'],
'class': 'b'
@@ -1887,7 +1939,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], {
marker = editor.markBufferRange([[9, 2], [9, 4]], {
invalidate: 'inside'
})
editor.decorateMarker(marker, {
@@ -2082,7 +2134,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is empty', function () {
it('renders an overlay decoration when added and removes the overlay when the decoration is destroyed', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2104,7 +2156,7 @@ describe('TextEditorComponent', function () {
})
it('renders the overlay element with the CSS class specified by the decoration', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2125,7 +2177,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is not empty', function () {
it('renders at the head of the marker by default', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], {
let marker = editor.markBufferRange([[2, 5], [2, 10]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2171,7 +2223,7 @@ describe('TextEditorComponent', function () {
})
it('slides horizontally left when near the right edge on #win32 and #darwin', async function () {
let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], {
let marker = editor.markBufferRange([[0, 26], [0, 26]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2753,20 +2805,60 @@ describe('TextEditorComponent', function () {
})
})
describe('when a line is folded', function () {
beforeEach(async function () {
editor.foldBufferRow(4)
describe('when a fold marker is clicked', function () {
function clickElementAtPosition (marker, position) {
linesNode.dispatchEvent(
buildMouseEvent('mousedown', clientCoordinatesForScreenPosition(position), {target: marker})
)
}
it('unfolds only the selected fold when other folds are on the same line', async function () {
editor.foldBufferRange([[4, 6], [4, 10]])
editor.foldBufferRange([[4, 15], [4, 20]])
await nextViewUpdatePromise()
let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(2)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 6])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 15])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(0)
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
describe('when the folded line\'s fold-marker is clicked', function () {
it('unfolds the buffer row', function () {
let target = component.lineNodeForScreenRow(4).querySelector('.fold-marker')
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {
target: target
}))
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
it('unfolds only the selected fold when other folds are inside it', async function () {
editor.foldBufferRange([[4, 10], [4, 15]])
editor.foldBufferRange([[4, 4], [4, 5]])
editor.foldBufferRange([[4, 4], [4, 20]])
await nextViewUpdatePromise()
let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 4])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 4])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 10])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(0)
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
})
@@ -3101,7 +3193,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), {
shiftKey: true
}))
expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [16, 0]])
expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [17, 0]])
})
})
})
@@ -3175,7 +3267,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), {
metaKey: true
}))
expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [19, 0]]])
expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [20, 0]]])
})
it('merges overlapping selections on mouseup', async function () {
@@ -3189,7 +3281,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), {
metaKey: true
}))
expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [19, 0]]])
expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [20, 0]]])
})
})
})
@@ -3204,7 +3296,7 @@ describe('TextEditorComponent', function () {
}))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11)))
await nextAnimationFramePromise()
expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 14]])
expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 5]])
})
})
@@ -4022,15 +4114,33 @@ describe('TextEditorComponent', function () {
})
})
describe('when changing the font', async function () {
it('measures the default char, the korean char, the double width char and the half width char widths', async function () {
expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0)
describe('when decreasing the fontSize', async function () {
it('decreases the widths of the korean char, the double width char and the half width char', async function () {
originalDefaultCharWidth = editor.getDefaultCharWidth()
koreanDefaultCharWidth = editor.getKoreanCharWidth()
doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth()
halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth()
component.setFontSize(10)
await nextViewUpdatePromise()
expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0)
expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0)
expect(editor.getDoubleWidthCharWidth()).toBe(10)
expect(editor.getHalfWidthCharWidth()).toBe(5)
expect(editor.getDefaultCharWidth()).toBeLessThan(originalDefaultCharWidth)
expect(editor.getKoreanCharWidth()).toBeLessThan(koreanDefaultCharWidth)
expect(editor.getDoubleWidthCharWidth()).toBeLessThan(doubleWidthDefaultCharWidth)
expect(editor.getHalfWidthCharWidth()).toBeLessThan(halfWidthDefaultCharWidth)
})
})
describe('when increasing the fontSize', function() {
it('increases the widths of the korean char, the double width char and the half width char', async function () {
originalDefaultCharWidth = editor.getDefaultCharWidth()
koreanDefaultCharWidth = editor.getKoreanCharWidth()
doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth()
halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth()
component.setFontSize(25)
await nextViewUpdatePromise()
expect(editor.getDefaultCharWidth()).toBeGreaterThan(originalDefaultCharWidth)
expect(editor.getKoreanCharWidth()).toBeGreaterThan(koreanDefaultCharWidth)
expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(doubleWidthDefaultCharWidth)
expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(halfWidthDefaultCharWidth)
})
})
@@ -4948,7 +5058,7 @@ describe('TextEditorComponent', function () {
function lineNumberForBufferRowHasClass (bufferRow, klass) {
let screenRow
screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow)
screenRow = editor.screenRowForBufferRow(bufferRow)
return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass)
}

View File

@@ -1143,53 +1143,6 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setScrollLeft(-300)
expect(getState(presenter).content.scrollLeft).toBe 0
describe ".indentGuidesVisible", ->
it "is initialized based on the editor.showIndentGuide config setting", ->
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
atom.config.set('editor.showIndentGuide', true)
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe true
it "updates when the editor.showIndentGuide config setting changes", ->
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', true)
expect(getState(presenter).content.indentGuidesVisible).toBe true
expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', false)
expect(getState(presenter).content.indentGuidesVisible).toBe false
it "updates when the editor's grammar changes", ->
atom.config.set('editor.showIndentGuide', true, scopeSelector: ".source.js")
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
stateUpdated = false
presenter.onDidUpdateState -> stateUpdated = true
waitsForPromise -> atom.packages.activatePackage('language-javascript')
runs ->
expect(stateUpdated).toBe true
expect(getState(presenter).content.indentGuidesVisible).toBe true
expectStateUpdate presenter, -> editor.setGrammar(atom.grammars.selectGrammar('.txt'))
expect(getState(presenter).content.indentGuidesVisible).toBe false
it "is always false when the editor is mini", ->
atom.config.set('editor.showIndentGuide', true)
editor.setMini(true)
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
editor.setMini(false)
expect(getState(presenter).content.indentGuidesVisible).toBe true
editor.setMini(true)
expect(getState(presenter).content.indentGuidesVisible).toBe false
describe ".backgroundColor", ->
it "is assigned to ::backgroundColor unless the editor is mini", ->
presenter = buildPresenter()
@@ -1229,9 +1182,19 @@ describe "TextEditorPresenter", ->
describe ".tiles", ->
lineStateForScreenRow = (presenter, row) ->
lineId = presenter.model.tokenizedLineForScreenRow(row).id
tileRow = presenter.tileForRow(row)
getState(presenter).content.tiles[tileRow]?.lines[lineId]
tilesState = getState(presenter).content.tiles
lineId = presenter.linesByScreenRow.get(row)?.id
tilesState[presenter.tileForRow(row)]?.lines[lineId]
tagsForCodes = (presenter, tagCodes) ->
openTags = []
closeTags = []
for tagCode in tagCodes when tagCode < 0 # skip text codes
if presenter.isOpenTagCode(tagCode)
openTags.push(presenter.tagForCode(tagCode))
else
closeTags.push(presenter.tagForCode(tagCode))
{openTags, closeTags}
tiledContentContract (presenter) -> getState(presenter).content
@@ -1241,73 +1204,12 @@ describe "TextEditorPresenter", ->
presenter.setExplicitHeight(3)
expect(lineStateForScreenRow(presenter, 2)).toBeUndefined()
line3 = editor.tokenizedLineForScreenRow(3)
expectValues lineStateForScreenRow(presenter, 3), {
screenRow: 3
text: line3.text
tags: line3.tags
specialTokens: line3.specialTokens
firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex
invisibles: line3.invisibles
}
line4 = editor.tokenizedLineForScreenRow(4)
expectValues lineStateForScreenRow(presenter, 4), {
screenRow: 4
text: line4.text
tags: line4.tags
specialTokens: line4.specialTokens
firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex
invisibles: line4.invisibles
}
line5 = editor.tokenizedLineForScreenRow(5)
expectValues lineStateForScreenRow(presenter, 5), {
screenRow: 5
text: line5.text
tags: line5.tags
specialTokens: line5.specialTokens
firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex
invisibles: line5.invisibles
}
line6 = editor.tokenizedLineForScreenRow(6)
expectValues lineStateForScreenRow(presenter, 6), {
screenRow: 6
text: line6.text
tags: line6.tags
specialTokens: line6.specialTokens
firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex
invisibles: line6.invisibles
}
line7 = editor.tokenizedLineForScreenRow(7)
expectValues lineStateForScreenRow(presenter, 7), {
screenRow: 7
text: line7.text
tags: line7.tags
specialTokens: line7.specialTokens
firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex
invisibles: line7.invisibles
}
line8 = editor.tokenizedLineForScreenRow(8)
expectValues lineStateForScreenRow(presenter, 8), {
screenRow: 8
text: line8.text
tags: line8.tags
specialTokens: line8.specialTokens
firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex
invisibles: line8.invisibles
}
expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}
expectValues lineStateForScreenRow(presenter, 4), {screenRow: 4, tagCodes: editor.screenLineForScreenRow(4).tagCodes}
expectValues lineStateForScreenRow(presenter, 5), {screenRow: 5, tagCodes: editor.screenLineForScreenRow(5).tagCodes}
expectValues lineStateForScreenRow(presenter, 6), {screenRow: 6, tagCodes: editor.screenLineForScreenRow(6).tagCodes}
expectValues lineStateForScreenRow(presenter, 7), {screenRow: 7, tagCodes: editor.screenLineForScreenRow(7).tagCodes}
expectValues lineStateForScreenRow(presenter, 8), {screenRow: 8, tagCodes: editor.screenLineForScreenRow(8).tagCodes}
expect(lineStateForScreenRow(presenter, 9)).toBeUndefined()
it "updates when the editor's content changes", ->
@@ -1315,34 +1217,20 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n")
line1 = editor.tokenizedLineForScreenRow(1)
expectValues lineStateForScreenRow(presenter, 1), {
text: line1.text
tags: line1.tags
}
line2 = editor.tokenizedLineForScreenRow(2)
expectValues lineStateForScreenRow(presenter, 2), {
text: line2.text
tags: line2.tags
}
line3 = editor.tokenizedLineForScreenRow(3)
expectValues lineStateForScreenRow(presenter, 3), {
text: line3.text
tags: line3.tags
}
expectValues lineStateForScreenRow(presenter, 1), {screenRow: 1, tagCodes: editor.screenLineForScreenRow(1).tagCodes}
expectValues lineStateForScreenRow(presenter, 2), {screenRow: 2, tagCodes: editor.screenLineForScreenRow(2).tagCodes}
expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}
it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", ->
editor.setText("hello\nworld\r\n")
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toBeNull()
expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toBeNull()
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).not.toContain('invisible-character eol')
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).not.toContain('invisible-character eol')
atom.config.set('editor.showInvisibles', true)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')]
expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')]
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).toContain('invisible-character eol')
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).toContain('invisible-character eol')
describe ".blockDecorations", ->
it "contains all block decorations that are present before/after a line, both initially and when decorations change", ->
@@ -2905,12 +2793,9 @@ describe "TextEditorPresenter", ->
describe ".content.tiles", ->
lineNumberStateForScreenRow = (presenter, screenRow) ->
editor = presenter.model
tileRow = presenter.tileForRow(screenRow)
line = editor.tokenizedLineForScreenRow(screenRow)
gutterState = getLineNumberGutterState(presenter)
gutterState.content.tiles[tileRow]?.lineNumbers[line?.id]
tilesState = getLineNumberGutterState(presenter).content.tiles
line = presenter.linesByScreenRow.get(screenRow)
tilesState[presenter.tileForRow(screenRow)]?.lineNumbers[line?.id]
tiledContentContract (presenter) -> getLineNumberGutterState(presenter).content
@@ -2919,7 +2804,7 @@ describe "TextEditorPresenter", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
editor.setEditorWidthInChars(51)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 2)
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
@@ -3184,6 +3069,16 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
it "applies the 'folded' decoration only to the initial screen row of a soft-wrapped buffer row", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(15)
editor.foldBufferRange([[0, 20], [0, 22]])
presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2)
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'folded'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
describe ".foldable", ->
it "marks line numbers at the start of a foldable region as foldable", ->
presenter = buildPresenter()

View File

@@ -10,6 +10,7 @@ describe "TextEditorRegistry", ->
it "gets added to the list of registered editors", ->
editor = {}
registry.add(editor)
expect(editor.registered).toBe true
expect(registry.editors.size).toBe 1
expect(registry.editors.has(editor)).toBe(true)
@@ -19,6 +20,16 @@ describe "TextEditorRegistry", ->
expect(registry.editors.size).toBe 1
disposable.dispose()
expect(registry.editors.size).toBe 0
expect(editor.registered).toBe false
it "can be removed", ->
editor = {}
registry.add(editor)
expect(registry.editors.size).toBe 1
success = registry.remove(editor)
expect(success).toBe true
expect(registry.editors.size).toBe 0
expect(editor.registered).toBe false
describe "when the registry is observed", ->
it "calls the callback for current and future editors until unsubscribed", ->

View File

@@ -39,21 +39,19 @@ describe "TextEditor", ->
it "preserves the invisibles setting", ->
atom.config.set('editor.showInvisibles', true)
previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles
previousLineText = editor.lineTextForScreenRow(0)
editor2 = TextEditor.deserialize(editor.serialize(), atom)
expect(previousInvisibles).toBeDefined()
expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles
expect(editor2.lineTextForScreenRow(0)).toBe(previousLineText)
it "updates invisibles if the settings have changed between serialization and deserialization", ->
atom.config.set('editor.showInvisibles', true)
previousLineText = editor.lineTextForScreenRow(0)
state = editor.serialize()
atom.config.set('editor.invisibles', eol: '?')
editor2 = TextEditor.deserialize(state, atom)
expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?'
expect(editor2.lineTextForScreenRow(0)).not.toBe(previousLineText)
expect(editor2.lineTextForScreenRow(0).endsWith('?')).toBe(true)
describe "when the editor is constructed with the largeFileMode option set to true", ->
it "loads the editor but doesn't tokenize", ->
@@ -64,15 +62,14 @@ describe "TextEditor", ->
runs ->
buffer = editor.getBuffer()
expect(editor.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0)
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # soft tab
expect(editor.tokenizedLineForScreenRow(12).text).toBe buffer.lineForRow(12)
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.lineTextForScreenRow(0)).toBe buffer.lineForRow(0)
expect(editor.tokensForScreenRow(0).length).toBe 1
expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab
expect(editor.lineTextForScreenRow(12)).toBe buffer.lineForRow(12)
expect(editor.getCursorScreenPosition()).toEqual [0, 0]
editor.insertText('hey"')
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # sof tab
expect(editor.tokensForScreenRow(0).length).toBe 1
expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab
describe ".copy()", ->
it "returns a different edit session with the same initial state", ->
@@ -182,17 +179,19 @@ describe "TextEditor", ->
expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1"
expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2"
it "returns '<filename> — <parent-directories>' when opened files have identical file and dir names", ->
it "returns '<filename> — <parent-directories>' when opened files have identical file names in subdirectories", ->
editor1 = null
editor2 = null
path1 = path.join('sample-theme-1', 'src', 'js')
path2 = path.join('sample-theme-2', 'src', 'js')
waitsForPromise ->
atom.workspace.open(path.join('sample-theme-1', 'src', 'js', 'main.js')).then (o) ->
atom.workspace.open(path.join(path1, 'main.js')).then (o) ->
editor1 = o
atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) ->
atom.workspace.open(path.join(path2, 'main.js')).then (o) ->
editor2 = o
runs ->
expect(editor1.getLongTitle()).toBe "main.js \u2014 sample-theme-1/src/js"
expect(editor2.getLongTitle()).toBe "main.js \u2014 sample-theme-2/src/js"
expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}"
expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}"
it "returns '<filename> — <parent-directories>' when opened files have identical file and same parent dir name", ->
editor1 = null
@@ -204,7 +203,7 @@ describe "TextEditor", ->
editor2 = o
runs ->
expect(editor1.getLongTitle()).toBe "main.js \u2014 js"
expect(editor2.getLongTitle()).toBe "main.js \u2014 js/plugin"
expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin')
it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", ->
observed = []
@@ -312,7 +311,7 @@ describe "TextEditor", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
editor.createFold(2, 3)
editor.foldBufferRowRange(2, 3)
it "positions the cursor at the buffer position that corresponds to the given screen position", ->
editor.setCursorScreenPosition([9, 0])
@@ -493,7 +492,7 @@ describe "TextEditor", ->
it "wraps to the end of the previous line", ->
editor.setCursorScreenPosition([4, 4])
editor.moveLeft()
expect(editor.getCursorScreenPosition()).toEqual [3, 50]
expect(editor.getCursorScreenPosition()).toEqual [3, 46]
describe "when the cursor is on the first line", ->
it "remains in the same position (0,0)", ->
@@ -681,7 +680,7 @@ describe "TextEditor", ->
editor.setCursorScreenPosition([0, 2])
editor.moveToEndOfLine()
cursor = editor.getLastCursor()
expect(cursor.getScreenPosition()).toEqual [3, 4]
expect(cursor.getScreenPosition()).toEqual [4, 4]
describe ".moveToFirstCharacterOfLine()", ->
describe "when soft wrap is on", ->
@@ -1187,14 +1186,10 @@ describe "TextEditor", ->
cursor2 = editor.addCursorAtBufferPosition([1, 4])
expect(cursor2.marker).toBe cursor1.marker
describe '.logCursorScope()', ->
beforeEach ->
spyOn(atom.notifications, 'addInfo')
it 'opens a notification', ->
editor.logCursorScope()
expect(atom.notifications.addInfo).toHaveBeenCalled()
describe '.getCursorScope()', ->
it 'returns the current scope', ->
descriptor = editor.getCursorScope()
expect(descriptor.scopes).toContain('source.js')
describe "selection", ->
selection = null
@@ -1800,22 +1795,22 @@ describe "TextEditor", ->
describe "when the 'preserveFolds' option is false (the default)", ->
it "removes folds that contain the selections", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
editor.createFold(1, 4)
editor.createFold(2, 3)
editor.createFold(6, 8)
editor.createFold(10, 11)
editor.foldBufferRowRange(1, 4)
editor.foldBufferRowRange(2, 3)
editor.foldBufferRowRange(6, 8)
editor.foldBufferRowRange(10, 11)
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]])
expect(editor.tokenizedLineForScreenRow(1).fold).toBeUndefined()
expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined()
expect(editor.tokenizedLineForScreenRow(6).fold).toBeUndefined()
expect(editor.tokenizedLineForScreenRow(10).fold).toBeDefined()
expect(editor.isFoldedAtScreenRow(1)).toBeFalsy()
expect(editor.isFoldedAtScreenRow(2)).toBeFalsy()
expect(editor.isFoldedAtScreenRow(6)).toBeFalsy()
expect(editor.isFoldedAtScreenRow(10)).toBeTruthy()
describe "when the 'preserveFolds' option is true", ->
it "does not remove folds that contain the selections", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
editor.createFold(1, 4)
editor.createFold(6, 8)
editor.foldBufferRowRange(1, 4)
editor.foldBufferRowRange(6, 8)
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(1)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2227,7 +2222,7 @@ describe "TextEditor", ->
it "moves the line to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]]
@@ -2255,7 +2250,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.lineTextForBufferRow(9)).toBe " };"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2293,7 +2288,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
@@ -2321,7 +2316,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2365,7 +2360,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.lineTextForBufferRow(9)).toBe " };"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2405,7 +2400,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRanges([
[[2, 2], [2, 9]],
[[4, 2], [4, 9]]
@@ -2443,7 +2438,7 @@ describe "TextEditor", ->
describe "when there is a fold", ->
it "moves all lines that spanned by a selection to preceding row, preserving all folds", ->
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2470,8 +2465,8 @@ describe "TextEditor", ->
describe 'and many selections intersects folded rows', ->
it 'moves and preserves all the folds', ->
editor.createFold(2, 4)
editor.createFold(7, 9)
editor.foldBufferRowRange(2, 4)
editor.foldBufferRowRange(7, 9)
editor.setSelectedBufferRanges([
[[1, 0], [5, 4]],
@@ -2555,7 +2550,7 @@ describe "TextEditor", ->
it "moves the line to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2581,7 +2576,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2635,7 +2630,7 @@ describe "TextEditor", ->
it "moves the lines to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
@@ -2663,7 +2658,7 @@ describe "TextEditor", ->
it "moves the lines to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2693,7 +2688,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2735,8 +2730,8 @@ describe "TextEditor", ->
describe 'and many selections intersects folded rows', ->
it 'moves and preserves all the folds', ->
editor.createFold(2, 4)
editor.createFold(7, 9)
editor.foldBufferRowRange(2, 4)
editor.foldBufferRowRange(7, 9)
editor.setSelectedBufferRanges([
[[2, 0], [2, 4]],
@@ -2765,7 +2760,7 @@ describe "TextEditor", ->
describe "when there is a fold below one of the selected row", ->
it "moves all lines spanned by a selection to the following row, preserving the fold", ->
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2788,7 +2783,7 @@ describe "TextEditor", ->
describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", ->
it "moves all the lines below the fold, preserving the fold", ->
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2813,7 +2808,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRanges([
[[2, 2], [2, 9]],
[[4, 2], [4, 9]]
@@ -2880,6 +2875,13 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(1)).toBe "1"
expect(editor.lineTextForBufferRow(2)).toBe "2"
describe "when the line is the last buffer row", ->
it "doesn't move it", ->
editor.setText("abc\ndef")
editor.setCursorBufferPosition([1, 0])
editor.moveLineDown()
expect(editor.getText()).toBe("abc\ndef")
describe ".insertText(text)", ->
describe "when there is a single selection", ->
beforeEach ->
@@ -2951,10 +2953,10 @@ describe "TextEditor", ->
describe "when there is a selection that ends on a folded line", ->
it "destroys the selection", ->
editor.createFold(2, 4)
editor.foldBufferRowRange(2, 4)
editor.setSelectedBufferRange([[1, 0], [2, 0]])
editor.insertText('holy cow')
expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined()
expect(editor.isFoldedAtScreenRow(2)).toBeFalsy()
describe "when there are ::onWillInsertText and ::onDidInsertText observers", ->
beforeEach ->
@@ -3176,7 +3178,7 @@ describe "TextEditor", ->
expect(editor.indentationForBufferRow(0)).toBe 1
expect(editor.indentationForBufferRow(1)).toBe 1
it "indents the new line to the correct level when editor.autoIndent is true and using a off-side rule language", ->
it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
@@ -3248,15 +3250,14 @@ describe "TextEditor", ->
editor.setCursorScreenPosition(row: 0, column: 0)
editor.backspace()
describe "when the cursor is on the first column of a line below a fold", ->
it "deletes the folded lines", ->
editor.setCursorScreenPosition([4, 0])
editor.foldCurrentRow()
editor.setCursorScreenPosition([5, 0])
describe "when the cursor is after a fold", ->
it "deletes the folded range", ->
editor.foldBufferRange([[4, 7], [5, 8]])
editor.setCursorBufferPosition([5, 8])
editor.backspace()
expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(buffer.lineForRow(4).fold).toBeUndefined()
expect(buffer.lineForRow(4)).toBe " whirrent = items.shift();"
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
describe "when the cursor is in the middle of a line below a fold", ->
it "backspaces as normal", ->
@@ -3269,14 +3270,13 @@ describe "TextEditor", ->
expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));"
describe "when the cursor is on a folded screen line", ->
it "deletes all of the folded lines along with the fold", ->
it "deletes the contents of the fold before the cursor", ->
editor.setCursorBufferPosition([3, 0])
editor.foldCurrentRow()
editor.backspace()
expect(buffer.lineForRow(1)).toBe ""
expect(buffer.lineForRow(2)).toBe " return sort(Array.apply(this, arguments));"
expect(editor.getCursorScreenPosition()).toEqual [1, 0]
expect(buffer.lineForRow(1)).toBe " var sort = function(items) var pivot = items.shift(), current, left = [], right = [];"
expect(editor.getCursorScreenPosition()).toEqual [1, 29]
describe "when there are multiple cursors", ->
describe "when cursors are on the same line", ->
@@ -3343,7 +3343,7 @@ describe "TextEditor", ->
editor.backspace()
expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {"
expect(editor.tokenizedLineForScreenRow(3).fold).toBeDefined()
expect(editor.isFoldedAtScreenRow(3)).toBe(true)
describe "when there are multiple selections", ->
it "removes all selected text", ->
@@ -3516,16 +3516,16 @@ describe "TextEditor", ->
editor.delete()
expect(buffer.lineForRow(12)).toBe '};'
describe "when the cursor is on the end of a line above a fold", ->
describe "when the cursor is before a fold", ->
it "only deletes the lines inside the fold", ->
editor.foldBufferRow(4)
editor.setCursorScreenPosition([3, Infinity])
editor.foldBufferRange([[3, 6], [4, 8]])
editor.setCursorScreenPosition([3, 6])
cursorPositionBefore = editor.getCursorScreenPosition()
editor.delete()
expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(buffer.lineForRow(3)).toBe " vae(items.length > 0) {"
expect(buffer.lineForRow(4)).toBe " current = items.shift();"
expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore
describe "when the cursor is in the middle a line above a fold", ->
@@ -3537,20 +3537,21 @@ describe "TextEditor", ->
editor.delete()
expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];"
expect(editor.tokenizedLineForScreenRow(4).fold).toBeDefined()
expect(editor.isFoldedAtScreenRow(4)).toBe(true)
expect(editor.getCursorScreenPosition()).toEqual [3, 4]
describe "when the cursor is on a folded line", ->
it "removes the lines contained by the fold", ->
editor.setSelectedBufferRange([[2, 0], [2, 0]])
editor.createFold(2, 4)
editor.createFold(2, 6)
oldLine7 = buffer.lineForRow(7)
oldLine8 = buffer.lineForRow(8)
describe "when the cursor is inside a fold", ->
it "removes the folded content after the cursor", ->
editor.foldBufferRange([[2, 6], [6, 21]])
editor.setCursorBufferPosition([4, 9])
editor.delete()
expect(editor.tokenizedLineForScreenRow(2).text).toBe oldLine7
expect(editor.tokenizedLineForScreenRow(3).text).toBe oldLine8
expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;'
expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
expect(buffer.lineForRow(4)).toBe ' while ? left.push(current) : right.push(current);'
expect(buffer.lineForRow(5)).toBe ' }'
expect(editor.getCursorBufferPosition()).toEqual [4, 9]
describe "when there are multiple cursors", ->
describe "when cursors are on the same line", ->
@@ -3807,10 +3808,10 @@ describe "TextEditor", ->
it "cuts up to the end of the line", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(10)
editor.setCursorScreenPosition([2, 2])
editor.setEditorWidthInChars(25)
editor.setCursorScreenPosition([2, 6])
editor.cutToEndOfLine()
expect(editor.tokenizedLineForScreenRow(2).text).toBe '= () {'
expect(editor.lineTextForScreenRow(2)).toBe ' var function(items) {'
describe "when soft wrap is off", ->
describe "when nothing is selected", ->
@@ -4695,7 +4696,8 @@ describe "TextEditor", ->
it '.lineTextForScreenRow(row)', ->
editor.foldBufferRow(4)
expect(editor.lineTextForScreenRow(5)).toEqual ' return sort(left).concat(pivot).concat(sort(right));'
expect(editor.lineTextForScreenRow(100)).not.toBeDefined()
expect(editor.lineTextForScreenRow(9)).toEqual '};'
expect(editor.lineTextForScreenRow(10)).toBeUndefined()
describe ".deleteLine()", ->
it "deletes the first line when the cursor is there", ->
@@ -5052,11 +5054,13 @@ describe "TextEditor", ->
it 'retokenizes when the tab length is updated via .setTabLength()', ->
expect(editor.getTabLength()).toBe 2
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(3)
editor.setTabLength(6)
expect(editor.getTabLength()).toBe 6
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(1)
changeHandler = jasmine.createSpy('changeHandler')
editor.onDidChange(changeHandler)
@@ -5065,21 +5069,25 @@ describe "TextEditor", ->
it 'retokenizes when the editor.tabLength setting is updated', ->
expect(editor.getTabLength()).toBe 2
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(3)
atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.js'
expect(editor.getTabLength()).toBe 6
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(1)
it 'updates the tab length when the grammar changes', ->
atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.coffee'
expect(editor.getTabLength()).toBe 2
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(3)
editor.setGrammar(coffeeEditor.getGrammar())
expect(editor.getTabLength()).toBe 6
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(1)
describe ".indentLevelForLine(line)", ->
it "returns the indent level when the line has only leading whitespace", ->
@@ -5115,11 +5123,11 @@ describe "TextEditor", ->
runs ->
expect(editor.getGrammar()).toBe atom.grammars.nullGrammar
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.tokensForScreenRow(0).length).toBe(1)
atom.grammars.addGrammar(jsGrammar)
expect(editor.getGrammar()).toBe jsGrammar
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBeGreaterThan 1
expect(editor.tokensForScreenRow(0).length).toBeGreaterThan 1
describe "editor.autoIndent", ->
describe "when editor.autoIndent is false (default)", ->
@@ -5155,7 +5163,7 @@ describe "TextEditor", ->
expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1
describe "when the line preceding the newline does't add a level of indentation", ->
it "indents the new line to the same level a as the preceding line", ->
it "indents the new line to the same level as the preceding line", ->
editor.setCursorBufferPosition([5, 14])
editor.insertText('\n')
expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5)
@@ -5263,7 +5271,7 @@ describe "TextEditor", ->
describe ".destroy()", ->
it "destroys marker layers associated with the text editor", ->
selectionsMarkerLayerId = editor.selectionsMarkerLayer.id
foldsMarkerLayerId = editor.displayBuffer.foldsMarkerLayer.id
foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id
editor.destroy()
expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined()
expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined()
@@ -5347,10 +5355,10 @@ describe "TextEditor", ->
expect(editor.getSelectedBufferRanges()).toEqual [[[3, 5], [3, 5]], [[9, 0], [14, 0]]]
# folds are also duplicated
expect(editor.tokenizedLineForScreenRow(5).fold).toBeDefined()
expect(editor.tokenizedLineForScreenRow(7).fold).toBeDefined()
expect(editor.tokenizedLineForScreenRow(7).text).toBe " while(items.length > 0) {"
expect(editor.tokenizedLineForScreenRow(8).text).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.isFoldedAtScreenRow(5)).toBe(true)
expect(editor.isFoldedAtScreenRow(7)).toBe(true)
expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {" + editor.displayLayer.foldCharacter
expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
it "duplicates all folded lines for empty selections on folded lines", ->
editor.foldBufferRow(4)
@@ -5546,17 +5554,15 @@ describe "TextEditor", ->
runs ->
editor.setText("// http://github.com")
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[1].value).toBe " http://github.com"
expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('language-hyperlink')
runs ->
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[2].value).toBe "http://github.com"
expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'markup.underline.link.http.hyperlink']
describe "when the grammar is updated", ->
it "retokenizes existing buffers that contain tokens that match the injection selector", ->
@@ -5566,25 +5572,22 @@ describe "TextEditor", ->
runs ->
editor.setText("// SELECT * FROM OCTOCATS")
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('package-with-injection-selector')
runs ->
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('language-sql')
runs ->
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[2].value).toBe "SELECT"
expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'keyword.other.DML.sql', 'keyword.operator.star.sql', 'keyword.other.DML.sql']
describe ".normalizeTabsInBufferRange()", ->
it "normalizes tabs depending on the editor's soft tab/tab length settings", ->
@@ -5712,6 +5715,19 @@ describe "TextEditor", ->
expect(editor.getFirstVisibleScreenRow()).toEqual 89
expect(editor.getVisibleRowRange()).toEqual [89, 99]
describe "::scrollToScreenPosition(position, [options])", ->
it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", ->
scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll")
editor.onDidRequestAutoscroll(scrollSpy)
editor.scrollToScreenPosition([8, 20])
editor.scrollToScreenPosition([8, 20], center: true)
editor.scrollToScreenPosition([8, 20], center: false, reversed: true)
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {})
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true})
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true})
describe '.get/setPlaceholderText()', ->
it 'can be created with placeholderText', ->
newEditor = atom.workspace.buildTextEditor(
@@ -5729,28 +5745,6 @@ describe "TextEditor", ->
expect(handler).toHaveBeenCalledWith 'OK'
expect(editor.getPlaceholderText()).toBe 'OK'
describe ".checkoutHeadRevision()", ->
it "reverts to the version of its file checked into the project repository", ->
atom.config.set("editor.confirmCheckoutHeadRevision", false)
editor.setCursorBufferPosition([0, 0])
editor.insertText("---\n")
expect(editor.lineTextForBufferRow(0)).toBe "---"
waitsForPromise ->
editor.checkoutHeadRevision()
runs ->
expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () {"
describe "when there's no repository for the editor's file", ->
it "doesn't do anything", ->
editor = atom.workspace.buildTextEditor()
editor.setText("stuff")
editor.checkoutHeadRevision()
waitsForPromise -> editor.checkoutHeadRevision()
describe 'gutters', ->
describe 'the TextEditor constructor', ->
it 'creates a line-number gutter', ->
@@ -5879,6 +5873,7 @@ describe "TextEditor", ->
expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual {
properties: {type: 'highlight', class: 'foo'}
screenRange: marker.getScreenRange(),
bufferRange: marker.getBufferRange(),
rangeIsReversed: false
}
@@ -5899,26 +5894,31 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker2.getRange(),
bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
bufferRange: marker3.getRange(),
rangeIsReversed: false
}
@@ -5930,16 +5930,19 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
bufferRange: marker3.getRange(),
rangeIsReversed: false
}
@@ -5948,6 +5951,7 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'quux'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
@@ -5956,6 +5960,7 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
@@ -5967,8 +5972,21 @@ describe "TextEditor", ->
it "ignores invisibles even if editor.showInvisibles is true", ->
atom.config.set('editor.showInvisibles', true)
invisibles = editor.tokenizedLineForScreenRow(0).invisibles
expect(invisibles).toBe(null)
expect(editor.lineTextForScreenRow(0).indexOf(atom.config.get('editor.invisibles.eol'))).toBe(-1)
describe "indent guides", ->
it "shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini", ->
editor.setText(" foo")
atom.config.set('editor.tabLength', 2)
atom.config.set('editor.showIndentGuide', false)
expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace']
atom.config.set('editor.showIndentGuide', true)
expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace indent-guide']
editor.setMini(true)
expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace']
describe "when the editor is constructed with the grammar option set", ->
beforeEach ->

View File

@@ -75,22 +75,23 @@ describe 'text utilities', ->
expect(textUtils.isKoreanCharacter("O")).toBe(false)
describe ".isCJKCharacter(character)", ->
it "returns true when the character is either a korean, half-width or double-width character", ->
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
describe ".isWrapBoundary(previousCharacter, character)", ->
it "returns true when the character is CJK or when the previous character is a space/tab", ->
anyCharacter = 'x'
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isDoubleWidthCharacter("a")).toBe(false)
expect(textUtils.isDoubleWidthCharacter("O")).toBe(false)
expect(textUtils.isDoubleWidthCharacter("z")).toBe(false)
expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('a', 'h')).toBe(false)

View File

@@ -175,7 +175,7 @@ describe "atom.themes", ->
expect(styleElementAddedHandler).toHaveBeenCalled()
element = document.querySelector('head style[source-path*="css.css"]')
expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(cssPath)
expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(cssPath)
expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8')
# doesn't append twice
@@ -194,7 +194,7 @@ describe "atom.themes", ->
expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1
element = document.querySelector('head style[source-path*="sample.less"]')
expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(lessPath)
expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(lessPath)
expect(element.textContent).toBe """
#header {
color: #4d926f;
@@ -213,9 +213,9 @@ describe "atom.themes", ->
it "supports requiring css and less stylesheets without an explicit extension", ->
atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css')
expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css'))
expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css'))
atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample')
expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less'))
expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less'))
document.querySelector('head style[source-path*="css.css"]').remove()
document.querySelector('head style[source-path*="sample.less"]').remove()

View File

@@ -0,0 +1,103 @@
/** @babel */
import TokenizedBufferIterator from '../src/tokenized-buffer-iterator'
import {Point} from 'text-buffer'
describe('TokenizedBufferIterator', () => {
it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => {
const tokenizedBuffer = {
tokenizedLineForRow () {
return {
tags: [-1, -2, -1, -2],
text: '',
openScopes: []
}
}
}
const grammarRegistry = {
scopeForId () {
return 'foo'
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
})
it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => {
const tokenizedBuffer = {
tokenizedLineForRow (row) {
if (row === 0) {
return {
tags: [-1, 3, -2, -3],
text: 'bar',
openScopes: []
}
} else if (row === 1) {
return {
tags: [3],
text: 'baz',
openScopes: [-1]
}
} else if (row === 2) {
return {
tags: [-2],
text: '',
openScopes: [-1]
}
}
}
}
const grammarRegistry = {
scopeForId (id) {
if (id === -2 || id === -1) {
return 'foo'
} else if (id === -3) {
return 'qux'
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['qux'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['qux'])
expect(iterator.getOpenTags()).toEqual([])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(1, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(2, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
})
})

View File

@@ -1,5 +1,5 @@
TokenizedBuffer = require '../src/tokenized-buffer'
TextBuffer = require 'text-buffer'
{Point} = TextBuffer = require 'text-buffer'
_ = require 'underscore-plus'
describe "TokenizedBuffer", ->
@@ -134,13 +134,10 @@ describe "TokenizedBuffer", ->
describe "on construction", ->
it "initially creates un-tokenized screen lines, then tokenizes lines chunk at a time in the background", ->
line0 = tokenizedBuffer.tokenizedLineForRow(0)
expect(line0.tokens.length).toBe 1
expect(line0.tokens[0]).toEqual(value: line0.text, scopes: ['source.js'])
expect(line0.tokens).toEqual([value: line0.text, scopes: ['source.js']])
line11 = tokenizedBuffer.tokenizedLineForRow(11)
expect(line11.tokens.length).toBe 2
expect(line11.tokens[0]).toEqual(value: " ", scopes: ['source.js'], isAtomic: true)
expect(line11.tokens[1]).toEqual(value: "return sort(Array.apply(this, arguments));", scopes: ['source.js'])
expect(line11.tokens).toEqual([value: " return sort(Array.apply(this, arguments));", scopes: ['source.js']])
# background tokenization has not begun
expect(tokenizedBuffer.tokenizedLineForRow(0).ruleStack).toBeUndefined()
@@ -236,7 +233,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js'])
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js'])
# line 2 is unchanged
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -283,9 +280,9 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
# lines below deleted regions should be shifted upward
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -331,7 +328,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
# previous line 3 is pushed down to become line 5
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -377,32 +374,6 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(5).ruleStack?).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(6).ruleStack?).toBeTruthy()
it "tokenizes leading whitespace based on the new tab length", ->
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " "
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " "
tokenizedBuffer.setTabLength(4)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " "
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeFalsy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " current "
it "does not tokenize whitespaces followed by combining characters as leading whitespace", ->
buffer.setText(" \u030b")
fullyTokenize(tokenizedBuffer)
{tokens} = tokenizedBuffer.tokenizedLineForRow(0)
expect(tokens[0].value).toBe " "
expect(tokens[0].hasLeadingWhitespace()).toBe true
expect(tokens[1].value).toBe " "
expect(tokens[1].hasLeadingWhitespace()).toBe true
expect(tokens[2].value).toBe " \u030b"
expect(tokens[2].hasLeadingWhitespace()).toBe false
it "does not break out soft tabs across a scope boundary", ->
waitsForPromise ->
atom.packages.activatePackage('language-gfm')
@@ -439,133 +410,6 @@ describe "TokenizedBuffer", ->
beforeEach ->
fullyTokenize(tokenizedBuffer)
it "renders each tab as its own atomic token with a value of size tabLength", ->
tabAsSpaces = _.multiplyString(' ', tokenizedBuffer.getTabLength())
screenLine0 = tokenizedBuffer.tokenizedLineForRow(0)
expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}"
{tokens} = screenLine0
expect(tokens.length).toBe 4
expect(tokens[0].value).toBe "#"
expect(tokens[1].value).toBe " Econ 101"
expect(tokens[2].value).toBe tabAsSpaces
expect(tokens[2].scopes).toEqual tokens[1].scopes
expect(tokens[2].isAtomic).toBeTruthy()
expect(tokens[3].value).toBe ""
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand"
it "aligns the hard tabs to the correct tab stop column", ->
buffer.setText """
1\t2 \t3\t4
12\t3 \t4\t5
123\t4 \t5\t6
"""
tokenizedBuffer.setTabLength(4)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
tokenizedBuffer.setTabLength(3)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 3
tokenizedBuffer.setTabLength(2)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
tokenizedBuffer.setTabLength(1)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
describe "when the buffer contains UTF-8 surrogate pairs", ->
beforeEach ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
runs ->
buffer = atom.project.bufferForPathSync 'sample-with-pairs.js'
buffer.setText """
'abc\uD835\uDF97def'
//\uD835\uDF97xyz
"""
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
afterEach ->
tokenizedBuffer.destroy()
buffer.release()
it "renders each UTF-8 surrogate pair as its own atomic token", ->
screenLine0 = tokenizedBuffer.tokenizedLineForRow(0)
expect(screenLine0.text).toBe "'abc\uD835\uDF97def'"
{tokens} = screenLine0
expect(tokens.length).toBe 5
expect(tokens[0].value).toBe "'"
expect(tokens[1].value).toBe "abc"
expect(tokens[2].value).toBe "\uD835\uDF97"
expect(tokens[2].isAtomic).toBeTruthy()
expect(tokens[3].value).toBe "def"
expect(tokens[4].value).toBe "'"
screenLine1 = tokenizedBuffer.tokenizedLineForRow(1)
expect(screenLine1.text).toBe "//\uD835\uDF97xyz"
{tokens} = screenLine1
expect(tokens.length).toBe 4
expect(tokens[0].value).toBe '//'
expect(tokens[1].value).toBe '\uD835\uDF97'
expect(tokens[1].value).toBeTruthy()
expect(tokens[2].value).toBe 'xyz'
expect(tokens[3].value).toBe ''
describe "when the grammar is tokenized", ->
it "emits the `tokenized` event", ->
editor = null
@@ -575,7 +419,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
expect(tokenizedHandler.callCount).toBe(1)
@@ -588,7 +432,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
fullyTokenize(tokenizedBuffer)
tokenizedBuffer.onDidTokenize tokenizedHandler
@@ -606,7 +450,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('coffee.coffee').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
tokenizedHandler.reset()
@@ -683,132 +527,7 @@ describe "TokenizedBuffer", ->
it "returns the range covered by all contigous tokens (within a single line)", ->
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]]
describe "when the editor.tabLength config value changes", ->
it "updates the tab length of the tokenized lines", ->
buffer = atom.project.bufferForPathSync('sample.js')
buffer.setText('\ttest')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 6)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
it "does not allow the tab length to be less than 1", ->
buffer = atom.project.bufferForPathSync('sample.js')
buffer.setText('\ttest')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 1)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 0)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
describe "when the invisibles value changes", ->
beforeEach ->
it "updates the tokens with the appropriate invisible characters", ->
buffer = new TextBuffer(text: " \t a line with tabs\tand \tspaces \t ")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
atom.config.set("editor.showInvisibles", true)
atom.config.set("editor.invisibles", space: 'S', tab: 'T')
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "SST Sa line with tabsTand T spacesSTS"
# Also needs to work for copies
expect(tokenizedBuffer.tokenizedLineForRow(0).copy().text).toBe "SST Sa line with tabsTand T spacesSTS"
it "assigns endOfLineInvisibles to tokenized lines", ->
buffer = new TextBuffer(text: "a line that ends in a carriage-return-line-feed \r\na line that ends in just a line-feed\na line with no ending")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
atom.config.set('editor.showInvisibles', true)
atom.config.set("editor.invisibles", cr: 'R', eol: 'N')
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R', 'N']
expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual ['N']
# Lines ending in soft wraps get no invisibles
[left, right] = tokenizedBuffer.tokenizedLineForRow(0).softWrapAt(20)
expect(left.endOfLineInvisibles).toBe null
expect(right.endOfLineInvisibles).toEqual ['R', 'N']
atom.config.set("editor.invisibles", cr: 'R', eol: false)
expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R']
expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual []
describe "leading and trailing whitespace", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
it "assigns ::firstNonWhitespaceIndex on tokens that have leading whitespace", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0].firstNonWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0].firstNonWhitespaceIndex).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].firstNonWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[0].firstNonWhitespaceIndex).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].firstNonWhitespaceIndex).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2].firstNonWhitespaceIndex).toBe null
# The 4th token *has* leading whitespace, but isn't entirely whitespace
buffer.insert([5, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3].firstNonWhitespaceIndex).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4].firstNonWhitespaceIndex).toBe null
# Lines that are *only* whitespace are not considered to have leading whitespace
buffer.insert([10, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstNonWhitespaceIndex).toBe null
it "assigns ::firstTrailingWhitespaceIndex on tokens that have trailing whitespace", ->
buffer.insert([0, Infinity], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[11].firstTrailingWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[12].firstTrailingWhitespaceIndex).toBe 0
# The last token *has* trailing whitespace, but isn't entirely whitespace
buffer.setTextInRange([[2, 39], [2, 40]], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[14].firstTrailingWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[15].firstTrailingWhitespaceIndex).toBe 6
# Lines that are *only* whitespace are considered to have trailing whitespace
buffer.insert([10, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstTrailingWhitespaceIndex).toBe 0
it "only marks trailing whitespace on the last segment of a soft-wrapped line", ->
buffer.insert([0, Infinity], ' ')
tokenizedLine = tokenizedBuffer.tokenizedLineForRow(0)
[segment1, segment2] = tokenizedLine.softWrapAt(16)
expect(segment1.tokens[5].value).toBe ' '
expect(segment1.tokens[5].firstTrailingWhitespaceIndex).toBe null
expect(segment2.tokens[6].value).toBe ' '
expect(segment2.tokens[6].firstTrailingWhitespaceIndex).toBe 0
it "sets leading and trailing whitespace correctly on a line with invisible characters that is copied", ->
buffer.setText(" \t a line with tabs\tand \tspaces \t ")
atom.config.set("editor.showInvisibles", true)
atom.config.set("editor.invisibles", space: 'S', tab: 'T')
fullyTokenize(tokenizedBuffer)
line = tokenizedBuffer.tokenizedLineForRow(0).copy()
expect(line.tokens[0].firstNonWhitespaceIndex).toBe 2
expect(line.tokens[line.tokens.length - 1].firstTrailingWhitespaceIndex).toBe 0
describe ".indentLevel on tokenized lines", ->
describe ".indentLevelForRow(row)", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({
@@ -818,43 +537,43 @@ describe "TokenizedBuffer", ->
describe "when the line is non-empty", ->
it "has an indent level based on the leading whitespace on the line", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).indentLevel).toBe 0
expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0
expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
buffer.insert([2, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2.5
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5
describe "when the line is empty", ->
it "assumes the indentation level of the first non-empty line below or above if one exists", ->
buffer.insert([12, 0], ' ')
buffer.insert([12, Infinity], '\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(14).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2
buffer.insert([1, Infinity], '\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(3).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2
buffer.setText('\n\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 0
expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0
describe "when the changed lines are surrounded by whitespace-only lines", ->
it "updates the indentLevel of empty lines that precede the change", ->
expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 0
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0
buffer.insert([12, 0], '\n')
buffer.insert([13, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 1
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1
it "updates empty line indent guides when the empty line is the last line", ->
buffer.insert([12, 2], '\n')
# The newline and the tab need to be in two different operations to surface the bug
buffer.insert([12, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 1
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1
buffer.insert([12, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(14)).not.toBeDefined()
it "updates the indentLevel of empty lines surrounding a change that inserts lines", ->
@@ -862,24 +581,24 @@ describe "TokenizedBuffer", ->
buffer.insert([7, 0], '\n\n')
buffer.insert([5, 0], '\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2
tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler')
buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four')
delete changeHandler.argsForCall[0][0].bufferChange
expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: 2)
expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: 2)
expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
it "updates the indentLevel of empty lines surrounding a change that removes lines", ->
# create some new lines
@@ -891,14 +610,14 @@ describe "TokenizedBuffer", ->
buffer.setTextInRange([[7, 0], [8, 65]], ' ok')
delete changeHandler.argsForCall[0][0].bufferChange
expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: -1)
expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: -1)
expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(7).indentLevel).toBe 2 # new text
expect(tokenizedBuffer.tokenizedLineForRow(8).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 2 # }
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text
expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # }
describe "::isFoldableAtRow(row)", ->
changes = null
@@ -1050,3 +769,107 @@ describe "TokenizedBuffer", ->
runs ->
expect(coffeeCalled).toBe true
describe "text decoration layer API", ->
describe "iterator", ->
it "iterates over the syntactic scope boundaries", ->
buffer = new TextBuffer(text: "var foo = 1 /*\nhello*/var bar = 2\n")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".js"))
fullyTokenize(tokenizedBuffer)
iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(0, 0))
expectedBoundaries = [
{position: Point(0, 0), closeTags: [], openTags: ["source.js", "storage.type.var.js"]}
{position: Point(0, 3), closeTags: ["storage.type.var.js"], openTags: []}
{position: Point(0, 8), closeTags: [], openTags: ["keyword.operator.assignment.js"]}
{position: Point(0, 9), closeTags: ["keyword.operator.assignment.js"], openTags: []}
{position: Point(0, 10), closeTags: [], openTags: ["constant.numeric.decimal.js"]}
{position: Point(0, 11), closeTags: ["constant.numeric.decimal.js"], openTags: []}
{position: Point(0, 12), closeTags: [], openTags: ["comment.block.js", "punctuation.definition.comment.js"]}
{position: Point(0, 14), closeTags: ["punctuation.definition.comment.js"], openTags: []}
{position: Point(1, 5), closeTags: [], openTags: ["punctuation.definition.comment.js"]}
{position: Point(1, 7), closeTags: ["punctuation.definition.comment.js", "comment.block.js"], openTags: ["storage.type.var.js"]}
{position: Point(1, 10), closeTags: ["storage.type.var.js"], openTags: []}
{position: Point(1, 15), closeTags: [], openTags: ["keyword.operator.assignment.js"]}
{position: Point(1, 16), closeTags: ["keyword.operator.assignment.js"], openTags: []}
{position: Point(1, 17), closeTags: [], openTags: ["constant.numeric.decimal.js"]}
{position: Point(1, 18), closeTags: ["constant.numeric.decimal.js"], openTags: []}
]
loop
boundary = {
position: iterator.getPosition(),
closeTags: iterator.getCloseTags(),
openTags: iterator.getOpenTags()
}
expect(boundary).toEqual(expectedBoundaries.shift())
break unless iterator.moveToSuccessor()
expect(iterator.seek(Point(0, 1))).toEqual(["source.js", "storage.type.var.js"])
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.seek(Point(0, 8))).toEqual(["source.js"])
expect(iterator.getPosition()).toEqual(Point(0, 8))
expect(iterator.seek(Point(1, 0))).toEqual(["source.js", "comment.block.js"])
expect(iterator.getPosition()).toEqual(Point(1, 5))
expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.decimal.js"])
expect(iterator.getPosition()).toEqual(Point(1, 18))
expect(iterator.seek(Point(2, 0))).toEqual(["source.js"])
iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test)
it "does not report columns beyond the length of the line", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
runs ->
buffer = new TextBuffer(text: "# hello\n# world")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".coffee"))
fullyTokenize(tokenizedBuffer)
iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(0, 0))
iterator.moveToSuccessor()
iterator.moveToSuccessor()
expect(iterator.getPosition().column).toBe(7)
iterator.moveToSuccessor()
expect(iterator.getPosition().column).toBe(0)
iterator.seek(Point(0, 7))
expect(iterator.getPosition().column).toBe(7)
iterator.seek(Point(0, 8))
expect(iterator.getPosition().column).toBe(7)
it "correctly terminates scopes at the beginning of the line (regression)", ->
grammar = atom.grammars.createGrammar('test', {
'scopeName': 'text.broken'
'name': 'Broken grammar'
'patterns': [
{'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
{'match': '.', 'name': 'yellow.broken'}
]
})
buffer = new TextBuffer(text: 'start x\nend x\nx')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(grammar)
fullyTokenize(tokenizedBuffer)
iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(1, 0))
expect(iterator.getPosition()).toEqual([1, 0])
expect(iterator.getCloseTags()).toEqual ['blue.broken']
expect(iterator.getOpenTags()).toEqual ['yellow.broken']

View File

@@ -1,19 +0,0 @@
describe "TokenizedLine", ->
editor = null
beforeEach ->
waitsForPromise -> atom.packages.activatePackage('language-coffee-script')
describe "::isOnlyWhitespace()", ->
beforeEach ->
waitsForPromise ->
atom.workspace.open('coffee.coffee').then (o) -> editor = o
it "returns true when the line is only whitespace", ->
expect(editor.tokenizedLineForScreenRow(3).isOnlyWhitespace()).toBe true
expect(editor.tokenizedLineForScreenRow(7).isOnlyWhitespace()).toBe true
expect(editor.tokenizedLineForScreenRow(23).isOnlyWhitespace()).toBe true
it "returns false when the line is not only whitespace", ->
expect(editor.tokenizedLineForScreenRow(0).isOnlyWhitespace()).toBe false
expect(editor.tokenizedLineForScreenRow(2).isOnlyWhitespace()).toBe false

View File

@@ -75,7 +75,7 @@ describe "WindowEventHandler", ->
describe "when a link is clicked", ->
it "opens the http/https links in an external application", ->
shell = require 'shell'
{shell} = require 'electron'
spyOn(shell, 'openExternal')
link = document.createElement('a')

View File

@@ -80,7 +80,8 @@ describe "Workspace", ->
expect(untitledEditor.getText()).toBe("An untitled editor.")
expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath()
expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{pathEscaped}///
describe "where there are no open panes or editors", ->
it "constructs the view with no open editors", ->
@@ -428,7 +429,7 @@ describe "Workspace", ->
workspace.open('sample.js').then (e) -> editor = e
runs ->
expect(editor.displayBuffer.largeFileMode).toBe true
expect(editor.largeFileMode).toBe true
describe "when the file is over 20MB", ->
it "prompts the user to make sure they want to open a file this big", ->
@@ -453,7 +454,7 @@ describe "Workspace", ->
runs ->
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(editor.displayBuffer.largeFileMode).toBe true
expect(editor.largeFileMode).toBe true
describe "when passed a path that matches a custom opener", ->
it "returns the resource returned by the custom opener", ->
@@ -833,25 +834,29 @@ describe "Workspace", ->
describe "when there is an active pane item", ->
it "sets the title to the pane item's title plus the project path", ->
item = atom.workspace.getActivePaneItem()
expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}///
describe "when the title of the active pane item changes", ->
it "updates the window title based on the item's new title", ->
editor = atom.workspace.getActivePaneItem()
editor.buffer.setPath(path.join(temp.dir, 'hi'))
expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}///
describe "when the active pane's item changes", ->
it "updates the title to the new item's title plus the project path", ->
atom.workspace.getActivePane().activateNextItem()
item = atom.workspace.getActivePaneItem()
expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}///
describe "when the last pane item is removed", ->
it "updates the title to contain the project's path", ->
atom.workspace.getActivePane().destroy()
expect(atom.workspace.getActivePaneItem()).toBeUndefined()
expect(document.title).toMatch ///^#{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{pathEscaped}///
describe "when an inactive pane's item changes", ->
it "does not update the title", ->
@@ -875,7 +880,8 @@ describe "Workspace", ->
})
workspace2.deserialize(atom.workspace.serialize(), atom.deserializers)
item = workspace2.getActivePaneItem()
expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}///
workspace2.destroy()
describe "document edited status", ->
@@ -1610,3 +1616,48 @@ describe "Workspace", ->
runs ->
expect(pane.getPendingItem()).toBeFalsy()
describe "grammar activation", ->
beforeEach ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
it "notifies the workspace of which grammar is used", ->
editor = null
grammarUsed = jasmine.createSpy()
atom.workspace.handleGrammarUsed = grammarUsed
waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
waitsFor -> grammarUsed.callCount is 1
runs ->
expect(grammarUsed.argsForCall[0][0].name).toBe 'JavaScript'
describe ".checkoutHeadRevision()", ->
editor = null
beforeEach ->
atom.config.set("editor.confirmCheckoutHeadRevision", false)
waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
it "reverts to the version of its file checked into the project repository", ->
editor.setCursorBufferPosition([0, 0])
editor.insertText("---\n")
expect(editor.lineTextForBufferRow(0)).toBe "---"
waitsForPromise ->
atom.workspace.checkoutHeadRevision(editor)
runs ->
expect(editor.lineTextForBufferRow(0)).toBe ""
describe "when there's no repository for the editor's file", ->
it "doesn't do anything", ->
editor = atom.workspace.buildTextEditor()
editor.setText("stuff")
atom.workspace.checkoutHeadRevision(editor)
waitsForPromise -> atom.workspace.checkoutHeadRevision(editor)
escapeStringRegex = (str) ->
str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')

View File

@@ -234,13 +234,14 @@ class AtomEnvironment extends Model
checkPortableHomeWritable()
attachSaveStateListeners: ->
saveState = => @saveState({isUnloading: false}) unless @unloaded
debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval)
@document.addEventListener('mousedown', debouncedSaveState, true)
@document.addEventListener('keydown', debouncedSaveState, true)
saveState = _.debounce((=>
window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
), @saveStateDebounceInterval)
@document.addEventListener('mousedown', saveState, true)
@document.addEventListener('keydown', saveState, true)
@disposables.add new Disposable =>
@document.removeEventListener('mousedown', debouncedSaveState, true)
@document.removeEventListener('keydown', debouncedSaveState, true)
@document.removeEventListener('mousedown', saveState, true)
@document.removeEventListener('keydown', saveState, true)
setConfigSchema: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@@ -255,7 +256,7 @@ class AtomEnvironment extends Model
@deserializers.add(TextBuffer)
registerDefaultCommands: ->
registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller})
registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications, @project, @clipboard})
registerDefaultViewProviders: ->
@views.addViewProvider Workspace, (model, env) ->
@@ -655,6 +656,7 @@ class AtomEnvironment extends Model
# Call this method when establishing a real application window.
startEditorWindow: ->
@unloaded = false
@loadState().then (state) =>
@windowDimensions = state?.windowDimensions
@displayWindow().then =>
@@ -842,16 +844,15 @@ class AtomEnvironment extends Model
return Promise.resolve() unless @enablePersistence
new Promise (resolve, reject) =>
window.requestIdleCallback =>
return if not @project
return if not @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
loadState: ->
if @enablePersistence

View File

@@ -138,7 +138,11 @@ class AtomApplication
return unless @socketPath?
@deleteSocketFile()
server = net.createServer (connection) =>
connection.on 'data', (data) =>
data = ''
connection.on 'data', (chunk) ->
data = data + chunk
connection.on 'end', =>
options = JSON.parse(data)
@openWithOptions(options)
@@ -170,9 +174,6 @@ class AtomApplication
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(getLoadSettings())
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@on 'application:open', -> @promptForPathToOpen('all', getLoadSettings())
@on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings())
@on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings())
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
@on 'application:inspect', ({x, y, atomWindow}) ->
@@ -255,6 +256,14 @@ class AtomApplication
ipcMain.on 'command', (event, command) =>
@emit(command)
ipcMain.on 'open-command', (event, command, args...) =>
defaultPath = args[0] if args.length > 0
switch command
when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath)
when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath)
when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath)
else console.log "Invalid open-command received: " + command
ipcMain.on 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
@@ -653,11 +662,13 @@ class AtomApplication
# :safeMode - A Boolean which controls whether any newly opened windows
# should be in safe mode or not.
# :window - An {AtomWindow} to use for opening a selected file path.
promptForPathToOpen: (type, {devMode, safeMode, window}) ->
@promptForPath type, (pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})
# :path - An optional String which controls the default path to which the
# file dialog opens.
promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) ->
@promptForPath type, ((pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})), path
promptForPath: (type, callback) ->
promptForPath: (type, callback, path) ->
properties =
switch type
when 'file' then ['openFile']
@@ -680,8 +691,8 @@ class AtomApplication
when 'folder' then 'Open Folder'
else 'Open'
if process.platform is 'linux'
if projectPath = @lastFocusedWindow?.projectPath
openOptions.defaultPath = projectPath
# File dialog defaults to project directory of currently active editor
if path?
openOptions.defaultPath = path
dialog.showOpenDialog(parentWindow, openOptions, callback)

View File

@@ -50,7 +50,7 @@ class BufferedProcess
options ?= {}
@command = command
# Related to joyent/node#2318
if process.platform is 'win32'
if process.platform is 'win32' and not options.shell?
# Quote all arguments and escapes inner quotes
if args?
cmdArgs = args.filter (arg) -> arg?

View File

@@ -244,11 +244,14 @@ class CommandRegistry
(@selectorBasedListenersByCommandName[event.type] ? [])
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
listeners = listeners.concat(selectorBasedListeners)
listeners = selectorBasedListeners.concat(listeners)
matched = true if listeners.length > 0
for listener in listeners
# Call inline listeners first in reverse registration order,
# and selector-based listeners by specificity and reverse
# registration order.
for listener in listeners by -1
break if immediatePropagationStopped
listener.callback.call(currentTarget, dispatchedEvent)
@@ -271,8 +274,8 @@ class SelectorBasedListener
@sequenceNumber = SequenceCount++
compare: (other) ->
other.specificity - @specificity or
other.sequenceNumber - @sequenceNumber
@specificity - other.specificity or
@sequenceNumber - other.sequenceNumber
class InlineListener
constructor: (@callback) ->

View File

@@ -155,6 +155,10 @@ module.exports =
type: 'boolean'
default: true
description: 'Show line numbers in the editor\'s gutter.'
atomicSoftTabs:
type: 'boolean'
default: true
description: 'Skip over tab-length runs of leading whitespace when moving the cursor.'
autoIndent:
type: 'boolean'
default: true

View File

@@ -9,7 +9,7 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
# where text can be inserted.
#
# Cursors belong to {TextEditor}s and have some metadata attached in the form
# of a {TextEditorMarker}.
# of a {DisplayMarker}.
module.exports =
class Cursor extends Model
screenPosition: null
@@ -129,7 +129,7 @@ class Cursor extends Model
Section: Cursor Position Details
###
# Public: Returns the underlying {TextEditorMarker} for the cursor.
# Public: Returns the underlying {DisplayMarker} for the cursor.
# Useful with overlay {Decoration}s.
getMarker: -> @marker
@@ -261,11 +261,11 @@ class Cursor extends Model
while columnCount > column and row > 0
columnCount -= column
column = @editor.lineTextForScreenRow(--row).length
column = @editor.lineLengthForScreenRow(--row)
columnCount-- # subtract 1 for the row move
column = column - columnCount
@setScreenPosition({row, column}, clip: 'backward')
@setScreenPosition({row, column}, clipDirection: 'backward')
# Public: Moves the cursor right one screen column.
#
@@ -280,7 +280,7 @@ class Cursor extends Model
else
{row, column} = @getScreenPosition()
maxLines = @editor.getScreenLineCount()
rowLength = @editor.lineTextForScreenRow(row).length
rowLength = @editor.lineLengthForScreenRow(row)
columnsRemainingInLine = rowLength - column
while columnCount > columnsRemainingInLine and row < maxLines - 1
@@ -288,11 +288,11 @@ class Cursor extends Model
columnCount-- # subtract 1 for the row move
column = 0
rowLength = @editor.lineTextForScreenRow(++row).length
rowLength = @editor.lineLengthForScreenRow(++row)
columnsRemainingInLine = rowLength
column = column + columnCount
@setScreenPosition({row, column}, clip: 'forward', wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
@setScreenPosition({row, column}, clipDirection: 'forward')
# Public: Moves the cursor to the top of the buffer.
moveToTop: ->

View File

@@ -0,0 +1,181 @@
{Emitter} = require 'event-kit'
Model = require './model'
Decoration = require './decoration'
LayerDecoration = require './layer-decoration'
module.exports =
class DecorationManager extends Model
didUpdateDecorationsEventScheduled: false
updatedSynchronously: false
constructor: (@displayLayer, @defaultMarkerLayer) ->
super
@emitter = new Emitter
@decorationsById = {}
@decorationsByMarkerId = {}
@overlayDecorationsById = {}
@layerDecorationsByMarkerLayerId = {}
@decorationCountsByLayerId = {}
@layerUpdateDisposablesByLayerId = {}
observeDecorations: (callback) ->
callback(decoration) for decoration in @getDecorations()
@onDidAddDecoration(callback)
onDidAddDecoration: (callback) ->
@emitter.on 'did-add-decoration', callback
onDidRemoveDecoration: (callback) ->
@emitter.on 'did-remove-decoration', callback
onDidUpdateDecorations: (callback) ->
@emitter.on 'did-update-decorations', callback
setUpdatedSynchronously: (@updatedSynchronously) ->
decorationForId: (id) ->
@decorationsById[id]
getDecorations: (propertyFilter) ->
allDecorations = []
for markerId, decorations of @decorationsByMarkerId
allDecorations.push(decorations...) if decorations?
if propertyFilter?
allDecorations = allDecorations.filter (decoration) ->
for key, value of propertyFilter
return false unless decoration.properties[key] is value
true
allDecorations
getLineDecorations: (propertyFilter) ->
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line')
getLineNumberDecorations: (propertyFilter) ->
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number')
getHighlightDecorations: (propertyFilter) ->
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight')
getOverlayDecorations: (propertyFilter) ->
result = []
for id, decoration of @overlayDecorationsById
result.push(decoration)
if propertyFilter?
result.filter (decoration) ->
for key, value of propertyFilter
return false unless decoration.properties[key] is value
true
else
result
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsByMarkerId = {}
for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
if decorations = @decorationsByMarkerId[marker.id]
decorationsByMarkerId[marker.id] = decorations
decorationsByMarkerId
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsState = {}
for layerId of @decorationCountsByLayerId
layer = @displayLayer.getMarkerLayer(layerId)
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
screenRange = marker.getScreenRange()
bufferRange = marker.getBufferRange()
rangeIsReversed = marker.isReversed()
if decorations = @decorationsByMarkerId[marker.id]
for decoration in decorations
decorationsState[decoration.id] = {
properties: decoration.properties
screenRange, bufferRange, rangeIsReversed
}
if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
for layerDecoration in layerDecorations
decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
screenRange, bufferRange, rangeIsReversed
}
decorationsState
decorateMarker: (marker, decorationParams) ->
throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
decoration = new Decoration(marker, this, decorationParams)
@decorationsByMarkerId[marker.id] ?= []
@decorationsByMarkerId[marker.id].push(decoration)
@overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
@decorationsById[decoration.id] = decoration
@observeDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
@emitter.emit 'did-add-decoration', decoration
decoration
decorateMarkerLayer: (markerLayer, decorationParams) ->
decoration = new LayerDecoration(markerLayer, this, decorationParams)
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
@observeDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
decoration
decorationsForMarkerId: (markerId) ->
@decorationsByMarkerId[markerId]
scheduleUpdateDecorationsEvent: ->
if @updatedSynchronously
@emitter.emit 'did-update-decorations'
return
unless @didUpdateDecorationsEventScheduled
@didUpdateDecorationsEventScheduled = true
process.nextTick =>
@didUpdateDecorationsEventScheduled = false
@emitter.emit 'did-update-decorations'
decorationDidChangeType: (decoration) ->
if decoration.isType('overlay')
@overlayDecorationsById[decoration.id] = decoration
else
delete @overlayDecorationsById[decoration.id]
didDestroyDecoration: (decoration) ->
{marker} = decoration
return unless decorations = @decorationsByMarkerId[marker.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @decorationsById[decoration.id]
@emitter.emit 'did-remove-decoration', decoration
delete @decorationsByMarkerId[marker.id] if decorations.length is 0
delete @overlayDecorationsById[decoration.id]
@unobserveDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
didDestroyLayerDecoration: (decoration) ->
{markerLayer} = decoration
return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
@unobserveDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
observeDecoratedLayer: (layer) ->
@decorationCountsByLayerId[layer.id] ?= 0
if ++@decorationCountsByLayerId[layer.id] is 1
@layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
unobserveDecoratedLayer: (layer) ->
if --@decorationCountsByLayerId[layer.id] is 0
@layerUpdateDisposablesByLayerId[layer.id].dispose()
delete @decorationCountsByLayerId[layer.id]
delete @layerUpdateDisposablesByLayerId[layer.id]

View File

@@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
decorationParams.gutterName = 'line-number'
decorationParams
# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is
# Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
# basically a visual representation of a marker. It allows you to add CSS
# classes to line numbers in the gutter, lines, and add selection-line regions
# around marked ranges of text.
@@ -25,7 +25,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
# ```
#
# Best practice for destroying the decoration is by destroying the {TextEditorMarker}.
# Best practice for destroying the decoration is by destroying the {DisplayMarker}.
#
# ```coffee
# marker.destroy()
@@ -62,7 +62,7 @@ class Decoration
Section: Construction and Destruction
###
constructor: (@marker, @displayBuffer, properties) ->
constructor: (@marker, @decorationManager, properties) ->
@emitter = new Emitter
@id = nextId()
@setProperties properties
@@ -71,14 +71,14 @@ class Decoration
# Essential: Destroy this marker.
#
# If you own the marker, you should use {TextEditorMarker::destroy} which will destroy
# If you own the marker, you should use {DisplayMarker::destroy} which will destroy
# this decoration.
destroy: ->
return if @destroyed
@markerDestroyDisposable.dispose()
@markerDestroyDisposable = null
@destroyed = true
@displayBuffer.didDestroyDecoration(this)
@decorationManager.didDestroyDecoration(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
@@ -149,8 +149,8 @@ class Decoration
oldProperties = @properties
@properties = translateDecorationParamsOldToNew(newProperties)
if newProperties.type?
@displayBuffer.decorationDidChangeType(this)
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.decorationDidChangeType(this)
@decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
###
@@ -175,5 +175,5 @@ class Decoration
@properties.flashCount++
@properties.flashClass = klass
@properties.flashDuration = duration
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-flash'

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +0,0 @@
{Point, Range} = require 'text-buffer'
# Represents a fold that collapses multiple buffer lines into a single
# line on the screen.
#
# Their creation is managed by the {DisplayBuffer}.
module.exports =
class Fold
id: null
displayBuffer: null
marker: null
constructor: (@displayBuffer, @marker) ->
@id = @marker.id
@displayBuffer.foldsByMarkerId[@marker.id] = this
@marker.onDidDestroy => @destroyed()
@marker.onDidChange ({isValid}) => @destroy() unless isValid
# Returns whether this fold is contained within another fold
isInsideLargerFold: ->
largestContainingFoldMarker = @displayBuffer.findFoldMarker(containsRange: @getBufferRange())
largestContainingFoldMarker and
not largestContainingFoldMarker.getRange().isEqual(@getBufferRange())
# Destroys this fold
destroy: ->
@marker.destroy()
# Returns the fold's {Range} in buffer coordinates
#
# includeNewline - A {Boolean} which, if `true`, includes the trailing newline
#
# Returns a {Range}.
getBufferRange: ({includeNewline}={}) ->
range = @marker.getRange()
if range.end.row > range.start.row and nextFold = @displayBuffer.largestFoldStartingAtBufferRow(range.end.row)
nextRange = nextFold.getBufferRange()
range = new Range(range.start, nextRange.end)
if includeNewline
range = range.copy()
range.end.row++
range.end.column = 0
range
getBufferRowRange: ->
{start, end} = @getBufferRange()
[start.row, end.row]
# Returns the fold's start row as a {Number}.
getStartRow: ->
@getBufferRange().start.row
# Returns the fold's end row as a {Number}.
getEndRow: ->
@getBufferRange().end.row
# Returns a {String} representation of the fold.
inspect: ->
"Fold(#{@getStartRow()}, #{@getEndRow()})"
# Retrieves the number of buffer rows spanned by the fold.
#
# Returns a {Number}.
getBufferRowCount: ->
@getEndRow() - @getStartRow() + 1
# Identifies if a fold is nested within a fold.
#
# fold - A {Fold} to check
#
# Returns a {Boolean}.
isContainedByFold: (fold) ->
@isContainedByRange(fold.getBufferRange())
updateDisplayBuffer: ->
unless @isInsideLargerFold()
@displayBuffer.updateScreenLines(@getStartRow(), @getEndRow() + 1, 0, updateMarkers: true)
destroyed: ->
delete @displayBuffer.foldsByMarkerId[@marker.id]
@updateDisplayBuffer()

View File

@@ -1,20 +1,7 @@
'use babel'
import fs from 'fs-plus'
import path from 'path'
import Git from 'nodegit'
import ResourcePool from './resource-pool'
import {Emitter, CompositeDisposable, Disposable} from 'event-kit'
const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE
const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW
const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED
const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE
const ignoredStatusFlags = 1 << 14 // TODO: compose this from libgit2 constants
const submoduleMode = 57344 // TODO: compose this from libgit2 constants
// Just using this for _.isEqual and _.object, we should impl our own here
import _ from 'underscore-plus'
import {Repository} from 'ohnogit'
import {CompositeDisposable, Disposable} from 'event-kit'
// For the most part, this class behaves the same as `GitRepository`, with a few
// notable differences:
@@ -29,39 +16,19 @@ export default class GitRepositoryAsync {
}
static get Git () {
return Git
return Repository.Git
}
// The name of the error thrown when an action is attempted on a destroyed
// repository.
static get DestroyedErrorName () {
return 'GitRepositoryAsync.destroyed'
return Repository.DestroyedErrorName
}
constructor (_path, options = {}) {
// We'll serialize our access manually.
Git.setThreadSafetyStatus(Git.THREAD_SAFETY.DISABLED)
this.repo = Repository.open(_path, options)
this.emitter = new Emitter()
this.subscriptions = new CompositeDisposable()
this.pathStatusCache = {}
this.path = null
// NB: These needs to happen before the following .openRepository call.
this.openedPath = _path
this._openExactPath = options.openExactPath || false
this.repoPromise = this.openRepository()
// NB: We don't currently _use_ the pooled object. But by giving it one
// thing, we're really just serializing all the work. Down the road, we
// could open multiple connections to the repository.
this.repoPool = new ResourcePool([this.repoPromise])
this.isCaseInsensitive = fs.isCaseInsensitive()
this.upstream = {}
this.submodules = {}
this._refreshingPromise = Promise.resolve()
let {refreshOnWindowFocus = true} = options
if (refreshOnWindowFocus) {
@@ -78,23 +45,26 @@ export default class GitRepositoryAsync {
}
}
// This exists to provide backwards compatibility.
get _refreshingPromise () {
return this.repo._refreshingPromise
}
get openedPath () {
return this.repo.openedPath
}
// Public: Destroy this {GitRepositoryAsync} object.
//
// This destroys any tasks and subscriptions and releases the underlying
// libgit2 repository handle. This method is idempotent.
destroy () {
if (this.emitter) {
this.emitter.emit('did-destroy')
this.emitter.dispose()
this.emitter = null
}
this.repo.destroy()
if (this.subscriptions) {
this.subscriptions.dispose()
this.subscriptions = null
}
this.repoPromise = null
}
// Event subscription
@@ -107,7 +77,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy (callback) {
return this.emitter.on('did-destroy', callback)
return this.repo.onDidDestroy(callback)
}
// Public: Invoke the given callback when a specific file's status has
@@ -122,7 +92,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatus (callback) {
return this.emitter.on('did-change-status', callback)
return this.repo.onDidChangeStatus(callback)
}
// Public: Invoke the given callback when a multiple files' statuses have
@@ -134,7 +104,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatuses (callback) {
return this.emitter.on('did-change-statuses', callback)
return this.repo.onDidChangeStatuses(callback)
}
// Repository details
@@ -151,25 +121,13 @@ export default class GitRepositoryAsync {
// Public: Returns a {Promise} which resolves to the {String} path of the
// repository.
getPath () {
return this.getRepo().then(repo => {
if (!this.path) {
this.path = repo.path().replace(/\/$/, '')
}
return this.path
})
return this.repo.getPath()
}
// Public: Returns a {Promise} which resolves to the {String} working
// directory path of the repository.
getWorkingDirectory (_path) {
return this.getRepo(_path).then(repo => {
if (!repo.cachedWorkdir) {
repo.cachedWorkdir = repo.workdir()
}
return repo.cachedWorkdir
})
return this.repo.getWorkingDirectory()
}
// Public: Returns a {Promise} that resolves to true if at the root, false if
@@ -191,8 +149,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to the relative {String} path.
relativizeToWorkingDirectory (_path) {
return this.getWorkingDirectory()
.then(wd => this.relativize(_path, wd))
return this.repo.relativizeToWorkingDirectory(_path)
}
// Public: Makes a path relative to the repository's working directory.
@@ -202,78 +159,13 @@ export default class GitRepositoryAsync {
//
// Returns the relative {String} path.
relativize (_path, workingDirectory) {
// The original implementation also handled null workingDirectory as it
// pulled it from a sync function that could return null. We require it
// to be passed here.
let openedWorkingDirectory
if (!_path || !workingDirectory) {
return _path
}
// If the opened directory and the workdir differ, this is a symlinked repo
// root, so we have to do all the checks below twice--once against the realpath
// and one against the opened path
const opened = this.openedPath.replace(/\/\.git$/, '')
if (path.relative(opened, workingDirectory) !== '') {
openedWorkingDirectory = opened
}
if (process.platform === 'win32') {
_path = _path.replace(/\\/g, '/')
} else {
if (_path[0] !== '/') {
return _path
}
}
workingDirectory = workingDirectory.replace(/\/$/, '')
// Depending on where the paths come from, they may have a '/private/'
// prefix. Standardize by stripping that out.
_path = _path.replace(/^\/private\//i, '/')
workingDirectory = workingDirectory.replace(/^\/private\//i, '/')
const originalPath = _path
const originalWorkingDirectory = workingDirectory
if (this.isCaseInsensitive) {
_path = _path.toLowerCase()
workingDirectory = workingDirectory.toLowerCase()
}
if (_path.indexOf(workingDirectory) === 0) {
return originalPath.substring(originalWorkingDirectory.length + 1)
} else if (_path === workingDirectory) {
return ''
}
if (openedWorkingDirectory) {
openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '')
openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//i, '/')
const originalOpenedWorkingDirectory = openedWorkingDirectory
if (this.isCaseInsensitive) {
openedWorkingDirectory = openedWorkingDirectory.toLowerCase()
}
if (_path.indexOf(openedWorkingDirectory) === 0) {
return originalPath.substring(originalOpenedWorkingDirectory.length + 1)
} else if (_path === openedWorkingDirectory) {
return ''
}
}
return _path
return this.repo.relativize(_path, workingDirectory)
}
// Public: Returns a {Promise} which resolves to whether the given branch
// exists.
hasBranch (branch) {
return this.repoPool.enqueue(() => {
return this.getRepo()
.then(repo => repo.getBranch(branch))
.then(branch => branch != null)
.catch(_ => false)
})
return this.repo.hasBranch(branch)
}
// Public: Retrieves a shortened version of the HEAD reference value.
@@ -287,11 +179,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to a {String}.
getShortHead (_path) {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => repo.getCurrentBranch())
.then(branch => branch.shorthand())
})
return this.repo.getShortHead(_path)
}
// Public: Is the given path a submodule in the repository?
@@ -301,19 +189,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} that resolves true if the given path is a submodule in
// the repository.
isSubmodule (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => {
return this.repoPool.enqueue(() => {
return this.getRepo()
.then(repo => repo.index())
.then(index => {
const entry = index.getByPath(relativePath)
if (!entry) return false
return entry.mode === submoduleMode
})
})
})
return this.repo.isSubmodule(_path)
}
// Public: Returns the number of commits behind the current branch is from the
@@ -327,18 +203,7 @@ export default class GitRepositoryAsync {
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getAheadBehindCount (reference, _path) {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => Promise.all([repo, repo.getBranch(reference)]))
.then(([repo, local]) => {
const upstream = Git.Branch.upstream(local)
return Promise.all([repo, local, upstream])
})
.then(([repo, local, upstream]) => {
return Git.Graph.aheadBehind(repo, local.target(), upstream.target())
})
.catch(_ => ({ahead: 0, behind: 0}))
})
return this.repo.getAheadBehindCount(reference, _path)
}
// Public: Get the cached ahead/behind commit counts for the current branch's
@@ -351,15 +216,7 @@ export default class GitRepositoryAsync {
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getCachedUpstreamAheadBehindCount (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this._submoduleForPath(_path))
.then(submodule => {
if (submodule) {
return submodule.getCachedUpstreamAheadBehindCount(_path)
} else {
return this.upstream
}
})
return this.repo.getCachedUpstreamAheadBehindCount(_path)
}
// Public: Returns the git configuration value specified by the key.
@@ -370,12 +227,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the {String} git configuration value
// specified by the key.
getConfigValue (key, _path) {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => repo.configSnapshot())
.then(config => config.getStringBuf(key))
.catch(_ => null)
})
return this.repo.getConfigValue(key, _path)
}
// Public: Get the URL for the 'origin' remote.
@@ -386,7 +238,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the {String} origin url of the
// repository.
getOriginURL (_path) {
return this.getConfigValue('remote.origin.url', _path)
return this.repo.getOriginURL(_path)
}
// Public: Returns the upstream branch for the current HEAD, or null if there
@@ -398,11 +250,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {String} branch name such as
// `refs/remotes/origin/master`.
getUpstreamBranch (_path) {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => repo.getCurrentBranch())
.then(branch => Git.Branch.upstream(branch))
})
return this.repo.getUpstreamBranch(_path)
}
// Public: Gets all the local and remote references.
@@ -415,25 +263,7 @@ export default class GitRepositoryAsync {
// * `remotes` An {Array} of remote reference names.
// * `tags` An {Array} of tag reference names.
getReferences (_path) {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL))
.then(refs => {
const heads = []
const remotes = []
const tags = []
for (const ref of refs) {
if (ref.isTag()) {
tags.push(ref.name())
} else if (ref.isRemote()) {
remotes.push(ref.name())
} else if (ref.isBranch()) {
heads.push(ref.name())
}
}
return {heads, remotes, tags}
})
})
return this.repo.getReferences(_path)
}
// Public: Get the SHA for the given reference.
@@ -445,11 +275,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the current {String} SHA for the
// given reference.
getReferenceTarget (reference, _path) {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => Git.Reference.nameToId(repo, reference))
.then(oid => oid.tostrS())
})
return this.repo.getReferenceTarget(reference, _path)
}
// Reading Status
@@ -462,9 +288,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is modified.
isPathModified (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this._getStatus([relativePath]))
.then(statuses => statuses.some(status => status.isModified()))
return this.repo.isPathModified(_path)
}
// Public: Resolves true if the given path is new.
@@ -474,9 +298,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is new.
isPathNew (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this._getStatus([relativePath]))
.then(statuses => statuses.some(status => status.isNew()))
return this.repo.isPathNew(_path)
}
// Public: Is the given path ignored?
@@ -486,17 +308,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is ignored.
isPathIgnored (_path) {
return this.getWorkingDirectory()
.then(wd => {
return this.repoPool.enqueue(() => {
return this.getRepo()
.then(repo => {
const relativePath = this.relativize(_path, wd)
return Git.Ignore.pathIsIgnored(repo, relativePath)
})
.then(ignored => Boolean(ignored))
})
})
return this.repo.isPathIgnored(_path)
}
// Get the status of a directory in the repository's working directory.
@@ -507,18 +319,7 @@ export default class GitRepositoryAsync {
// value can be passed to {::isStatusModified} or {::isStatusNew} to get more
// information.
getDirectoryStatus (directoryPath) {
return this.relativizeToWorkingDirectory(directoryPath)
.then(relativePath => {
const pathspec = relativePath + '/**'
return this._getStatus([pathspec])
})
.then(statuses => {
return Promise.all(statuses.map(s => s.statusBit())).then(bits => {
return bits
.filter(b => b > 0)
.reduce((status, bit) => status | bit, 0)
})
})
return this.repo.getDirectoryStatus(directoryPath)
}
// Refresh the status bit for the given path.
@@ -531,27 +332,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Number} which is the refreshed
// status bit for the path.
refreshStatusForPath (_path) {
let relativePath
return this.getWorkingDirectory()
.then(wd => {
relativePath = this.relativize(_path, wd)
return this._getStatus([relativePath])
})
.then(statuses => {
const cachedStatus = this.pathStatusCache[relativePath] || 0
const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT
if (status !== cachedStatus) {
if (status === Git.Status.STATUS.CURRENT) {
delete this.pathStatusCache[relativePath]
} else {
this.pathStatusCache[relativePath] = status
}
this.emitter.emit('did-change-status', {path: _path, pathStatus: status})
}
return status
})
return this.repo.refreshStatusForPath(_path)
}
// Returns a Promise that resolves to the status bit of a given path if it has
@@ -567,8 +348,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a status {Number} or null if the
// path is not in the cache.
getCachedPathStatus (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this.pathStatusCache[relativePath])
return this.repo.getCachedPathStatus(_path)
}
// Public: Get the cached statuses for the repository.
@@ -576,7 +356,7 @@ export default class GitRepositoryAsync {
// Returns an {Object} of {Number} statuses, keyed by {String} working
// directory-relative file names.
getCachedPathStatuses () {
return this.pathStatusCache
return this.repo.pathStatusCache
}
// Public: Returns true if the given status indicates modification.
@@ -585,7 +365,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean} that's true if the `statusBit` indicates modification.
isStatusModified (statusBit) {
return (statusBit & modifiedStatusFlags) > 0
return this.repo.isStatusModified(statusBit)
}
// Public: Returns true if the given status indicates a new path.
@@ -594,7 +374,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean} that's true if the `statusBit` indicates a new path.
isStatusNew (statusBit) {
return (statusBit & newStatusFlags) > 0
return this.repo.isStatusNew(statusBit)
}
// Public: Returns true if the given status indicates the path is staged.
@@ -604,7 +384,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// staged.
isStatusStaged (statusBit) {
return (statusBit & indexStatusFlags) > 0
return this.repo.isStatusStaged(statusBit)
}
// Public: Returns true if the given status indicates the path is ignored.
@@ -614,7 +394,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// ignored.
isStatusIgnored (statusBit) {
return (statusBit & ignoredStatusFlags) > 0
return this.repo.isStatusIgnored(statusBit)
}
// Public: Returns true if the given status indicates the path is deleted.
@@ -624,7 +404,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// deleted.
isStatusDeleted (statusBit) {
return (statusBit & deletedStatusFlags) > 0
return this.repo.isStatusDeleted(statusBit)
}
// Retrieving Diffs
@@ -640,40 +420,7 @@ export default class GitRepositoryAsync {
// * `added` The {Number} of added lines.
// * `deleted` The {Number} of deleted lines.
getDiffStats (_path) {
return this.getWorkingDirectory(_path)
.then(wd => {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => Promise.all([repo, repo.getHeadCommit()]))
.then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()]))
.then(([repo, tree]) => {
const options = new Git.DiffOptions()
options.contextLines = 0
options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH
options.pathspec = this.relativize(_path, wd)
if (process.platform === 'win32') {
// Ignore eol of line differences on windows so that files checked in
// as LF don't report every line modified when the text contains CRLF
// endings.
options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
}
return Git.Diff.treeToWorkdir(repo, tree, options)
})
.then(diff => this._getDiffLines(diff))
.then(lines => {
const stats = {added: 0, deleted: 0}
for (const line of lines) {
const origin = line.origin()
if (origin === Git.Diff.LINE.ADDITION) {
stats.added++
} else if (origin === Git.Diff.LINE.DELETION) {
stats.deleted++
}
}
return stats
})
})
})
return this.repo.getDiffStats(_path)
}
// Public: Retrieves the line diffs comparing the `HEAD` version of the given
@@ -688,30 +435,7 @@ export default class GitRepositoryAsync {
// * `oldLines` The {Number} of lines in the old hunk.
// * `newLines` The {Number} of lines in the new hunk
getLineDiffs (_path, text) {
return this.getWorkingDirectory(_path)
.then(wd => {
let relativePath = null
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => {
relativePath = this.relativize(_path, wd)
return repo.getHeadCommit()
})
.then(commit => commit.getEntry(relativePath))
.then(entry => entry.getBlob())
.then(blob => {
const options = new Git.DiffOptions()
options.contextLines = 0
if (process.platform === 'win32') {
// Ignore eol of line differences on windows so that files checked in
// as LF don't report every line modified when the text contains CRLF
// endings.
options.flags = Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
}
return this._diffBlobToBuffer(blob, text, options)
})
})
})
return this.repo.getLineDiffs(_path, text)
}
// Checking Out
@@ -732,19 +456,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} that resolves or rejects depending on whether the
// method was successful.
checkoutHead (_path) {
return this.getWorkingDirectory(_path)
.then(wd => {
return this.repoPool.enqueue(() => {
return this.getRepo(_path)
.then(repo => {
const checkoutOptions = new Git.CheckoutOptions()
checkoutOptions.paths = [this.relativize(_path, wd)]
checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH
return Git.Checkout.head(repo, checkoutOptions)
})
})
})
.then(() => this.refreshStatusForPath(_path))
return this.repo.checkoutHead(_path)
}
// Public: Checks out a branch in your repository.
@@ -755,19 +467,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} that resolves if the method was successful.
checkoutReference (reference, create) {
return this.repoPool.enqueue(() => {
return this.getRepo()
.then(repo => repo.checkoutBranch(reference))
})
.catch(error => {
if (create) {
return this._createBranch(reference)
.then(_ => this.checkoutReference(reference, false))
} else {
throw error
}
})
.then(_ => null)
return this.repo.checkoutReference(reference, create)
}
// Private
@@ -786,107 +486,10 @@ export default class GitRepositoryAsync {
return this.checkoutHead(filePath)
}
// Create a new branch with the given name.
// Refreshes the git status.
//
// * `name` The {String} name of the new branch.
//
// Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the
// created branch.
_createBranch (name) {
return this.repoPool.enqueue(() => {
return this.getRepo()
.then(repo => Promise.all([repo, repo.getHeadCommit()]))
.then(([repo, commit]) => repo.createBranch(name, commit))
})
}
// Get all the hunks in the diff.
//
// * `diff` The {NodeGit.Diff} whose hunks should be retrieved.
//
// Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}.
_getDiffHunks (diff) {
return diff.patches()
.then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array<Patch>
.then(hunks => _.flatten(hunks)) // hunks :: Array<Array<Hunk>>
}
// Get all the lines contained in the diff.
//
// * `diff` The {NodeGit.Diff} use lines should be retrieved.
//
// Returns a {Promise} which resolves to an {Array} of {NodeGit.Line}.
_getDiffLines (diff) {
return this._getDiffHunks(diff)
.then(hunks => Promise.all(hunks.map(h => h.lines())))
.then(lines => _.flatten(lines)) // lines :: Array<Array<Line>>
}
// Diff the given blob and buffer with the provided options.
//
// * `blob` The {NodeGit.Blob}
// * `buffer` The {String} buffer.
// * `options` The {NodeGit.DiffOptions}
//
// Returns a {Promise} which resolves to an {Array} of {Object}s which have
// the following keys:
// * `oldStart` The {Number} of the old starting line.
// * `newStart` The {Number} of the new starting line.
// * `oldLines` The {Number} of old lines.
// * `newLines` The {Number} of new lines.
_diffBlobToBuffer (blob, buffer, options) {
const hunks = []
const hunkCallback = (delta, hunk, payload) => {
hunks.push({
oldStart: hunk.oldStart(),
newStart: hunk.newStart(),
oldLines: hunk.oldLines(),
newLines: hunk.newLines()
})
}
return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null)
.then(_ => hunks)
}
// Get the current branch and update this.branch.
//
// Returns a {Promise} which resolves to a {boolean} indicating whether the
// branch name changed.
_refreshBranch () {
return this.repoPool.enqueue(() => {
return this.getRepo()
.then(repo => repo.getCurrentBranch())
.then(ref => ref.name())
.then(branchName => {
const changed = branchName !== this.branch
this.branch = branchName
return changed
})
})
}
// Refresh the cached ahead/behind count with the given branch.
//
// * `branchName` The {String} name of the branch whose ahead/behind should be
// used for the refresh.
//
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// the ahead/behind count changed.
_refreshAheadBehindCount (branchName) {
return this.getAheadBehindCount(branchName)
.then(counts => {
const changed = !_.isEqual(counts, this.upstream)
this.upstream = counts
return changed
})
}
// Get the status for this repository.
//
// Returns a {Promise} that will resolve to an object of {String} paths to the
// {Number} status.
_getRepositoryStatus () {
// Returns a {Promise} which will resolve to {null} when refresh is complete.
refreshStatus () {
let projectPathsPromises = [Promise.resolve('')]
if (this.project) {
projectPathsPromises = this.project.getPaths()
@@ -895,163 +498,7 @@ export default class GitRepositoryAsync {
return Promise.all(projectPathsPromises)
.then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*'))
.then(projectPaths => {
return this._getStatus(projectPaths.length > 0 ? projectPaths : null)
})
.then(statuses => {
const statusPairs = statuses.map(status => [status.path(), status.statusBit()])
return _.object(statusPairs)
})
}
// Get the status for the given submodule.
//
// * `submodule` The {GitRepositoryAsync} for the submodule.
//
// Returns a {Promise} which resolves to an {Object}, keyed by {String}
// repo-relative {Number} statuses.
async _getSubmoduleStatus (submodule) {
// At this point, we've called submodule._refreshSubmodules(), which would
// have refreshed the status on *its* submodules, etc. So we know that its
// cached path statuses are up-to-date.
//
// Now we just need to hoist those statuses into our repository by changing
// their paths to be relative to us.
const statuses = submodule.getCachedPathStatuses()
const repoRelativeStatuses = {}
const submoduleRepo = await submodule.getRepo()
const submoduleWorkDir = submoduleRepo.workdir()
for (const relativePath in statuses) {
const statusBit = statuses[relativePath]
const absolutePath = path.join(submoduleWorkDir, relativePath)
const repoRelativePath = await this.relativizeToWorkingDirectory(absolutePath)
repoRelativeStatuses[repoRelativePath] = statusBit
}
return repoRelativeStatuses
}
// Refresh the list of submodules in the repository.
//
// Returns a {Promise} which resolves to an {Object} keyed by {String}
// submodule names with {GitRepositoryAsync} values.
async _refreshSubmodules () {
const repo = await this.getRepo()
const wd = await this.getWorkingDirectory()
const submoduleNames = await repo.getSubmoduleNames()
for (const name of submoduleNames) {
const alreadyExists = Boolean(this.submodules[name])
if (alreadyExists) continue
const submodule = await Git.Submodule.lookup(repo, name)
const absolutePath = path.join(wd, submodule.path())
const submoduleRepo = GitRepositoryAsync.open(absolutePath, {openExactPath: true, refreshOnWindowFocus: false})
this.submodules[name] = submoduleRepo
}
for (const name in this.submodules) {
const repo = this.submodules[name]
const gone = submoduleNames.indexOf(name) < 0
if (gone) {
repo.destroy()
delete this.submodules[name]
} else {
try {
await repo.refreshStatus()
} catch (e) {
// libgit2 will sometimes report submodules that aren't actually valid
// (https://github.com/libgit2/libgit2/issues/3580). So check the
// validity of the submodules by removing any that fail.
repo.destroy()
delete this.submodules[name]
}
}
}
return _.values(this.submodules)
}
// Get the status for the submodules in the repository.
//
// Returns a {Promise} that will resolve to an object of {String} paths to the
// {Number} status.
_getSubmoduleStatuses () {
return this._refreshSubmodules()
.then(repos => {
return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo)))
})
.then(statuses => _.extend({}, ...statuses))
}
// Refresh the cached status.
//
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// any statuses changed.
_refreshStatus () {
return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()])
.then(([repositoryStatus, submoduleStatus]) => {
const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus)
const changed = !_.isEqual(this.pathStatusCache, statusesByPath)
this.pathStatusCache = statusesByPath
return changed
})
}
// Refreshes the git status.
//
// Returns a {Promise} which will resolve to {null} when refresh is complete.
refreshStatus () {
const status = this._refreshStatus()
const branch = this._refreshBranch()
const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch))
this._refreshingPromise = this._refreshingPromise.then(_ => {
return Promise.all([status, branch, aheadBehind])
.then(([statusChanged, branchChanged, aheadBehindChanged]) => {
if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) {
this.emitter.emit('did-change-statuses')
}
return null
})
// Because all these refresh steps happen asynchronously, it's entirely
// possible the repository was destroyed while we were working. In which
// case we should just swallow the error.
.catch(e => {
if (this._isDestroyed()) {
return null
} else {
return Promise.reject(e)
}
})
.catch(e => {
console.error('Error refreshing repository status:')
console.error(e)
return Promise.reject(e)
})
})
return this._refreshingPromise
}
// Get the submodule for the given path.
//
// Returns a {Promise} which resolves to the {GitRepositoryAsync} submodule or
// null if it isn't a submodule path.
async _submoduleForPath (_path) {
let relativePath = await this.relativizeToWorkingDirectory(_path)
for (const submodulePath in this.submodules) {
const submoduleRepo = this.submodules[submodulePath]
if (relativePath === submodulePath) {
return submoduleRepo
} else if (relativePath.indexOf(`${submodulePath}/`) === 0) {
relativePath = relativePath.substring(submodulePath.length + 1)
const innerSubmodule = await submoduleRepo._submoduleForPath(relativePath)
return innerSubmodule || submoduleRepo
}
}
return null
.then(pathspecs => this.repo.refreshStatus(pathspecs))
}
// Get the NodeGit repository for the given path.
@@ -1062,16 +509,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to the {NodeGit.Repository}.
getRepo (_path) {
if (this._isDestroyed()) {
const error = new Error('Repository has been destroyed')
error.name = GitRepositoryAsync.DestroyedErrorName
return Promise.reject(error)
}
if (!_path) return this.repoPromise
return this._submoduleForPath(_path)
.then(submodule => submodule ? submodule.getRepo() : this.repoPromise)
return this.repo.getRepo(_path)
}
// Open a new instance of the underlying {NodeGit.Repository}.
@@ -1081,11 +519,7 @@ export default class GitRepositoryAsync {
//
// Returns the new {NodeGit.Repository}.
openRepository () {
if (this._openExactPath) {
return Git.Repository.open(this.openedPath)
} else {
return Git.Repository.openExt(this.openedPath, 0, '')
}
return this.repo.openRepository()
}
// Section: Private
@@ -1095,7 +529,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean}.
_isDestroyed () {
return this.repoPromise == null
return this.repo._isDestroyed()
}
// Subscribe to events on the given buffer.
@@ -1121,28 +555,4 @@ export default class GitRepositoryAsync {
this.subscriptions.add(bufferSubscriptions)
}
// Get the status for the given paths.
//
// * `paths` The {String} paths whose status is wanted. If undefined, get the
// status for the whole repository.
//
// Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile}
// statuses for the paths.
_getStatus (paths) {
return this.repoPool.enqueue(() => {
return this.getRepo()
.then(repo => {
const opts = {
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS
}
if (paths) {
opts.pathspec = paths
}
return repo.getStatusExt(opts)
})
})
}
}

View File

@@ -166,12 +166,7 @@ class GitRepository
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatuses: (callback) ->
@async.onDidChangeStatuses ->
# Defer the callback to the next tick so that we've reset
# `@statusesByPath` by the time it's called. Otherwise reads from within
# the callback could be inconsistent.
# See https://github.com/atom/atom/issues/11396
process.nextTick callback
@emitter.on 'did-change-statuses', callback
###
Section: Repository Details
@@ -496,9 +491,27 @@ class GitRepository
#
# Returns a promise that resolves when the repository has been refreshed.
refreshStatus: ->
statusesChanged = false
# Listen for `did-change-statuses` so we know if something changed. But we
# need to wait to propagate it until after we've set the branch and cleared
# the `statusesByPath` cache. So just set a flag, and we'll emit the event
# after refresh is done.
subscription = @async.onDidChangeStatuses ->
subscription?.dispose()
subscription = null
statusesChanged = true
asyncRefresh = @async.refreshStatus().then =>
@statusesByPath = {}
subscription?.dispose()
subscription = null
@branch = @async?.branch
@statusesByPath = {}
if statusesChanged
@emitter.emit 'did-change-statuses'
syncRefresh = new Promise (resolve, reject) =>
@handlerPath ?= require.resolve('./repository-status-handler')

View File

@@ -71,13 +71,13 @@ class Gutter
isVisible: ->
@visible
# Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves,
# Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
#
# ## Arguments
#
# * `marker` A {TextEditorMarker} you want this decoration to follow.
# * `marker` A {DisplayMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration. It is passed
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
# all options documented there.

View File

@@ -90,30 +90,36 @@ class LanguageMode
# Folds all the foldable lines in the buffer.
foldAll: ->
@unfoldAll()
foldedRowRanges = {}
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
@editor.createFold(startRow, endRow)
continue if foldedRowRanges[rowRange]
@editor.foldBufferRowRange(startRow, endRow)
foldedRowRanges[rowRange] = true
return
# Unfolds all the foldable lines in the buffer.
unfoldAll: ->
for fold in @editor.displayBuffer.foldsIntersectingBufferRowRange(0, @buffer.getLastRow()) by -1
fold.destroy()
return
@editor.displayLayer.destroyAllFolds()
# Fold all comment and code blocks at a given indentLevel
#
# indentLevel - A {Number} indicating indentLevel; 0 based.
foldAllAtIndentLevel: (indentLevel) ->
@unfoldAll()
foldedRowRanges = {}
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
continue if foldedRowRanges[rowRange]
# assumption: startRow will always be the min indent level for the entire range
if @editor.indentationForBufferRow(startRow) is indentLevel
@editor.createFold(startRow, endRow)
@editor.foldBufferRowRange(startRow, endRow)
foldedRowRanges[rowRange] = true
return
# Given a buffer row, creates a fold at it.
@@ -125,8 +131,8 @@ class LanguageMode
for currentRow in [bufferRow..0] by -1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow)
return @editor.createFold(startRow, endRow) unless fold
unless @editor.isFoldedAtBufferRow(startRow)
return @editor.foldBufferRowRange(startRow, endRow)
# Find the row range for a fold at a given bufferRow. Will handle comments
# and code.
@@ -140,19 +146,19 @@ class LanguageMode
rowRange
rowRangeForCommentAtBufferRow: (bufferRow) ->
return unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
return unless @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
startRow = bufferRow
endRow = bufferRow
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
return [startRow, endRow] if startRow isnt endRow
@@ -175,13 +181,13 @@ class LanguageMode
[bufferRow, foldEndRow]
isFoldableAtBufferRow: (bufferRow) ->
@editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow)
@editor.tokenizedBuffer.isFoldableAtRow(bufferRow)
# Returns a {Boolean} indicating whether the line at the given buffer
# row is a comment.
isLineCommentedAtBufferRow: (bufferRow) ->
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
@editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
@editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
# Find a row range for a 'paragraph' around specified bufferRow. A paragraph
# is a block of text bounded by and empty line or a block of text that is not
@@ -234,11 +240,11 @@ class LanguageMode
# Returns a {Number}.
suggestedIndentForBufferRow: (bufferRow, options) ->
line = @buffer.lineForRow(bufferRow)
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)
tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->

View File

@@ -7,7 +7,7 @@ nextId = -> idCounter++
# layer. Created via {TextEditor::decorateMarkerLayer}.
module.exports =
class LayerDecoration
constructor: (@markerLayer, @displayBuffer, @properties) ->
constructor: (@markerLayer, @decorationManager, @properties) ->
@id = nextId()
@destroyed = false
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
@@ -19,7 +19,7 @@ class LayerDecoration
@markerLayerDestroyedDisposable.dispose()
@markerLayerDestroyedDisposable = null
@destroyed = true
@displayBuffer.didDestroyLayerDecoration(this)
@decorationManager.didDestroyLayerDecoration(this)
# Essential: Determine whether this decoration is destroyed.
#
@@ -44,11 +44,11 @@ class LayerDecoration
setProperties: (newProperties) ->
return if @destroyed
@properties = newProperties
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()
# Essential: Override the decoration properties for a specific marker.
#
# * `marker` The {TextEditorMarker} or {Marker} for which to override
# * `marker` The {DisplayMarker} or {Marker} for which to override
# properties.
# * `properties` An {Object} containing properties to apply to this marker.
# Pass `null` to clear the override.
@@ -58,4 +58,4 @@ class LayerDecoration
@overridePropertiesByMarkerId[marker.id] = properties
else
delete @overridePropertiesByMarkerId[marker.id]
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()

View File

@@ -93,9 +93,9 @@ class LineNumberGutterComponent extends TiledComponent
{target} = event
lineNumber = target.parentNode
if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
if target.classList.contains('icon-right')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
@editor.unfoldBufferRow(bufferRow)
else
else if lineNumber.classList.contains('foldable')
@editor.foldBufferRow(bufferRow)

View File

@@ -43,7 +43,7 @@ class LinesComponent extends TiledComponent
@domNode
shouldRecreateAllTilesOnUpdate: ->
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible or @newState.continuousReflow
@newState.continuousReflow
beforeUpdateSync: (state) ->
if @newState.maxHeight isnt @oldState.maxHeight
@@ -70,8 +70,6 @@ class LinesComponent extends TiledComponent
@cursorsComponent.updateSync(state)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars})
buildEmptyState: ->
@@ -97,10 +95,14 @@ class LinesComponent extends TiledComponent
@presenter.setLineHeight(lineHeightInPixels)
@presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
lineIdForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.lineNodeForLineId(lineId)
@getComponentForTile(tile)?.lineIdForScreenRow(screenRow)
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
lineNodeForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.textNodesForLineId(lineId)
@getComponentForTile(tile)?.lineNodeForScreenRow(screenRow)
textNodesForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.textNodesForScreenRow(screenRow)

View File

@@ -1,10 +1,10 @@
_ = require 'underscore-plus'
HighlightsComponent = require './highlights-component'
TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
ZERO_WIDTH_NBSP = '\ufeff'
cloneObject = (object) ->
clone = {}
@@ -14,7 +14,6 @@ cloneObject = (object) ->
module.exports =
class LinesTileComponent
constructor: ({@presenter, @id, @domElementPool, @assert, grammars}) ->
@tokenIterator = new TokenIterator(grammarRegistry: grammars)
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@@ -69,13 +68,10 @@ class LinesTileComponent
@oldTileState.top = @newTileState.top
@oldTileState.left = @newTileState.left
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
@highlightsComponent.updateSync(@newTileState)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
removeLineNodes: ->
@removeLineNode(id) for id of @oldTileState.lines
return
@@ -195,8 +191,7 @@ class LinesTileComponent
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineNode: (id) ->
{width} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
{lineText, tagCodes, screenRow, decorationClasses} = @newTileState.lines[id]
lineNode = @domElementPool.buildElement("div", "line")
lineNode.dataset.screenRow = screenRow
@@ -205,185 +200,40 @@ class LinesTileComponent
for decorationClass in decorationClasses
lineNode.classList.add(decorationClass)
@currentLineTextNodes = []
if text is ""
@setEmptyLineInnerNodes(id, lineNode)
else
@setLineInnerNodes(id, lineNode)
@textNodesByLineId[id] = @currentLineTextNodes
lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold
lineNode
setEmptyLineInnerNodes: (id, lineNode) ->
{indentGuidesVisible} = @newState
{indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
for i in [0...indentLevel]
indentGuide = @domElementPool.buildElement("span", "indent-guide")
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
indentGuide.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
else
textNode = @domElementPool.buildText(" ")
indentGuide.appendChild(textNode)
@currentLineTextNodes.push(textNode)
lineNode.appendChild(indentGuide)
while invisibleIndex < endOfLineInvisibles?.length
invisible = endOfLineInvisibles[invisibleIndex++]
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
lineNode.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
else
unless @appendEndOfLineNodes(id, lineNode)
textNode = @domElementPool.buildText("\u00a0")
lineNode.appendChild(textNode)
@currentLineTextNodes.push(textNode)
setLineInnerNodes: (id, lineNode) ->
lineState = @newTileState.lines[id]
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
@tokenIterator.reset(lineState)
textNodes = []
lineLength = 0
startIndex = 0
openScopeNode = lineNode
while @tokenIterator.next()
for scope in @tokenIterator.getScopeEnds()
for tagCode in tagCodes when tagCode isnt 0
if @presenter.isCloseTagCode(tagCode)
openScopeNode = openScopeNode.parentElement
for scope in @tokenIterator.getScopeStarts()
else if @presenter.isOpenTagCode(tagCode)
scope = @presenter.tagForCode(tagCode)
newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' '))
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
tokenStart = @tokenIterator.getScreenStart()
tokenEnd = @tokenIterator.getScreenEnd()
tokenText = @tokenIterator.getText()
isHardTab = @tokenIterator.isHardTab()
if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
else
tokenFirstNonWhitespaceIndex = null
textNode = @domElementPool.buildText(lineText.substr(startIndex, tagCode))
startIndex += tagCode
openScopeNode.appendChild(textNode)
textNodes.push(textNode)
if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
else
tokenFirstTrailingWhitespaceIndex = null
if startIndex is 0
textNode = @domElementPool.buildText(' ')
lineNode.appendChild(textNode)
textNodes.push(textNode)
hasIndentGuide =
@newState.indentGuidesVisible and
(hasLeadingWhitespace or lineIsWhitespaceOnly)
if lineText.endsWith(@presenter.displayLayer.foldCharacter)
# Insert a zero-width non-breaking whitespace, so that
# LinesYardstick can take the fold-marker::after pseudo-element
# into account during measurements when such marker is the last
# character on the line.
textNode = @domElementPool.buildText(ZERO_WIDTH_NBSP)
lineNode.appendChild(textNode)
textNodes.push(textNode)
hasInvisibleCharacters =
(invisibles?.tab and isHardTab) or
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
@appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode)
@appendEndOfLineNodes(id, lineNode)
appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) ->
if isHardTab
textNode = @domElementPool.buildText(tokenText)
hardTabNode = @domElementPool.buildElement("span", "hard-tab")
hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex?
hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex?
hardTabNode.classList.add("indent-guide") if hasIndentGuide
hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters
hardTabNode.appendChild(textNode)
scopeNode.appendChild(hardTabNode)
@currentLineTextNodes.push(textNode)
else
startIndex = 0
endIndex = tokenText.length
leadingWhitespaceNode = null
leadingWhitespaceTextNode = null
trailingWhitespaceNode = null
trailingWhitespaceTextNode = null
if firstNonWhitespaceIndex?
leadingWhitespaceTextNode =
@domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex))
leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace")
leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide
leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
leadingWhitespaceNode.appendChild(leadingWhitespaceTextNode)
startIndex = firstNonWhitespaceIndex
if firstTrailingWhitespaceIndex?
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
trailingWhitespaceTextNode =
@domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex))
trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace")
trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
trailingWhitespaceNode.appendChild(trailingWhitespaceTextNode)
endIndex = firstTrailingWhitespaceIndex
if leadingWhitespaceNode?
scopeNode.appendChild(leadingWhitespaceNode)
@currentLineTextNodes.push(leadingWhitespaceTextNode)
if tokenText.length > MaxTokenLength
while startIndex < endIndex
textNode = @domElementPool.buildText(
@sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
)
textSpan = @domElementPool.buildElement("span")
textSpan.appendChild(textNode)
scopeNode.appendChild(textSpan)
startIndex += MaxTokenLength
@currentLineTextNodes.push(textNode)
else
textNode = @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex))
scopeNode.appendChild(textNode)
@currentLineTextNodes.push(textNode)
if trailingWhitespaceNode?
scopeNode.appendChild(trailingWhitespaceNode)
@currentLineTextNodes.push(trailingWhitespaceTextNode)
sliceText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
tokenText = tokenText.slice(startIndex, endIndex)
tokenText
appendEndOfLineNodes: (id, lineNode) ->
{endOfLineInvisibles} = @newTileState.lines[id]
hasInvisibles = false
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
hasInvisibles = true
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
textNode = @domElementPool.buildText(invisible)
invisibleSpan.appendChild(textNode)
lineNode.appendChild(invisibleSpan)
@currentLineTextNodes.push(textNode)
hasInvisibles
@textNodesByLineId[id] = textNodes
lineNode
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
@@ -436,3 +286,9 @@ class LinesTileComponent
textNodesForLineId: (lineId) ->
@textNodesByLineId[lineId].slice()
lineIdForScreenRow: (screenRow) ->
@lineIdsByScreenRow[screenRow]
textNodesForScreenRow: (screenRow) ->
@textNodesByLineId[@lineIdsByScreenRow[screenRow]]?.slice()

View File

@@ -1,15 +1,14 @@
TokenIterator = require './token-iterator'
{Point} = require 'text-buffer'
{isPairedCharacter} = require './text-utils'
module.exports =
class LinesYardstick
constructor: (@model, @lineNodesProvider, @lineTopIndex, grammarRegistry) ->
@tokenIterator = new TokenIterator({grammarRegistry})
@rangeForMeasurement = document.createRange()
@invalidateCache()
invalidateCache: ->
@pixelPositionsByLineIdAndColumn = {}
@leftPixelPositionCache = {}
measuredRowForPixelPosition: (pixelPosition) ->
targetTop = pixelPosition.top
@@ -21,61 +20,63 @@ class LinesYardstick
targetLeft = pixelPosition.left
defaultCharWidth = @model.getDefaultCharWidth()
row = @lineTopIndex.rowForPixelPosition(targetTop)
targetLeft = 0 if targetTop < 0
targetLeft = 0 if targetTop < 0 or targetLeft < 0
targetLeft = Infinity if row > @model.getLastScreenRow()
row = Math.min(row, @model.getLastScreenRow())
row = Math.max(0, row)
line = @model.tokenizedLineForScreenRow(row)
lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
return Point(row, 0) unless lineNode
return Point(row, 0) unless lineNode? and line?
textNodes = @lineNodesProvider.textNodesForScreenRow(row)
lineOffset = lineNode.getBoundingClientRect().left
targetLeft += lineOffset
textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
column = 0
previousColumn = 0
previousLeft = 0
textNodeIndex = 0
low = 0
high = textNodes.length - 1
while low <= high
mid = low + (high - low >> 1)
textNode = textNodes[mid]
rangeRect = @clientRectForRange(textNode, 0, textNode.length)
if targetLeft < rangeRect.left
high = mid - 1
textNodeIndex = Math.max(0, mid - 1)
else if targetLeft > rangeRect.right
low = mid + 1
textNodeIndex = Math.min(textNodes.length - 1, mid + 1)
else
textNodeIndex = mid
break
@tokenIterator.reset(line, false)
while @tokenIterator.next()
text = @tokenIterator.getText()
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
textNode = textNodes[textNodeIndex]
characterIndex = 0
low = 0
high = textNode.textContent.length - 1
while low <= high
charIndex = low + (high - low >> 1)
if isPairedCharacter(textNode.textContent, charIndex)
nextCharIndex = charIndex + 2
else
nextCharIndex = charIndex + 1
rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex)
if targetLeft < rangeRect.left
high = charIndex - 1
characterIndex = Math.max(0, charIndex - 1)
else if targetLeft > rangeRect.right
low = nextCharIndex
characterIndex = Math.min(textNode.textContent.length, nextCharIndex)
else
if targetLeft <= ((rangeRect.left + rangeRect.right) / 2)
characterIndex = charIndex
else
char = text[textIndex]
charLength = 1
textIndex++
characterIndex = nextCharIndex
break
unless textNode?
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = 0
nextTextNodeIndex = textNodeLength
while nextTextNodeIndex <= column
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNodeLength
indexWithinTextNode = column - textNodeIndex
left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode)
charWidth = left - previousLeft
return Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2)
previousLeft = left
previousColumn = column
column += charLength
if targetLeft <= previousLeft + (charWidth / 2)
Point(row, previousColumn)
else
Point(row, column)
textNodeStartColumn = 0
textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1
Point(row, textNodeStartColumn + characterIndex)
pixelPositionForScreenPosition: (screenPosition) ->
targetRow = screenPosition.row
@@ -87,76 +88,41 @@ class LinesYardstick
{top, left}
leftPixelPositionForScreenPosition: (row, column) ->
line = @model.tokenizedLineForScreenRow(row)
lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
lineId = @lineNodesProvider.lineIdForScreenRow(row)
return 0 unless line? and lineNode?
return 0 unless lineNode?
if cachedPosition = @pixelPositionsByLineIdAndColumn[line.id]?[column]
if cachedPosition = @leftPixelPositionCache[lineId]?[column]
return cachedPosition
textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
indexWithinTextNode = null
charIndex = 0
textNodes = @lineNodesProvider.textNodesForScreenRow(row)
textNodeStartColumn = 0
@tokenIterator.reset(line, false)
while @tokenIterator.next()
break if foundIndexWithinTextNode?
text = @tokenIterator.getText()
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
else
char = text[textIndex]
charLength = 1
textIndex++
unless textNode?
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = 0
nextTextNodeIndex = textNodeLength
while nextTextNodeIndex <= charIndex
textNode = textNodes.shift()
textNodeLength = textNode.textContent.length
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNodeLength
if charIndex is column
foundIndexWithinTextNode = charIndex - textNodeIndex
break
charIndex += charLength
for textNode in textNodes
textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
if textNodeEndColumn > column
indexInTextNode = column - textNodeStartColumn
break
else
textNodeStartColumn = textNodeEndColumn
if textNode?
foundIndexWithinTextNode ?= textNode.textContent.length
position = @leftPixelPositionForCharInTextNode(
lineNode, textNode, foundIndexWithinTextNode
)
@pixelPositionsByLineIdAndColumn[line.id] ?= {}
@pixelPositionsByLineIdAndColumn[line.id][column] = position
position
indexInTextNode ?= textNode.textContent.length
lineOffset = lineNode.getBoundingClientRect().left
if indexInTextNode is 0
leftPixelPosition = @clientRectForRange(textNode, 0, 1).left
else
leftPixelPosition = @clientRectForRange(textNode, 0, indexInTextNode).right
leftPixelPosition -= lineOffset
@leftPixelPositionCache[lineId] ?= {}
@leftPixelPositionCache[lineId][column] = leftPixelPosition
leftPixelPosition
else
0
leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) ->
if charIndex is 0
width = 0
else
@rangeForMeasurement.setStart(textNode, 0)
@rangeForMeasurement.setEnd(textNode, charIndex)
width = @rangeForMeasurement.getBoundingClientRect().width
@rangeForMeasurement.setStart(textNode, 0)
@rangeForMeasurement.setEnd(textNode, textNode.textContent.length)
left = @rangeForMeasurement.getBoundingClientRect().left
offset = lineNode.getBoundingClientRect().left
left + width - offset
clientRectForRange: (textNode, startIndex, endIndex) ->
@rangeForMeasurement.setStart(textNode, startIndex)
@rangeForMeasurement.setEnd(textNode, endIndex)
@rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect()

View File

@@ -1,9 +1,9 @@
module.exports =
class MarkerObservationWindow
constructor: (@displayBuffer, @bufferWindow) ->
constructor: (@decorationManager, @bufferWindow) ->
setScreenRange: (range) ->
@bufferWindow.setRange(@displayBuffer.bufferRangeForScreenRange(range))
@bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range))
setBufferRange: (range) ->
@bufferWindow.setRange(range)

View File

@@ -202,13 +202,13 @@ registerBuiltins = (devMode) ->
atomShellRoot = path.join(process.resourcesPath, 'atom.asar')
commonRoot = path.join(atomShellRoot, 'common', 'api', 'lib')
commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell']
commonRoot = path.join(atomShellRoot, 'common', 'api')
commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'shell']
for builtin in commonBuiltins
cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js")
rendererRoot = path.join(atomShellRoot, 'renderer', 'api', 'lib')
rendererBuiltins = ['ipc-renderer', 'remote']
rendererRoot = path.join(atomShellRoot, 'renderer', 'api')
rendererBuiltins = ['ipc-renderer', 'remote', 'screen']
for builtin in rendererBuiltins
cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js")

View File

@@ -577,15 +577,23 @@ class Pane extends Model
else
return true
chosen = @applicationDelegate.confirm
message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?"
detailedMessage: "Your changes will be lost if you close this item without saving."
buttons: ["Save", "Cancel", "Don't Save"]
saveDialog = (saveButtonText, saveFn, message) =>
chosen = @applicationDelegate.confirm
message: message
detailedMessage: "Your changes will be lost if you close this item without saving."
buttons: [saveButtonText, "Cancel", "Don't save"]
switch chosen
when 0 then saveFn(item, saveError)
when 1 then false
when 2 then true
switch chosen
when 0 then @saveItem(item, -> true)
when 1 then false
when 2 then true
saveError = (error) =>
if error
saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}")
else
true
saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?")
# Public: Save the active item.
saveActiveItem: (nextAction) ->
@@ -602,9 +610,11 @@ class Pane extends Model
# Public: Save the given item.
#
# * `item` The item to save.
# * `nextAction` (optional) {Function} which will be called after the item is
# successfully saved.
saveItem: (item, nextAction) ->
# * `nextAction` (optional) {Function} which will be called with no argument
# after the item is successfully saved, or with the error if it failed.
# The return value will be that of `nextAction` or `undefined` if it was not
# provided
saveItem: (item, nextAction) =>
if typeof item?.getURI is 'function'
itemURI = item.getURI()
else if typeof item?.getUri is 'function'
@@ -613,9 +623,12 @@ class Pane extends Model
if itemURI?
try
item.save?()
nextAction?()
catch error
@handleSaveError(error, item)
nextAction?()
if nextAction
nextAction(error)
else
@handleSaveError(error, item)
else
@saveItemAs(item, nextAction)
@@ -623,9 +636,11 @@ class Pane extends Model
# path they select.
#
# * `item` The item to save.
# * `nextAction` (optional) {Function} which will be called after the item is
# successfully saved.
saveItemAs: (item, nextAction) ->
# * `nextAction` (optional) {Function} which will be called with no argument
# after the item is successfully saved, or with the error if it failed.
# The return value will be that of `nextAction` or `undefined` if it was not
# provided
saveItemAs: (item, nextAction) =>
return unless item?.saveAs?
saveOptions = item.getSaveDialogOptions?() ? {}
@@ -634,9 +649,12 @@ class Pane extends Model
if newItemPath
try
item.saveAs(newItemPath)
nextAction?()
catch error
@handleSaveError(error, item)
nextAction?()
if nextAction
nextAction(error)
else
@handleSaveError(error, item)
# Public: Save all items.
saveItems: ->

View File

@@ -391,6 +391,9 @@ class Project extends Model
subscribeToBuffer: (buffer) ->
buffer.onDidDestroy => @removeBuffer(buffer)
buffer.onDidChangePath =>
unless @getPaths().length > 0
@setPaths([path.dirname(buffer.getPath())])
buffer.onWillThrowWatchError ({error, handle}) =>
handle()
@notificationManager.addWarning """

View File

@@ -1,6 +1,6 @@
{ipcRenderer} = require 'electron'
module.exports = ({commandRegistry, commandInstaller, config}) ->
module.exports = ({commandRegistry, commandInstaller, config, notificationManager, project, clipboard}) ->
commandRegistry.add 'atom-workspace',
'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem()
'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem()
@@ -31,9 +31,15 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications')
'application:new-window': -> ipcRenderer.send('command', 'application:new-window')
'application:new-file': -> ipcRenderer.send('command', 'application:new-file')
'application:open': -> ipcRenderer.send('command', 'application:open')
'application:open-file': -> ipcRenderer.send('command', 'application:open-file')
'application:open-folder': -> ipcRenderer.send('command', 'application:open-folder')
'application:open': ->
defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
ipcRenderer.send('open-command', 'application:open', defaultPath)
'application:open-file': ->
defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
ipcRenderer.send('open-command', 'application:open-file', defaultPath)
'application:open-folder': ->
defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
ipcRenderer.send('open-command', 'application:open-folder', defaultPath)
'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev')
'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe')
'application:add-project-folder': -> atom.addProjectFolder()
@@ -187,9 +193,9 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6)
'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7)
'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8)
'editor:log-cursor-scope': -> @logCursorScope()
'editor:copy-path': -> @copyPathToClipboard(false)
'editor:copy-project-path': -> @copyPathToClipboard(true)
'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager)
'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false)
'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true)
'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide'))
'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers'))
'editor:scroll-to-cursor': -> @scrollToCursorPosition()
@@ -204,7 +210,7 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'editor:newline-below': -> @insertNewlineBelow()
'editor:newline-above': -> @insertNewlineAbove()
'editor:toggle-line-comments': -> @toggleLineCommentsInSelection()
'editor:checkout-head-revision': -> @checkoutHeadRevision()
'editor:checkout-head-revision': -> atom.workspace.checkoutHeadRevision(this)
'editor:move-line-up': -> @moveLineUp()
'editor:move-line-down': -> @moveLineDown()
'editor:move-selection-left': -> @moveSelectionLeft()
@@ -232,3 +238,15 @@ stopEventPropagationAndGroupUndo = (config, commandListeners) ->
model.transact config.get('editor.undoGroupingInterval'), ->
commandListener.call(model, event)
newCommandListeners
showCursorScope = (descriptor, notificationManager) ->
list = descriptor.scopes.toString().split(',')
list = list.map (item) -> "* #{item}"
content = "Scopes at Cursor\n#{list.join('\n')}"
notificationManager.addInfo(content, dismissable: true)
copyPathToClipboard = (editor, project, clipboard, relative) ->
if filePath = editor.getPath()
filePath = project.relativize(filePath) if relative
clipboard.write(filePath)

View File

@@ -1,57 +0,0 @@
/** @babel */
// Manages a pool of some resource.
export default class ResourcePool {
constructor (pool) {
this.pool = pool
this.queue = []
}
// Enqueue the given function. The function will be given an object from the
// pool. The function must return a {Promise}.
enqueue (fn) {
let resolve = null
let reject = null
const wrapperPromise = new Promise((resolve_, reject_) => {
resolve = resolve_
reject = reject_
})
this.queue.push(this.wrapFunction(fn, resolve, reject))
this.dequeueIfAble()
return wrapperPromise
}
wrapFunction (fn, resolve, reject) {
return (resource) => {
const promise = fn(resource)
promise
.then(result => {
resolve(result)
this.taskDidComplete(resource)
}, error => {
reject(error)
this.taskDidComplete(resource)
})
}
}
taskDidComplete (resource) {
this.pool.push(resource)
this.dequeueIfAble()
}
dequeueIfAble () {
if (!this.pool.length || !this.queue.length) return
const fn = this.queue.shift()
const resource = this.pool.shift()
fn(resource)
}
getQueueDepth () { return this.queue.length }
}

View File

@@ -87,7 +87,7 @@ class Selection extends Model
setBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
options.reversed ?= @isReversed()
@editor.destroyFoldsContainingBufferRange(bufferRange) unless options.preserveFolds
@editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@modifySelection =>
needsFlash = options.flash
delete options.flash if options.flash?
@@ -174,7 +174,7 @@ class Selection extends Model
# range. Defaults to `true` if this is the most recently added selection,
# `false` otherwise.
clear: (options) ->
@marker.setProperties(goalScreenRange: null)
@goalScreenRange = null
@marker.clearTail() unless @retainSelection
@autoscroll() if options?.autoscroll ? @isLastSelection()
@finalize()
@@ -365,7 +365,6 @@ class Selection extends Model
# * `undo` if `skip`, skips the undo stack for this operation.
insertText: (text, options={}) ->
oldBufferRange = @getBufferRange()
@editor.unfoldBufferRow(oldBufferRange.end.row)
wasReversed = @isReversed()
@clear()
@@ -394,7 +393,7 @@ class Selection extends Model
if options.select
@setBufferRange(newBufferRange, reversed: wasReversed)
else
@cursor.setBufferPosition(newBufferRange.end, clip: 'forward') if wasReversed
@cursor.setBufferPosition(newBufferRange.end) if wasReversed
if autoIndentFirstLine
@editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
@@ -411,7 +410,7 @@ class Selection extends Model
# Public: Removes the first character before the selection if the selection
# is empty otherwise it deletes the selection.
backspace: ->
@selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow())
@selectLeft() if @isEmpty()
@deleteSelectedText()
# Public: Removes the selection or, if nothing is selected, then all
@@ -446,11 +445,7 @@ class Selection extends Model
# Public: Removes the selection or the next character after the start of the
# selection if the selection is empty.
delete: ->
if @isEmpty()
if @cursor.isAtEndOfLine() and fold = @editor.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1)
@selectToBufferPosition(fold.getBufferRange().end)
else
@selectRight()
@selectRight() if @isEmpty()
@deleteSelectedText()
# Public: If the selection is empty, removes all text from the cursor to the
@@ -483,8 +478,6 @@ class Selection extends Model
# Public: Removes only the selected text.
deleteSelectedText: ->
bufferRange = @getBufferRange()
if bufferRange.isEmpty() and fold = @editor.largestFoldContainingBufferRow(bufferRange.start.row)
bufferRange = bufferRange.union(fold.getBufferRange(includeNewline: true))
@editor.buffer.delete(bufferRange) unless bufferRange.isEmpty()
@cursor?.setBufferPosition(bufferRange.start)
@@ -516,7 +509,7 @@ class Selection extends Model
if selectedRange.isEmpty()
return if selectedRange.start.row is @editor.buffer.getLastRow()
else
joinMarker = @editor.markBufferRange(selectedRange, invalidationStrategy: 'never')
joinMarker = @editor.markBufferRange(selectedRange, invalidate: 'never')
rowCount = Math.max(1, selectedRange.getRowCount() - 1)
for row in [0...rowCount]
@@ -635,8 +628,9 @@ class Selection extends Model
# Public: Creates a fold containing the current selection.
fold: ->
range = @getBufferRange()
@editor.createFold(range.start.row, range.end.row)
@cursor.setBufferPosition([range.end.row + 1, 0])
unless range.isEmpty()
@editor.foldBufferRange(range)
@cursor.setBufferPosition(range.end)
# Private: Increase the indentation level of the given text by given number
# of levels. Leaves the first line unchanged.
@@ -690,7 +684,7 @@ class Selection extends Model
# Public: Moves the selection down one row.
addSelectionBelow: ->
range = (@getGoalScreenRange() ? @getScreenRange()).copy()
range = @getGoalScreenRange().copy()
nextRow = range.end.row + 1
for row in [nextRow..@editor.getLastScreenRow()]
@@ -703,14 +697,15 @@ class Selection extends Model
else
continue if clippedRange.isEmpty()
@editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
selection = @editor.addSelectionForScreenRange(clippedRange)
selection.setGoalScreenRange(range)
break
return
# Public: Moves the selection up one row.
addSelectionAbove: ->
range = (@getGoalScreenRange() ? @getScreenRange()).copy()
range = @getGoalScreenRange().copy()
previousRow = range.end.row - 1
for row in [previousRow..0]
@@ -723,7 +718,8 @@ class Selection extends Model
else
continue if clippedRange.isEmpty()
@editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
selection = @editor.addSelectionForScreenRange(clippedRange)
selection.setGoalScreenRange(range)
break
return
@@ -762,6 +758,12 @@ class Selection extends Model
Section: Private Utilities
###
setGoalScreenRange: (range) ->
@goalScreenRange = Range.fromObject(range)
getGoalScreenRange: ->
@goalScreenRange ? @getScreenRange()
markerDidChange: (e) ->
{oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
@@ -832,7 +834,3 @@ class Selection extends Model
# Returns a {Point} representing the new tail position.
plantTail: ->
@marker.plantTail()
getGoalScreenRange: ->
if goalScreenRange = @marker.getProperties().goalScreenRange
Range.fromObject(goalScreenRange)

View File

@@ -494,7 +494,7 @@ class TextEditorComponent
unless @presenter.isRowVisible(screenPosition.row)
@presenter.setScreenRowsToMeasure([screenPosition.row])
unless @linesComponent.lineNodeForLineIdAndScreenRow(@presenter.lineIdForScreenRow(screenPosition.row), screenPosition.row)?
unless @linesComponent.lineNodeForScreenRow(screenPosition.row)?
@updateSyncPreMeasurement()
pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
@@ -560,8 +560,8 @@ class TextEditorComponent
screenPosition = @screenPositionForMouseEvent(event)
if event.target?.classList.contains('fold-marker')
bufferRow = @editor.bufferRowForScreenRow(screenPosition.row)
@editor.unfoldBufferRow(bufferRow)
bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition)
@editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition])
return
switch detail
@@ -607,7 +607,7 @@ class TextEditorComponent
clickedScreenRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
@editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
@editor.addSelectionForScreenRange(initialScreenRange, autoscroll: false)
@handleGutterDrag(initialScreenRange)
onGutterShiftClick: (event) =>
@@ -890,10 +890,7 @@ class TextEditorComponent
e.abortKeyBinding() unless @editor.consolidateSelections()
lineNodeForScreenRow: (screenRow) ->
tileRow = @presenter.tileForRow(screenRow)
tileComponent = @linesComponent.getComponentForTile(tileRow)
tileComponent?.lineNodeForScreenRow(screenRow)
@linesComponent.lineNodeForScreenRow(screenRow)
lineNumberNodeForScreenRow: (screenRow) ->
tileRow = @presenter.tileForRow(screenRow)
@@ -950,7 +947,7 @@ class TextEditorComponent
screenPositionForMouseEvent: (event, linesClientRect) ->
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
@screenPositionForPixelPosition(pixelPosition, true)
@screenPositionForPixelPosition(pixelPosition)
pixelPositionForMouseEvent: (event, linesClientRect) ->
{clientX, clientY} = event

View File

@@ -96,7 +96,7 @@ class TextEditorElement extends HTMLElement
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace?
throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert?
throw new Error("Must pass an assert parameter when initializing TextEditorElements") unless @assert?
throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles?
throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars?

View File

@@ -1,192 +0,0 @@
TextEditorMarker = require './text-editor-marker'
# Public: *Experimental:* A container for a related set of markers at the
# {TextEditor} level. Wraps an underlying {MarkerLayer} on the editor's
# {TextBuffer}.
#
# This API is experimental and subject to change on any release.
module.exports =
class TextEditorMarkerLayer
constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) ->
@id = @bufferMarkerLayer.id
@markersById = {}
###
Section: Lifecycle
###
# Essential: Destroy this layer.
destroy: ->
if @isDefaultLayer
marker.destroy() for id, marker of @markersById
else
@bufferMarkerLayer.destroy()
###
Section: Querying
###
# Essential: Get an existing marker by its id.
#
# Returns a {TextEditorMarker}.
getMarker: (id) ->
if editorMarker = @markersById[id]
editorMarker
else if bufferMarker = @bufferMarkerLayer.getMarker(id)
@markersById[id] = new TextEditorMarker(this, bufferMarker)
# Essential: Get all markers in the layer.
#
# Returns an {Array} of {TextEditorMarker}s.
getMarkers: ->
@bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id)
# Public: Get the number of markers in the marker layer.
#
# Returns a {Number}.
getMarkerCount: ->
@bufferMarkerLayer.getMarkerCount()
# Public: Find markers in the layer conforming to the given parameters.
#
# See the documentation for {TextEditor::findMarkers}.
findMarkers: (params) ->
params = @translateToBufferMarkerParams(params)
@bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id)
###
Section: Marker creation
###
# Essential: Create a marker on this layer with the given range in buffer
# coordinates.
#
# See the documentation for {TextEditor::markBufferRange}
markBufferRange: (bufferRange, options) ->
@getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id)
# Essential: Create a marker on this layer with the given range in screen
# coordinates.
#
# See the documentation for {TextEditor::markScreenRange}
markScreenRange: (screenRange, options) ->
bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange)
@markBufferRange(bufferRange, options)
# Public: Create a marker on this layer with the given buffer position and no
# tail.
#
# See the documentation for {TextEditor::markBufferPosition}
markBufferPosition: (bufferPosition, options) ->
@getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id)
# Public: Create a marker on this layer with the given screen position and no
# tail.
#
# See the documentation for {TextEditor::markScreenPosition}
markScreenPosition: (screenPosition, options) ->
bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition)
@markBufferPosition(bufferPosition, options)
###
Section: Event Subscription
###
# Public: Subscribe to be notified asynchronously whenever markers are
# created, updated, or destroyed on this layer. *Prefer this method for
# optimal performance when interacting with layers that could contain large
# numbers of markers.*
#
# * `callback` A {Function} that will be called with no arguments when changes
# occur on this layer.
#
# Subscribers are notified once, asynchronously when any number of changes
# occur in a given tick of the event loop. You should re-query the layer
# to determine the state of markers in which you're interested in. It may
# be counter-intuitive, but this is much more efficient than subscribing to
# events on individual markers, which are expensive to deliver.
#
# Returns a {Disposable}.
onDidUpdate: (callback) ->
@bufferMarkerLayer.onDidUpdate(callback)
# Public: Subscribe to be notified synchronously whenever markers are created
# on this layer. *Avoid this method for optimal performance when interacting
# with layers that could contain large numbers of markers.*
#
# * `callback` A {Function} that will be called with a {TextEditorMarker}
# whenever a new marker is created.
#
# You should prefer {onDidUpdate} when synchronous notifications aren't
# absolutely necessary.
#
# Returns a {Disposable}.
onDidCreateMarker: (callback) ->
@bufferMarkerLayer.onDidCreateMarker (bufferMarker) =>
callback(@getMarker(bufferMarker.id))
# Public: Subscribe to be notified synchronously when this layer is destroyed.
#
# Returns a {Disposable}.
onDidDestroy: (callback) ->
@bufferMarkerLayer.onDidDestroy(callback)
###
Section: Private
###
refreshMarkerScreenPositions: ->
for marker in @getMarkers()
marker.notifyObservers(textChanged: false)
return
didDestroyMarker: (marker) ->
delete @markersById[marker.id]
translateToBufferMarkerParams: (params) ->
bufferMarkerParams = {}
for key, value of params
switch key
when 'startBufferPosition'
key = 'startPosition'
when 'endBufferPosition'
key = 'endPosition'
when 'startScreenPosition'
key = 'startPosition'
value = @displayBuffer.bufferPositionForScreenPosition(value)
when 'endScreenPosition'
key = 'endPosition'
value = @displayBuffer.bufferPositionForScreenPosition(value)
when 'startBufferRow'
key = 'startRow'
when 'endBufferRow'
key = 'endRow'
when 'startScreenRow'
key = 'startRow'
value = @displayBuffer.bufferRowForScreenRow(value)
when 'endScreenRow'
key = 'endRow'
value = @displayBuffer.bufferRowForScreenRow(value)
when 'intersectsBufferRowRange'
key = 'intersectsRowRange'
when 'intersectsScreenRowRange'
key = 'intersectsRowRange'
[startRow, endRow] = value
value = [@displayBuffer.bufferRowForScreenRow(startRow), @displayBuffer.bufferRowForScreenRow(endRow)]
when 'containsBufferRange'
key = 'containsRange'
when 'containsBufferPosition'
key = 'containsPosition'
when 'containedInBufferRange'
key = 'containedInRange'
when 'containedInScreenRange'
key = 'containedInRange'
value = @displayBuffer.bufferRangeForScreenRange(value)
when 'intersectsBufferRange'
key = 'intersectsRange'
when 'intersectsScreenRange'
key = 'intersectsRange'
value = @displayBuffer.bufferRangeForScreenRange(value)
bufferMarkerParams[key] = value
bufferMarkerParams

View File

@@ -1,371 +0,0 @@
_ = require 'underscore-plus'
{CompositeDisposable, Emitter} = require 'event-kit'
# Essential: Represents a buffer annotation that remains logically stationary
# even as the buffer changes. This is used to represent cursors, folds, snippet
# targets, misspelled words, and anything else that needs to track a logical
# location in the buffer over time.
#
# ### TextEditorMarker Creation
#
# Use {TextEditor::markBufferRange} rather than creating Markers directly.
#
# ### Head and Tail
#
# Markers always have a *head* and sometimes have a *tail*. If you think of a
# marker as an editor selection, the tail is the part that's stationary and the
# head is the part that moves when the mouse is moved. A marker without a tail
# always reports an empty range at the head position. A marker with a head position
# greater than the tail is in a "normal" orientation. If the head precedes the
# tail the marker is in a "reversed" orientation.
#
# ### Validity
#
# Markers are considered *valid* when they are first created. Depending on the
# invalidation strategy you choose, certain changes to the buffer can cause a
# marker to become invalid, for example if the text surrounding the marker is
# deleted. The strategies, in order of descending fragility:
#
# * __never__: The marker is never marked as invalid. This is a good choice for
# markers representing selections in an editor.
# * __surround__: The marker is invalidated by changes that completely surround it.
# * __overlap__: The marker is invalidated by changes that surround the
# start or end of the marker. This is the default.
# * __inside__: The marker is invalidated by changes that extend into the
# inside of the marker. Changes that end at the marker's start or
# start at the marker's end do not invalidate the marker.
# * __touch__: The marker is invalidated by a change that touches the marked
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
# See {TextEditor::markBufferRange} for usage.
module.exports =
class TextEditorMarker
bufferMarkerSubscription: null
oldHeadBufferPosition: null
oldHeadScreenPosition: null
oldTailBufferPosition: null
oldTailScreenPosition: null
wasValid: true
hasChangeObservers: false
###
Section: Construction and Destruction
###
constructor: (@layer, @bufferMarker) ->
{@displayBuffer} = @layer
@emitter = new Emitter
@disposables = new CompositeDisposable
@id = @bufferMarker.id
@disposables.add @bufferMarker.onDidDestroy => @destroyed()
# Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once
# destroyed, a marker cannot be restored by undo/redo operations.
destroy: ->
@bufferMarker.destroy()
@disposables.dispose()
# Essential: Creates and returns a new {TextEditorMarker} with the same properties as
# this marker.
#
# {Selection} markers (markers with a custom property `type: "selection"`)
# should be copied with a different `type` value, for example with
# `marker.copy({type: null})`. Otherwise, the new marker's selection will
# be merged with this marker's selection, and a `null` value will be
# returned.
#
# * `properties` (optional) {Object} properties to associate with the new
# marker. The new marker's properties are computed by extending this marker's
# properties with `properties`.
#
# Returns a {TextEditorMarker}.
copy: (properties) ->
@layer.getMarker(@bufferMarker.copy(properties).id)
###
Section: Event Subscription
###
# Essential: Invoke the given callback when the state of the marker changes.
#
# * `callback` {Function} to be called when the marker changes.
# * `event` {Object} with the following keys:
# * `oldHeadBufferPosition` {Point} representing the former head buffer position
# * `newHeadBufferPosition` {Point} representing the new head buffer position
# * `oldTailBufferPosition` {Point} representing the former tail buffer position
# * `newTailBufferPosition` {Point} representing the new tail buffer position
# * `oldHeadScreenPosition` {Point} representing the former head screen position
# * `newHeadScreenPosition` {Point} representing the new head screen position
# * `oldTailScreenPosition` {Point} representing the former tail screen position
# * `newTailScreenPosition` {Point} representing the new tail screen position
# * `wasValid` {Boolean} indicating whether the marker was valid before the change
# * `isValid` {Boolean} indicating whether the marker is now valid
# * `hadTail` {Boolean} indicating whether the marker had a tail before the change
# * `hasTail` {Boolean} indicating whether the marker now has a tail
# * `oldProperties` {Object} containing the marker's custom properties before the change.
# * `newProperties` {Object} containing the marker's custom properties after the change.
# * `textChanged` {Boolean} indicating whether this change was caused by a textual change
# to the buffer or whether the marker was manipulated directly via its public API.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChange: (callback) ->
unless @hasChangeObservers
@oldHeadBufferPosition = @getHeadBufferPosition()
@oldHeadScreenPosition = @getHeadScreenPosition()
@oldTailBufferPosition = @getTailBufferPosition()
@oldTailScreenPosition = @getTailScreenPosition()
@wasValid = @isValid()
@disposables.add @bufferMarker.onDidChange (event) => @notifyObservers(event)
@hasChangeObservers = true
@emitter.on 'did-change', callback
# Essential: Invoke the given callback when the marker is destroyed.
#
# * `callback` {Function} to be called when the marker is destroyed.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy: (callback) ->
@emitter.on 'did-destroy', callback
###
Section: TextEditorMarker Details
###
# Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be
# invalidated when a region surrounding them in the buffer is changed.
isValid: ->
@bufferMarker.isValid()
# Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker
# can be invalid without being destroyed, in which case undoing the invalidating
# operation would restore the marker. Once a marker is destroyed by calling
# {TextEditorMarker::destroy}, no undo/redo operation can ever bring it back.
isDestroyed: ->
@bufferMarker.isDestroyed()
# Essential: Returns a {Boolean} indicating whether the head precedes the tail.
isReversed: ->
@bufferMarker.isReversed()
# Essential: Get the invalidation strategy for this marker.
#
# Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`.
#
# Returns a {String}.
getInvalidationStrategy: ->
@bufferMarker.getInvalidationStrategy()
# Essential: Returns an {Object} containing any custom properties associated with
# the marker.
getProperties: ->
@bufferMarker.getProperties()
# Essential: Merges an {Object} containing new properties into the marker's
# existing properties.
#
# * `properties` {Object}
setProperties: (properties) ->
@bufferMarker.setProperties(properties)
matchesProperties: (attributes) ->
attributes = @layer.translateToBufferMarkerParams(attributes)
@bufferMarker.matchesParams(attributes)
###
Section: Comparing to other markers
###
# Essential: Returns a {Boolean} indicating whether this marker is equivalent to
# another marker, meaning they have the same range and options.
#
# * `other` {TextEditorMarker} other marker
isEqual: (other) ->
return false unless other instanceof @constructor
@bufferMarker.isEqual(other.bufferMarker)
# Essential: Compares this marker to another based on their ranges.
#
# * `other` {TextEditorMarker}
#
# Returns a {Number}
compare: (other) ->
@bufferMarker.compare(other.bufferMarker)
###
Section: Managing the marker's range
###
# Essential: Gets the buffer range of the display marker.
#
# Returns a {Range}.
getBufferRange: ->
@bufferMarker.getRange()
# Essential: Modifies the buffer range of the display marker.
#
# * `bufferRange` The new {Range} to use
# * `properties` (optional) {Object} properties to associate with the marker.
# * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
setBufferRange: (bufferRange, properties) ->
@bufferMarker.setRange(bufferRange, properties)
# Essential: Gets the screen range of the display marker.
#
# Returns a {Range}.
getScreenRange: ->
@displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true)
# Essential: Modifies the screen range of the display marker.
#
# * `screenRange` The new {Range} to use
# * `properties` (optional) {Object} properties to associate with the marker.
# * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
setScreenRange: (screenRange, options) ->
@setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options)
# Essential: Retrieves the buffer position of the marker's start. This will always be
# less than or equal to the result of {TextEditorMarker::getEndBufferPosition}.
#
# Returns a {Point}.
getStartBufferPosition: ->
@bufferMarker.getStartPosition()
# Essential: Retrieves the screen position of the marker's start. This will always be
# less than or equal to the result of {TextEditorMarker::getEndScreenPosition}.
#
# Returns a {Point}.
getStartScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true)
# Essential: Retrieves the buffer position of the marker's end. This will always be
# greater than or equal to the result of {TextEditorMarker::getStartBufferPosition}.
#
# Returns a {Point}.
getEndBufferPosition: ->
@bufferMarker.getEndPosition()
# Essential: Retrieves the screen position of the marker's end. This will always be
# greater than or equal to the result of {TextEditorMarker::getStartScreenPosition}.
#
# Returns a {Point}.
getEndScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true)
# Extended: Retrieves the buffer position of the marker's head.
#
# Returns a {Point}.
getHeadBufferPosition: ->
@bufferMarker.getHeadPosition()
# Extended: Sets the buffer position of the marker's head.
#
# * `bufferPosition` The new {Point} to use
# * `properties` (optional) {Object} properties to associate with the marker.
setHeadBufferPosition: (bufferPosition, properties) ->
@bufferMarker.setHeadPosition(bufferPosition, properties)
# Extended: Retrieves the screen position of the marker's head.
#
# Returns a {Point}.
getHeadScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true)
# Extended: Sets the screen position of the marker's head.
#
# * `screenPosition` The new {Point} to use
# * `properties` (optional) {Object} properties to associate with the marker.
setHeadScreenPosition: (screenPosition, properties) ->
@setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties))
# Extended: Retrieves the buffer position of the marker's tail.
#
# Returns a {Point}.
getTailBufferPosition: ->
@bufferMarker.getTailPosition()
# Extended: Sets the buffer position of the marker's tail.
#
# * `bufferPosition` The new {Point} to use
# * `properties` (optional) {Object} properties to associate with the marker.
setTailBufferPosition: (bufferPosition) ->
@bufferMarker.setTailPosition(bufferPosition)
# Extended: Retrieves the screen position of the marker's tail.
#
# Returns a {Point}.
getTailScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true)
# Extended: Sets the screen position of the marker's tail.
#
# * `screenPosition` The new {Point} to use
# * `properties` (optional) {Object} properties to associate with the marker.
setTailScreenPosition: (screenPosition, options) ->
@setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options))
# Extended: Returns a {Boolean} indicating whether the marker has a tail.
hasTail: ->
@bufferMarker.hasTail()
# Extended: Plants the marker's tail at the current head position. After calling
# the marker's tail position will be its head position at the time of the
# call, regardless of where the marker's head is moved.
#
# * `properties` (optional) {Object} properties to associate with the marker.
plantTail: ->
@bufferMarker.plantTail()
# Extended: Removes the marker's tail. After calling the marker's head position
# will be reported as its current tail position until the tail is planted
# again.
#
# * `properties` (optional) {Object} properties to associate with the marker.
clearTail: (properties) ->
@bufferMarker.clearTail(properties)
###
Section: Private utility methods
###
# Returns a {String} representation of the marker
inspect: ->
"TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})"
destroyed: ->
@layer.didDestroyMarker(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
notifyObservers: ({textChanged}) ->
textChanged ?= false
newHeadBufferPosition = @getHeadBufferPosition()
newHeadScreenPosition = @getHeadScreenPosition()
newTailBufferPosition = @getTailBufferPosition()
newTailScreenPosition = @getTailScreenPosition()
isValid = @isValid()
return if isValid is @wasValid and
newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and
newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and
newTailBufferPosition.isEqual(@oldTailBufferPosition) and
newTailScreenPosition.isEqual(@oldTailScreenPosition)
changeEvent = {
@oldHeadScreenPosition, newHeadScreenPosition,
@oldTailScreenPosition, newTailScreenPosition,
@oldHeadBufferPosition, newHeadBufferPosition,
@oldTailBufferPosition, newTailBufferPosition,
textChanged,
isValid
}
@oldHeadBufferPosition = newHeadBufferPosition
@oldHeadScreenPosition = newHeadScreenPosition
@oldTailBufferPosition = newTailBufferPosition
@oldTailScreenPosition = newTailScreenPosition
@wasValid = isValid
@emitter.emit 'did-change', changeEvent

View File

@@ -16,6 +16,7 @@ class TextEditorPresenter
{@model, @config, @lineTopIndex, scrollPastEnd} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
{@contentFrameWidth} = params
{@displayLayer} = @model
@gutterWidth = 0
@tileSize ?= 6
@@ -23,6 +24,7 @@ class TextEditorPresenter
@realScrollLeft = @scrollLeft
@disposables = new CompositeDisposable
@emitter = new Emitter
@linesByScreenRow = new Map
@visibleHighlights = {}
@characterWidthsByScope = {}
@lineDecorationsByScreenRow = {}
@@ -87,6 +89,8 @@ class TextEditorPresenter
@updateCommonGutterState()
@updateReflowState()
@updateLines()
if @shouldUpdateDecorations
@fetchDecorations()
@updateLineDecorations()
@@ -106,6 +110,8 @@ class TextEditorPresenter
@clearPendingScrollPosition()
@updateRowsPerPage()
@updateLines()
@updateFocusedState()
@updateHeightState()
@updateVerticalScrollState()
@@ -132,8 +138,11 @@ class TextEditorPresenter
@shouldUpdateDecorations = true
observeModel: ->
@disposables.add @model.onDidChange ({start, end, screenDelta}) =>
@spliceBlockDecorationsInRange(start, end, screenDelta)
@disposables.add @model.displayLayer.onDidChangeSync (changes) =>
for change in changes
startRow = change.start.row
endRow = startRow + change.oldExtent.row
@spliceBlockDecorationsInRange(startRow, endRow, change.newExtent.row - change.oldExtent.row)
@shouldUpdateDecorations = true
@emitDidUpdateState()
@@ -166,7 +175,6 @@ class TextEditorPresenter
@scrollPastEnd = @config.get('editor.scrollPastEnd', configParams)
@showLineNumbers = @config.get('editor.showLineNumbers', configParams)
@showIndentGuide = @config.get('editor.showIndentGuide', configParams)
if @configDisposables?
@configDisposables?.dispose()
@@ -175,10 +183,6 @@ class TextEditorPresenter
@configDisposables = new CompositeDisposable
@disposables.add(@configDisposables)
@configDisposables.add @config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
@showIndentGuide = newValue
@emitDidUpdateState()
@configDisposables.add @config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
@scrollPastEnd = newValue
@updateScrollHeight()
@@ -286,7 +290,6 @@ class TextEditorPresenter
@state.content.width = Math.max(@contentWidth + @verticalScrollbarWidth, @contentFrameWidth)
@state.content.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide
@state.content.backgroundColor = if @model.isMini() then null else @backgroundColor
@state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null
@@ -297,15 +300,15 @@ class TextEditorPresenter
Math.max(0, Math.min(row, @model.getScreenLineCount()))
getStartTileRow: ->
@constrainRow(@tileForRow(@startRow))
@constrainRow(@tileForRow(@startRow ? 0))
getEndTileRow: ->
@constrainRow(@tileForRow(@endRow))
@constrainRow(@tileForRow(@endRow ? 0))
isValidScreenRow: (screenRow) ->
screenRow >= 0 and screenRow < @model.getScreenLineCount()
getScreenRows: ->
getScreenRowsToRender: ->
startRow = @getStartTileRow()
endRow = @constrainRow(@getEndTileRow() + @tileSize)
@@ -320,6 +323,22 @@ class TextEditorPresenter
screenRows.sort (a, b) -> a - b
_.uniq(screenRows, true)
getScreenRangesToRender: ->
screenRows = @getScreenRowsToRender()
screenRows.push(Infinity) # makes the loop below inclusive
startRow = screenRows[0]
endRow = startRow - 1
screenRanges = []
for row in screenRows
if row is endRow + 1
endRow++
else
screenRanges.push([startRow, endRow])
startRow = endRow = row
screenRanges
setScreenRowsToMeasure: (screenRows) ->
return if not screenRows? or screenRows.length is 0
@@ -332,7 +351,7 @@ class TextEditorPresenter
updateTilesState: ->
return unless @startRow? and @endRow? and @lineHeight?
screenRows = @getScreenRows()
screenRows = @getScreenRowsToRender()
visibleTiles = {}
startRow = screenRows[0]
endRow = screenRows[screenRows.length - 1]
@@ -375,7 +394,7 @@ class TextEditorPresenter
visibleTiles[tileStartRow] = true
zIndex++
if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
if @mouseWheelScreenRow? and 0 <= @mouseWheelScreenRow < @model.getScreenLineCount()
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
unless visibleTiles[mouseWheelTile]?
@@ -393,7 +412,7 @@ class TextEditorPresenter
tileState.lines ?= {}
visibleLineIds = {}
for screenRow in screenRows
line = @model.tokenizedLineForScreenRow(screenRow)
line = @linesByScreenRow.get(screenRow)
unless line?
throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}")
@@ -411,18 +430,8 @@ class TextEditorPresenter
else
tileState.lines[line.id] =
screenRow: screenRow
text: line.text
openScopes: line.openScopes
tags: line.tags
specialTokens: line.specialTokens
firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
invisibles: line.invisibles
endOfLineInvisibles: line.endOfLineInvisibles
isOnlyWhitespace: line.isOnlyWhitespace()
indentLevel: line.indentLevel
tabLength: line.tabLength
fold: line.fold
lineText: line.lineText
tagCodes: line.tagCodes
decorationClasses: @lineDecorationClassesForRow(screenRow)
precedingBlockDecorations: precedingBlockDecorations
followingBlockDecorations: followingBlockDecorations
@@ -459,19 +468,21 @@ class TextEditorPresenter
pixelPosition = @pixelPositionForScreenPosition(screenPosition)
top = pixelPosition.top + @lineHeight
left = pixelPosition.left + @gutterWidth
# Fixed positioning.
top = @boundingClientRect.top + pixelPosition.top + @lineHeight
left = @boundingClientRect.left + pixelPosition.left + @gutterWidth
if overlayDimensions = @overlayDimensions[decoration.id]
{itemWidth, itemHeight, contentMargin} = overlayDimensions
rightDiff = left + @boundingClientRect.left + itemWidth + contentMargin - @windowWidth
rightDiff = left + itemWidth + contentMargin - @windowWidth
left -= rightDiff if rightDiff > 0
leftDiff = left + @boundingClientRect.left + contentMargin
leftDiff = left + contentMargin
left -= leftDiff if leftDiff < 0
if top + @boundingClientRect.top + itemHeight > @windowHeight and top - (itemHeight + @lineHeight) >= 0
if top + itemHeight > @windowHeight and
top - (itemHeight + @lineHeight) >= 0
top -= itemHeight + @lineHeight
pixelPosition.top = top
@@ -616,7 +627,7 @@ class TextEditorPresenter
softWrapped = false
screenRow = startRow + i
line = @model.tokenizedLineForScreenRow(screenRow)
lineId = @linesByScreenRow.get(screenRow).id
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow)
blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight
@@ -624,8 +635,8 @@ class TextEditorPresenter
blockDecorationsAfterPreviousScreenRowHeight = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - @lineHeight - @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow - 1)
blockDecorationsHeight += blockDecorationsAfterPreviousScreenRowHeight
tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight}
visibleLineNumberIds[line.id] = true
tileState.lineNumbers[lineId] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight}
visibleLineNumberIds[lineId] = true
for id of tileState.lineNumbers
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
@@ -685,9 +696,7 @@ class TextEditorPresenter
updateHorizontalDimensions: ->
if @baseCharacterWidth?
oldContentWidth = @contentWidth
rightmostPosition = Point(@model.getLongestScreenRow(), @model.getMaxScreenLineLength())
if @model.tokenizedLineForScreenRow(rightmostPosition.row)?.isSoftWrapped()
rightmostPosition = @model.clipScreenPosition(rightmostPosition)
rightmostPosition = @model.getRightmostScreenPosition()
@contentWidth = @pixelPositionForScreenPosition(rightmostPosition).left
@contentWidth += @scrollLeft
@contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
@@ -1055,6 +1064,16 @@ class TextEditorPresenter
rect.height = Math.round(rect.height)
rect
updateLines: ->
@linesByScreenRow.clear()
for [startRow, endRow] in @getScreenRangesToRender()
for line, index in @displayLayer.getScreenLines(startRow, endRow + 1)
@linesByScreenRow.set(startRow + index, line)
lineIdForScreenRow: (screenRow) ->
@linesByScreenRow.get(screenRow)?.id
fetchDecorations: ->
return unless 0 <= @startRow <= @endRow <= Infinity
@decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1)
@@ -1102,9 +1121,9 @@ class TextEditorPresenter
@customGutterDecorationsByGutterName = {}
for decorationId, decorationState of @decorations
{properties, screenRange, rangeIsReversed} = decorationState
{properties, bufferRange, screenRange, rangeIsReversed} = decorationState
if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number')
@addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed)
@addToLineDecorationCaches(decorationId, properties, bufferRange, screenRange, rangeIsReversed)
else if Decoration.isType(properties, 'gutter') and properties.gutterName?
@customGutterDecorationsByGutterName[properties.gutterName] ?= {}
@@ -1125,7 +1144,7 @@ class TextEditorPresenter
return
addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) ->
addToLineDecorationCaches: (decorationId, properties, bufferRange, screenRange, rangeIsReversed) ->
if screenRange.isEmpty()
return if properties.onlyNonEmpty
else
@@ -1133,21 +1152,28 @@ class TextEditorPresenter
omitLastRow = screenRange.end.column is 0
if rangeIsReversed
headPosition = screenRange.start
headScreenPosition = screenRange.start
headBufferPosition = bufferRange.start
else
headPosition = screenRange.end
headScreenPosition = screenRange.end
headBufferPosition = bufferRange.end
for row in [screenRange.start.row..screenRange.end.row] by 1
continue if properties.onlyHead and row isnt headPosition.row
continue if omitLastRow and row is screenRange.end.row
if properties.class is 'folded' and Decoration.isType(properties, 'line-number')
screenRow = @model.screenRowForBufferRow(headBufferPosition.row)
@lineNumberDecorationsByScreenRow[screenRow] ?= {}
@lineNumberDecorationsByScreenRow[screenRow][decorationId] = properties
else
for row in [screenRange.start.row..screenRange.end.row] by 1
continue if properties.onlyHead and row isnt headScreenPosition.row
continue if omitLastRow and row is screenRange.end.row
if Decoration.isType(properties, 'line')
@lineDecorationsByScreenRow[row] ?= {}
@lineDecorationsByScreenRow[row][decorationId] = properties
if Decoration.isType(properties, 'line')
@lineDecorationsByScreenRow[row] ?= {}
@lineDecorationsByScreenRow[row][decorationId] = properties
if Decoration.isType(properties, 'line-number')
@lineNumberDecorationsByScreenRow[row] ?= {}
@lineNumberDecorationsByScreenRow[row][decorationId] = properties
if Decoration.isType(properties, 'line-number')
@lineNumberDecorationsByScreenRow[row] ?= {}
@lineNumberDecorationsByScreenRow[row][decorationId] = properties
return
@@ -1527,5 +1553,11 @@ class TextEditorPresenter
isRowVisible: (row) ->
@startRow <= row < @endRow
lineIdForScreenRow: (screenRow) ->
@model.tokenizedLineForScreenRow(screenRow)?.id
isOpenTagCode: (tagCode) ->
@displayLayer.isOpenTagCode(tagCode)
isCloseTagCode: (tagCode) ->
@displayLayer.isCloseTagCode(tagCode)
tagForCode: (tagCode) ->
@displayLayer.tagForCode(tagCode)

View File

@@ -26,8 +26,20 @@ class TextEditorRegistry
# editor is destroyed.
add: (editor) ->
@editors.add(editor)
editor.registered = true
@emitter.emit 'did-add-editor', editor
new Disposable => @editors.delete(editor)
new Disposable => @remove(editor)
# Remove a `TextEditor`.
#
# * `editor` The editor to remove.
#
# Returns a {Boolean} indicating whether the editor was successfully removed.
remove: (editor) ->
removed = @editors.delete(editor)
editor.registered = false
removed
# Invoke the given callback with all the current and future registered
# `TextEditors`.

File diff suppressed because it is too large Load Diff

View File

@@ -94,6 +94,13 @@ isCJKCharacter = (character) ->
isHalfWidthCharacter(character) or
isKoreanCharacter(character)
isWordStart = (previousCharacter, character) ->
(previousCharacter is ' ' or previousCharacter is '\t') and
(character isnt ' ' and character isnt '\t')
isWrapBoundary = (previousCharacter, character) ->
isWordStart(previousCharacter, character) or isCJKCharacter(character)
# Does the given string contain at least surrogate pair, variation sequence,
# or combined character?
#
@@ -107,4 +114,8 @@ hasPairedCharacter = (string) ->
index++
false
module.exports = {isPairedCharacter, hasPairedCharacter, isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isCJKCharacter}
module.exports = {
isPairedCharacter, hasPairedCharacter,
isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter,
isWrapBoundary
}

View File

@@ -1,106 +1,57 @@
{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter} = require './text-utils'
module.exports =
class TokenIterator
constructor: ({@grammarRegistry}, line, enableScopes) ->
@reset(line, enableScopes) if line?
constructor: ({@grammarRegistry}, line) ->
@reset(line) if line?
reset: (@line, @enableScopes=true) ->
reset: (@line) ->
@index = null
@bufferStart = @line.startBufferColumn
@bufferEnd = @bufferStart
@screenStart = 0
@screenEnd = 0
@resetScopes() if @enableScopes
@startColumn = 0
@endColumn = 0
@scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id)
@scopeStarts = @scopes.slice()
@scopeEnds = []
this
next: ->
{tags} = @line
if @index?
@startColumn = @endColumn
@scopeEnds.length = 0
@scopeStarts.length = 0
@index++
@bufferStart = @bufferEnd
@screenStart = @screenEnd
@clearScopeStartsAndEnds() if @enableScopes
else
@index = 0
while @index < tags.length
tag = tags[@index]
if tag < 0
@handleScopeForTag(tag) if @enableScopes
scope = @grammarRegistry.scopeForId(tag)
if tag % 2 is 0
if @scopeStarts[@scopeStarts.length - 1] is scope
@scopeStarts.pop()
else
@scopeEnds.push(scope)
@scopes.pop()
else
@scopeStarts.push(scope)
@scopes.push(scope)
@index++
else
if @isHardTab()
@screenEnd = @screenStart + tag
@bufferEnd = @bufferStart + 1
else if @isSoftWrapIndentation()
@screenEnd = @screenStart + tag
@bufferEnd = @bufferStart + 0
else
@screenEnd = @screenStart + tag
@bufferEnd = @bufferStart + tag
@text = @line.text.substring(@screenStart, @screenEnd)
@endColumn += tag
@text = @line.text.substring(@startColumn, @endColumn)
return true
false
resetScopes: ->
@scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id)
@scopeStarts = @scopes.slice()
@scopeEnds = []
clearScopeStartsAndEnds: ->
@scopeEnds.length = 0
@scopeStarts.length = 0
handleScopeForTag: (tag) ->
scope = @grammarRegistry.scopeForId(tag)
if tag % 2 is 0
if @scopeStarts[@scopeStarts.length - 1] is scope
@scopeStarts.pop()
else
@scopeEnds.push(scope)
@scopes.pop()
else
@scopeStarts.push(scope)
@scopes.push(scope)
getBufferStart: -> @bufferStart
getBufferEnd: -> @bufferEnd
getScreenStart: -> @screenStart
getScreenEnd: -> @screenEnd
getScopes: -> @scopes
getScopeStarts: -> @scopeStarts
getScopeEnds: -> @scopeEnds
getScopes: -> @scopes
getScopeEnds: -> @scopeEnds
getText: -> @text
isSoftTab: ->
@line.specialTokens[@index] is SoftTab
getBufferStart: -> @startColumn
isHardTab: ->
@line.specialTokens[@index] is HardTab
isSoftWrapIndentation: ->
@line.specialTokens[@index] is SoftWrapIndent
isPairedCharacter: ->
@line.specialTokens[@index] is PairedCharacter
hasDoubleWidthCharacterAt: (charIndex) ->
isDoubleWidthCharacter(@getText()[charIndex])
hasHalfWidthCharacterAt: (charIndex) ->
isHalfWidthCharacter(@getText()[charIndex])
hasKoreanCharacterAt: (charIndex) ->
isKoreanCharacter(@getText()[charIndex])
isAtomic: ->
@isSoftTab() or @isHardTab() or @isSoftWrapIndentation() or @isPairedCharacter()
getBufferEnd: -> @endColumn

View File

@@ -7,41 +7,20 @@ WhitespaceRegex = /\S/
module.exports =
class Token
value: null
hasPairedCharacter: false
scopes: null
isAtomic: null
isHardTab: null
firstNonWhitespaceIndex: null
firstTrailingWhitespaceIndex: null
hasInvisibleCharacters: false
constructor: (properties) ->
{@value, @scopes, @isAtomic, @isHardTab, @bufferDelta} = properties
{@hasInvisibleCharacters, @hasPairedCharacter, @isSoftWrapIndentation} = properties
@firstNonWhitespaceIndex = properties.firstNonWhitespaceIndex ? null
@firstTrailingWhitespaceIndex = properties.firstTrailingWhitespaceIndex ? null
@screenDelta = @value.length
@bufferDelta ?= @screenDelta
{@value, @scopes} = properties
isEqual: (other) ->
# TODO: scopes is deprecated. This is here for the sake of lang package tests
@value is other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic is !!other.isAtomic
@value is other.value and _.isEqual(@scopes, other.scopes)
isBracket: ->
/^meta\.brace\b/.test(_.last(@scopes))
isOnlyWhitespace: ->
not WhitespaceRegex.test(@value)
matchesScopeSelector: (selector) ->
targetClasses = selector.replace(StartDotRegex, '').split('.')
_.any @scopes, (scope) ->
scopeClasses = scope.split('.')
_.isSubset(targetClasses, scopeClasses)
hasLeadingWhitespace: ->
@firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0
hasTrailingWhitespace: ->
@firstTrailingWhitespaceIndex? and @firstTrailingWhitespaceIndex < @value.length

View File

@@ -0,0 +1,122 @@
{Point} = require 'text-buffer'
module.exports =
class TokenizedBufferIterator
constructor: (@tokenizedBuffer, @grammarRegistry) ->
@openTags = null
@closeTags = null
@containingTags = null
seek: (position) ->
@openTags = []
@closeTags = []
@tagIndex = null
currentLine = @tokenizedBuffer.tokenizedLineForRow(position.row)
@currentTags = currentLine.tags
@currentLineOpenTags = currentLine.openScopes
@currentLineLength = currentLine.text.length
@containingTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id)
currentColumn = 0
for tag, index in @currentTags
if tag >= 0
if currentColumn >= position.column and @isAtTagBoundary()
@tagIndex = index
break
else
currentColumn += tag
@containingTags.pop() while @closeTags.shift()
@containingTags.push(tag) while tag = @openTags.shift()
else
scopeName = @grammarRegistry.scopeForId(tag)
if tag % 2 is 0
if @openTags.length > 0
@tagIndex = index
break
else
@closeTags.push(scopeName)
else
@openTags.push(scopeName)
@tagIndex ?= @currentTags.length
@position = Point(position.row, Math.min(@currentLineLength, currentColumn))
@containingTags.slice()
moveToSuccessor: ->
@containingTags.pop() for tag in @closeTags
@containingTags.push(tag) for tag in @openTags
@openTags = []
@closeTags = []
loop
if @tagIndex is @currentTags.length
if @isAtTagBoundary()
break
else
if @shouldMoveToNextLine
@moveToNextLine()
@openTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id)
@shouldMoveToNextLine = false
else if @nextLineHasMismatchedContainingTags()
@closeTags = @containingTags.slice().reverse()
@containingTags = []
@shouldMoveToNextLine = true
else
return false unless @moveToNextLine()
else
tag = @currentTags[@tagIndex]
if tag >= 0
if @isAtTagBoundary()
break
else
@position = Point(@position.row, Math.min(@currentLineLength, @position.column + @currentTags[@tagIndex]))
else
scopeName = @grammarRegistry.scopeForId(tag)
if tag % 2 is 0
if @openTags.length > 0
break
else
@closeTags.push(scopeName)
else
@openTags.push(scopeName)
@tagIndex++
true
getPosition: ->
@position
getCloseTags: ->
@closeTags.slice()
getOpenTags: ->
@openTags.slice()
###
Section: Private Methods
###
nextLineHasMismatchedContainingTags: ->
if line = @tokenizedBuffer.tokenizedLineForRow(@position.row + 1)
return true if line.openScopes.length isnt @containingTags.length
for i in [0...@containingTags.length] by 1
if @containingTags[i] isnt @grammarRegistry.scopeForId(line.openScopes[i])
return true
false
else
false
moveToNextLine: ->
@position = Point(@position.row + 1, 0)
if tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(@position.row)
@currentTags = tokenizedLine.tags
@currentLineLength = tokenizedLine.text.length
@currentLineOpenTags = tokenizedLine.openScopes
@tagIndex = 0
true
else
false
isAtTagBoundary: ->
@closeTags.length > 0 or @openTags.length > 0

View File

@@ -7,6 +7,7 @@ TokenizedLine = require './tokenized-line'
TokenIterator = require './token-iterator'
Token = require './token'
ScopeDescriptor = require './scope-descriptor'
TokenizedBufferIterator = require './tokenized-buffer-iterator'
module.exports =
class TokenizedBuffer extends Model
@@ -29,14 +30,13 @@ class TokenizedBuffer extends Model
state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath)
state.config = atomEnvironment.config
state.grammarRegistry = atomEnvironment.grammars
state.packageManager = atomEnvironment.packages
state.assert = atomEnvironment.assert
new this(state)
constructor: (params) ->
{
@buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config,
@grammarRegistry, @packageManager, @assert, grammarScopeName
@buffer, @tabLength, @largeFileMode, @config,
@grammarRegistry, @assert, grammarScopeName
} = params
@emitter = new Emitter
@@ -58,13 +58,24 @@ class TokenizedBuffer extends Model
destroyed: ->
@disposables.dispose()
buildIterator: ->
new TokenizedBufferIterator(this, @grammarRegistry)
getInvalidatedRanges: ->
if @invalidatedRange?
[@invalidatedRange]
else
[]
onDidInvalidateRange: (fn) ->
@emitter.on 'did-invalidate-range', fn
serialize: ->
state = {
deserializer: 'TokenizedBuffer'
bufferPath: @buffer.getPath()
bufferId: @buffer.getId()
tabLength: @tabLength
ignoreInvisibles: @ignoreInvisibles
largeFileMode: @largeFileMode
}
state.grammarScopeName = @grammar?.scopeName unless @buffer.getPath()
@@ -105,28 +116,18 @@ class TokenizedBuffer extends Model
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
@disposables.add(@grammarUpdateDisposable)
scopeOptions = {scope: @rootScopeDescriptor}
@configSettings =
tabLength: @config.get('editor.tabLength', scopeOptions)
invisibles: @config.get('editor.invisibles', scopeOptions)
showInvisibles: @config.get('editor.showInvisibles', scopeOptions)
@configSettings = {tabLength: @config.get('editor.tabLength', {scope: @rootScopeDescriptor})}
if @configSubscriptions?
@configSubscriptions.dispose()
@disposables.remove(@configSubscriptions)
@configSubscriptions = new CompositeDisposable
@configSubscriptions.add @config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) =>
@configSubscriptions.add @config.onDidChange 'editor.tabLength', {scope: @rootScopeDescriptor}, ({newValue}) =>
@configSettings.tabLength = newValue
@retokenizeLines()
['invisibles', 'showInvisibles'].forEach (key) =>
@configSubscriptions.add @config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) =>
oldInvisibles = @getInvisiblesToShow()
@configSettings[key] = newValue
@retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles)
@disposables.add(@configSubscriptions)
@retokenizeLines()
@packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used")
@emitter.emit 'did-change-grammar', grammar
getGrammarSelectionContent: ->
@@ -163,13 +164,6 @@ class TokenizedBuffer extends Model
return if tabLength is @tabLength
@tabLength = tabLength
@retokenizeLines()
setIgnoreInvisibles: (ignoreInvisibles) ->
if ignoreInvisibles isnt @ignoreInvisibles
@ignoreInvisibles = ignoreInvisibles
if @configSettings.showInvisibles and @configSettings.invisibles?
@retokenizeLines()
tokenizeInBackground: ->
return if not @visible or @pendingChunk or not @isAlive()
@@ -212,6 +206,7 @@ class TokenizedBuffer extends Model
event = {start: startRow, end: endRow, delta: 0}
@emitter.emit 'did-change', event
@emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0))
if @firstInvalidRow()?
@tokenizeInBackground()
@@ -262,26 +257,15 @@ class TokenizedBuffer extends Model
newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start))
_.spliceWithArray(@tokenizedLines, start, end - start + 1, newTokenizedLines)
start = @retokenizeWhitespaceRowsIfIndentLevelChanged(start - 1, -1)
end = @retokenizeWhitespaceRowsIfIndentLevelChanged(newRange.end.row + 1, 1) - delta
newEndStack = @stackForRow(end + delta)
if newEndStack and not _.isEqual(newEndStack, previousEndStack)
@invalidateRow(end + delta + 1)
@invalidatedRange = Range(start, end)
event = {start, end, delta, bufferChange: e}
@emitter.emit 'did-change', event
retokenizeWhitespaceRowsIfIndentLevelChanged: (row, increment) ->
line = @tokenizedLineForRow(row)
if line?.isOnlyWhitespace() and @indentLevelForRow(row) isnt line.indentLevel
while line?.isOnlyWhitespace()
@tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row))
row += increment
line = @tokenizedLineForRow(row)
row - increment
isFoldableAtRow: (row) ->
if @largeFileMode
false
@@ -346,26 +330,16 @@ class TokenizedBuffer extends Model
openScopes = [@grammar.startIdForScope(@grammar.scopeName)]
text = @buffer.lineForRow(row)
tags = [text.length]
tabLength = @getTabLength()
indentLevel = @indentLevelForRow(row)
lineEnding = @buffer.lineEndingForRow(row)
new TokenizedLine({openScopes, text, tags, tabLength, indentLevel, invisibles: @getInvisiblesToShow(), lineEnding, @tokenIterator})
new TokenizedLine({openScopes, text, tags, lineEnding, @tokenIterator})
buildTokenizedLineForRow: (row, ruleStack, openScopes) ->
@buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes)
buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
lineEnding = @buffer.lineEndingForRow(row)
tabLength = @getTabLength()
indentLevel = @indentLevelForRow(row)
{tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, indentLevel, invisibles: @getInvisiblesToShow(), @tokenIterator})
getInvisiblesToShow: ->
if @configSettings.showInvisibles and not @ignoreInvisibles
@configSettings.invisibles
else
null
new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator})
tokenizedLineForRow: (bufferRow) ->
if 0 <= bufferRow < @tokenizedLines.length
@@ -406,6 +380,7 @@ class TokenizedBuffer extends Model
filePath: @buffer.getPath()
fileContents: @buffer.getText()
}
break
scopes
indentLevelForRow: (bufferRow) ->

View File

@@ -1,187 +1,18 @@
_ = require 'underscore-plus'
{isPairedCharacter, isCJKCharacter} = require './text-utils'
Token = require './token'
{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
NonWhitespaceRegex = /\S/
LeadingWhitespaceRegex = /^\s*/
TrailingWhitespaceRegex = /\s*$/
RepeatedSpaceRegex = /[ ]/g
CommentScopeRegex = /(\b|\.)comment/
TabCharCode = 9
SpaceCharCode = 32
SpaceString = ' '
TabStringsByLength = {
1: ' '
2: ' '
3: ' '
4: ' '
}
idCounter = 1
getTabString = (length) ->
TabStringsByLength[length] ?= buildTabString(length)
buildTabString = (length) ->
string = SpaceString
string += SpaceString for i in [1...length] by 1
string
module.exports =
class TokenizedLine
endOfLineInvisibles: null
lineIsWhitespaceOnly: false
firstNonWhitespaceIndex: 0
constructor: (properties) ->
@id = idCounter++
return unless properties?
@specialTokens = {}
{@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties
{@startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles} = properties
@startBufferColumn ?= 0
@bufferDelta = @text.length
@transformContent()
@buildEndOfLineInvisibles() if @invisibles? and @lineEnding?
transformContent: ->
text = ''
bufferColumn = 0
screenColumn = 0
tokenIndex = 0
tokenOffset = 0
firstNonWhitespaceColumn = null
lastNonWhitespaceColumn = null
substringStart = 0
substringEnd = 0
while bufferColumn < @text.length
# advance to next token if we've iterated over its length
if tokenOffset is @tags[tokenIndex]
tokenIndex++
tokenOffset = 0
# advance to next token tag
tokenIndex++ while @tags[tokenIndex] < 0
charCode = @text.charCodeAt(bufferColumn)
# split out unicode surrogate pairs
if isPairedCharacter(@text, bufferColumn)
prefix = tokenOffset
suffix = @tags[tokenIndex] - tokenOffset - 2
i = tokenIndex
@tags.splice(i, 1)
@tags.splice(i++, 0, prefix) if prefix > 0
@tags.splice(i++, 0, 2)
@tags.splice(i, 0, suffix) if suffix > 0
firstNonWhitespaceColumn ?= screenColumn
lastNonWhitespaceColumn = screenColumn + 1
substringEnd += 2
screenColumn += 2
bufferColumn += 2
tokenIndex++ if prefix > 0
@specialTokens[tokenIndex] = PairedCharacter
tokenIndex++
tokenOffset = 0
# split out leading soft tabs
else if charCode is SpaceCharCode
if firstNonWhitespaceColumn?
substringEnd += 1
else
if (screenColumn + 1) % @tabLength is 0
suffix = @tags[tokenIndex] - @tabLength
if suffix >= 0
@specialTokens[tokenIndex] = SoftTab
@tags.splice(tokenIndex, 1, @tabLength)
@tags.splice(tokenIndex + 1, 0, suffix) if suffix > 0
if @invisibles?.space
if substringEnd > substringStart
text += @text.substring(substringStart, substringEnd)
substringStart = substringEnd
text += @invisibles.space
substringStart += 1
substringEnd += 1
screenColumn++
bufferColumn++
tokenOffset++
# expand hard tabs to the next tab stop
else if charCode is TabCharCode
if substringEnd > substringStart
text += @text.substring(substringStart, substringEnd)
substringStart = substringEnd
tabLength = @tabLength - (screenColumn % @tabLength)
if @invisibles?.tab
text += @invisibles.tab
text += getTabString(tabLength - 1) if tabLength > 1
else
text += getTabString(tabLength)
substringStart += 1
substringEnd += 1
prefix = tokenOffset
suffix = @tags[tokenIndex] - tokenOffset - 1
i = tokenIndex
@tags.splice(i, 1)
@tags.splice(i++, 0, prefix) if prefix > 0
@tags.splice(i++, 0, tabLength)
@tags.splice(i, 0, suffix) if suffix > 0
screenColumn += tabLength
bufferColumn++
tokenIndex++ if prefix > 0
@specialTokens[tokenIndex] = HardTab
tokenIndex++
tokenOffset = 0
# continue past any other character
else
firstNonWhitespaceColumn ?= screenColumn
lastNonWhitespaceColumn = screenColumn
substringEnd += 1
screenColumn++
bufferColumn++
tokenOffset++
if substringEnd > substringStart
unless substringStart is 0 and substringEnd is @text.length
text += @text.substring(substringStart, substringEnd)
@text = text
else
@text = text
@firstNonWhitespaceIndex = firstNonWhitespaceColumn
if lastNonWhitespaceColumn?
if lastNonWhitespaceColumn + 1 < @text.length
@firstTrailingWhitespaceIndex = lastNonWhitespaceColumn + 1
if @invisibles?.space
@text =
@text.substring(0, @firstTrailingWhitespaceIndex) +
@text.substring(@firstTrailingWhitespaceIndex)
.replace(RepeatedSpaceRegex, @invisibles.space)
else
@lineIsWhitespaceOnly = true
@firstTrailingWhitespaceIndex = 0
{@openScopes, @text, @tags, @ruleStack, @tokenIterator} = properties
getTokenIterator: -> @tokenIterator.reset(this, arguments...)
@@ -190,285 +21,21 @@ class TokenizedLine
tokens = []
while iterator.next()
properties = {
tokens.push(new Token({
value: iterator.getText()
scopes: iterator.getScopes().slice()
isAtomic: iterator.isAtomic()
isHardTab: iterator.isHardTab()
hasPairedCharacter: iterator.isPairedCharacter()
isSoftWrapIndentation: iterator.isSoftWrapIndentation()
}
if iterator.isHardTab()
properties.bufferDelta = 1
properties.hasInvisibleCharacters = true if @invisibles?.tab
if iterator.getScreenStart() < @firstNonWhitespaceIndex
properties.firstNonWhitespaceIndex =
Math.min(@firstNonWhitespaceIndex, iterator.getScreenEnd()) - iterator.getScreenStart()
properties.hasInvisibleCharacters = true if @invisibles?.space
if @lineEnding? and iterator.getScreenEnd() > @firstTrailingWhitespaceIndex
properties.firstTrailingWhitespaceIndex =
Math.max(0, @firstTrailingWhitespaceIndex - iterator.getScreenStart())
properties.hasInvisibleCharacters = true if @invisibles?.space
tokens.push(new Token(properties))
}))
tokens
copy: ->
copy = new TokenizedLine
copy.tokenIterator = @tokenIterator
copy.openScopes = @openScopes
copy.text = @text
copy.tags = @tags
copy.specialTokens = @specialTokens
copy.startBufferColumn = @startBufferColumn
copy.bufferDelta = @bufferDelta
copy.ruleStack = @ruleStack
copy.lineEnding = @lineEnding
copy.invisibles = @invisibles
copy.endOfLineInvisibles = @endOfLineInvisibles
copy.indentLevel = @indentLevel
copy.tabLength = @tabLength
copy.firstNonWhitespaceIndex = @firstNonWhitespaceIndex
copy.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex
copy.fold = @fold
copy
# This clips a given screen column to a valid column that's within the line
# and not in the middle of any atomic tokens.
#
# column - A {Number} representing the column to clip
# options - A hash with the key clip. Valid values for this key:
# 'closest' (default): clip to the closest edge of an atomic token.
# 'forward': clip to the forward edge.
# 'backward': clip to the backward edge.
#
# Returns a {Number} representing the clipped column.
clipScreenColumn: (column, options={}) ->
return 0 if @tags.length is 0
{clip} = options
column = Math.min(column, @getMaxScreenColumn())
tokenStartColumn = 0
iterator = @getTokenIterator()
while iterator.next()
break if iterator.getScreenEnd() > column
if iterator.isSoftWrapIndentation()
iterator.next() while iterator.isSoftWrapIndentation()
iterator.getScreenStart()
else if iterator.isAtomic() and iterator.getScreenStart() < column
if clip is 'forward'
iterator.getScreenEnd()
else if clip is 'backward'
iterator.getScreenStart()
else #'closest'
if column > ((iterator.getScreenStart() + iterator.getScreenEnd()) / 2)
iterator.getScreenEnd()
else
iterator.getScreenStart()
else
column
screenColumnForBufferColumn: (targetBufferColumn, options) ->
iterator = @getTokenIterator()
while iterator.next()
tokenBufferStart = iterator.getBufferStart()
tokenBufferEnd = iterator.getBufferEnd()
if tokenBufferStart <= targetBufferColumn < tokenBufferEnd
overshoot = targetBufferColumn - tokenBufferStart
return Math.min(
iterator.getScreenStart() + overshoot,
iterator.getScreenEnd()
)
iterator.getScreenEnd()
bufferColumnForScreenColumn: (targetScreenColumn) ->
iterator = @getTokenIterator()
while iterator.next()
tokenScreenStart = iterator.getScreenStart()
tokenScreenEnd = iterator.getScreenEnd()
if tokenScreenStart <= targetScreenColumn < tokenScreenEnd
overshoot = targetScreenColumn - tokenScreenStart
return Math.min(
iterator.getBufferStart() + overshoot,
iterator.getBufferEnd()
)
iterator.getBufferEnd()
getMaxScreenColumn: ->
if @fold
0
else
@text.length
getMaxBufferColumn: ->
@startBufferColumn + @bufferDelta
# Given a boundary column, finds the point where this line would wrap.
#
# maxColumn - The {Number} where you want soft wrapping to occur
#
# Returns a {Number} representing the `line` position where the wrap would take place.
# Returns `null` if a wrap wouldn't occur.
findWrapColumn: (maxColumn) ->
return unless maxColumn?
return unless @text.length > maxColumn
if /\s/.test(@text[maxColumn])
# search forward for the start of a word past the boundary
for column in [maxColumn..@text.length]
return column if /\S/.test(@text[column])
return @text.length
else if isCJKCharacter(@text[maxColumn])
maxColumn
else
# search backward for the start of the word on the boundary
for column in [maxColumn..@firstNonWhitespaceIndex]
if /\s/.test(@text[column]) or isCJKCharacter(@text[column])
return column + 1
return maxColumn
softWrapAt: (column, hangingIndent) ->
return [null, this] if column is 0
leftText = @text.substring(0, column)
rightText = @text.substring(column)
leftTags = []
rightTags = []
leftSpecialTokens = {}
rightSpecialTokens = {}
rightOpenScopes = @openScopes.slice()
screenColumn = 0
for tag, index in @tags
# tag represents a token
if tag >= 0
# token ends before the soft wrap column
if screenColumn + tag <= column
if specialToken = @specialTokens[index]
leftSpecialTokens[index] = specialToken
leftTags.push(tag)
screenColumn += tag
# token starts before and ends after the split column
else if screenColumn <= column
leftSuffix = column - screenColumn
rightPrefix = screenColumn + tag - column
leftTags.push(leftSuffix) if leftSuffix > 0
softWrapIndent = @indentLevel * @tabLength + (hangingIndent ? 0)
for i in [0...softWrapIndent] by 1
rightText = ' ' + rightText
remainingSoftWrapIndent = softWrapIndent
while remainingSoftWrapIndent > 0
indentToken = Math.min(remainingSoftWrapIndent, @tabLength)
rightSpecialTokens[rightTags.length] = SoftWrapIndent
rightTags.push(indentToken)
remainingSoftWrapIndent -= indentToken
rightTags.push(rightPrefix) if rightPrefix > 0
screenColumn += tag
# token is after split column
else
if specialToken = @specialTokens[index]
rightSpecialTokens[rightTags.length] = specialToken
rightTags.push(tag)
# tag represents the start of a scope
else if (tag % 2) is -1
if screenColumn < column
leftTags.push(tag)
rightOpenScopes.push(tag)
else
rightTags.push(tag)
# tag represents the end of a scope
else
if screenColumn <= column
leftTags.push(tag)
rightOpenScopes.pop()
else
rightTags.push(tag)
splitBufferColumn = @bufferColumnForScreenColumn(column)
leftFragment = new TokenizedLine
leftFragment.tokenIterator = @tokenIterator
leftFragment.openScopes = @openScopes
leftFragment.text = leftText
leftFragment.tags = leftTags
leftFragment.specialTokens = leftSpecialTokens
leftFragment.startBufferColumn = @startBufferColumn
leftFragment.bufferDelta = splitBufferColumn - @startBufferColumn
leftFragment.ruleStack = @ruleStack
leftFragment.invisibles = @invisibles
leftFragment.lineEnding = null
leftFragment.indentLevel = @indentLevel
leftFragment.tabLength = @tabLength
leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex)
leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex)
rightFragment = new TokenizedLine
rightFragment.tokenIterator = @tokenIterator
rightFragment.openScopes = rightOpenScopes
rightFragment.text = rightText
rightFragment.tags = rightTags
rightFragment.specialTokens = rightSpecialTokens
rightFragment.startBufferColumn = splitBufferColumn
rightFragment.bufferDelta = @startBufferColumn + @bufferDelta - splitBufferColumn
rightFragment.ruleStack = @ruleStack
rightFragment.invisibles = @invisibles
rightFragment.lineEnding = @lineEnding
rightFragment.indentLevel = @indentLevel
rightFragment.tabLength = @tabLength
rightFragment.endOfLineInvisibles = @endOfLineInvisibles
rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent)
rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent)
[leftFragment, rightFragment]
isSoftWrapped: ->
@lineEnding is null
isColumnInsideSoftWrapIndentation: (targetColumn) ->
targetColumn < @getSoftWrapIndentationDelta()
getSoftWrapIndentationDelta: ->
delta = 0
for tag, index in @tags
if tag >= 0
if @specialTokens[index] is SoftWrapIndent
delta += tag
else
break
delta
hasOnlySoftWrapIndentation: ->
@getSoftWrapIndentationDelta() is @text.length
tokenAtBufferColumn: (bufferColumn) ->
@tokens[@tokenIndexAtBufferColumn(bufferColumn)]
tokenIndexAtBufferColumn: (bufferColumn) ->
delta = 0
column = 0
for token, index in @tokens
delta += token.bufferDelta
return index if delta > bufferColumn
column += token.value.length
return index if column > bufferColumn
index - 1
tokenStartColumnForBufferColumn: (bufferColumn) ->
@@ -479,17 +46,6 @@ class TokenizedLine
delta = nextDelta
delta
buildEndOfLineInvisibles: ->
@endOfLineInvisibles = []
{cr, eol} = @invisibles
switch @lineEnding
when '\r\n'
@endOfLineInvisibles.push(cr) if cr
@endOfLineInvisibles.push(eol) if eol
when '\n'
@endOfLineInvisibles.push(eol) if eol
isComment: ->
return @isCommentLine if @isCommentLine?
@@ -505,9 +61,6 @@ class TokenizedLine
break
@isCommentLine
isOnlyWhitespace: ->
@lineIsWhitespaceOnly
tokenAtIndex: (index) ->
@tokens[index]

View File

@@ -4,6 +4,7 @@ path = require 'path'
{join} = path
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
fs = require 'fs-plus'
{Directory} = require 'pathwatcher'
DefaultDirectorySearcher = require './default-directory-searcher'
Model = require './model'
TextEditor = require './text-editor'
@@ -550,9 +551,17 @@ class Workspace extends Model
@project.bufferForPath(filePath, options).then (buffer) =>
editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options))
disposable = atom.textEditors.add(editor)
editor.onDidDestroy -> disposable.dispose()
grammarSubscription = editor.observeGrammar(@handleGrammarUsed.bind(this))
editor.onDidDestroy ->
grammarSubscription.dispose()
disposable.dispose()
editor
handleGrammarUsed: (grammar) ->
return unless grammar?
@packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used")
# Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`.
#
# * `object` An {Object} you want to perform the check against.
@@ -564,8 +573,7 @@ class Workspace extends Model
# Returns a {TextEditor}.
buildTextEditor: (params) ->
params = _.extend({
@config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
@grammarRegistry, @project, @assert, @applicationDelegate
@config, @clipboard, @grammarRegistry, @assert
}, params)
new TextEditor(params)
@@ -1079,3 +1087,22 @@ class Workspace extends Model
inProcessFinished = true
checkFinished()
checkoutHeadRevision: (editor) ->
if editor.getPath()
checkoutHead = =>
@project.repositoryForDirectory(new Directory(editor.getDirectoryPath()))
.then (repository) ->
repository?.async.checkoutHeadForEditor(editor)
if @config.get('editor.confirmCheckoutHeadRevision')
@applicationDelegate.confirm
message: 'Confirm Checkout HEAD Revision'
detailedMessage: "Are you sure you want to discard all changes to \"#{editor.getFileName()}\" since the last Git commit?"
buttons:
OK: checkoutHead
Cancel: null
else
checkoutHead()
else
Promise.resolve(false)

View File

@@ -15,7 +15,7 @@ atom-text-editor[mini] {
}
atom-overlay {
position: absolute;
position: fixed;
display: block;
z-index: 4;
}