Merge remote-tracking branch 'origin/master' into cj-windows-build-instructions-update

Conflicts:
	docs/build-instructions/windows.md
This commit is contained in:
probablycorey
2014-05-20 09:51:08 -07:00
43 changed files with 822 additions and 468 deletions

View File

@@ -45,8 +45,7 @@ in the proper package's repository.
* Beware of platform differences
* Use `require('atom').fs.getHomeDirectory()` to get the home directory.
* Use `path.join()` to concatenate filenames.
* Temporary directory is not `/tmp` on Windows, use `os.tmpdir()` when
possible
* Use `os.tmpdir()` instead of `/tmp
## Git Commit Messages
* Use the present tense
@@ -57,6 +56,12 @@ in the proper package's repository.
* :non-potable_water: `:non-potable_water:` when plugging memory leaks
* :memo: `:memo:` when writing docs
* :penguin: `:penguin:` when fixing something on Linux
* :apple: `:apple:` when fixing something on Mac OS
* :bug: `:bug:` when fixing a bug
* :fire: `:fire:` when removing code or files
* :green_heart: `:green_heart:` when fixing the CI build
* :white_check_mark: `:white_check_mark:` when adding tests
* :lock: `:lock:` when dealing with security
## CoffeeScript Styleguide

View File

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

View File

@@ -70,16 +70,18 @@ elif [ $OS == 'Linux' ]; then
USR_DIRECTORY=$(readlink -f $(dirname $SCRIPT)/..)
ATOM_PATH="$USR_DIRECTORY/share/atom/atom"
[ -x "$ATOM_PATH" ] || ATOM_PATH='/tmp/atom-build/Atom/atom'
: ${TMPDIR:=/tmp}
[ -x "$ATOM_PATH" ] || ATOM_PATH="$TMPDIR/atom-build/Atom/atom"
if [ $EXPECT_OUTPUT ]; then
"$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@"
exit $?
else
(
nohup "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > /tmp/atom-nohup.out 2>&1
nohup "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > "$TMPDIR/atom-nohup.out" 2>&1
if [ $? -ne 0 ]; then
cat /tmp/atom-nohup.out
cat "$TMPDIR/atom-nohup.out"
exit $?
fi
) &

View File

@@ -36,35 +36,23 @@ module.exports = (grunt) ->
grunt.log.write = (args...) -> grunt.log
[major, minor, patch] = packageJson.version.split('.')
tmpDir = os.tmpdir()
appName = if process.platform is 'darwin' then 'Atom.app' else 'Atom'
buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build')
atomShellDownloadDir = path.join(os.tmpdir(), 'atom-cached-atom-shells')
symbolsDir = path.join(buildDir, 'Atom.breakpad.syms')
shellAppDir = path.join(buildDir, appName)
if process.platform is 'win32'
appName = 'Atom'
tmpDir = os.tmpdir()
buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build')
symbolsDir = path.join(buildDir, 'Atom.breakpad.syms')
shellAppDir = path.join(buildDir, appName)
contentsDir = shellAppDir
appDir = path.join(shellAppDir, 'resources', 'app')
atomShellDownloadDir = path.join(os.tmpdir(), 'atom-cached-atom-shells')
installDir = path.join(process.env.ProgramFiles, appName)
else if process.platform is 'darwin'
appName = 'Atom.app'
tmpDir = '/tmp'
buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build')
symbolsDir = path.join(buildDir, 'Atom.breakpad.syms')
shellAppDir = path.join(buildDir, appName)
contentsDir = path.join(shellAppDir, 'Contents')
appDir = path.join(contentsDir, 'Resources', 'app')
atomShellDownloadDir = '/tmp/atom-cached-atom-shells'
installDir = path.join('/Applications', appName)
else
appName = 'Atom'
tmpDir = '/tmp'
buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build')
symbolsDir = path.join(buildDir, 'Atom.breakpad.syms')
shellAppDir = path.join(buildDir, appName)
contentsDir = shellAppDir
appDir = path.join(shellAppDir, 'resources', 'app')
atomShellDownloadDir = '/tmp/atom-cached-atom-shells'
installDir = process.env.INSTALL_PREFIX ? '/usr/local'
coffeeConfig =

View File

@@ -5,7 +5,7 @@ module.exports = (grunt) ->
{rm} = require('./task-helpers')(grunt)
grunt.registerTask 'partial-clean', 'Delete some of the build files', ->
tmpdir = if process.platform is 'win32' then os.tmpdir() else '/tmp'
tmpdir = os.tmpdir()
rm grunt.config.get('atom.buildDir')
rm require('../src/coffee-cache').cacheDir

View File

@@ -10,8 +10,8 @@ keystrokes pass through elements with the class `.editor`:
```coffee
'.editor':
'cmd-delete': 'editor:backspace-to-beginning-of-line'
'alt-backspace': 'editor:backspace-to-beginning-of-word'
'cmd-delete': 'editor:delete-to-beginning-of-line'
'alt-backspace': 'editor:delete-to-beginning-of-word'
'ctrl-A': 'editor:select-to-first-character-of-line'
'ctrl-shift-e': 'editor:select-to-end-of-line'
'cmd-left': 'editor:move-to-first-character-of-line'
@@ -24,7 +24,7 @@ keystrokes pass through elements with the class `.editor`:
Beneath the first selector are several bindings, mapping specific *keystroke
patterns* to *commands*. When an element with the `.editor` class is focused and
`cmd-delete` is pressed, an custom DOM event called
`editor:backspace-to-beginning-of-line` is emitted on the `.editor` element.
`editor:delete-to-beginning-of-line` is emitted on the `.editor` element.
The second selector group also targets editors, but only if they don't have the
`.mini` class. In this example, the commands for code folding don't really make

View File

@@ -15,7 +15,7 @@ FreeBSD -RELEASE 64-bit is the recommended platform.
```sh
git clone https://github.com/atom/atom
cd atom
script/build # Creates application at /tmp/atom-build/Atom
script/build # Creates application at $TMPDIR/atom-build/Atom
sudo script/grunt install # Installs command to /usr/local/bin/atom
```

View File

@@ -15,9 +15,9 @@ Ubuntu LTS 12.04 64-bit is the recommended platform.
```sh
git clone https://github.com/atom/atom
cd atom
script/build # Creates application at /tmp/atom-build/Atom
script/build # Creates application at $TMPDIR/atom-build/Atom
sudo script/grunt install # Installs command to /usr/local/bin/atom
script/grunt mkdeb # Generates a .deb package at /tmp/atom-build
script/grunt mkdeb # Generates a .deb package at $TMPDIR/atom-build
```
## Troubleshooting

View File

@@ -13,6 +13,7 @@
```bat
# Use the `Git Shell` app which was installed by GitHub for Windows. Also Make
# sure you have logged into the GitHub for Windows GUI App.
cd C:\
git clone https://github.com/atom/atom/
cd atom
script\build

View File

@@ -103,16 +103,16 @@
'alt-shift-right': 'editor:select-to-end-of-word'
# Apple Specific
'cmd-backspace': 'editor:backspace-to-beginning-of-line'
'cmd-shift-backspace': 'editor:backspace-to-beginning-of-line'
'cmd-delete': 'editor:backspace-to-beginning-of-line'
'cmd-backspace': 'editor:delete-to-beginning-of-line'
'cmd-shift-backspace': 'editor:delete-to-beginning-of-line'
'cmd-delete': 'editor:delete-to-beginning-of-line'
'ctrl-A': 'editor:select-to-first-character-of-line'
'ctrl-E': 'editor:select-to-end-of-line'
'cmd-left': 'editor:move-to-first-character-of-line'
'cmd-right': 'editor:move-to-end-of-screen-line'
'cmd-shift-left': 'editor:select-to-first-character-of-line'
'cmd-shift-right': 'editor:select-to-end-of-line'
'alt-backspace': 'editor:backspace-to-beginning-of-word'
'alt-backspace': 'editor:delete-to-beginning-of-word'
'alt-delete': 'editor:delete-to-end-of-word'
'ctrl-a': 'editor:move-to-beginning-of-line'
'ctrl-e': 'editor:move-to-end-of-line'

View File

@@ -3,5 +3,5 @@
'alt-F': 'editor:select-to-end-of-word'
'alt-b': 'editor:move-to-beginning-of-word'
'alt-B': 'editor:select-to-beginning-of-word'
'alt-h': 'editor:backspace-to-beginning-of-word'
'alt-h': 'editor:delete-to-beginning-of-word'
'alt-d': 'editor:delete-to-end-of-word'

View File

@@ -65,10 +65,12 @@
'ctrl-right': 'editor:move-to-end-of-word'
'ctrl-shift-left': 'editor:select-to-beginning-of-word'
'ctrl-shift-right': 'editor:select-to-end-of-word'
'ctrl-backspace': 'editor:backspace-to-beginning-of-word'
'ctrl-backspace': 'editor:delete-to-beginning-of-word'
'ctrl-delete': 'editor:delete-to-end-of-word'
'ctrl-home': 'core:move-to-top'
'ctrl-end': 'core:move-to-bottom'
'ctrl-shift-home': 'core:select-to-top'
'ctrl-shift-end': 'core:select-to-bottom'
# Sublime Parity
'ctrl-a': 'core:select-all'

View File

@@ -68,7 +68,7 @@
'ctrl-right': 'editor:move-to-end-of-word'
'ctrl-shift-left': 'editor:select-to-beginning-of-word'
'ctrl-shift-right': 'editor:select-to-end-of-word'
'ctrl-backspace': 'editor:backspace-to-beginning-of-word'
'ctrl-backspace': 'editor:delete-to-beginning-of-word'
'ctrl-delete': 'editor:delete-to-end-of-word'
'ctrl-home': 'core:move-to-top'
'ctrl-end': 'core:move-to-bottom'

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "0.96.0",
"version": "0.97.0",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/browser/main.js",
"repository": {
@@ -17,7 +17,7 @@
"url": "http://github.com/atom/atom/raw/master/LICENSE.md"
}
],
"atomShellVersion": "0.12.4",
"atomShellVersion": "0.12.5",
"dependencies": {
"async": "0.2.6",
"atom-keymap": "^0.19.0",
@@ -58,7 +58,7 @@
"temp": "0.5.0",
"text-buffer": "^2.2.2",
"theorist": "^1",
"underscore-plus": "^1.2.1",
"underscore-plus": "^1.2.2",
"vm-compatibility-layer": "0.1.0"
},
"packageDependencies": {
@@ -70,21 +70,21 @@
"solarized-dark-syntax": "0.14.0",
"solarized-light-syntax": "0.7.0",
"archive-view": "0.31.0",
"autocomplete": "0.27.0",
"autocomplete": "0.28.0",
"autoflow": "0.17.0",
"autosave": "0.13.0",
"background-tips": "0.13.0",
"bookmarks": "0.22.0",
"bracket-matcher": "0.33.0",
"bracket-matcher": "0.36.0",
"command-palette": "0.21.0",
"deprecation-cop": "0.5.0",
"dev-live-reload": "0.30.0",
"exception-reporting": "0.17.0",
"feedback": "0.33.0",
"find-and-replace": "0.101.0",
"fuzzy-finder": "0.50.0",
"find-and-replace": "0.106.0",
"fuzzy-finder": "0.51.0",
"git-diff": "0.28.0",
"go-to-line": "0.19.0",
"go-to-line": "0.21.0",
"grammar-selector": "0.26.0",
"image-view": "0.33.0",
"keybinding-resolver": "0.17.0",
@@ -93,16 +93,16 @@
"metrics": "0.32.0",
"open-on-github": "0.28.0",
"package-generator": "0.30.0",
"release-notes": "0.29.0",
"settings-view": "0.114.0",
"release-notes": "0.31.0",
"settings-view": "0.115.0",
"snippets": "0.43.0",
"spell-check": "0.35.0",
"status-bar": "0.40.0",
"styleguide": "0.29.0",
"symbols-view": "0.52.0",
"tabs": "0.39.0",
"tabs": "0.40.0",
"timecop": "0.18.0",
"tree-view": "0.92.0",
"tree-view": "0.93.0",
"update-package-dependencies": "0.6.0",
"welcome": "0.14.0",
"whitespace": "0.22.0",
@@ -110,10 +110,10 @@
"language-c": "0.15.0",
"language-coffee-script": "0.22.0",
"language-css": "0.16.0",
"language-gfm": "0.37.0",
"language-gfm": "0.38.0",
"language-git": "0.9.0",
"language-go": "0.11.0",
"language-html": "0.21.0",
"language-go": "0.12.0",
"language-html": "0.22.0",
"language-hyperlink": "0.9.0",
"language-java": "0.10.0",
"language-javascript": "0.26.0",
@@ -124,17 +124,17 @@
"language-perl": "0.8.0",
"language-php": "0.14.0",
"language-property-list": "0.7.0",
"language-python": "0.15.0",
"language-ruby": "0.24.0",
"language-python": "0.17.0",
"language-ruby": "0.25.0",
"language-ruby-on-rails": "0.14.0",
"language-sass": "0.10.0",
"language-sass": "0.11.0",
"language-shellscript": "0.8.0",
"language-source": "0.7.0",
"language-sql": "0.8.0",
"language-text": "0.6.0",
"language-todo": "0.10.0",
"language-toml": "0.12.0",
"language-xml": "0.12.0",
"language-xml": "0.13.0",
"language-yaml": "0.6.0"
},
"private": true,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

@@ -8,7 +8,7 @@ var productName = require('../package.json').productName;
process.chdir(path.dirname(__dirname));
var home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
var tmpdir = process.platform === 'win32' ? os.tmpdir() : '/tmp';
var tmpdir = os.tmpdir();
// Windows: Use START as a way to ignore error if Atom.exe isnt running
var killatom = process.platform === 'win32' ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';

View File

@@ -1,6 +1,8 @@
#!/bin/bash
# mkdeb version control-file-path deb-file-path
set -e
SCRIPT=`readlink -f "$0"`
ROOT=`readlink -f $(dirname $SCRIPT)/..`
cd $ROOT
@@ -12,6 +14,7 @@ ICON_FILE="$4"
DEB_PATH="$5"
TARGET_ROOT="`mktemp -d`"
chmod 755 "$TARGET_ROOT"
TARGET="$TARGET_ROOT/atom-$VERSION-amd64"
mkdir -p "$TARGET/usr"

View File

@@ -510,20 +510,20 @@ describe "the `atom` global", ->
# enabling of theme
pack = atom.packages.enablePackage(packageName)
activatedPackages = null
waitsFor ->
activatedPackages = atom.packages.getActivePackages()
activatedPackages.length > 0
pack in atom.packages.getActivePackages()
runs ->
expect(activatedPackages).toContain(pack)
expect(atom.config.get('core.themes')).toContain packageName
expect(atom.config.get('core.disabledPackages')).not.toContain packageName
# disabling of theme
pack = atom.packages.disablePackage(packageName)
activatedPackages = atom.packages.getActivePackages()
expect(activatedPackages).not.toContain(pack)
waitsFor ->
not (pack in atom.packages.getActivePackages())
runs ->
expect(atom.config.get('core.themes')).not.toContain packageName
expect(atom.config.get('core.themes')).not.toContain packageName
expect(atom.config.get('core.disabledPackages')).not.toContain packageName

View File

@@ -5,6 +5,10 @@ fs = require 'fs-plus'
describe "Config", ->
dotAtomPath = path.join(temp.dir, 'dot-atom-dir')
dotAtomPath = null
beforeEach ->
dotAtomPath = temp.path('dot-atom-dir')
describe ".get(keyPath)", ->
it "allows a key path's value to be read", ->
@@ -258,8 +262,10 @@ describe "Config", ->
describe ".initializeConfigDirectory()", ->
beforeEach ->
if fs.existsSync(dotAtomPath)
fs.removeSync(dotAtomPath)
atom.config.configDirPath = dotAtomPath
expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy()
afterEach ->
fs.removeSync(dotAtomPath)

View File

@@ -4,9 +4,11 @@ nbsp = String.fromCharCode(160)
describe "EditorComponent", ->
[contentNode, editor, wrapperView, component, node, verticalScrollbarNode, horizontalScrollbarNode] = []
[lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame] = []
[lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame, lineOverdrawMargin] = []
beforeEach ->
lineOverdrawMargin = 2
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
@@ -29,7 +31,7 @@ describe "EditorComponent", ->
contentNode = document.querySelector('#jasmine-content')
contentNode.style.width = '1000px'
wrapperView = new ReactEditorView(editor)
wrapperView = new ReactEditorView(editor, {lineOverdrawMargin})
wrapperView.attachToDom()
{component} = wrapperView
component.setLineHeight(1.3)
@@ -41,57 +43,62 @@ describe "EditorComponent", ->
verticalScrollbarNode = node.querySelector('.vertical-scrollbar')
horizontalScrollbarNode = node.querySelector('.horizontal-scrollbar')
node.style.height = editor.getLineCount() * lineHeightInPixels + 'px'
node.style.width = '1000px'
component.measureHeightAndWidth()
afterEach ->
contentNode.style.width = ''
describe "line rendering", ->
it "renders only the currently-visible lines", ->
it "renders the currently-visible lines plus the overdraw margin", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
component.measureHeightAndWidth()
lines = node.querySelectorAll('.line')
expect(lines.length).toBe 6
expect(lines[0].textContent).toBe editor.lineForScreenRow(0).text
expect(lines[5].textContent).toBe editor.lineForScreenRow(5).text
linesNode = node.querySelector('.lines')
expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
expect(node.querySelectorAll('.line').length).toBe 6 + 2 # no margin above
expect(component.lineNodeForScreenRow(0).textContent).toBe editor.lineForScreenRow(0).text
expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNodeForScreenRow(5).textContent).toBe editor.lineForScreenRow(5).text
expect(component.lineNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
expect(node.querySelector('.scroll-view-content').style['-webkit-transform']).toBe "translate3d(0px, #{-2.5 * lineHeightInPixels}px, 0)"
expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, #{-4.5 * lineHeightInPixels}px, 0px)"
expect(node.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below
expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNodeForScreenRow(2).textContent).toBe editor.lineForScreenRow(2).text
expect(component.lineNodeForScreenRow(9).offsetTop).toBe 9 * lineHeightInPixels
expect(component.lineNodeForScreenRow(9).textContent).toBe editor.lineForScreenRow(9).text
lineNodes = node.querySelectorAll('.line')
expect(lineNodes.length).toBe 6
expect(lineNodes[0].offsetTop).toBe 2 * lineHeightInPixels
expect(lineNodes[0].textContent).toBe editor.lineForScreenRow(2).text
expect(lineNodes[5].textContent).toBe editor.lineForScreenRow(7).text
it "updates absolute positions of subsequent lines when lines are inserted or removed", ->
it "updates the top position of subsequent lines when lines are inserted or removed", ->
editor.getBuffer().deleteRows(0, 1)
lineNodes = node.querySelectorAll('.line')
expect(lineNodes[0].offsetTop).toBe 0
expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels
expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
editor.getBuffer().insert([0, 0], '\n\n')
lineNodes = node.querySelectorAll('.line')
expect(lineNodes[0].offsetTop).toBe 0
expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels
expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels
expect(lineNodes[3].offsetTop).toBe 3 * lineHeightInPixels
expect(lineNodes[4].offsetTop).toBe 4 * lineHeightInPixels
expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels
expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels
expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels
describe "when indent guides are enabled", ->
beforeEach ->
component.setShowIndentGuide(true)
it "adds an 'indent-guide' class to spans comprising the leading whitespace", ->
lines = node.querySelectorAll('.line')
line1LeafNodes = getLeafNodes(lines[1])
line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1))
expect(line1LeafNodes[0].textContent).toBe ' '
expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true
expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false
line2LeafNodes = getLeafNodes(lines[2])
line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2))
expect(line2LeafNodes[0].textContent).toBe ' '
expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true
expect(line2LeafNodes[1].textContent).toBe ' '
@@ -101,8 +108,7 @@ describe "EditorComponent", ->
it "renders leading whitespace spans with the 'indent-guide' class for empty lines", ->
editor.getBuffer().insert([1, Infinity], '\n')
lines = node.querySelectorAll('.line')
line2LeafNodes = getLeafNodes(lines[2])
line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2))
expect(line2LeafNodes.length).toBe 3
expect(line2LeafNodes[0].textContent).toBe ' '
@@ -114,8 +120,7 @@ describe "EditorComponent", ->
it "renders indent guides correctly on lines containing only whitespace", ->
editor.getBuffer().insert([1, Infinity], '\n ')
lines = node.querySelectorAll('.line')
line2LeafNodes = getLeafNodes(lines[2])
line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2))
expect(line2LeafNodes.length).toBe 3
expect(line2LeafNodes[0].textContent).toBe ' '
expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true
@@ -125,9 +130,8 @@ describe "EditorComponent", ->
expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true
it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", ->
editor.getBuffer().setText (" hi ")
lines = node.querySelectorAll('.line')
line0LeafNodes = getLeafNodes(lines[0])
editor.getBuffer().setText " hi "
line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0))
expect(line0LeafNodes[0].textContent).toBe ' '
expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true
expect(line0LeafNodes[1].textContent).toBe ' '
@@ -144,40 +148,39 @@ describe "EditorComponent", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
component.measureHeightAndWidth()
lines = node.querySelectorAll('.line-number')
expect(lines.length).toBe 6
expect(lines[0].textContent).toBe "#{nbsp}1"
expect(lines[5].textContent).toBe "#{nbsp}6"
expect(node.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number
expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1"
expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}6"
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
expect(node.querySelector('.line-numbers').style['-webkit-transform']).toBe "translate3d(0, #{-2.5 * lineHeightInPixels}px, 0)"
expect(node.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number
lineNumberNodes = node.querySelectorAll('.line-number')
expect(lineNumberNodes.length).toBe 6
expect(lineNumberNodes[0].offsetTop).toBe 2 * lineHeightInPixels
expect(lineNumberNodes[5].offsetTop).toBe 7 * lineHeightInPixels
expect(lineNumberNodes[0].textContent).toBe "#{nbsp}3"
expect(lineNumberNodes[5].textContent).toBe "#{nbsp}8"
expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3"
expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
return
expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}8"
expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 7 * lineHeightInPixels
it "updates absolute positions of subsequent line numbers when lines are inserted or removed", ->
it "updates the translation of subsequent line numbers when lines are inserted or removed", ->
editor.getBuffer().insert([0, 0], '\n\n')
lineNumberNodes = node.querySelectorAll('.line-number')
expect(lineNumberNodes[0].offsetTop).toBe 0
expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels
expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels
expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels
expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels
editor.getBuffer().insert([0, 0], '\n\n')
lineNumberNodes = node.querySelectorAll('.line-number')
expect(lineNumberNodes[0].offsetTop).toBe 0
expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels
expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels
expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels
expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0
expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels
expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 6 * lineHeightInPixels
it "renders • characters for soft-wrapped lines", ->
editor.setSoftWrap(true)
@@ -185,43 +188,50 @@ describe "EditorComponent", ->
node.style.width = 30 * charWidth + 'px'
component.measureHeightAndWidth()
lines = node.querySelectorAll('.line-number')
expect(lines.length).toBe 6
expect(lines[0].textContent).toBe "#{nbsp}1"
expect(lines[1].textContent).toBe "#{nbsp}"
expect(lines[2].textContent).toBe "#{nbsp}2"
expect(lines[3].textContent).toBe "#{nbsp}"
expect(lines[4].textContent).toBe "#{nbsp}3"
expect(lines[5].textContent).toBe "#{nbsp}"
expect(node.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line node
expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1"
expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}"
expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2"
expect(component.lineNumberNodeForScreenRow(3).textContent).toBe "#{nbsp}"
expect(component.lineNumberNodeForScreenRow(4).textContent).toBe "#{nbsp}3"
expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}"
it "pads line numbers to be right justified based on the maximum number of line number digits", ->
it "pads line numbers to be right-justified based on the maximum number of line number digits", ->
editor.getBuffer().setText([1..10].join('\n'))
lineNumberNodes = toArray(node.querySelectorAll('.line-number'))
for screenRow in [0..8]
expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}"
expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10"
for node, i in lineNumberNodes[0..8]
expect(node.textContent).toBe "#{nbsp}#{i + 1}"
expect(lineNumberNodes[9].textContent).toBe '10'
gutterNode = node.querySelector('.gutter')
initialGutterWidth = gutterNode.offsetWidth
# Removes padding when the max number of digits goes down
editor.getBuffer().delete([[1, 0], [2, 0]])
lineNumberNodes = toArray(node.querySelectorAll('.line-number'))
for node, i in lineNumberNodes
expect(node.textContent).toBe "#{i + 1}"
for screenRow in [0..8]
expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}"
expect(gutterNode.offsetWidth).toBeLessThan initialGutterWidth
# Increases padding when the max number of digits goes up
editor.getBuffer().insert([0, 0], '\n\n')
for screenRow in [0..8]
expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}"
expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10"
expect(gutterNode.offsetWidth).toBe initialGutterWidth
describe "cursor rendering", ->
it "renders the currently visible cursors", ->
it "renders the currently visible cursors, translated relative to the scroll position", ->
cursor1 = editor.getCursor()
cursor1.setScreenPosition([0, 5])
node.style.height = 4.5 * lineHeightInPixels + 'px'
node.style.width = 20 * lineHeightInPixels + 'px'
component.measureHeightAndWidth()
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels
expect(cursorNodes[0].offsetWidth).toBe charWidth
expect(cursorNodes[0].offsetTop).toBe 0
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)"
cursor2 = editor.addCursorAtScreenPosition([6, 11])
cursor3 = editor.addCursorAtScreenPosition([4, 10])
@@ -229,25 +239,23 @@ describe "EditorComponent", ->
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].offsetTop).toBe 0
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{10 * charWidth}px, #{4 * lineHeightInPixels}px, 0px)"
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
horizontalScrollbarNode.scrollLeft = 3.5 * charWidth
horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 2.5) * lineHeightInPixels}px, 0px)"
cursor3.destroy()
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)"
it "accounts for character widths when positioning cursors", ->
atom.config.set('editor.fontFamily', 'sans-serif')
@@ -256,7 +264,7 @@ describe "EditorComponent", ->
cursor = node.querySelector('.cursor')
cursorRect = cursor.getBoundingClientRect()
cursorLocationTextNode = node.querySelector('.storage.type.function.js').firstChild.firstChild
cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild
range = document.createRange()
range.setStart(cursorLocationTextNode, 0)
range.setEnd(cursorLocationTextNode, 1)
@@ -266,44 +274,19 @@ describe "EditorComponent", ->
expect(cursorRect.width).toBe rangeRect.width
it "blinks cursors when they aren't moving", ->
editor.addCursorAtScreenPosition([1, 0])
[cursorNode1, cursorNode2] = node.querySelectorAll('.cursor')
expect(cursorNode1.classList.contains('blink-off')).toBe false
expect(cursorNode2.classList.contains('blink-off')).toBe false
jasmine.unspy(window, 'setTimeout')
advanceClock(component.props.cursorBlinkPeriod / 2)
expect(cursorNode1.classList.contains('blink-off')).toBe true
expect(cursorNode2.classList.contains('blink-off')).toBe true
cursorsNode = node.querySelector('.cursors')
expect(cursorsNode.classList.contains('blinking')).toBe true
advanceClock(component.props.cursorBlinkPeriod / 2)
expect(cursorNode1.classList.contains('blink-off')).toBe false
expect(cursorNode2.classList.contains('blink-off')).toBe false
advanceClock(component.props.cursorBlinkPeriod / 2)
expect(cursorNode1.classList.contains('blink-off')).toBe true
expect(cursorNode2.classList.contains('blink-off')).toBe true
# Stop blinking immediately when cursors move
advanceClock(component.props.cursorBlinkPeriod / 4)
expect(cursorNode1.classList.contains('blink-off')).toBe true
expect(cursorNode2.classList.contains('blink-off')).toBe true
# Stop blinking for one full period after moving the cursor
# Stop blinking after moving the cursor
editor.moveCursorRight()
expect(cursorNode1.classList.contains('blink-off')).toBe false
expect(cursorNode2.classList.contains('blink-off')).toBe false
expect(cursorsNode.classList.contains('blinking')).toBe false
advanceClock(component.props.cursorBlinkResumeDelay / 2)
expect(cursorNode1.classList.contains('blink-off')).toBe false
expect(cursorNode2.classList.contains('blink-off')).toBe false
advanceClock(component.props.cursorBlinkResumeDelay / 2)
expect(cursorNode1.classList.contains('blink-off')).toBe true
expect(cursorNode2.classList.contains('blink-off')).toBe true
advanceClock(component.props.cursorBlinkPeriod / 2)
expect(cursorNode1.classList.contains('blink-off')).toBe false
expect(cursorNode2.classList.contains('blink-off')).toBe false
# Resume blinking after resume delay passes
waits component.props.cursorBlinkResumeDelay
runs ->
expect(cursorsNode.classList.contains('blinking')).toBe true
it "renders the hidden input field at the position of the last cursor if it is on screen", ->
inputNode = node.querySelector('.hidden-input')
@@ -330,13 +313,13 @@ describe "EditorComponent", ->
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
expect(cursorNodes[0].offsetLeft).toBe 8 * charWidth
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{8 * charWidth}px, #{6 * lineHeightInPixels}px, 0px)"
describe "selection rendering", ->
scrollViewClientLeft = null
[scrollViewNode, scrollViewClientLeft] = []
beforeEach ->
scrollViewNode = node.querySelector('.scroll-view')
scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left
it "renders 1 region for 1-line selections", ->
@@ -360,7 +343,7 @@ describe "EditorComponent", ->
expect(region1Rect.top).toBe 1 * lineHeightInPixels
expect(region1Rect.height).toBe 1 * lineHeightInPixels
expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth
expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed
expect(region1Rect.right).toBe scrollViewNode.getBoundingClientRect().right
region2Rect = regions[1].getBoundingClientRect()
expect(region2Rect.top).toBe 2 * lineHeightInPixels
@@ -377,13 +360,13 @@ describe "EditorComponent", ->
expect(region1Rect.top).toBe 1 * lineHeightInPixels
expect(region1Rect.height).toBe 1 * lineHeightInPixels
expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth
expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed
expect(region1Rect.right).toBe scrollViewNode.getBoundingClientRect().right
region2Rect = regions[1].getBoundingClientRect()
expect(region2Rect.top).toBe 2 * lineHeightInPixels
expect(region2Rect.height).toBe 3 * lineHeightInPixels
expect(region2Rect.left).toBe scrollViewClientLeft + 0
expect(Math.ceil(region2Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed
expect(region2Rect.right).toBe scrollViewNode.getBoundingClientRect().right
region3Rect = regions[2].getBoundingClientRect()
expect(region3Rect.top).toBe 5 * lineHeightInPixels
@@ -391,9 +374,12 @@ describe "EditorComponent", ->
expect(region3Rect.left).toBe scrollViewClientLeft + 0
expect(region3Rect.width).toBe 10 * charWidth
it "does not render empty selections", ->
expect(editor.getSelection().isEmpty()).toBe true
expect(node.querySelectorAll('.selection').length).toBe 0
it "does not render empty selections unless they are the first selection (to prevent a Chromium rendering artifact caused by removing it)", ->
editor.addSelectionForBufferRange([[2, 2], [2, 2]])
expect(editor.getSelection(0).isEmpty()).toBe true
expect(editor.getSelection(1).isEmpty()).toBe true
expect(node.querySelectorAll('.selection').length).toBe 1
describe "mouse interactions", ->
linesNode = null
@@ -527,16 +513,16 @@ describe "EditorComponent", ->
editor.setScrollTop(10)
expect(verticalScrollbarNode.scrollTop).toBe 10
it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", ->
it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", ->
node.style.width = 30 * charWidth + 'px'
component.measureHeightAndWidth()
scrollViewContentNode = node.querySelector('.scroll-view-content')
expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0)"
linesNode = node.querySelector('.lines')
expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)"
expect(horizontalScrollbarNode.scrollLeft).toBe 0
editor.setScrollLeft(100)
expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0)"
expect(linesNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)"
expect(horizontalScrollbarNode.scrollLeft).toBe 100
it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", ->
@@ -554,7 +540,7 @@ describe "EditorComponent", ->
node.style.width = 10 * charWidth + 'px'
component.measureHeightAndWidth()
editor.setScrollBottom(editor.getScrollHeight())
lastLineNode = last(node.querySelectorAll('.line'))
lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow())
bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom
topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top
expect(bottomOfLastLine).toBe topOfHorizontalScrollbar
@@ -562,7 +548,6 @@ describe "EditorComponent", ->
# Scroll so there's no space below the last line when the horizontal scrollbar disappears
node.style.width = 100 * charWidth + 'px'
component.measureHeightAndWidth()
lastLineNode = last(node.querySelectorAll('.line'))
bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom
bottomOfEditor = node.getBoundingClientRect().bottom
expect(bottomOfLastLine).toBe bottomOfEditor
@@ -574,11 +559,9 @@ describe "EditorComponent", ->
editor.setScrollLeft(Infinity)
lineNodes = node.querySelectorAll('.line')
rightOfLongestLine = lineNodes[6].getBoundingClientRect().right
rightOfLongestLine = component.lineNodeForScreenRow(6).getBoundingClientRect().right
leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left
expect(rightOfLongestLine).toBe leftOfVerticalScrollbar - 1 # Leave 1 px so the cursor is visible on the end of the line
expect(Math.round(rightOfLongestLine)).toBe leftOfVerticalScrollbar - 1 # Leave 1 px so the cursor is visible on the end of the line
it "only displays dummy scrollbars when scrollable in that direction", ->
expect(verticalScrollbarNode.style.display).toBe 'none'
@@ -670,6 +653,32 @@ describe "EditorComponent", ->
expect(verticalScrollbarNode.scrollTop).toBe 10
expect(horizontalScrollbarNode.scrollLeft).toBe 15
describe "when the mousewheel event's target is a line", ->
it "keeps the line on the DOM if it is scrolled off-screen", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
node.style.width = 20 * charWidth + 'px'
component.measureHeightAndWidth()
lineNode = node.querySelector('.line')
wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500)
Object.defineProperty(wheelEvent, 'target', get: -> lineNode)
node.dispatchEvent(wheelEvent)
expect(node.contains(lineNode)).toBe true
describe "when the mousewheel event's target is a line number", ->
it "keeps the line number on the DOM if it is scrolled off-screen", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
node.style.width = 20 * charWidth + 'px'
component.measureHeightAndWidth()
lineNumberNode = node.querySelectorAll('.line-number')[1]
wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500)
Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode)
node.dispatchEvent(wheelEvent)
expect(node.contains(lineNumberNode)).toBe true
describe "input events", ->
inputNode = null

View File

@@ -1731,26 +1731,26 @@ describe "Editor", ->
editor.backspace()
expect(editor.lineForBufferRow(0)).toBe 'var = () {'
describe ".backspaceToBeginningOfWord()", ->
describe ".deleteToBeginningOfWord()", ->
describe "when no text is selected", ->
it "deletes all text between the cursor and the beginning of the word", ->
editor.setCursorBufferPosition([1, 24])
editor.addCursorAtBufferPosition([3, 5])
[cursor1, cursor2] = editor.getCursors()
editor.backspaceToBeginningOfWord()
editor.deleteToBeginningOfWord()
expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {'
expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];'
expect(cursor1.getBufferPosition()).toEqual [1, 22]
expect(cursor2.getBufferPosition()).toEqual [3, 4]
editor.backspaceToBeginningOfWord()
editor.deleteToBeginningOfWord()
expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {'
expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];'
expect(cursor1.getBufferPosition()).toEqual [1, 21]
expect(cursor2.getBufferPosition()).toEqual [2, 39]
editor.backspaceToBeginningOfWord()
editor.deleteToBeginningOfWord()
expect(buffer.lineForRow(1)).toBe ' var sort = ems) {'
expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];'
expect(cursor1.getBufferPosition()).toEqual [1, 13]
@@ -1758,24 +1758,24 @@ describe "Editor", ->
editor.setText(' var sort')
editor.setCursorBufferPosition([0, 2])
editor.backspaceToBeginningOfWord()
editor.deleteToBeginningOfWord()
expect(buffer.lineForRow(0)).toBe 'var sort'
describe "when text is selected", ->
it "deletes only selected text", ->
editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]])
editor.backspaceToBeginningOfWord()
editor.deleteToBeginningOfWord()
expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {'
expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;'
describe ".backspaceToBeginningOfLine()", ->
describe ".deleteToBeginningOfLine()", ->
describe "when no text is selected", ->
it "deletes all text between the cursor and the beginning of the line", ->
editor.setCursorBufferPosition([1, 24])
editor.addCursorAtBufferPosition([2, 5])
[cursor1, cursor2] = editor.getCursors()
editor.backspaceToBeginningOfLine()
editor.deleteToBeginningOfLine()
expect(buffer.lineForRow(1)).toBe 'ems) {'
expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;'
expect(cursor1.getBufferPosition()).toEqual [1, 0]
@@ -1784,13 +1784,13 @@ describe "Editor", ->
describe "when at the beginning of the line", ->
it "deletes the newline", ->
editor.setCursorBufferPosition([2])
editor.backspaceToBeginningOfLine()
editor.deleteToBeginningOfLine()
expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;'
describe "when text is selected", ->
it "still deletes all text to begginning of the line", ->
editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]])
editor.backspaceToBeginningOfLine()
editor.deleteToBeginningOfLine()
expect(buffer.lineForRow(1)).toBe 'ems) {'
expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;'

View File

@@ -1803,6 +1803,13 @@ describe "EditorView", ->
expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} "
expect(editorView.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol
describe "when editor.showIndentGuide is set to false", ->
it "does not render the indent guide on whitespace only lines (regression)", ->
editorView.attachToDom()
editor.setText(' ')
atom.config.set('editor.showIndentGuide', false)
expect(editorView.renderedLines.find('.line:eq(0) .indent-guide').length).toBe 0
describe "when soft-wrap is enabled", ->
beforeEach ->
jasmine.unspy(window, 'setTimeout')

View File

@@ -286,5 +286,10 @@ describe "ThemeManager", ->
runs ->
spyOn(console, 'warn')
expect(-> atom.config.set('core.themes', ['atom-light-ui', 'theme-really-does-not-exist'])).not.toThrow()
waitsFor (done) ->
themeManager.once 'reloaded', done
runs ->
expect(console.warn.callCount).toBe 1
expect(console.warn.argsForCall[0][0].length).toBeGreaterThan 0

View File

@@ -98,5 +98,6 @@ class ContextMenuManager
showForEvent: (event) ->
@activeElement = event.target
menuTemplate = @combinedMenuTemplateForElement(event.target)
return unless menuTemplate?.length > 0
@executeBuildHandlers(event, menuTemplate)
remote.getCurrentWindow().emit('context-menu', menuTemplate)

View File

@@ -6,8 +6,10 @@ CursorComponent = React.createClass
displayName: 'CursorComponent'
render: ->
{top, left, height, width} = @props.cursor.getPixelRect()
className = 'cursor'
className += ' blink-off' if @props.blinkOff
{cursor, scrollTop, scrollLeft} = @props
{top, left, height, width} = cursor.getPixelRect()
top -= scrollTop
left -= scrollLeft
WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)"
div className: className, style: {top, left, height, width}
div className: 'cursor', style: {height, width, WebkitTransform}

View File

@@ -78,7 +78,11 @@ class CursorView extends View
setVisible: (visible) ->
unless @visible is visible
@visible = visible
@toggle(@visible)
hiddenCursor = 'hidden-cursor'
if visible
@removeClass hiddenCursor
else
@addClass hiddenCursor
stopBlinking: ->
@constructor.stopBlinking(this) if @blinking

View File

@@ -1,10 +1,9 @@
React = require 'react'
{div} = require 'reactionary'
{debounce} = require 'underscore-plus'
{debounce, toArray} = require 'underscore-plus'
SubscriberMixin = require './subscriber-mixin'
CursorComponent = require './cursor-component'
module.exports =
CursorsComponent = React.createClass
displayName: 'CursorsComponent'
@@ -13,22 +12,24 @@ CursorsComponent = React.createClass
cursorBlinkIntervalHandle: null
render: ->
{editor} = @props
blinkOff = @state.blinkCursorsOff
{editor, scrollTop, scrollLeft} = @props
{blinking} = @state
div className: 'cursors',
className = 'cursors'
className += ' blinking' if blinking
div {className},
if @isMounted()
for selection in editor.getSelections()
if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
{cursor} = selection
CursorComponent({key: cursor.id, cursor, blinkOff})
CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft})
getInitialState: ->
blinkCursorsOff: false
blinking: true
componentDidMount: ->
{editor} = @props
@startBlinkingCursors()
componentWillUnmount: ->
clearInterval(@cursorBlinkIntervalHandle)
@@ -36,15 +37,21 @@ CursorsComponent = React.createClass
componentWillUpdate: ({cursorsMoved}) ->
@pauseCursorBlinking() if cursorsMoved
componentDidUpdate: ->
@syncCursorAnimations() if @props.selectionAdded
startBlinkingCursors: ->
@cursorBlinkIntervalHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2)
@setState(blinking: true) if @isMounted()
startBlinkingCursorsAfterDelay: null # Created lazily
toggleCursorBlink: -> @setState(blinkCursorsOff: not @state.blinkCursorsOff)
pauseCursorBlinking: ->
@state.blinkCursorsOff = false
clearInterval(@cursorBlinkIntervalHandle)
@state.blinking = false
@startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay)
@startBlinkingCursorsAfterDelay()
syncCursorAnimations: ->
node = @getDOMNode()
cursorNodes = toArray(node.children)
node.removeChild(cursorNode) for cursorNode in cursorNodes
node.appendChild(cursorNode) for cursorNode in cursorNodes

View File

@@ -241,7 +241,8 @@ class DisplayBuffer extends Model
heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1
startRow = Math.floor(@getScrollTop() / @getLineHeight())
endRow = Math.min(@getLineCount(), Math.ceil(startRow + heightInLines))
endRow = Math.min(@getLineCount(), startRow + heightInLines)
[startRow, endRow]
intersectsVisibleRowRange: (startRow, endRow) ->

View File

@@ -20,15 +20,19 @@ EditorComponent = React.createClass
batchingUpdates: false
updateRequested: false
cursorsMoved: false
preservedRowRange: null
selectionChanged: false
selectionAdded: false
scrollingVertically: false
gutterWidth: 0
refreshingScrollbars: false
measuringScrollbars: true
pendingVerticalScrollDelta: 0
pendingHorizontalScrollDelta: 0
mouseWheelScreenRow: null
render: ->
{focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
{editor, cursorBlinkResumeDelay} = @props
maxLineNumberDigits = editor.getScreenLineCount().toString().length
if @isMounted()
@@ -48,16 +52,17 @@ EditorComponent = React.createClass
div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1,
GutterComponent {
editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight,
lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges,
onWidthChanged: @onGutterWidthChanged
ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits,
scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily,
@pendingChanges, onWidthChanged: @onGutterWidthChanged, @mouseWheelScreenRow
}
EditorScrollViewComponent {
ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide
scrollHeight, scrollWidth, lineHeight: lineHeightInPixels,
renderedRowRange, @pendingChanges, @scrollingVertically, @cursorsMoved,
cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred
ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide,
lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges,
scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically,
@cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay,
@onInputFocused, @onInputBlurred, @mouseWheelScreenRow
}
ScrollbarComponent
@@ -93,17 +98,17 @@ EditorComponent = React.createClass
width: verticalScrollbarWidth
getRenderedRowRange: ->
renderedRowRange = @props.editor.getVisibleRowRange()
if @preservedRowRange?
renderedRowRange[0] = Math.min(@preservedRowRange[0], renderedRowRange[0])
renderedRowRange[1] = Math.max(@preservedRowRange[1], renderedRowRange[1])
renderedRowRange
{editor, lineOverdrawMargin} = @props
[visibleStartRow, visibleEndRow] = editor.getVisibleRowRange()
renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin)
renderedEndRow = Math.min(editor.getLineCount(), visibleEndRow + lineOverdrawMargin)
[renderedStartRow, renderedEndRow]
getInitialState: -> {}
getDefaultProps: ->
cursorBlinkPeriod: 800
cursorBlinkResumeDelay: 200
cursorBlinkResumeDelay: 100
lineOverdrawMargin: 8
componentWillMount: ->
@pendingChanges = []
@@ -130,6 +135,8 @@ EditorComponent = React.createClass
componentDidUpdate: ->
@pendingChanges.length = 0
@cursorsMoved = false
@selectionChanged = false
@selectionAdded = false
@refreshingScrollbars = false
@measureScrollbars() if @measuringScrollbars
@props.parentView.trigger 'editor:display-updated'
@@ -140,9 +147,8 @@ EditorComponent = React.createClass
@subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded
@subscribe editor, 'screen-lines-changed', @onScreenLinesChanged
@subscribe editor, 'cursors-moved', @onCursorsMoved
@subscribe editor, 'selection-screen-range-changed', @requestUpdate
@subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged
@subscribe editor, 'selection-added', @onSelectionAdded
@subscribe editor, 'selection-removed', @onSelectionAdded
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
@subscribe editor.$scrollLeft.changes, @requestUpdate
@subscribe editor.$height.changes, @requestUpdate
@@ -174,8 +180,8 @@ EditorComponent = React.createClass
'editor:move-to-previous-word': => editor.moveCursorToPreviousWord()
'editor:select-word': => editor.selectWord()
'editor:consolidate-selections': @consolidateSelections
'editor:backspace-to-beginning-of-word': => editor.backspaceToBeginningOfWord()
'editor:backspace-to-beginning-of-line': => editor.backspaceToBeginningOfLine()
'editor:delete-to-beginning-of-word': => editor.deleteToBeginningOfWord()
'editor:delete-to-beginning-of-line': => editor.deleteToBeginningOfLine()
'editor:delete-to-end-of-word': => editor.deleteToEndOfWord()
'editor:delete-line': => editor.deleteLine()
'editor:cut-to-end-of-line': => editor.cutToEndOfLine()
@@ -319,14 +325,32 @@ EditorComponent = React.createClass
@pendingScrollLeft = null
onMouseWheel: (event) ->
event.preventDefault()
screenRow = @screenRowForNode(event.target)
@mouseWheelScreenRow = screenRow if screenRow?
animationFramePending = @pendingHorizontalScrollDelta isnt 0 or @pendingVerticalScrollDelta isnt 0
# Only scroll in one direction at a time
{wheelDeltaX, wheelDeltaY} = event
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
@refs.horizontalScrollbar.getDOMNode().scrollLeft -= wheelDeltaX
@pendingHorizontalScrollDelta -= wheelDeltaX
else
@refs.verticalScrollbar.getDOMNode().scrollTop -= wheelDeltaY
@pendingVerticalScrollDelta -= wheelDeltaY
event.preventDefault()
unless animationFramePending
requestAnimationFrame =>
{editor} = @props
editor.setScrollTop(editor.getScrollTop() + @pendingVerticalScrollDelta)
editor.setScrollLeft(editor.getScrollLeft() + @pendingHorizontalScrollDelta)
@pendingVerticalScrollDelta = 0
@pendingHorizontalScrollDelta = 0
screenRowForNode: (node) ->
while node isnt document
if screenRow = node.dataset.screenRow
return parseInt(screenRow)
node = node.parentNode
null
onStylesheetsChanged: (stylesheet) ->
@refreshScrollbars() if @containsScrollbarSelector(stylesheet)
@@ -357,13 +381,6 @@ EditorComponent = React.createClass
# if the editor's content and dimensions require them to be visible.
@requestUpdate()
clearPreservedRowRange: ->
@preservedRowRange = null
@scrollingVertically = false
@requestUpdate()
clearPreservedRowRangeAfterDelay: null # Created lazily
onBatchedUpdatesStarted: ->
@batchingUpdates = true
@@ -379,20 +396,31 @@ EditorComponent = React.createClass
@pendingChanges.push(change)
@requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events
onSelectionChanged: (selection) ->
{editor} = @props
if editor.selectionIntersectsVisibleRowRange(selection)
@selectionChanged = true
@requestUpdate()
onSelectionAdded: (selection) ->
{editor} = @props
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
if editor.selectionIntersectsVisibleRowRange(selection)
@selectionChanged = true
@selectionAdded = true
@requestUpdate()
onScrollTopChanged: ->
@preservedRowRange = @getRenderedRowRange()
@scrollingVertically = true
@clearPreservedRowRangeAfterDelay ?= debounce(@clearPreservedRowRange, 200)
@clearPreservedRowRangeAfterDelay()
@requestUpdate()
@stopScrollingAfterDelay ?= debounce(@onStoppedScrolling, 100)
@stopScrollingAfterDelay()
onStoppedScrolling: ->
@scrollingVertically = false
@mouseWheelScreenRow = null
@requestUpdate()
onSelectionRemoved: (selection) ->
{editor} = @props
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
stopScrollingAfterDelay: null # created lazily
onCursorsMoved: ->
@cursorsMoved = true
@@ -411,3 +439,7 @@ EditorComponent = React.createClass
consolidateSelections: (e) ->
e.abortKeyBinding() unless @props.editor.consolidateSelections()
lineNodeForScreenRow: (screenRow) -> @refs.scrollView.lineNodeForScreenRow(screenRow)
lineNumberNodeForScreenRow: (screenRow) -> @refs.gutter.lineNumberNodeForScreenRow(screenRow)

View File

@@ -17,19 +17,14 @@ EditorScrollViewComponent = React.createClass
render: ->
{editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props
{scrollHeight, scrollWidth, renderedRowRange, pendingChanges, scrollingVertically} = @props
{cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props
{renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically, mouseWheelScreenRow} = @props
{selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props
if @isMounted()
inputStyle = @getHiddenInputPosition()
inputStyle.WebkitTransform = 'translateZ(0)'
contentStyle =
height: scrollHeight
minWidth: scrollWidth
WebkitTransform: "translate3d(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px, 0)"
div className: 'scroll-view',
div className: 'scroll-view', onMouseDown: @onMouseDown,
InputComponent
ref: 'input'
className: 'hidden-input'
@@ -38,14 +33,12 @@ EditorScrollViewComponent = React.createClass
onFocus: onInputFocused
onBlur: onInputBlurred
div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown,
CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay})
LinesComponent {
ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide,
renderedRowRange, pendingChanges, scrollingVertically
}
div className: 'underlayer',
SelectionsComponent({editor})
CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay})
LinesComponent {
ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide,
renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically,
selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow
}
componentDidMount: ->
@getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged
@@ -198,3 +191,5 @@ EditorScrollViewComponent = React.createClass
focus: ->
@refs.input.focus()
lineNodeForScreenRow: (screenRow) -> @refs.lines.lineNodeForScreenRow(screenRow)

View File

@@ -154,8 +154,8 @@ class EditorView extends View
'editor:move-to-previous-word': => @editor.moveCursorToPreviousWord()
'editor:select-word': => @editor.selectWord()
'editor:consolidate-selections': (event) => @consolidateSelections(event)
'editor:backspace-to-beginning-of-word': => @editor.backspaceToBeginningOfWord()
'editor:backspace-to-beginning-of-line': => @editor.backspaceToBeginningOfLine()
'editor:delete-to-beginning-of-word': => @editor.deleteToBeginningOfWord()
'editor:delete-to-beginning-of-line': => @editor.deleteToBeginningOfLine()
'editor:delete-to-end-of-word': => @editor.deleteToEndOfWord()
'editor:delete-line': => @editor.deleteLine()
'editor:cut-to-end-of-line': => @editor.cutToEndOfLine()
@@ -1490,7 +1490,7 @@ class EditorView extends View
position = 0
for token in tokens
@updateScopeStack(line, scopeStack, token.scopes)
hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly)
hasIndentGuide = not mini and showIndentGuide and (token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly))
line.push(token.getValueAsHtml({invisibles, hasIndentGuide}))
position += token.value.length

View File

@@ -109,8 +109,8 @@ TextMateScopeSelector = require('first-mate').ScopeSelector
# - {::insertNewlineAbove}
# - {::insertNewlineBelow}
# - {::backspace}
# - {::backspaceToBeginningOfWord}
# - {::backspaceToBeginningOfLine}
# - {::deleteToBeginningOfWord}
# - {::deleteToBeginningOfLine}
# - {::delete}
# - {::deleteToEndOfWord}
# - {::deleteLine}
@@ -658,17 +658,27 @@ class Editor extends Model
backspace: ->
@mutateSelectedText (selection) -> selection.backspace()
# Deprecated: Use {::deleteToBeginningOfWord} instead.
backspaceToBeginningOfWord: ->
deprecate("Use Editor::deleteToBeginningOfWord() instead")
@deleteToBeginningOfWord()
# Deprecated: Use {::deleteToBeginningOfLine} instead.
backspaceToBeginningOfLine: ->
deprecate("Use Editor::deleteToBeginningOfLine() instead")
@deleteToBeginningOfLine()
# Public: For each selection, if the selection is empty, delete all characters
# of the containing word that precede the cursor. Otherwise delete the
# selected text.
backspaceToBeginningOfWord: ->
@mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord()
deleteToBeginningOfWord: ->
@mutateSelectedText (selection) -> selection.deleteToBeginningOfWord()
# Public: For each selection, if the selection is empty, delete all characters
# of the containing line that precede the cursor. Otherwise delete the
# selected text.
backspaceToBeginningOfLine: ->
@mutateSelectedText (selection) -> selection.backspaceToBeginningOfLine()
deleteToBeginningOfLine: ->
@mutateSelectedText (selection) -> selection.deleteToBeginningOfLine()
# Public: For each selection, if the selection is empty, delete the character
# preceding the cursor. Otherwise delete the selected text.
@@ -1259,6 +1269,9 @@ class Editor extends Model
# Returns: An {Array} of {Selection}s.
getSelections: -> new Array(@selections...)
selectionsForScreenRows: (startRow, endRow) ->
@getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow)
# Public: Get the most recent {Selection} or the selection at the given
# index.
#

View File

@@ -1,46 +1,33 @@
React = require 'react'
{div} = require 'reactionary'
{isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus'
{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
SubscriberMixin = require './subscriber-mixin'
WrapperDiv = document.createElement('div')
module.exports =
GutterComponent = React.createClass
displayName: 'GutterComponent'
mixins: [SubscriberMixin]
lastMeasuredWidth: null
dummyLineNumberNode: null
render: ->
{scrollHeight, scrollTop} = @props
div className: 'gutter',
@renderLineNumbers() if @isMounted()
div className: 'line-numbers', ref: 'lineNumbers', style:
height: scrollHeight
WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)"
renderLineNumbers: ->
{editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight} = @props
[startRow, endRow] = renderedRowRange
charWidth = editor.getDefaultCharWidth()
lineHeight = editor.getLineHeight()
style =
width: charWidth * (maxLineNumberDigits + 1.5)
height: scrollHeight
WebkitTransform: "translate3d(0, #{-scrollTop}px, 0)"
componentWillMount: ->
@lineNumberNodesById = {}
@lineNumberIdsByScreenRow = {}
@screenRowsByLineNumberId = {}
lineNumbers = []
tokenizedLines = editor.linesForScreenRows(startRow, endRow - 1)
tokenizedLines.push({id: 0}) if tokenizedLines.length is 0
for bufferRow, i in editor.bufferRowsForScreenRows(startRow, endRow - 1)
if bufferRow is lastBufferRow
lineNumber = ''
else
lastBufferRow = bufferRow
lineNumber = (bufferRow + 1).toString()
key = tokenizedLines[i].id
screenRow = startRow + i
lineNumbers.push(LineNumberComponent({key, lineNumber, maxLineNumberDigits, bufferRow, screenRow, lineHeight}))
lastBufferRow = bufferRow
div className: 'line-numbers', style: style,
lineNumbers
componentDidMount: ->
@appendDummyLineNumber()
# Only update the gutter if the visible row range has changed or if a
# non-zero-delta change to the screen lines has occurred within the current
@@ -49,39 +36,131 @@ GutterComponent = React.createClass
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight', 'fontSize')
{renderedRowRange, pendingChanges} = newProps
for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0
for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
false
componentDidUpdate: (oldProps) ->
unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily')
width = @getDOMNode().offsetWidth
if width isnt @lastMeasuredWidth
@lastMeasuredWidth = width
@props.onWidthChanged(width)
unless oldProps.maxLineNumberDigits is @props.maxLineNumberDigits
@updateDummyLineNumber()
@removeLineNumberNodes()
LineNumberComponent = React.createClass
displayName: 'LineNumberComponent'
@measureWidth() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily')
@clearScreenRowCaches() unless oldProps.lineHeight is @props.lineHeight
@updateLineNumbers()
render: ->
{bufferRow, screenRow, lineHeight} = @props
div
className: "line-number line-number-#{bufferRow}"
style: {top: screenRow * lineHeight}
'data-buffer-row': bufferRow
'data-screen-row': screenRow
dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
clearScreenRowCaches: ->
@lineNumberIdsByScreenRow = {}
@screenRowsByLineNumberId = {}
buildInnerHTML: ->
{lineNumber, maxLineNumberDigits} = @props
if lineNumber.length < maxLineNumberDigits
padding = multiplyString('&nbsp;', maxLineNumberDigits - lineNumber.length)
padding + lineNumber + @iconDivHTML
# This dummy line number element holds the gutter to the appropriate width,
# since the real line numbers are absolutely positioned for performance reasons.
appendDummyLineNumber: ->
{maxLineNumberDigits} = @props
WrapperDiv.innerHTML = @buildLineNumberHTML(0, false, maxLineNumberDigits)
@dummyLineNumberNode = WrapperDiv.children[0]
@refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits)
updateLineNumbers: ->
lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes()
@removeLineNumberNodes(lineNumberIdsToPreserve)
appendOrUpdateVisibleLineNumberNodes: ->
{editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props
[startRow, endRow] = renderedRowRange
newLineNumberIds = null
newLineNumbersHTML = null
visibleLineNumberIds = new Set
wrapCount = 0
for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1)
screenRow = startRow + index
if bufferRow is lastBufferRow
id = "#{bufferRow}-#{wrapCount++}"
else
id = bufferRow.toString()
lastBufferRow = bufferRow
wrapCount = 0
visibleLineNumberIds.add(id)
if @hasLineNumberNode(id)
@updateLineNumberNode(id, screenRow)
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow)
@screenRowsByLineNumberId[id] = screenRow
@lineNumberIdsByScreenRow[screenRow] = id
if newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = toArray(WrapperDiv.children)
node = @refs.lineNumbers.getDOMNode()
for lineNumberId, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[lineNumberId] = lineNumberNode
node.appendChild(lineNumberNode)
visibleLineNumberIds
removeLineNumberNodes: (lineNumberIdsToPreserve) ->
{mouseWheelScreenRow} = @props
node = @refs.lineNumbers.getDOMNode()
for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId)
screenRow = @screenRowsByLineNumberId[lineNumberId]
unless screenRow is mouseWheelScreenRow
delete @lineNumberNodesById[lineNumberId]
delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId
delete @screenRowsByLineNumberId[lineNumberId]
node.removeChild(lineNumberNode)
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) ->
if screenRow?
{lineHeight} = @props
style = "position: absolute; top: #{screenRow * lineHeight}px;"
else
lineNumber + @iconDivHTML
style = "visibility: hidden;"
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits)
iconDivHTML: '<div class="icon-right"></div>'
"<div class=\"line-number editor-colors\" style=\"#{style}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
shouldComponentUpdate: (newProps) ->
not isEqualForProperties(newProps, @props, 'lineHeight', 'screenRow', 'maxLineNumberDigits')
buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) ->
if softWrapped
lineNumber = ""
else
lineNumber = (bufferRow + 1).toString()
padding = multiplyString('&nbsp;', maxLineNumberDigits - lineNumber.length)
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
updateLineNumberNode: (lineNumberId, screenRow) ->
unless @screenRowsByLineNumberId[lineNumberId] is screenRow
{lineHeight} = @props
@lineNumberNodesById[lineNumberId].style.top = screenRow * lineHeight + 'px'
@lineNumberNodesById[lineNumberId].dataset.screenRow = screenRow
@screenRowsByLineNumberId[lineNumberId] = screenRow
@lineNumberIdsByScreenRow[screenRow] = lineNumberId
hasLineNumberNode: (lineNumberId) ->
@lineNumberNodesById.hasOwnProperty(lineNumberId)
lineNumberNodeForScreenRow: (screenRow) ->
@lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]]
measureWidth: ->
lineNumberNode = @refs.lineNumbers.getDOMNode().firstChild
# return unless lineNumberNode?
width = lineNumberNode.offsetWidth
if width isnt @lastMeasuredWidth
@props.onWidthChanged(@lastMeasuredWidth = width)

View File

@@ -10,7 +10,7 @@ InputComponent = React.createClass
render: ->
{className, style, onFocus, onBlur} = @props
input {className, style, onFocus, onBlur}
input {className, style, onFocus, onBlur, 'data-react-skip-selection-restoration': true}
getInitialState: ->
{lastChar: ''}

View File

@@ -1,10 +1,13 @@
React = require 'react'
{div, span} = require 'reactionary'
{debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus'
{debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
{$$} = require 'space-pen'
SelectionsComponent = require './selections-component'
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
module.exports =
LinesComponent = React.createClass
@@ -12,23 +15,27 @@ LinesComponent = React.createClass
render: ->
if @isMounted()
{editor, renderedRowRange, lineHeight, showIndentGuide} = @props
[startRow, endRow] = renderedRowRange
{editor, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeight} = @props
style =
height: scrollHeight
width: scrollWidth
WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)"
lines =
for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1)
LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, screenRow: startRow + i})
div {className: 'lines'}, lines
div {className: 'lines', style},
SelectionsComponent({editor, lineHeight}) if @isMounted()
componentWillMount: ->
@measuredLines = new WeakSet
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
componentDidMount: ->
@measureLineHeightAndCharWidth()
shouldComponentUpdate: (newProps) ->
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide')
return true if newProps.selectionChanged
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically')
{renderedRowRange, pendingChanges} = newProps
for change in pendingChanges
@@ -38,9 +45,144 @@ LinesComponent = React.createClass
componentDidUpdate: (prevProps) ->
@measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
@clearScreenRowCaches() unless prevProps.lineHeight is @props.lineHeight
@removeLineNodes() unless prevProps.showIndentGuide is @props.showIndentGuide
@updateLines()
@clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily')
@measureCharactersInNewLines() unless @props.scrollingVertically
clearScreenRowCaches: ->
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
updateLines: ->
{editor, renderedRowRange, showIndentGuide, selectionChanged} = @props
[startRow, endRow] = renderedRowRange
visibleLines = editor.linesForScreenRows(startRow, endRow - 1)
@removeLineNodes(visibleLines)
@appendOrUpdateVisibleLineNodes(visibleLines, startRow)
removeLineNodes: (visibleLines=[]) ->
{mouseWheelScreenRow} = @props
visibleLineIds = new Set
visibleLineIds.add(line.id.toString()) for line in visibleLines
node = @getDOMNode()
for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId)
screenRow = @screenRowsByLineId[lineId]
unless screenRow is mouseWheelScreenRow
delete @lineNodesByLineId[lineId]
delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId
delete @screenRowsByLineId[lineId]
node.removeChild(lineNode)
appendOrUpdateVisibleLineNodes: (visibleLines, startRow) ->
{lineHeight} = @props
newLines = null
newLinesHTML = null
for line, index in visibleLines
screenRow = startRow + index
if @hasLineNode(line.id)
@updateLineNode(line, screenRow)
else
newLines ?= []
newLinesHTML ?= ""
newLines.push(line)
newLinesHTML += @buildLineHTML(line, screenRow)
@screenRowsByLineId[line.id] = screenRow
@lineIdsByScreenRow[screenRow] = line.id
return unless newLines?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = toArray(WrapperDiv.children)
node = @getDOMNode()
for line, i in newLines
lineNode = newLineNodes[i]
@lineNodesByLineId[line.id] = lineNode
node.appendChild(lineNode)
hasLineNode: (lineId) ->
@lineNodesByLineId.hasOwnProperty(lineId)
buildLineHTML: (line, screenRow) ->
{editor, mini, showIndentGuide, lineHeight} = @props
{tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line
top = screenRow * lineHeight
lineHTML = "<div class=\"line\" style=\"position: absolute; top: #{top}px;\" data-screen-row=\"#{screenRow}\">"
if text is ""
lineHTML += @buildEmptyLineInnerHTML(line)
else
lineHTML += @buildLineInnerHTML(line)
lineHTML += "</div>"
lineHTML
buildEmptyLineInnerHTML: (line) ->
{showIndentGuide} = @props
{indentLevel, tabLength} = line
if showIndentGuide and indentLevel > 0
indentSpan = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
multiplyString(indentSpan, indentLevel + 1)
else
"&nbsp;"
buildLineInnerHTML: (line) ->
{invisibles, mini, showIndentGuide} = @props
{tokens, text} = line
innerHTML = ""
scopeStack = []
firstTrailingWhitespacePosition = text.search(/\s*$/)
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
for token in tokens
innerHTML += @updateScopeStack(scopeStack, token.scopes)
hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly)
innerHTML += token.getValueAsHtml({invisibles, hasIndentGuide})
innerHTML += @popScope(scopeStack) while scopeStack.length > 0
innerHTML
updateScopeStack: (scopeStack, desiredScopes) ->
html = ""
# Find a common prefix
for scope, i in desiredScopes
break unless scopeStack[i]?.scope is desiredScopes[i]
# Pop scopes until we're at the common prefx
until scopeStack.length is i
html += @popScope(scopeStack)
# Push onto common prefix until scopeStack equals desiredScopes
for j in [i...desiredScopes.length]
html += @pushScope(scopeStack, desiredScopes[j])
html
popScope: (scopeStack) ->
scopeStack.pop()
"</span>"
pushScope: (scopeStack, scope) ->
scopeStack.push(scope)
"<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
updateLineNode: (line, screenRow) ->
unless @screenRowsByLineId[line.id] is screenRow
{lineHeight} = @props
lineNode = @lineNodesByLineId[line.id]
lineNode.style.top = screenRow * lineHeight + 'px'
lineNode.dataset.screenRow = screenRow
@screenRowsByLineId[line.id] = screenRow
@lineIdsByScreenRow[screenRow] = line.id
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
measureLineHeightAndCharWidth: ->
node = @getDOMNode()
node.appendChild(DummyLineNode)
@@ -56,9 +198,9 @@ LinesComponent = React.createClass
[visibleStartRow, visibleEndRow] = @props.renderedRowRange
node = @getDOMNode()
for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
for tokenizedLine in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
unless @measuredLines.has(tokenizedLine)
lineNode = node.children[i]
lineNode = @lineNodesByLineId[tokenizedLine.id]
@measureCharactersInLine(tokenizedLine, lineNode)
measureCharactersInLine: (tokenizedLine, lineNode) ->
@@ -97,44 +239,3 @@ LinesComponent = React.createClass
clearScopedCharWidths: ->
@measuredLines.clear()
@props.editor.clearScopedCharWidths()
LineComponent = React.createClass
displayName: 'LineComponent'
render: ->
{screenRow, lineHeight} = @props
style =
top: screenRow * lineHeight
position: 'absolute'
div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
buildInnerHTML: ->
if @props.tokenizedLine.text.length is 0
@buildEmptyLineHTML()
else
@buildScopeTreeHTML(@props.tokenizedLine.getScopeTree())
buildEmptyLineHTML: ->
{showIndentGuide, tokenizedLine} = @props
{indentLevel, tabLength} = tokenizedLine
if showIndentGuide and indentLevel > 0
indentSpan = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
multiplyString(indentSpan, indentLevel + 1)
else
"&nbsp;"
buildScopeTreeHTML: (scopeTree) ->
if scopeTree.children?
html = "<span class='#{scopeTree.scope.replace(/\./g, ' ')}'>"
html += @buildScopeTreeHTML(child) for child in scopeTree.children
html += "</span>"
html
else
"<span>#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}</span>"
shouldComponentUpdate: (newProps) ->
not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow')

View File

@@ -58,7 +58,7 @@ class MenuManager
testBody = document.createElement('body')
testBody.classList.add(@classesForElement(document.body)...)
testWorkspace = document.createElement('body')
testWorkspace = document.createElement('div')
workspaceClasses = @classesForElement(document.body.querySelector('.workspace'))
workspaceClasses = ['workspace'] if workspaceClasses.length is 0
testWorkspace.classList.add(workspaceClasses...)
@@ -69,7 +69,12 @@ class MenuManager
@testEditor.classList.add('editor')
testWorkspace.appendChild(@testEditor)
@testEditor.webkitMatchesSelector(selector)
element = @testEditor
while element
return true if element.webkitMatchesSelector(selector)
element = element.parentElement
false
# Public: Refreshes the currently visible menu.
update: ->

View File

@@ -1,6 +1,7 @@
{View, $} = require 'space-pen'
React = require 'react'
EditorComponent = require './editor-component'
{defaults} = require 'underscore-plus'
module.exports =
class ReactEditorView extends View
@@ -8,7 +9,7 @@ class ReactEditorView extends View
focusOnAttach: false
constructor: (@editor) ->
constructor: (@editor, @props) ->
super
getEditor: -> @editor
@@ -37,11 +38,12 @@ class ReactEditorView extends View
afterAttach: (onDom) ->
return unless onDom
@attached = true
@component = React.renderComponent(EditorComponent({@editor, parentView: this}), @element)
props = defaults({@editor, parentView: this}, @props)
@component = React.renderComponent(EditorComponent(props), @element)
node = @component.getDOMNode()
@underlayer = $(node).find('.underlayer')
@underlayer = $(node).find('.selections')
@gutter = $(node).find('.gutter')
@gutter.removeClassFromAllLines = (klass) =>
@@ -64,7 +66,8 @@ class ReactEditorView extends View
appendToLinesView: (view) ->
view.css('position', 'absolute')
@find('.scroll-view-content').prepend(view)
view.css('z-index', 1)
@find('.lines').prepend(view)
beforeRemove: ->
React.unmountComponentAtNode(@element)

View File

@@ -6,6 +6,60 @@ SelectionComponent = React.createClass
displayName: 'SelectionComponent'
render: ->
{editor, screenRange, lineHeight} = @props
{start, end} = screenRange
rowCount = end.row - start.row + 1
startPixelPosition = editor.pixelPositionForScreenPosition(start)
endPixelPosition = editor.pixelPositionForScreenPosition(end)
div className: 'selection',
for regionRect, i in @props.selection.getRegionRects()
div className: 'region', key: i, style: regionRect
if rowCount is 1
@renderSingleLineRegions(startPixelPosition, endPixelPosition)
else
@renderMultiLineRegions(startPixelPosition, endPixelPosition, rowCount)
renderSingleLineRegions: (startPixelPosition, endPixelPosition) ->
{lineHeight} = @props
[
div className: 'region', key: 0, style:
top: startPixelPosition.top
height: lineHeight
left: startPixelPosition.left
width: endPixelPosition.left - startPixelPosition.left
]
renderMultiLineRegions: (startPixelPosition, endPixelPosition, rowCount) ->
{lineHeight} = @props
regions = []
index = 0
# First row, extending from selection start to the right side of screen
regions.push(
div className: 'region', key: index++, style:
top: startPixelPosition.top
left: startPixelPosition.left
height: lineHeight
right: 0
)
# Middle rows, extending from left side to right side of screen
if rowCount > 2
regions.push(
div className: 'region', key: index++, style:
top: startPixelPosition.top + lineHeight
height: (rowCount - 2) * lineHeight
left: 0
right: 0
)
# Last row, extending from left side of screen to selection end
regions.push(
div className: 'region', key: index, style:
top: endPixelPosition.top
height: lineHeight
left: 0
width: endPixelPosition.left
)
regions

View File

@@ -382,15 +382,25 @@ class Selection extends Model
@selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow())
@deleteSelectedText()
# Deprecated: Use {::deleteToBeginningOfWord} instead.
backspaceToBeginningOfWord: ->
deprecate("Use Selection::deleteToBeginningOfWord() instead")
@deleteToBeginningOfWord()
# Deprecated: Use {::deleteToBeginningOfLine} instead.
backspaceToBeginningOfLine: ->
deprecate("Use Selection::deleteToBeginningOfLine() instead")
@deleteToBeginningOfLine()
# Public: Removes from the start of the selection to the beginning of the
# current word if the selection is empty otherwise it deletes the selection.
backspaceToBeginningOfWord: ->
deleteToBeginningOfWord: ->
@selectToBeginningOfWord() if @isEmpty()
@deleteSelectedText()
# Public: Removes from the beginning of the line which the selection begins on
# all the way through to the end of the selection.
backspaceToBeginningOfLine: ->
deleteToBeginningOfLine: ->
if @isEmpty() and @cursor.isAtBeginningOfLine()
@selectLeft()
else
@@ -553,6 +563,12 @@ class Selection extends Model
intersectsBufferRange: (bufferRange) ->
@getBufferRange().intersectsWith(bufferRange)
intersectsScreenRowRange: (startRow, endRow) ->
@getScreenRange().intersectsRowRange(startRow, endRow)
intersectsScreenRow: (screenRow) ->
@getScreenRange().intersectsRow(screenRow)
# Public: Identifies if a selection intersects with another selection.
#
# otherSelection - A {Selection} to check against.
@@ -585,47 +601,6 @@ class Selection extends Model
compare: (otherSelection) ->
@getBufferRange().compare(otherSelection.getBufferRange())
# Get the pixel dimensions of rectangular regions that cover selection's area
# on the screen. Used by SelectionComponent for rendering.
getRegionRects: ->
lineHeight = @editor.getLineHeight()
{start, end} = @getScreenRange()
rowCount = end.row - start.row + 1
startPixelPosition = @editor.pixelPositionForScreenPosition(start)
endPixelPosition = @editor.pixelPositionForScreenPosition(end)
if rowCount is 1
# Single line selection
rects = [{
top: startPixelPosition.top
height: lineHeight
left: startPixelPosition.left
width: endPixelPosition.left - startPixelPosition.left
}]
else
# Multi-line selection
rects = []
# First row, extending from selection start to the right side of screen
rects.push {
top: startPixelPosition.top
left: startPixelPosition.left
height: lineHeight
right: 0
}
if rowCount > 2
# Middle rows, extending from left side to right side of screen
rects.push {
top: startPixelPosition.top + lineHeight
height: (rowCount - 2) * lineHeight
left: 0
right: 0
}
# Last row, extending from left side of screen to selection end
rects.push {top: endPixelPosition.top, height: lineHeight, left: 0, width: endPixelPosition.left }
rects
screenRangeChanged: ->
screenRange = @getScreenRange()
@emit 'screen-range-changed', screenRange

View File

@@ -7,10 +7,37 @@ SelectionsComponent = React.createClass
displayName: 'SelectionsComponent'
render: ->
{editor} = @props
div className: 'selections', @renderSelections()
div className: 'selections',
if @isMounted()
for selection in editor.getSelections()
if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
SelectionComponent({key: selection.id, selection})
renderSelections: ->
{editor, lineHeight} = @props
selectionComponents = []
for selectionId, screenRange of @selectionRanges
selectionComponents.push(SelectionComponent({key: selectionId, screenRange, editor, lineHeight}))
selectionComponents
componentWillMount: ->
@selectionRanges = {}
shouldComponentUpdate: ->
{editor} = @props
oldSelectionRanges = @selectionRanges
newSelectionRanges = {}
@selectionRanges = newSelectionRanges
for selection, index in editor.getSelections()
# Rendering artifacts occur on the lines GPU layer if we remove the last selection
if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection))
newSelectionRanges[selection.id] = selection.getScreenRange()
for id, range of newSelectionRanges
if oldSelectionRanges.hasOwnProperty(id)
return true unless range.isEqual(oldSelectionRanges[id])
else
return true
for id of oldSelectionRanges
return true unless newSelectionRanges.hasOwnProperty(id)
false

View File

@@ -7,11 +7,22 @@ class TokenizedLine
constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, @tabLength, @indentLevel}) ->
@tokens = @breakOutAtomicTokens(tokens)
@startBufferColumn ?= 0
@text = _.pluck(@tokens, 'value').join('')
@bufferDelta = _.sum(_.pluck(@tokens, 'bufferDelta'))
@text = @buildText()
@bufferDelta = @buildBufferDelta()
@id = idCounter++
@markLeadingAndTrailingWhitespaceTokens()
buildText: ->
text = ""
text += token.value for token in @tokens
text
buildBufferDelta: ->
delta = 0
delta += token.bufferDelta for token in @tokens
delta
copy: ->
new TokenizedLine({@tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold})

View File

@@ -113,6 +113,7 @@ class WorkspaceView extends View
@command 'application:quit', -> ipc.send('command', 'application:quit')
@command 'application:hide', -> ipc.send('command', 'application:hide')
@command 'application:hide-other-applications', -> ipc.send('command', 'application:hide-other-applications')
@command 'application:install-update', -> ipc.send('command', 'application:install-update')
@command 'application:unhide-all-applications', -> ipc.send('command', 'application:unhide-all-applications')
@command 'application:new-window', -> ipc.send('command', 'application:new-window')
@command 'application:new-file', -> ipc.send('command', 'application:new-file')

View File

@@ -13,7 +13,23 @@
}
.lines {
z-index: -1;
min-width: 100%;
}
.cursor {
z-index: 1;
}
&.is-focused .cursors.blinking .cursor {
-webkit-animation: blink 0.8s;
-webkit-animation-iteration-count: infinite;
}
@-webkit-keyframes blink {
0% { opacity: .7; }
50% { opacity: .7; }
51% { opacity: 0; }
100% { opacity: 0; }
}
.horizontal-scrollbar {
@@ -45,6 +61,7 @@
.scroll-view {
overflow: hidden;
z-index: 0;
}
.scroll-view-content {
@@ -53,15 +70,9 @@
}
.gutter {
padding-left: 0.5em;
padding-right: 0.5em;
.line-number {
position: absolute;
left: 0;
right: 0;
padding: 0;
white-space: nowrap;
padding: 0 .5em;
.icon-right {
padding: 0;
@@ -226,6 +237,10 @@
visibility: visible;
}
.cursor.hidden-cursor {
display: none;
}
.editor .hidden-input {
padding: 0;
border: 0;