Merge branch 'master' into mq-replace-optimist

This commit is contained in:
Machiste Quintana
2015-05-19 19:35:29 -04:00
67 changed files with 2074 additions and 1259 deletions

View File

@@ -2,7 +2,11 @@ git:
depth: 10
env:
- NODE_VERSION=0.12
global:
- ATOM_ACCESS_TOKEN=da809a6077bb1b0aa7c5623f7b2d5f1fec2faae4
matrix:
- NODE_VERSION=0.12
os:
- linux

View File

@@ -7,6 +7,9 @@ Atom is a hackable text editor for the 21st century, built on [Electron](https:/
Visit [atom.io](https://atom.io) to learn more or visit the [Atom forum](https://discuss.atom.io).
Follow [@AtomEditor](https://twitter.com/atomeditor) on Twitter for important
announcements.
Visit [issue #3684](https://github.com/atom/atom/issues/3684) to learn more
about the Atom 1.0 roadmap.

View File

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

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
if [ "$(uname)" == 'Darwin' ]; then
OS='Mac'

View File

@@ -6,7 +6,7 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
"asar": "^0.4.4",
"asar": "^0.5.0",
"async": "~0.2.9",
"donna": "1.0.10",
"formidable": "~1.0.14",

View File

@@ -2,6 +2,7 @@ path = require 'path'
CSON = require 'season'
fs = require 'fs-plus'
_ = require 'underscore-plus'
normalizePackageData = require 'normalize-package-data'
OtherPlatforms = ['darwin', 'freebsd', 'linux', 'sunos', 'win32'].filter (platform) -> platform isnt process.platform
@@ -32,6 +33,7 @@ module.exports = (grunt) ->
modulesDirectory = path.join(appDir, 'node_modules')
packages = {}
invalidPackages = false
for moduleDirectory in fs.listSync(modulesDirectory)
continue if path.basename(moduleDirectory) is '.bin'
@@ -40,6 +42,13 @@ module.exports = (grunt) ->
metadata = grunt.file.readJSON(metadataPath)
continue unless metadata?.engines?.atom?
reportPackageError = (msg) ->
invalidPackages = true
grunt.log.error("#{metadata.name}: #{msg}")
normalizePackageData metadata, reportPackageError, true
if metadata.repository?.type is 'git'
metadata.repository.url = metadata.repository.url?.replace(/^git\+/, '')
moduleCache = metadata._atomModuleCache ? {}
rm metadataPath
@@ -79,3 +88,4 @@ module.exports = (grunt) ->
metadata._atomKeymaps = getKeymaps(appDir)
grunt.file.write(path.join(appDir, 'package.json'), JSON.stringify(metadata))
not invalidPackages

View File

@@ -14,6 +14,7 @@ module.exports = (grunt) ->
'ctags-darwin'
'ctags-linux'
'ctags-win32.exe'
'**/node_modules/spellchecker/**'
]
unpack = "{#{unpack.join(',')}}"

View File

@@ -54,14 +54,14 @@ module.exports = (grunt) ->
if process.platform in ['darwin', 'linux']
options =
cmd: appPath
args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}"]
args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}", '--one']
opts:
cwd: packagePath
env: _.extend({}, process.env, ATOM_PATH: rootDir)
else if process.platform is 'win32'
options =
cmd: process.env.comspec
args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}", "--log-file=ci.log"]
args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}", "--log-file=ci.log", '--one']
opts:
cwd: packagePath
env: _.extend({}, process.env, ATOM_PATH: rootDir)

View File

@@ -38,7 +38,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform.
### openSUSE
* `sudo zypper install nodejs make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools`
* `sudo zypper install nodejs nodejs-devel make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools`
## Instructions

View File

@@ -77,6 +77,15 @@
'ctrl-k ctrl-down': 'window:focus-pane-below'
'ctrl-k ctrl-left': 'window:focus-pane-on-left'
'ctrl-k ctrl-right': 'window:focus-pane-on-right'
'alt-1': 'pane:show-item-1'
'alt-2': 'pane:show-item-2'
'alt-3': 'pane:show-item-3'
'alt-4': 'pane:show-item-4'
'alt-5': 'pane:show-item-5'
'alt-6': 'pane:show-item-6'
'alt-7': 'pane:show-item-7'
'alt-8': 'pane:show-item-8'
'alt-9': 'pane:show-item-9'
'atom-workspace atom-text-editor':
# Platform Bindings

View File

@@ -81,6 +81,8 @@
{ label: 'Lower Case', command: 'editor:lower-case' }
{ type: 'separator' }
{ label: 'Delete to End of Word', command: 'editor:delete-to-end-of-word' }
{ label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' }
{ label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' }
{ label: 'Delete Line', command: 'editor:delete-line' }
{ type: 'separator' }
{ label: 'Transpose', command: 'editor:transpose' }
@@ -168,6 +170,10 @@
]
}
{ type: 'separator' }
{ label: 'Increase Font Size', command: 'window:increase-font-size' }
{ label: 'Decrease Font Size', command: 'window:decrease-font-size' }
{ label: 'Reset Font Size', command: 'window:reset-font-size' }
{ type: 'separator' }
{ label: 'Toggle Soft Wrap', command: 'editor:toggle-soft-wrap' }
]
}

View File

@@ -55,6 +55,8 @@
{ label: '&Lower Case', command: 'editor:lower-case' }
{ type: 'separator' }
{ label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' }
{ label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' }
{ label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' }
{ label: '&Delete Line', command: 'editor:delete-line' }
{ type: 'separator' }
{ label: '&Transpose', command: 'editor:transpose' }
@@ -125,6 +127,10 @@
]
}
{ type: 'separator' }
{ label: '&Increase Font Size', command: 'window:increase-font-size' }
{ label: '&Decrease Font Size', command: 'window:decrease-font-size' }
{ label: 'Re&set Font Size', command: 'window:reset-font-size' }
{ type: 'separator' }
{ label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' }
]
}

View File

@@ -62,6 +62,8 @@
{ label: '&Lower Case', command: 'editor:lower-case' }
{ type: 'separator' }
{ label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' }
{ label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' }
{ label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' }
{ label: '&Delete Line', command: 'editor:delete-line' }
{ type: 'separator' }
{ label: '&Transpose', command: 'editor:transpose' }
@@ -124,6 +126,10 @@
]
}
{ type: 'separator' }
{ label: '&Increase Font Size', command: 'window:increase-font-size' }
{ label: '&Decrease Font Size', command: 'window:decrease-font-size' }
{ label: 'Re&set Font Size', command: 'window:reset-font-size' }
{ type: 'separator' }
{ label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' }
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "0.195.0",
"version": "0.200.0",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/browser/main.js",
"repository": {
@@ -27,23 +27,24 @@
"clear-cut": "^2.0.1",
"coffee-cash": "0.8.0",
"coffee-script": "1.8.0",
"coffeestack": "^1.1.1",
"coffeestack": "^1.1.2",
"color": "^0.7.3",
"delegato": "^1",
"emissary": "^1.3.3",
"event-kit": "^1.1",
"event-kit": "^1.1.1",
"first-mate": "^3.1",
"fs-plus": "^2.7.1",
"fs-plus": "^2.8.0",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "^3.0.0",
"grim": "1.2.2",
"grim": "1.4.1",
"jasmine-json": "~0.0",
"jasmine-tagged": "^1.1.4",
"jquery": "^2.1.1",
"less-cache": "0.22",
"marked": "^0.3.3",
"mixto": "^1",
"normalize-package-data": "^2.0.0",
"nslog": "^2.0.0",
"oniguruma": "^4.1",
"pathwatcher": "^4.4",
@@ -63,96 +64,99 @@
"space-pen": "3.8.2",
"stacktrace-parser": "0.1.1",
"temp": "0.8.1",
"text-buffer": "^5.2",
"text-buffer": "6.0.0",
"theorist": "^1.0.2",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": ">=3.7.2 <4.0"
},
"packageDependencies": {
"atom-dark-syntax": "0.26.0",
"atom-dark-syntax": "0.27.0",
"atom-dark-ui": "0.49.0",
"atom-light-syntax": "0.26.0",
"atom-light-syntax": "0.28.0",
"atom-light-ui": "0.41.0",
"base16-tomorrow-dark-theme": "0.25.0",
"base16-tomorrow-light-theme": "0.8.0",
"one-dark-ui": "0.6.3",
"one-dark-syntax": "0.3.0",
"one-light-syntax": "0.4.0",
"one-light-ui": "0.5.3",
"solarized-dark-syntax": "0.32.0",
"solarized-light-syntax": "0.19.0",
"archive-view": "0.55.0",
"autocomplete": "0.44.0",
"autoflow": "0.22.0",
"base16-tomorrow-dark-theme": "0.26.0",
"base16-tomorrow-light-theme": "0.9.0",
"one-dark-ui": "0.8.1",
"one-dark-syntax": "0.5.0",
"one-light-syntax": "0.6.0",
"one-light-ui": "0.8.1",
"solarized-dark-syntax": "0.35.0",
"solarized-light-syntax": "0.21.0",
"archive-view": "0.57.0",
"autocomplete-atom-api": "0.9.0",
"autocomplete-css": "0.7.2",
"autocomplete-html": "0.7.2",
"autocomplete-plus": "2.17.0",
"autocomplete-snippets": "1.6.1",
"autoflow": "0.23.0",
"autosave": "0.20.0",
"background-tips": "0.24.0",
"bookmarks": "0.35.0",
"bracket-matcher": "0.73.0",
"command-palette": "0.35.0",
"deprecation-cop": "0.40.0",
"dev-live-reload": "0.45.0",
"encoding-selector": "0.19.0",
"bracket-matcher": "0.74.0",
"command-palette": "0.36.0",
"deprecation-cop": "0.48.0",
"dev-live-reload": "0.46.0",
"encoding-selector": "0.20.0",
"exception-reporting": "0.24.0",
"feedback": "0.38.0",
"find-and-replace": "0.160.0",
"fuzzy-finder": "0.81.0",
"git-diff": "0.54.0",
"find-and-replace": "0.166.0",
"fuzzy-finder": "0.85.0",
"git-diff": "0.55.0",
"go-to-line": "0.30.0",
"grammar-selector": "0.46.0",
"grammar-selector": "0.47.0",
"image-view": "0.54.0",
"incompatible-packages": "0.24.0",
"keybinding-resolver": "0.31.0",
"keybinding-resolver": "0.32.0",
"link": "0.30.0",
"markdown-preview": "0.148.0",
"metrics": "0.45.0",
"notifications": "0.41.0",
"open-on-github": "0.36.0",
"package-generator": "0.38.0",
"markdown-preview": "0.149.0",
"metrics": "0.48.0",
"notifications": "0.47.0",
"open-on-github": "0.37.0",
"package-generator": "0.39.0",
"release-notes": "0.52.0",
"settings-view": "0.195.0",
"snippets": "0.88.0",
"spell-check": "0.55.0",
"status-bar": "0.69.0",
"settings-view": "0.199.0",
"snippets": "0.89.0",
"spell-check": "0.58.0",
"status-bar": "0.72.0",
"styleguide": "0.44.0",
"symbols-view": "0.94.0",
"tabs": "0.67.0",
"symbols-view": "0.97.0",
"tabs": "0.68.0",
"timecop": "0.31.0",
"tree-view": "0.171.0",
"update-package-dependencies": "0.9.0",
"update-package-dependencies": "0.10.0",
"welcome": "0.27.0",
"whitespace": "0.29.0",
"wrap-guide": "0.32.0",
"wrap-guide": "0.33.0",
"language-c": "0.44.0",
"language-clojure": "0.14.0",
"language-coffee-script": "0.39.0",
"language-clojure": "0.15.0",
"language-coffee-script": "0.40.0",
"language-csharp": "0.5.0",
"language-css": "0.28.0",
"language-gfm": "0.69.0",
"language-css": "0.29.0",
"language-gfm": "0.75.0",
"language-git": "0.10.0",
"language-go": "0.25.0",
"language-html": "0.32.0",
"language-hyperlink": "0.12.2",
"language-java": "0.14.0",
"language-javascript": "0.74.0",
"language-go": "0.26.0",
"language-html": "0.37.0",
"language-hyperlink": "0.13.0",
"language-java": "0.15.0",
"language-javascript": "0.77.0",
"language-json": "0.14.0",
"language-less": "0.26.0",
"language-less": "0.27.0",
"language-make": "0.14.0",
"language-mustache": "0.11.0",
"language-objective-c": "0.15.0",
"language-perl": "0.23.0",
"language-php": "0.22.0",
"language-perl": "0.24.0",
"language-php": "0.23.0",
"language-property-list": "0.8.0",
"language-python": "0.34.0",
"language-ruby": "0.52.0",
"language-python": "0.35.0",
"language-ruby": "0.53.0",
"language-ruby-on-rails": "0.21.0",
"language-sass": "0.36.0",
"language-shellscript": "0.13.0",
"language-sass": "0.38.0",
"language-shellscript": "0.14.0",
"language-source": "0.9.0",
"language-sql": "0.15.0",
"language-text": "0.6.0",
"language-todo": "0.18.0",
"language-toml": "0.15.0",
"language-todo": "0.21.0",
"language-toml": "0.16.0",
"language-xml": "0.28.0",
"language-yaml": "0.22.0"
},

View File

@@ -13,21 +13,44 @@ describe "BufferedProcess", ->
window.onerror = oldOnError
describe "when there is an error handler specified", ->
it "calls the error handler and does not throw an exception", ->
process = new BufferedProcess
command: 'bad-command-nope'
args: ['nothing']
options: {}
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'
args: ['nothing']
options: {}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
process.onWillThrowError(errorSpy)
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
process.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
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'
runs ->
expect(window.onerror).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT'
describe "when an error is thrown spawning the process", ->
it "calls the error handler and does not throw an exception", ->
spyOn(ChildProcess, 'spawn').andCallFake ->
error = new Error('Something is really wrong')
error.code = 'EAGAIN'
throw error
process = new BufferedProcess
command: 'ls'
args: []
options: {}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
process.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
runs ->
expect(window.onerror).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
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", ->
@@ -73,3 +96,21 @@ describe "BufferedProcess", ->
expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'
expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/c'
expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"dir"'
it "calls the specified stdout, stderr, and exit callbacks ", ->
stdout = ''
stderr = ''
exitCallback = jasmine.createSpy('exit callback')
process = new BufferedProcess
command: atom.packages.getApmPath()
args: ['-h']
options: {}
stdout: (lines) -> stdout += lines
stderr: (lines) -> stderr += lines
exit: exitCallback
waitsFor -> exitCallback.callCount is 1
runs ->
expect(stderr).toContain 'apm - Atom Package Manager'
expect(stdout).toEqual ''

View File

@@ -30,15 +30,12 @@ describe "CustomGutterComponent", ->
buildTestState = (customDecorations) ->
mockTestState =
gutters:
content: if customDecorations then customDecorations else {}
styles:
scrollHeight: 100
scrollTop: 10
backgroundColor: 'black'
sortedDescriptions: [{gutter, visible: true}]
customDecorations: customDecorations
lineNumberGutter:
maxLineNumberDigits: 10
lineNumbers: {}
mockTestState
it "sets the custom-decoration wrapper's scrollHeight, scrollTop, and background color", ->
@@ -53,7 +50,7 @@ describe "CustomGutterComponent", ->
expect(decorationsWrapperNode.style.backgroundColor).not.toBe ''
it "creates a new DOM node for a new decoration and adds it to the gutter at the right place", ->
customDecorations = 'test-gutter':
customDecorations =
'decoration-id-1':
top: 0
height: 10
@@ -75,7 +72,7 @@ describe "CustomGutterComponent", ->
expect(decorationItem).toBe decorationItem1
it "updates the existing DOM node for a decoration that existed but has new properties", ->
initialCustomDecorations = 'test-gutter':
initialCustomDecorations =
'decoration-id-1':
top: 0
height: 10
@@ -86,7 +83,7 @@ describe "CustomGutterComponent", ->
# Change the dimensions and item, remove the class.
decorationItem2 = document.createElement('div')
changedCustomDecorations = 'test-gutter':
changedCustomDecorations =
'decoration-id-1':
top: 10
height: 20
@@ -103,7 +100,7 @@ describe "CustomGutterComponent", ->
expect(decorationItem).toBe decorationItem2
# Remove the item, add a class.
changedCustomDecorations = 'test-gutter':
changedCustomDecorations =
'decoration-id-1':
top: 10
height: 20
@@ -118,7 +115,7 @@ describe "CustomGutterComponent", ->
expect(changedDecorationNode.children.length).toBe 0
it "removes any decorations that existed previously but aren't in the latest update", ->
customDecorations = 'test-gutter':
customDecorations =
'decoration-id-1':
top: 0
height: 10
@@ -127,6 +124,6 @@ describe "CustomGutterComponent", ->
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
expect(decorationsWrapperNode.children.length).toBe 1
emptyCustomDecorations = 'test-gutter': {}
emptyCustomDecorations = {}
gutterComponent.updateSync(buildTestState(emptyCustomDecorations))
expect(decorationsWrapperNode.children.length).toBe 0

View File

@@ -1,5 +1,6 @@
DefaultDirectoryProvider = require "../src/default-directory-provider"
path = require "path"
fs = require 'fs-plus'
temp = require "temp"
describe "DefaultDirectoryProvider", ->

View File

@@ -1,5 +1,5 @@
{
"name": "no events",
"name": "package-with-empty-activation-commands",
"version": "0.1.0",
"activationCommands": {"atom-workspace": []}
}

View File

@@ -1,5 +1,5 @@
{
"name": "package-with-incompatible-native-module",
"version": "1.0",
"version": "1.0.0",
"main": "./main.js"
}

View File

@@ -0,0 +1,4 @@
{
"name": "package-with-invalid-url-package-json",
"repository": "foo"
}

View File

@@ -0,0 +1,4 @@
{
"name": "package-with-short-url-package-json",
"repository": "example/repo"
}

View File

@@ -5,17 +5,20 @@ describe "GutterContainerComponent", ->
[gutterContainerComponent] = []
mockGutterContainer = {}
buildTestState = (sortedDescriptions) ->
mockTestState =
gutters:
scrollHeight: 100
scrollTop: 10
backgroundColor: 'black'
sortedDescriptions: sortedDescriptions
customDecorations: {}
lineNumberGutter:
maxLineNumberDigits: 10
lineNumbers: {}
buildTestState = (gutters) ->
styles =
scrollHeight: 100
scrollTop: 10
backgroundColor: 'black'
mockTestState = {gutters: []}
for gutter in gutters
if gutter.name is 'line-number'
content = {maxLineNumberDigits: 10, lineNumbers: {}}
else
content = {}
mockTestState.gutters.push({gutter, styles, content, visible: gutter.visible})
mockTestState
beforeEach ->
@@ -30,7 +33,7 @@ describe "GutterContainerComponent", ->
describe "when updated with state that contains a new line-number gutter", ->
it "adds a LineNumberGutterComponent to its children", ->
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
testState = buildTestState([{gutter: lineNumberGutter, visible: true}])
testState = buildTestState([lineNumberGutter])
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
gutterContainerComponent.updateSync(testState)
@@ -45,7 +48,7 @@ describe "GutterContainerComponent", ->
describe "when updated with state that contains a new custom gutter", ->
it "adds a CustomGutterComponent to its children", ->
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
testState = buildTestState([{gutter: customGutter, visible: true}])
testState = buildTestState([customGutter])
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
gutterContainerComponent.updateSync(testState)
@@ -57,15 +60,16 @@ describe "GutterContainerComponent", ->
describe "when updated with state that contains a new gutter that is not visible", ->
it "creates the gutter view but hides it, and unhides it when it is later updated to be visible", ->
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
testState = buildTestState([{gutter: customGutter, visible: false}])
customGutter = new Gutter(mockGutterContainer, {name: 'custom', visible: false})
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
expect(expectedCustomGutterNode.style.display).toBe 'none'
testState = buildTestState([{gutter: customGutter, visible: true}])
customGutter.show()
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
@@ -74,20 +78,20 @@ describe "GutterContainerComponent", ->
describe "when updated with a gutter that already exists", ->
it "reuses the existing gutter view, instead of recreating it", ->
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
testState = buildTestState([{gutter: customGutter, visible: true}])
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
testState = buildTestState([{gutter: customGutter, visible: true}])
testState = buildTestState([customGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
expect(gutterContainerComponent.getDomNode().children.item(0)).toBe expectedCustomGutterNode
it "removes a gutter from the DOM if it does not appear in the latest state update", ->
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
testState = buildTestState([{gutter: lineNumberGutter, visible: true}])
testState = buildTestState([lineNumberGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
@@ -99,7 +103,7 @@ describe "GutterContainerComponent", ->
it "positions (and repositions) the gutters to match the order they appear in each state update", ->
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
customGutter1 = new Gutter(mockGutterContainer, {name: 'custom', priority: -100})
testState = buildTestState([{gutter: customGutter1, visible: true}, {gutter: lineNumberGutter, visible: true}])
testState = buildTestState([customGutter1, lineNumberGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 2
@@ -110,11 +114,7 @@ describe "GutterContainerComponent", ->
# Add a gutter.
customGutter2 = new Gutter(mockGutterContainer, {name: 'custom2', priority: -10})
testState = buildTestState([
{gutter: customGutter1, visible: true},
{gutter: customGutter2, visible: true},
{gutter: lineNumberGutter, visible: true}
])
testState = buildTestState([customGutter1, customGutter2, lineNumberGutter])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0)
@@ -125,12 +125,9 @@ describe "GutterContainerComponent", ->
expect(expectedLineNumbersNode).toBe atom.views.getView(lineNumberGutter)
# Hide one gutter, reposition one gutter, remove one gutter; and add a new gutter.
customGutter2.hide()
customGutter3 = new Gutter(mockGutterContainer, {name: 'custom3', priority: 100})
testState = buildTestState([
{gutter: customGutter2, visible: false},
{gutter: customGutter1, visible: true},
{gutter: customGutter3, visible: true}
])
testState = buildTestState([customGutter2, customGutter1, customGutter3])
gutterContainerComponent.updateSync(testState)
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(0)

View File

@@ -15,15 +15,23 @@ ChromedriverPort = 9515
ChromedriverURLBase = "/wd/hub"
ChromedriverStatusURL = "http://localhost:#{ChromedriverPort}#{ChromedriverURLBase}/status"
pollChromeDriver = (done) ->
chromeDriverUp = (done) ->
checkStatus = ->
http.get(ChromedriverStatusURL, (response) ->
if response.statusCode is 200
done()
else
pollChromeDriver(done)
).on("error", -> pollChromeDriver(done))
http
.get ChromedriverStatusURL, (response) ->
if response.statusCode is 200
done()
else
chromeDriverUp(done)
.on("error", -> chromeDriverUp(done))
setTimeout(checkStatus, 100)
chromeDriverDown = (done) ->
checkStatus = ->
http
.get ChromedriverStatusURL, (response) ->
chromeDriverDown(done)
.on("error", done)
setTimeout(checkStatus, 100)
buildAtomClient = (args, env) ->
@@ -137,9 +145,9 @@ module.exports = (args, env, fn) ->
chromedriver.stderr.on "close", ->
resolve(errorCode)
waitsFor("webdriver to start", pollChromeDriver, 15000)
waitsFor("webdriver to start", chromeDriverUp, 15000)
waitsFor("webdriver to finish", (done) ->
waitsFor("tests to run", (done) ->
finish = once ->
client
.simulateQuit()
@@ -162,3 +170,5 @@ module.exports = (args, env, fn) ->
fn(client.init()).then(finish)
, 30000)
waitsFor("webdriver to stop", chromeDriverDown, 15000)

View File

@@ -9,7 +9,7 @@ return if process.env.TRAVIS
fs = require "fs"
path = require "path"
temp = require("temp").track()
runAtom = require("./helpers/start-atom")
runAtom = require "./helpers/start-atom"
describe "Starting Atom", ->
[tempDirPath, otherTempDirPath, atomHome] = []
@@ -41,6 +41,69 @@ describe "Starting Atom", ->
.then ({value}) -> expect(value).toBe "Hello!"
.dispatchCommand("editor:delete-line")
it "opens the file to the specified line number", ->
filePath = path.join(fs.realpathSync(tempDirPath), "new-file")
fs.writeFileSync filePath, """
1
2
3
4
"""
runAtom ["#{filePath}:3"], {ATOM_HOME: atomHome}, (client) ->
client
.waitForWindowCount(1, 1000)
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(1, 1000)
.waitForExist("atom-text-editor", 5000)
.then (exists) -> expect(exists).toBe true
.execute -> atom.workspace.getActiveTextEditor().getPath()
.then ({value}) -> expect(value).toBe filePath
.execute -> atom.workspace.getActiveTextEditor().getCursorBufferPosition()
.then ({value}) ->
expect(value.row).toBe 2
expect(value.column).toBe 0
it "opens the file to the specified line number and column number", ->
filePath = path.join(fs.realpathSync(tempDirPath), "new-file")
fs.writeFileSync filePath, """
1
2
3
4
"""
runAtom ["#{filePath}:2:2"], {ATOM_HOME: atomHome}, (client) ->
client
.waitForWindowCount(1, 1000)
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(1, 1000)
.waitForExist("atom-text-editor", 5000)
.then (exists) -> expect(exists).toBe true
.execute -> atom.workspace.getActiveTextEditor().getPath()
.then ({value}) -> expect(value).toBe filePath
.execute -> atom.workspace.getActiveTextEditor().getCursorBufferPosition()
.then ({value}) ->
expect(value.row).toBe 1
expect(value.column).toBe 1
it "removes all trailing whitespace and colons from the specified path", ->
filePath = path.join(tempDirPath, "new-file")
runAtom ["#{filePath}: "], {ATOM_HOME: atomHome}, (client) ->
client
.waitForWindowCount(1, 1000)
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(1, 1000)
.waitForExist("atom-text-editor", 5000)
.then (exists) -> expect(exists).toBe true
.execute -> atom.workspace.getActiveTextEditor().getPath()
.then ({value}) -> expect(value).toBe filePath
describe "when there is already a window open", ->
it "reuses that window when opening files, but not when opening directories", ->
tempFilePath = path.join(temp.mkdirSync("a-third-dir"), "a-file")
@@ -150,10 +213,12 @@ describe "Starting Atom", ->
.waitForWindowCount(2, 10000)
.then ({value: windowHandles}) ->
@window(windowHandles[0])
.waitForExist("atom-workspace")
.treeViewRootDirectories()
.then ({value: directories}) -> windowProjectPaths.push(directories)
.window(windowHandles[1])
.waitForExist("atom-workspace")
.treeViewRootDirectories()
.then ({value: directories}) -> windowProjectPaths.push(directories)

View File

@@ -41,6 +41,15 @@ describe "PackageManager", ->
expect(addErrorHandler.callCount).toBe 1
expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package")
it "normalizes short repository urls in package.json", ->
{metadata} = atom.packages.loadPackage("package-with-short-url-package-json")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "https://github.com/example/repo.git"
{metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "foo"
it "returns null if the package is not found in any package directory", ->
spyOn(console, 'warn')
expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull()
@@ -815,3 +824,37 @@ describe "PackageManager", ->
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
describe "deleting non-bundled autocomplete packages", ->
[autocompleteCSSPath, autocompletePlusPath] = []
fs = require 'fs-plus'
path = require 'path'
beforeEach ->
fixturePath = path.resolve(__dirname, './fixtures/packages')
autocompleteCSSPath = path.join(fixturePath, 'autocomplete-css')
autocompletePlusPath = path.join(fixturePath, 'autocomplete-plus')
try
fs.mkdirSync(autocompleteCSSPath)
fs.writeFileSync(path.join(autocompleteCSSPath, 'package.json'), '{}')
fs.symlinkSync(path.join(fixturePath, 'package-with-main'), autocompletePlusPath, 'dir')
expect(fs.isDirectorySync(autocompleteCSSPath)).toBe true
expect(fs.isSymbolicLinkSync(autocompletePlusPath)).toBe true
jasmine.unspy(atom.packages, 'uninstallAutocompletePlus')
afterEach ->
try
fs.unlink autocompletePlusPath, ->
it "removes the packages", ->
atom.packages.loadPackages()
waitsFor ->
not fs.isDirectorySync(autocompleteCSSPath)
runs ->
expect(fs.isDirectorySync(autocompleteCSSPath)).toBe false
expect(fs.isSymbolicLinkSync(autocompletePlusPath)).toBe true

View File

@@ -132,3 +132,25 @@ describe "PaneContainerElement", ->
# dynamically close pane, the pane's flexscale will recorver to origin value
lowerPane.close()
expectPaneScale [leftPane, 0.5], [rightPane, 1.5]
it "unsubscribes from mouse events when the pane is detached", ->
container.getActivePane().splitRight()
element = getResizeElement(0)
spyOn(document, 'addEventListener').andCallThrough()
spyOn(document, 'removeEventListener').andCallThrough()
spyOn(element, 'resizeStopped').andCallThrough()
element.dispatchEvent(new MouseEvent('mousedown',
view: window
bubbles: true
button: 0
))
waitsFor ->
document.addEventListener.callCount is 2
runs ->
expect(element.resizeStopped.callCount).toBe 0
container.destroy()
expect(element.resizeStopped.callCount).toBe 1
expect(document.removeEventListener.callCount).toBe 2

View File

@@ -140,6 +140,8 @@ beforeEach ->
spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text
spyOn(clipboard, 'readText').andCallFake -> clipboardContent
spyOn(atom.packages, 'uninstallAutocompletePlus')
addCustomMatchers(this)
afterEach ->
@@ -311,7 +313,7 @@ window.waitsForPromise = (args...) ->
else
promise.then(moveOn)
promise.catch.call promise, (error) ->
jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{jasmine.pp(error)}")
jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with: #{error?.message} #{jasmine.pp(error)}")
moveOn()
window.resetTimeouts = ->

View File

@@ -2266,13 +2266,13 @@ describe "TextEditorComponent", ->
editor.setText("")
componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode))
currentTime += 99
currentTime += 100
componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode))
currentTime += 99
currentTime += 100
componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true))
currentTime += 100
currentTime += 101
componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true))
expect(editor.getText()).toBe "xy\nxy\nxy"

View File

@@ -65,6 +65,35 @@ describe "TextEditorElement", ->
element.getModel().destroy()
expect(component.mounted).toBe false
describe "when the editor is detached from the DOM and then reattached", ->
it "does not render duplicate line numbers", ->
editor = new TextEditor
editor.setText('1\n2\n3')
element = atom.views.getView(editor)
jasmine.attachToDOM(element)
initialCount = element.shadowRoot.querySelectorAll('.line-number').length
element.remove()
jasmine.attachToDOM(element)
expect(element.shadowRoot.querySelectorAll('.line-number').length).toBe initialCount
it "does not render duplicate decorations in custom gutters", ->
editor = new TextEditor
editor.setText('1\n2\n3')
editor.addGutter({name: 'test-gutter'})
marker = editor.markBufferRange([[0,0],[2,0]])
editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'})
element = atom.views.getView(editor)
jasmine.attachToDOM(element)
initialDecorationCount = element.shadowRoot.querySelectorAll('.decoration').length
element.remove()
jasmine.attachToDOM(element)
expect(element.shadowRoot.querySelectorAll('.decoration').length).toBe initialDecorationCount
describe "focus and blur handling", ->
describe "when the editor.useShadowDOM config option is true", ->
it "proxies focus/blur events to/from the hidden input inside the shadow root", ->

File diff suppressed because it is too large Load Diff

View File

@@ -36,23 +36,21 @@ describe "TextEditor", ->
it "preserves the invisibles setting", ->
atom.config.set('editor.showInvisibles', true)
previousInvisibles = editor.displayBuffer.invisibles
previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles
editor2 = editor.testSerialization()
expect(editor2.displayBuffer.invisibles).toEqual previousInvisibles
expect(editor2.displayBuffer.tokenizedBuffer.invisibles).toEqual previousInvisibles
expect(previousInvisibles).toBeDefined()
expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles
it "updates invisibles if the settings have changed between serialization and deserialization", ->
atom.config.set('editor.showInvisibles', true)
previousInvisibles = editor.displayBuffer.invisibles
state = editor.serialize()
atom.config.set('editor.invisibles', eol: '?')
editor2 = TextEditor.deserialize(state)
expect(editor2.displayBuffer.invisibles.eol).toBe '?'
expect(editor2.displayBuffer.tokenizedBuffer.invisibles.eol).toBe '?'
expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?'
describe "when the editor is constructed with an initialLine option", ->
it "positions the cursor on the specified line", ->
@@ -2343,6 +2341,62 @@ describe "TextEditor", ->
editor.backspace()
expect(editor.lineTextForBufferRow(0)).toBe 'var = () {'
describe ".deleteToPreviousWordBoundary()", ->
describe "when no text is selected", ->
it "deletes to the previous word boundary", ->
editor.setCursorBufferPosition([0, 16])
editor.addCursorAtBufferPosition([1, 21])
[cursor1, cursor2] = editor.getCursors()
editor.deleteToPreviousWordBoundary()
expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {'
expect(buffer.lineForRow(1)).toBe ' var sort = (items) {'
expect(cursor1.getBufferPosition()).toEqual [0, 15]
expect(cursor2.getBufferPosition()).toEqual [1, 13]
editor.deleteToPreviousWordBoundary()
expect(buffer.lineForRow(0)).toBe 'var quicksort function () {'
expect(buffer.lineForRow(1)).toBe ' var sort =(items) {'
expect(cursor1.getBufferPosition()).toEqual [0, 14]
expect(cursor2.getBufferPosition()).toEqual [1, 12]
describe "when text is selected", ->
it "deletes only selected text", ->
editor.setSelectedBufferRange([[1, 24], [1, 27]])
editor.deleteToPreviousWordBoundary()
expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {'
describe ".deleteToNextWordBoundary()", ->
describe "when no text is selected", ->
it "deletes to the next word boundary", ->
editor.setCursorBufferPosition([0, 15])
editor.addCursorAtBufferPosition([1, 24])
[cursor1, cursor2] = editor.getCursors()
editor.deleteToNextWordBoundary()
expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {'
expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {'
expect(cursor1.getBufferPosition()).toEqual [0, 15]
expect(cursor2.getBufferPosition()).toEqual [1, 24]
editor.deleteToNextWordBoundary()
expect(buffer.lineForRow(0)).toBe 'var quicksort = () {'
expect(buffer.lineForRow(1)).toBe ' var sort = function(it {'
expect(cursor1.getBufferPosition()).toEqual [0, 15]
expect(cursor2.getBufferPosition()).toEqual [1, 24]
editor.deleteToNextWordBoundary()
expect(buffer.lineForRow(0)).toBe 'var quicksort =() {'
expect(buffer.lineForRow(1)).toBe ' var sort = function(it{'
expect(cursor1.getBufferPosition()).toEqual [0, 15]
expect(cursor2.getBufferPosition()).toEqual [1, 24]
describe "when text is selected", ->
it "deletes only selected text", ->
editor.setSelectedBufferRange([[1, 24], [1, 27]])
editor.deleteToNextWordBoundary()
expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {'
describe ".deleteToBeginningOfWord()", ->
describe "when no text is selected", ->
it "deletes all text between the cursor and the beginning of the word", ->

View File

@@ -611,7 +611,8 @@ describe "TokenizedBuffer", ->
tokenizedBuffer = new TokenizedBuffer({buffer})
fullyTokenize(tokenizedBuffer)
tokenizedBuffer.setInvisibles(space: 'S', tab: 'T')
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"
@@ -623,7 +624,7 @@ describe "TokenizedBuffer", ->
tokenizedBuffer = new TokenizedBuffer({buffer})
atom.config.set('editor.showInvisibles', true)
tokenizedBuffer.setInvisibles(cr: 'R', eol: 'N')
atom.config.set("editor.invisibles", cr: 'R', eol: 'N')
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R', 'N']
@@ -634,7 +635,7 @@ describe "TokenizedBuffer", ->
expect(left.endOfLineInvisibles).toBe null
expect(right.endOfLineInvisibles).toEqual ['R', 'N']
tokenizedBuffer.setInvisibles(cr: 'R', eol: false)
atom.config.set("editor.invisibles", cr: 'R', eol: false)
expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R']
expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual []
@@ -688,7 +689,8 @@ describe "TokenizedBuffer", ->
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 ")
tokenizedBuffer.setInvisibles(space: 'S', tab: 'T')
atom.config.set("editor.showInvisibles", true)
atom.config.set("editor.invisibles", space: 'S', tab: 'T')
fullyTokenize(tokenizedBuffer)
line = tokenizedBuffer.tokenizedLineForRow(0).copy()
@@ -696,7 +698,8 @@ describe "TokenizedBuffer", ->
expect(line.tokens[line.tokens.length - 1].firstTrailingWhitespaceIndex).toBe 0
it "sets the ::firstNonWhitespaceIndex and ::firstTrailingWhitespaceIndex correctly when tokens are split for soft-wrapping", ->
tokenizedBuffer.setInvisibles(space: 'S')
atom.config.set("editor.showInvisibles", true)
atom.config.set("editor.invisibles", space: 'S')
buffer.setText(" token ")
fullyTokenize(tokenizedBuffer)
token = tokenizedBuffer.tokenizedLines[0].tokens[0]

View File

@@ -0,0 +1,44 @@
ipc = require 'ipc'
path = require 'path'
temp = require('temp').track()
describe "WorkspaceElement", ->
workspaceElement = null
beforeEach ->
workspaceElement = atom.views.getView(atom.workspace)
describe "the 'window:run-package-specs' command", ->
it "runs the package specs for the active item's project path, or the first project path", ->
spyOn(ipc, 'send')
# No project paths. Don't try to run specs.
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).not.toHaveBeenCalledWith("run-package-specs")
projectPaths = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")]
atom.project.setPaths(projectPaths)
# No active item. Use first project directory.
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipc.send.reset()
# Active item doesn't implement ::getPath(). Use first project directory.
item = document.createElement("div")
atom.workspace.getActivePane().activateItem(item)
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipc.send.reset()
# Active item has no path. Use first project directory.
item.getPath = -> null
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipc.send.reset()
# Active item has path. Use project path for item path.
item.getPath = -> path.join(projectPaths[1], "a-file.txt")
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[1], "spec"))
ipc.send.reset()

View File

@@ -426,6 +426,35 @@ describe "Workspace", ->
workspace.decreaseFontSize()
expect(atom.config.get('editor.fontSize')).toBe 1
describe "::resetFontSize()", ->
it "resets the font size to the window's starting font size", ->
originalFontSize = atom.config.get('editor.fontSize')
workspace.increaseFontSize()
expect(atom.config.get('editor.fontSize')).toBe originalFontSize + 1
workspace.resetFontSize()
expect(atom.config.get('editor.fontSize')).toBe originalFontSize
workspace.decreaseFontSize()
expect(atom.config.get('editor.fontSize')).toBe originalFontSize - 1
workspace.resetFontSize()
expect(atom.config.get('editor.fontSize')).toBe originalFontSize
it "does nothing if the font size has not been changed", ->
originalFontSize = atom.config.get('editor.fontSize')
workspace.resetFontSize()
expect(atom.config.get('editor.fontSize')).toBe originalFontSize
it "resets the font size when the editor's font size changes", ->
originalFontSize = atom.config.get('editor.fontSize')
atom.config.set('editor.fontSize', originalFontSize + 1)
workspace.resetFontSize()
expect(atom.config.get('editor.fontSize')).toBe originalFontSize
atom.config.set('editor.fontSize', originalFontSize - 1)
workspace.resetFontSize()
expect(atom.config.get('editor.fontSize')).toBe originalFontSize
describe "::openLicense()", ->
it "opens the license as plain-text in a buffer", ->
waitsForPromise -> workspace.openLicense()

View File

@@ -242,10 +242,15 @@ describe "WorkspaceView", ->
describe "the scrollbar visibility class", ->
it "has a class based on the style of the scrollbar", ->
style = 'legacy'
scrollbarStyle = require 'scrollbar-style'
scrollbarStyle.emitValue 'legacy'
spyOn(scrollbarStyle, 'getPreferredScrollbarStyle').andCallFake -> style
atom.workspaceView.element.observeScrollbarStyle()
expect(atom.workspaceView).toHaveClass 'scrollbars-visible-always'
scrollbarStyle.emitValue 'overlay'
style = 'overlay'
atom.workspaceView.element.observeScrollbarStyle()
expect(atom.workspaceView).toHaveClass 'scrollbars-visible-when-scrolling'
describe "editor font styling", ->

View File

@@ -151,7 +151,7 @@ class Atom extends Model
# Public: A {TooltipManager} instance
tooltips: null
# Experimental: A {NotificationManager} instance
# Public: A {NotificationManager} instance
notifications: null
# Public: A {Project} instance
@@ -223,6 +223,8 @@ class Atom extends Model
@disposables?.dispose()
@disposables = new CompositeDisposable
@displayWindow() unless @inSpecMode()
@setBodyPlatformClass()
@loadTime = null
@@ -483,22 +485,27 @@ class Atom extends Model
# Extended: Set the full screen state of the current window.
setFullScreen: (fullScreen=false) ->
ipc.send('call-window-method', 'setFullScreen', fullScreen)
if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen")
if fullScreen
document.body.classList.add("fullscreen")
else
document.body.classList.remove("fullscreen")
# Extended: Toggle the full screen state of the current window.
toggleFullScreen: ->
@setFullScreen(not @isFullScreen())
# Schedule the window to be shown and focused on the next tick.
# Restore the window to its previous dimensions and show it.
#
# This is done in a next tick to prevent a white flicker from occurring
# if called synchronously.
displayWindow: ({maximize}={}) ->
# Also restores the full screen and maximized state on the next tick to
# prevent resize glitches.
displayWindow: ->
dimensions = @restoreWindowDimensions()
@show()
setImmediate =>
@show()
@focus()
@setFullScreen(true) if @workspace.fullScreen
@maximize() if maximize
@setFullScreen(true) if @workspace?.fullScreen
@maximize() if dimensions?.maximized and process.platform isnt 'darwin'
# Get the dimensions of this window.
#
@@ -572,6 +579,13 @@ class Atom extends Model
dimensions = @getWindowDimensions()
@state.windowDimensions = dimensions if @isValidDimensions(dimensions)
storeWindowBackground: ->
return if @inSpecMode()
workspaceElement = @views.getView(@workspace)
backgroundColor = window.getComputedStyle(workspaceElement)['background-color']
window.localStorage.setItem('atom:window-background-color', backgroundColor)
# Call this method when establishing a real application window.
startEditorWindow: ->
{safeMode} = @getLoadSettings()
@@ -582,7 +596,6 @@ class Atom extends Model
CommandInstaller.installApmCommand false, (error) ->
console.warn error.message if error?
dimensions = @restoreWindowDimensions()
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@@ -602,12 +615,10 @@ class Atom extends Model
@openInitialEmptyEditorIfNecessary()
maximize = dimensions?.maximized and process.platform isnt 'darwin'
@displayWindow({maximize})
unloadEditorWindow: ->
return if not @project
@storeWindowBackground()
@state.grammars = @grammars.serialize()
@state.project = @project.serialize()
@state.workspace = @workspace.serialize()
@@ -747,7 +758,7 @@ class Atom extends Model
# Only reload stylesheets from non-theme packages
for pack in @packages.getActivePackages() when pack.getType() isnt 'theme'
pack.reloadStylesheets?()
null
return
# Notify the browser project of the window's current project path
watchProjectPath: ->

View File

@@ -32,11 +32,6 @@ defaultOptions =
# Target a version of the regenerator runtime that
# supports yield so the transpiled code is cleaner/smaller.
'asyncToGenerator'
# Because Atom is currently packaged with a fork of React v0.11,
# it makes sense to use the reactCompat transform so the React
# JSX transformer produces pre-v0.12 code.
'reactCompat'
]
# Includes support for es7 features listed at:

View File

@@ -9,11 +9,10 @@ _ = require 'underscore-plus'
# and maintain the state of all menu items.
module.exports =
class ApplicationMenu
constructor: (@version) ->
constructor: (@version, @autoUpdateManager) ->
@windowTemplates = new WeakMap()
@setActiveTemplate(@getDefaultTemplate())
global.atomApplication.autoUpdateManager.on 'state-changed', (state) =>
@showUpdateMenuItem(state)
@autoUpdateManager.on 'state-changed', (state) => @showUpdateMenuItem(state)
# Public: Updates the entire menu with the given keybindings.
#
@@ -33,7 +32,7 @@ class ApplicationMenu
@menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(@menu)
@showUpdateMenuItem(global.atomApplication.autoUpdateManager.getState())
@showUpdateMenuItem(@autoUpdateManager.getState())
# Register a BrowserWindow with this application menu.
addWindow: (window) ->
@@ -162,8 +161,8 @@ class ApplicationMenu
firstKeystroke = keystrokesByCommand[command]?[0]
return null unless firstKeystroke
modifiers = firstKeystroke.split('-')
key = modifiers.pop()
modifiers = firstKeystroke.split(/-(?=.)/)
key = modifiers.pop().toUpperCase().replace('+', 'Plus')
modifiers = modifiers.map (modifier) ->
modifier.replace(/shift/ig, "Shift")
@@ -171,5 +170,5 @@ class ApplicationMenu
.replace(/ctrl/ig, "Ctrl")
.replace(/alt/ig, "Alt")
keys = modifiers.concat([key.toUpperCase()])
keys = modifiers.concat([key])
keys.join("+")

View File

@@ -56,6 +56,7 @@ class AtomApplication
atomProtocolHandler: null
resourcePath: null
version: null
quitting: false
exit: (status) -> app.exit(status)
@@ -71,8 +72,8 @@ class AtomApplication
@pathsToOpen ?= []
@windows = []
@autoUpdateManager = new AutoUpdateManager(@version)
@applicationMenu = new ApplicationMenu(@version)
@autoUpdateManager = new AutoUpdateManager(@version, options.test)
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@listenForArgumentsFromNewProcess()
@@ -85,20 +86,26 @@ class AtomApplication
else
@loadState() or @openPath(options)
openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, apiPreviewMode, newWindow, specDirectory, logFile}) ->
openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, apiPreviewMode, newWindow, specDirectory, logFile, profileStartup}) ->
if test
@runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile})
@runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile, apiPreviewMode})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode})
@openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode, apiPreviewMode}) for urlToOpen in urlsToOpen
else
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode}) # Always open a editor window if this is the first instance of Atom.
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@windows.splice @windows.indexOf(window), 1
@applicationMenu?.enableWindowSpecificItems(false) if @windows.length is 0
if @windows.length is 0
@applicationMenu?.enableWindowSpecificItems(false)
if process.platform in ['win32', 'linux']
app.quit()
return
@saveState() unless window.isSpec or @quitting
# Public: Adds the {AtomWindow} to the global window list.
addWindow: (window) ->
@@ -109,10 +116,14 @@ class AtomApplication
unless window.isSpec
focusHandler = => @lastFocusedWindow = window
blurHandler = => @saveState()
window.browserWindow.on 'focus', focusHandler
window.browserWindow.on 'blur', blurHandler
window.browserWindow.once 'closed', =>
@lastFocusedWindow = null if window is @lastFocusedWindow
window.browserWindow.removeListener 'focus', focusHandler
window.browserWindow.removeListener 'blur', blurHandler
window.browserWindow.webContents.once 'did-finish-load', => @saveState()
# Creates server to listen for additional atom application launches.
#
@@ -175,7 +186,7 @@ class AtomApplication
@on 'application:report-issue', -> require('shell').openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues')
@on 'application:search-issues', -> require('shell').openExternal('https://github.com/issues?q=+is%3Aissue+user%3Aatom')
@on 'application:install-update', -> @autoUpdateManager.install()
@on 'application:install-update', => @autoUpdateManager.install()
@on 'application:check-for-update', => @autoUpdateManager.check()
if process.platform is 'darwin'
@@ -198,17 +209,15 @@ class AtomApplication
@openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
app.on 'window-all-closed', ->
app.quit() if process.platform in ['win32', 'linux']
app.on 'before-quit', =>
@saveState()
@quitting = true
app.on 'will-quit', =>
@killAllProcesses()
@deleteSocketFile()
app.on 'will-exit', =>
@saveState() unless @windows.every (window) -> window.isSpec
@killAllProcesses()
@deleteSocketFile()
@@ -343,9 +352,10 @@ class AtomApplication
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
# :apiPreviewMode - Boolean to control the opened window's 1.0 API preview mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window})
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup, window}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup, window})
# Public: Opens multiple paths, in existing windows if possible.
#
@@ -358,7 +368,7 @@ class AtomApplication
# :apiPreviewMode - Boolean to control the opened window's 1.0 API preview mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, windowDimensions, window}={}) ->
openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, windowDimensions, profileStartup, window}={}) ->
pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen)
locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen)
@@ -388,7 +398,7 @@ class AtomApplication
bootstrapScript ?= require.resolve('../window-bootstrap')
resourcePath ?= @resourcePath
openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, apiPreviewMode, windowDimensions})
openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, apiPreviewMode, windowDimensions, profileStartup})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -420,14 +430,9 @@ class AtomApplication
saveState: ->
states = []
for window in @windows
if loadSettings = window.getLoadSettings()
unless loadSettings.isSpec
states.push(_.pick(loadSettings,
'initialPaths'
'devMode'
'safeMode'
'apiPreviewMode'
))
unless window.isSpec
if loadSettings = window.getLoadSettings()
states.push(initialPaths: loadSettings.initialPaths)
@storageFolder.store('application.json', states)
loadState: ->
@@ -436,9 +441,9 @@ class AtomApplication
@openWithOptions({
pathsToOpen: state.initialPaths
urlsToOpen: []
devMode: state.devMode
safeMode: state.safeMode
apiPreviewMode: state.apiPreviewMode
devMode: @devMode
safeMode: @safeMode
apiPreviewMode: @apiPreviewMode
})
true
else
@@ -484,7 +489,7 @@ class AtomApplication
# :specPath - The directory to load specs from.
# :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
# and ~/.atom/dev/packages, defaults to false.
runSpecs: ({exitWhenDone, resourcePath, specDirectory, logFile, safeMode}) ->
runSpecs: ({exitWhenDone, resourcePath, specDirectory, logFile, safeMode, apiPreviewMode}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
@@ -496,7 +501,8 @@ class AtomApplication
isSpec = true
devMode = true
safeMode ?= false
new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode})
apiPreviewMode ?= false
new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode, apiPreviewMode})
runBenchmarks: ({exitWhenDone, specDirectory}={}) ->
try
@@ -514,13 +520,15 @@ class AtomApplication
return {pathToOpen} unless pathToOpen
return {pathToOpen} if fs.existsSync(pathToOpen)
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
[fileToOpen, initialLine, initialColumn] = path.basename(pathToOpen).split(':')
return {pathToOpen} unless initialLine
return {pathToOpen} unless parseInt(initialLine) > 0
return {pathToOpen} unless parseInt(initialLine) >= 0
# Convert line numbers to a base of 0
initialLine -= 1 if initialLine
initialColumn -= 1 if initialColumn
initialLine = Math.max(0, initialLine - 1) if initialLine
initialColumn = Math.max(0, initialColumn - 1) if initialColumn
pathToOpen = path.join(path.dirname(pathToOpen), fileToOpen)
{pathToOpen, initialLine, initialColumn}

View File

@@ -15,7 +15,7 @@ module.exports =
class AutoUpdateManager
_.extend @prototype, EventEmitter.prototype
constructor: (@version) ->
constructor: (@version, @testMode) ->
@state = IdleState
if process.platform is 'win32'
# Squirrel for Windows can't handle query params
@@ -53,7 +53,7 @@ class AutoUpdateManager
@emitUpdateAvailableEvent(@getWindows()...)
# Only released versions should check for updates.
@check(hidePopups: true) unless /\w{7}/.test(@version)
@scheduleUpdateCheck() unless /\w{7}/.test(@version)
switch process.platform
when 'win32'
@@ -75,15 +75,21 @@ class AutoUpdateManager
getState: ->
@state
scheduleUpdateCheck: ->
checkForUpdates = => @check(hidePopups: true)
fourHours = 1000 * 60 * 60 * 4
setInterval(checkForUpdates, fourHours)
checkForUpdates()
check: ({hidePopups}={}) ->
unless hidePopups
autoUpdater.once 'update-not-available', @onUpdateNotAvailable
autoUpdater.once 'error', @onUpdateError
autoUpdater.checkForUpdates()
autoUpdater.checkForUpdates() unless @testMode
install: ->
autoUpdater.quitAndInstall()
autoUpdater.quitAndInstall() unless @testMode
onUpdateNotAvailable: =>
autoUpdater.removeListener 'error', @onUpdateError

View File

@@ -110,6 +110,7 @@ parseCommandLine = ->
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.')
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.')
options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.')
options.alias('s', 'spec-directory').string('s').describe('s', 'Set the directory from which to run package specs (default: Atom\'s spec directory).')
options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.')
@@ -132,13 +133,13 @@ parseCommandLine = ->
safeMode = args['safe']
apiPreviewMode = args['one']
pathsToOpen = args._
pathsToOpen = [executedFrom] if executedFrom and pathsToOpen.length is 0
test = args['test']
specDirectory = args['spec-directory']
newWindow = args['new-window']
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
profileStartup = args['profile-startup']
if args['resource-path']
devMode = true
@@ -165,6 +166,6 @@ parseCommandLine = ->
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed,
devMode, apiPreviewMode, safeMode, newWindow, specDirectory, logFile,
socketPath}
socketPath, profileStartup}
start()

View File

@@ -48,6 +48,7 @@ class BufferedProcess
constructor: ({command, args, options, stdout, stderr, exit}={}) ->
@emitter = new Emitter
options ?= {}
@command = command
# Related to joyent/node#2318
if process.platform is 'win32'
# Quote all arguments and escapes inner quotes
@@ -69,50 +70,12 @@ class BufferedProcess
cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""]
cmdOptions = _.clone(options)
cmdOptions.windowsVerbatimArguments = true
@process = ChildProcess.spawn(@getCmdPath(), cmdArgs, cmdOptions)
@spawn(@getCmdPath(), cmdArgs, cmdOptions)
else
@process = ChildProcess.spawn(command, args, options)
@spawn(command, args, options)
@killed = false
stdoutClosed = true
stderrClosed = true
processExited = true
exitCode = 0
triggerExitCallback = ->
return if @killed
if stdoutClosed and stderrClosed and processExited
exit?(exitCode)
if stdout
stdoutClosed = false
@bufferStream @process.stdout, stdout, ->
stdoutClosed = true
triggerExitCallback()
if stderr
stderrClosed = false
@bufferStream @process.stderr, stderr, ->
stderrClosed = true
triggerExitCallback()
if exit
processExited = false
@process.on 'exit', (code) ->
exitCode = code
processExited = true
triggerExitCallback()
@process.on 'error', (error) =>
handled = false
handle = -> handled = true
@emitter.emit 'will-throw-error', {error, handle}
if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
error = new Error("Failed to spawn command `#{command}`. Make sure `#{command}` is installed and on your PATH", error.path)
error.name = 'BufferedProcessError'
throw error unless handled
@handleEvents(stdout, stderr, exit)
###
Section: Event Subscription
@@ -164,6 +127,8 @@ class BufferedProcess
# This is required since killing the cmd.exe does not terminate child
# processes.
killOnWindows: ->
return unless @process?
parentPid = @process.pid
cmd = 'wmic'
args = [
@@ -174,7 +139,12 @@ class BufferedProcess
'processid'
]
wmicProcess = ChildProcess.spawn(cmd, args)
try
wmicProcess = ChildProcess.spawn(cmd, args)
catch spawnError
@killProcess()
return
wmicProcess.on 'error', -> # ignore errors
output = ''
wmicProcess.stdout.on 'data', (data) -> output += data
@@ -220,3 +190,55 @@ class BufferedProcess
@killProcess()
undefined
spawn: (command, args, options) ->
try
@process = ChildProcess.spawn(command, args, options)
catch spawnError
process.nextTick => @handleError(spawnError)
handleEvents: (stdout, stderr, exit) ->
return unless @process?
stdoutClosed = true
stderrClosed = true
processExited = true
exitCode = 0
triggerExitCallback = ->
return if @killed
if stdoutClosed and stderrClosed and processExited
exit?(exitCode)
if stdout
stdoutClosed = false
@bufferStream @process.stdout, stdout, ->
stdoutClosed = true
triggerExitCallback()
if stderr
stderrClosed = false
@bufferStream @process.stderr, stderr, ->
stderrClosed = true
triggerExitCallback()
if exit
processExited = false
@process.on 'exit', (code) ->
exitCode = code
processExited = true
triggerExitCallback()
@process.on 'error', (error) => @handleError(error)
return
handleError: (error) ->
handled = false
handle = -> handled = true
@emitter.emit 'will-throw-error', {error, handle}
if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
error = new Error("Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH", error.path)
error.name = 'BufferedProcessError'
throw error unless handled

View File

@@ -98,9 +98,6 @@ module.exports =
type: ['string', 'null']
# These can be used as globals or scoped, thus defaults.
completions:
type: ['array', 'object']
default: []
fontFamily:
type: 'string'
default: ''
@@ -108,6 +105,7 @@ module.exports =
type: 'integer'
default: 16
minimum: 1
maximum: 100
lineHeight:
type: ['string', 'number']
default: 1.3

View File

@@ -105,9 +105,9 @@ class ContextMenuManager
# Detect deprecated file path as first argument
if itemsBySelector? and typeof itemsBySelector isnt 'object'
Grim.deprecate """
ContextMenuManager::add has changed to take a single object as its
`ContextMenuManager::add` has changed to take a single object as its
argument. Please see
https://atom.io/docs/api/latest/ContextMenuManager for more info.
https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format for more info.
"""
itemsBySelector = arguments[1]
devMode = arguments[2]?.devMode
@@ -116,9 +116,9 @@ class ContextMenuManager
for key, value of itemsBySelector
unless _.isArray(value)
Grim.deprecate """
ContextMenuManager::add has changed to take a single object as its
`ContextMenuManager::add` has changed to take a single object as its
argument. Please see
https://atom.io/docs/api/latest/ContextMenuManager for more info.
https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format for more info.
"""
itemsBySelector = @convertLegacyItemsBySelector(itemsBySelector, devMode)

View File

@@ -13,6 +13,8 @@ class CustomGutterComponent
@domNode = atom.views.getView(@gutter)
@decorationsNode = @domNode.firstChild
# Clear the contents in case the domNode is being reused.
@decorationsNode.innerHTML = ''
getDomNode: ->
@domNode
@@ -27,13 +29,14 @@ class CustomGutterComponent
@domNode.style.removeProperty('display')
@visible = true
# `state` is a subset of the TextEditorPresenter state that is specific
# to this line number gutter.
updateSync: (state) ->
@oldDimensionsAndBackgroundState ?= {}
newDimensionsAndBackgroundState = state.gutters
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, newDimensionsAndBackgroundState, @decorationsNode)
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode)
@oldDecorationPositionState ?= {}
decorationState = state.gutters.customDecorations[@gutter.name]
decorationState = state.content
updatedDecorationIds = new Set
for decorationId, decorationInfo of decorationState

View File

@@ -201,6 +201,6 @@ if Grim.includeDeprecatedAPIs
Grim.deprecate 'Use Decoration::getProperties instead'
@getProperties()
Decoration::update = -> (newProperties) ->
Decoration::update = (newProperties) ->
Grim.deprecate 'Use Decoration::setProperties instead'
@setProperties(newProperties)

View File

@@ -24,13 +24,13 @@ class DisplayBuffer extends Model
horizontalScrollMargin: 6
scopedCharacterWidthsChangeCount: 0
constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @invisibles}={}) ->
constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles}={}) ->
super
@emitter = new Emitter
@disposables = new CompositeDisposable
@tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, @invisibles})
@tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, ignoreInvisibles})
@buffer = @tokenizedBuffer.buffer
@charWidthsByScope = {}
@markers = {}
@@ -39,9 +39,9 @@ class DisplayBuffer extends Model
@decorationsByMarkerId = {}
@disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
@disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange
@disposables.add @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated
@disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated
@updateAllScreenLines()
@foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id})
@createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())
subscribeToScopedConfigSettings: =>
@@ -86,14 +86,13 @@ class DisplayBuffer extends Model
scrollTop: @scrollTop
scrollLeft: @scrollLeft
tokenizedBuffer: @tokenizedBuffer.serialize()
invisibles: _.clone(@invisibles)
deserializeParams: (params) ->
params.tokenizedBuffer = TokenizedBuffer.deserialize(params.tokenizedBuffer)
params
copy: ->
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength(), @invisibles})
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength()})
newDisplayBuffer.setScrollTop(@getScrollTop())
newDisplayBuffer.setScrollLeft(@getScrollLeft())
@@ -153,12 +152,12 @@ class DisplayBuffer extends Model
@emitter.on 'did-update-markers', callback
emitDidChange: (eventProperties, refreshMarkers=true) ->
if refreshMarkers
@pauseMarkerChangeEvents()
@refreshMarkerScreenPositions()
@emit 'changed', eventProperties if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', eventProperties
@resumeMarkerChangeEvents()
if refreshMarkers
@refreshMarkerScreenPositions()
@emit 'markers-updated' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-update-markers'
updateWrappedScreenLines: ->
start = 0
@@ -428,8 +427,8 @@ class DisplayBuffer extends Model
setTabLength: (tabLength) ->
@tokenizedBuffer.setTabLength(tabLength)
setInvisibles: (@invisibles) ->
@tokenizedBuffer.setInvisibles(@invisibles)
setIgnoreInvisibles: (ignoreInvisibles) ->
@tokenizedBuffer.setIgnoreInvisibles(ignoreInvisibles)
setSoftWrapped: (softWrapped) ->
if softWrapped isnt @softWrapped
@@ -886,7 +885,7 @@ class DisplayBuffer extends Model
getDecorations: (propertyFilter) ->
allDecorations = []
for markerId, decorations of @decorationsByMarkerId
allDecorations = allDecorations.concat(decorations) if decorations?
allDecorations.push(decorations...) if decorations?
if propertyFilter?
allDecorations = allDecorations.filter (decoration) ->
for key, value of propertyFilter
@@ -1075,17 +1074,11 @@ class DisplayBuffer extends Model
findFoldMarkers: (attributes) ->
@buffer.findMarkers(@getFoldMarkerAttributes(attributes))
getFoldMarkerAttributes: (attributes={}) ->
_.extend(attributes, class: 'fold', displayBufferId: @id)
pauseMarkerChangeEvents: ->
marker.pauseChangeEvents() for marker in @getMarkers()
return
resumeMarkerChangeEvents: ->
marker.resumeChangeEvents() for marker in @getMarkers()
@emit 'markers-updated' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-update-markers'
getFoldMarkerAttributes: (attributes) ->
if attributes
_.extend(attributes, @foldMarkerAttributes)
else
@foldMarkerAttributes
refreshMarkerScreenPositions: ->
for marker in @getMarkers()
@@ -1109,7 +1102,7 @@ class DisplayBuffer extends Model
handleTokenizedBufferChange: (tokenizedBufferChange) =>
{start, end, delta, bufferChange} = tokenizedBufferChange
@updateScreenLines(start, end + 1, delta, delayChangeEvent: bufferChange?)
@updateScreenLines(start, end + 1, delta, refreshMarkers: false)
@setScrollTop(Math.min(@getScrollTop(), @getMaxScrollTop())) if delta < 0
updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) ->
@@ -1120,7 +1113,7 @@ class DisplayBuffer extends Model
{screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta)
screenDelta = screenLines.length - (endScreenRow - startScreenRow)
@screenLines[startScreenRow...endScreenRow] = screenLines
_.spliceWithArray(@screenLines, startScreenRow, endScreenRow - startScreenRow, screenLines, 10000)
@rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions)
@findMaxLineLength(startScreenRow, endScreenRow, screenLines, screenDelta)
@@ -1132,11 +1125,7 @@ class DisplayBuffer extends Model
screenDelta: screenDelta
bufferDelta: bufferDelta
if options.delayChangeEvent
@pauseMarkerChangeEvents()
@pendingChangeEvent = changeEvent
else
@emitDidChange(changeEvent, options.refreshMarkers)
@emitDidChange(changeEvent, options.refreshMarkers)
buildScreenLines: (startBufferRow, endBufferRow) ->
screenLines = []
@@ -1216,11 +1205,6 @@ class DisplayBuffer extends Model
@scrollWidth += 1 unless @isSoftWrapped()
@setScrollLeft(Math.min(@getScrollLeft(), @getMaxScrollLeft()))
handleBufferMarkersUpdated: =>
if event = @pendingChangeEvent
@pendingChangeEvent = null
@emitDidChange(event, false)
handleBufferMarkerCreated: (textBufferMarker) =>
@createFoldForMarker(textBufferMarker) if textBufferMarker.matchesParams(@getFoldMarkerAttributes())
if marker = @getMarker(textBufferMarker.id)

View File

@@ -1,3 +1,4 @@
_ = require 'underscore-plus'
CustomGutterComponent = require './custom-gutter-component'
LineNumberGutterComponent = require './line-number-gutter-component'
@@ -26,11 +27,11 @@ class GutterContainerComponent
updateSync: (state) ->
# The GutterContainerComponent expects the gutters to be sorted in the order
# they should appear.
newState = state.gutters.sortedDescriptions
newState = state.gutters
newGutterComponents = []
newGutterComponentsByGutterName = {}
for {gutter, visible} in newState
for {gutter, visible, styles, content} in newState
gutterComponent = @gutterComponentsByGutterName[gutter.name]
if not gutterComponent
if gutter.name is 'line-number'
@@ -38,8 +39,20 @@ class GutterContainerComponent
@lineNumberGutterComponent = gutterComponent
else
gutterComponent = new CustomGutterComponent({gutter})
if visible then gutterComponent.showNode() else gutterComponent.hideNode()
gutterComponent.updateSync(state)
# Pass the gutter only the state that it needs.
if gutter.name is 'line-number'
# For ease of use in the line number gutter component, set the shared
# 'styles' as a field under the 'content'.
gutterSubstate = _.clone(content)
gutterSubstate.styles = styles
else
# Custom gutter 'content' is keyed on gutter name, so we cannot set
# 'styles' as a subfield directly under it.
gutterSubstate = {content, styles}
gutterComponent.updateSync(gutterSubstate)
newGutterComponents.push({
name: gutter.name,
component: gutterComponent,

View File

@@ -13,6 +13,7 @@ class LineNumberGutterComponent
@domNode = atom.views.getView(@gutter)
@lineNumbersNode = @domNode.firstChild
@lineNumbersNode.innerHTML = ''
@domNode.addEventListener 'click', @onClick
@domNode.addEventListener 'mousedown', @onMouseDown
@@ -30,19 +31,25 @@ class LineNumberGutterComponent
@domNode.style.removeProperty('display')
@visible = true
# `state` is a subset of the TextEditorPresenter state that is specific
# to this line number gutter.
updateSync: (state) ->
@newState = state.gutters.lineNumberGutter
@oldState ?= {lineNumbers: {}}
@newState = state
@oldState ?=
lineNumbers: {}
styles: {}
@appendDummyLineNumber() unless @dummyLineNumberNode?
newDimensionsAndBackgroundState = state.gutters
setDimensionsAndBackground(@oldState, newDimensionsAndBackgroundState, @lineNumbersNode)
setDimensionsAndBackground(@oldState.styles, @newState.styles, @lineNumbersNode)
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
@updateDummyLineNumber()
node.remove() for id, node of @lineNumberNodesById
@oldState = {maxLineNumberDigits: @newState.maxLineNumberDigits, lineNumbers: {}}
@oldState =
maxLineNumberDigits: @newState.maxLineNumberDigits
lineNumbers: {}
styles: {}
@lineNumberNodesById = {}
@updateLineNumbers()

View File

@@ -48,7 +48,6 @@ class Marker
oldTailBufferPosition: null
oldTailScreenPosition: null
wasValid: true
deferredChangeEvents: null
###
Section: Construction and Destruction
@@ -332,11 +331,11 @@ class Marker
newTailScreenPosition = @getTailScreenPosition()
isValid = @isValid()
return if _.isEqual(isValid, @wasValid) and
_.isEqual(newHeadBufferPosition, @oldHeadBufferPosition) and
_.isEqual(newHeadScreenPosition, @oldHeadScreenPosition) and
_.isEqual(newTailBufferPosition, @oldTailBufferPosition) and
_.isEqual(newTailScreenPosition, @oldTailScreenPosition)
return if isValid is @wasValid and
newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and
newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and
newTailBufferPosition.isEqual(@oldTailBufferPosition) and
newTailScreenPosition.isEqual(@oldTailScreenPosition)
changeEvent = {
@oldHeadScreenPosition, newHeadScreenPosition,
@@ -347,11 +346,8 @@ class Marker
isValid
}
if @deferredChangeEvents?
@deferredChangeEvents.push(changeEvent)
else
@emit 'changed', changeEvent if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', changeEvent
@emit 'changed', changeEvent if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', changeEvent
@oldHeadBufferPosition = newHeadBufferPosition
@oldHeadScreenPosition = newHeadScreenPosition
@@ -359,18 +355,6 @@ class Marker
@oldTailScreenPosition = newTailScreenPosition
@wasValid = isValid
pauseChangeEvents: ->
@deferredChangeEvents = []
resumeChangeEvents: ->
if deferredChangeEvents = @deferredChangeEvents
@deferredChangeEvents = null
for event in deferredChangeEvents
@emit 'changed', event if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', event
return
getPixelRange: ->
@displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false)

View File

@@ -1,8 +1,8 @@
{Emitter, Disposable} = require 'event-kit'
Notification = require '../src/notification'
# Experimental: Allows messaging the user. This will likely change, dont use
# quite yet!
# Public: A notification manager used to create {Notification}s to be shown
# to the user.
module.exports =
class NotificationManager
constructor: ->
@@ -13,6 +13,12 @@ class NotificationManager
Section: Events
###
# Public: Invoke the given callback after a notification has been added.
#
# * `callback` {Function} to be called after the notification is added.
# * `notification` The {Notification} that was added.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddNotification: (callback) ->
@emitter.on 'did-add-notification', callback
@@ -20,18 +26,43 @@ class NotificationManager
Section: Adding Notifications
###
# Public: Add a success notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addSuccess: (message, options) ->
@addNotification(new Notification('success', message, options))
# Public: Add an informational notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addInfo: (message, options) ->
@addNotification(new Notification('info', message, options))
# Public: Add a warning notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addWarning: (message, options) ->
@addNotification(new Notification('warning', message, options))
# Public: Add an error notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addError: (message, options) ->
@addNotification(new Notification('error', message, options))
# Public: Add a fatal error notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addFatalError: (message, options) ->
@addNotification(new Notification('fatal', message, options))
@@ -47,7 +78,10 @@ class NotificationManager
Section: Getting Notifications
###
getNotifications: -> @notifications
# Public: Get all the notifications.
#
# Returns an {Array} of {Notifications}s.
getNotifications: -> @notifications.slice()
###
Section: Managing Notifications

View File

@@ -1,6 +1,6 @@
{Emitter} = require 'event-kit'
# Experimental: This will likely change, do not use.
# Public: A notification to the user containing a message and type.
module.exports =
class Notification
constructor: (@type, @message, @options={}) ->
@@ -18,8 +18,10 @@ class Notification
getOptions: -> @options
# Public: Retrieves the {String} type.
getType: -> @type
# Public: Retrieves the {String} message.
getMessage: -> @message
getTimestamp: -> @timestamp

View File

@@ -303,6 +303,9 @@ class PackageManager
# of the first package isn't skewed by being the first to require atom
require '../exports/atom'
# TODO: remove after a few atom versions.
@uninstallAutocompletePlus()
packagePaths = @getAvailablePackagePaths()
packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
@@ -409,6 +412,39 @@ class PackageManager
message = "Failed to load the #{path.basename(packagePath)} package"
atom.notifications.addError(message, {stack, detail, dismissable: true})
# TODO: remove these autocomplete-plus specific helpers after a few versions.
uninstallAutocompletePlus: ->
packageDir = null
devDir = path.join("dev", "packages")
for packageDirPath in @packageDirPaths
if not packageDirPath.endsWith(devDir)
packageDir = packageDirPath
break
if packageDir?
dirsToRemove = [
path.join(packageDir, 'autocomplete-plus')
path.join(packageDir, 'autocomplete-atom-api')
path.join(packageDir, 'autocomplete-css')
path.join(packageDir, 'autocomplete-html')
path.join(packageDir, 'autocomplete-snippets')
]
for dirToRemove in dirsToRemove
@uninstallDirectory(dirToRemove)
return
uninstallDirectory: (directory) ->
symlinkPromise = new Promise (resolve) ->
fs.isSymbolicLink directory, (isSymLink) -> resolve(isSymLink)
dirPromise = new Promise (resolve) ->
fs.isDirectory directory, (isDir) -> resolve(isDir)
Promise.all([symlinkPromise, dirPromise]).then (values) ->
[isSymLink, isDir] = values
if not isSymLink and isDir
fs.remove directory, ->
if Grim.includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(PackageManager)

View File

@@ -1,4 +1,5 @@
path = require 'path'
normalizePackageData = null
_ = require 'underscore-plus'
async = require 'async'
@@ -24,6 +25,13 @@ class Package
@resourcePathWithTrailingSlash ?= "#{atom.packages.resourcePath}#{path.sep}"
packagePath?.startsWith(@resourcePathWithTrailingSlash)
@normalizeMetadata: (metadata) ->
unless metadata?._id
normalizePackageData ?= require 'normalize-package-data'
normalizePackageData(metadata)
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')
@loadMetadata: (packagePath, ignoreErrors=false) ->
packageName = path.basename(packagePath)
if @isBundledPackagePath(packagePath)
@@ -32,8 +40,10 @@ class Package
if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
try
metadata = CSON.readFileSync(metadataPath)
@normalizeMetadata(metadata)
catch error
throw error unless ignoreErrors
metadata ?= {}
metadata.name = packageName
@@ -161,9 +171,9 @@ class Package
if @mainModule.config? and typeof @mainModule.config is 'object'
atom.config.setSchema @name, {type: 'object', properties: @mainModule.config}
else if includeDeprecatedAPIs and @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object'
deprecate """Use a config schema instead. See the configuration section
deprecate("""Use a config schema instead. See the configuration section
of https://atom.io/docs/latest/hacking-atom-package-word-count and
https://atom.io/docs/api/latest/Config for more details"""
https://atom.io/docs/api/latest/Config for more details""", {packageName: @name})
atom.config.setDefaults(@name, @mainModule.configDefaults)
@mainModule.activateConfig?()
@configActivated = true
@@ -191,7 +201,20 @@ class Package
for [menuPath, map] in @menus when map['context-menu']?
try
@activationDisposables.add(atom.contextMenu.add(map['context-menu']))
itemsBySelector = map['context-menu']
if includeDeprecatedAPIs
# Detect deprecated format for items object
for key, value of itemsBySelector
unless _.isArray(value)
deprecate("""
The context menu CSON format has changed. Please see
https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format
for more info.
""", {packageName: @name})
itemsBySelector = atom.contextMenu.convertLegacyItemsBySelector(itemsBySelector)
@activationDisposables.add(atom.contextMenu.add(itemsBySelector))
catch error
if error.code is 'EBADSELECTOR'
error.message += " in #{menuPath}"
@@ -465,7 +488,7 @@ class Package
@activationCommands[selector].push(commands...)
if includeDeprecatedAPIs and @metadata.activationEvents?
deprecate """
deprecate("""
Use `activationCommands` instead of `activationEvents` in your package.json
Commands should be grouped by selector as follows:
```json
@@ -474,7 +497,7 @@ class Package
"atom-text-editor": ["foo:quux"]
}
```
"""
""", {packageName: @name})
if _.isArray(@metadata.activationEvents)
for eventName in @metadata.activationEvents
@activationCommands['atom-workspace'] ?= []

View File

@@ -12,6 +12,9 @@ class PaneResizeHandleElement extends HTMLElement
@isHorizontal = @parentElement.classList.contains("horizontal")
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
detachedCallback: ->
@resizeStopped()
resizeToFitContent: ->
# clear flex-grow css style of both pane
@previousSibling.model.setFlexScale(1)
@@ -43,6 +46,7 @@ class PaneResizeHandleElement extends HTMLElement
resizePane: ({clientX, clientY, which}) ->
return @resizeStopped() unless which is 1
return @resizeStopped() unless @previousSibling? and @nextSibling?
if @isHorizontal
totalWidth = @previousSibling.clientWidth + @nextSibling.clientWidth

View File

@@ -404,6 +404,20 @@ class Selection extends Model
@selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow())
@deleteSelectedText()
# Public: Removes the selection or, if nothing is selected, then all
# characters from the start of the selection back to the previous word
# boundary.
deleteToPreviousWordBoundary: ->
@selectToPreviousWordBoundary() if @isEmpty()
@deleteSelectedText()
# Public: Removes the selection or, if nothing is selected, then all
# characters from the start of the selection up to the next word
# boundary.
deleteToNextWordBoundary: ->
@selectToNextWordBoundary() if @isEmpty()
@deleteSelectedText()
# 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.
deleteToBeginningOfWord: ->

View File

@@ -70,7 +70,7 @@ class TextEditorComponent
@scrollViewNode.classList.add('scroll-view')
@domNode.appendChild(@scrollViewNode)
@mountGutterContainerComponent() if @presenter.getState().gutters.sortedDescriptions.length
@mountGutterContainerComponent() if @presenter.getState().gutters.length
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
@@ -137,7 +137,7 @@ class TextEditorComponent
else
@domNode.style.height = ''
if @newState.gutters.sortedDescriptions.length
if @newState.gutters.length
@mountGutterContainerComponent() unless @gutterContainerComponent?
@gutterContainerComponent.updateSync(@newState)
else
@@ -349,15 +349,15 @@ class TextEditorComponent
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
# Scrolling horizontally
previousScrollLeft = @editor.getScrollLeft()
previousScrollLeft = @presenter.getScrollLeft()
@presenter.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity))
event.preventDefault() unless previousScrollLeft is @editor.getScrollLeft()
event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft()
else
# Scrolling vertically
@presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
previousScrollTop = @presenter.scrollTop
previousScrollTop = @presenter.getScrollTop()
@presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity))
event.preventDefault() unless previousScrollTop is @editor.getScrollTop()
event.preventDefault() unless previousScrollTop is @presenter.getScrollTop()
onScrollViewScroll: =>
if @mounted
@@ -619,6 +619,7 @@ class TextEditorComponent
if clientWidth > 0
@presenter.setContentFrameWidth(clientWidth)
@presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0)
@presenter.setBoundingClientRect(@hostElement.getBoundingClientRect())
measureWindowSize: ->

View File

@@ -281,6 +281,8 @@ atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
'core:cut': -> @cutSelectedText()
'core:copy': -> @copySelectedText()
'core:paste': -> @pasteText()
'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary()
'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary()
'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord()
'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine()
'editor:delete-to-end-of-line': -> @deleteToEndOfLine()

View File

@@ -13,12 +13,13 @@ class TextEditorPresenter
overlayDimensions: {}
constructor: (params) ->
{@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight} = params
{@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params
{horizontalScrollbarHeight, verticalScrollbarWidth} = params
{@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params
@measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
@measuredVerticalScrollbarWidth = verticalScrollbarWidth
@gutterWidth ?= 0
@disposables = new CompositeDisposable
@emitter = new Emitter
@@ -55,18 +56,6 @@ class TextEditorPresenter
isBatching: ->
@updating is false
# Private: Executes `fn` if `isBatching()` is false, otherwise sets `@[flagName]` to `true` for later processing. In either cases, it calls `emitDidUpdateState`.
# * `flagName` {String} name of a property of this presenter
# * `fn` {Function} to call when not batching.
batch: (flagName, fn) ->
if @isBatching()
@[flagName] = true
else
fn.apply(this)
@[flagName] = false
@emitDidUpdateState()
# Public: Gets this presenter's state, updating it just in time before returning from this function.
# Returns a state {Object}, useful for rendering to screen.
getState: ->
@@ -94,40 +83,68 @@ class TextEditorPresenter
@updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState
@updating = false
@resetTrackedUpdates()
@state
resetTrackedUpdates: ->
@shouldUpdateFocusedState = false
@shouldUpdateHeightState = false
@shouldUpdateVerticalScrollState = false
@shouldUpdateHorizontalScrollState = false
@shouldUpdateScrollbarsState = false
@shouldUpdateHiddenInputState = false
@shouldUpdateContentState = false
@shouldUpdateDecorations = false
@shouldUpdateLinesState = false
@shouldUpdateCursorsState = false
@shouldUpdateOverlaysState = false
@shouldUpdateLineNumberGutterState = false
@shouldUpdateLineNumbersState = false
@shouldUpdateGutterOrderState = false
@shouldUpdateCustomGutterDecorationState = false
observeModel: ->
@disposables.add @model.onDidChange =>
@updateContentDimensions()
@updateEndRow()
@updateHeightState()
@updateVerticalScrollState()
@updateHorizontalScrollState()
@updateScrollbarsState()
@updateContentState()
@updateDecorations()
@updateLinesState()
@updateLineNumberGutterState()
@updateLineNumbersState()
@updateGutterOrderState()
@updateCustomGutterDecorationState()
@shouldUpdateHeightState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
@disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this))
@disposables.add @model.onDidChangePlaceholderText(@updateContentState.bind(this))
@disposables.add @model.onDidChangePlaceholderText =>
@shouldUpdateContentState = true
@emitDidUpdateState()
@disposables.add @model.onDidChangeMini =>
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@updateScrollbarDimensions()
@updateScrollbarsState()
@updateContentState()
@updateDecorations()
@updateLinesState()
@updateLineNumberGutterState()
@updateLineNumbersState()
@updateCommonGutterState()
@updateGutterOrderState()
@updateCustomGutterDecorationState()
@emitDidUpdateState()
@disposables.add @model.onDidChangeLineNumberGutterVisible =>
@updateLineNumberGutterState()
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@updateCommonGutterState()
@updateGutterOrderState()
@emitDidUpdateState()
@disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
@disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
@disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this))
@@ -153,24 +170,32 @@ class TextEditorPresenter
@configDisposables.add atom.config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
@showIndentGuide = newValue
@updateContentState()
@shouldUpdateContentState = true
@emitDidUpdateState()
@configDisposables.add atom.config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
@scrollPastEnd = newValue
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@updateScrollHeight()
@updateVerticalScrollState()
@updateScrollbarsState()
@emitDidUpdateState()
@configDisposables.add atom.config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) =>
@showLineNumbers = newValue
@updateLineNumberGutterState()
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@updateCommonGutterState()
@updateGutterOrderState()
@emitDidUpdateState()
didChangeGrammar: ->
@observeConfig()
@updateContentState()
@updateLineNumberGutterState()
@shouldUpdateContentState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@updateCommonGutterState()
@updateGutterOrderState()
@emitDidUpdateState()
buildState: ->
@state =
@@ -183,11 +208,13 @@ class TextEditorPresenter
lines: {}
highlights: {}
overlays: {}
gutters:
sortedDescriptions: []
customDecorations: {}
lineNumberGutter:
lineNumbers: {}
gutters: []
# Shared state that is copied into ``@state.gutters`.
@sharedGutterStyles = {}
@customGutterDecorations = {}
@lineNumberGutter =
lineNumbers: {}
@updateState()
updateState: ->
@@ -213,32 +240,34 @@ class TextEditorPresenter
@updateGutterOrderState()
@updateCustomGutterDecorationState()
updateFocusedState: -> @batch "shouldUpdateFocusedState", ->
@resetTrackedUpdates()
updateFocusedState: ->
@state.focused = @focused
updateHeightState: -> @batch "shouldUpdateHeightState", ->
updateHeightState: ->
if @autoHeight
@state.height = @contentHeight
else
@state.height = null
updateVerticalScrollState: -> @batch "shouldUpdateVerticalScrollState", ->
updateVerticalScrollState: ->
@state.content.scrollHeight = @scrollHeight
@state.gutters.scrollHeight = @scrollHeight
@sharedGutterStyles.scrollHeight = @scrollHeight
@state.verticalScrollbar.scrollHeight = @scrollHeight
@state.content.scrollTop = @scrollTop
@state.gutters.scrollTop = @scrollTop
@sharedGutterStyles.scrollTop = @scrollTop
@state.verticalScrollbar.scrollTop = @scrollTop
updateHorizontalScrollState: -> @batch "shouldUpdateHorizontalScrollState", ->
updateHorizontalScrollState: ->
@state.content.scrollWidth = @scrollWidth
@state.horizontalScrollbar.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@state.horizontalScrollbar.scrollLeft = @scrollLeft
updateScrollbarsState: -> @batch "shouldUpdateScrollbarsState", ->
updateScrollbarsState: ->
@state.horizontalScrollbar.visible = @horizontalScrollbarHeight > 0
@state.horizontalScrollbar.height = @measuredHorizontalScrollbarHeight
@state.horizontalScrollbar.right = @verticalScrollbarWidth
@@ -247,7 +276,7 @@ class TextEditorPresenter
@state.verticalScrollbar.width = @measuredVerticalScrollbarWidth
@state.verticalScrollbar.bottom = @horizontalScrollbarHeight
updateHiddenInputState: -> @batch "shouldUpdateHiddenInputState", ->
updateHiddenInputState: ->
return unless lastCursor = @model.getLastCursor()
{top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange())
@@ -264,14 +293,14 @@ class TextEditorPresenter
@state.hiddenInput.height = height
@state.hiddenInput.width = Math.max(width, 2)
updateContentState: -> @batch "shouldUpdateContentState", ->
updateContentState: ->
@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
updateLinesState: -> @batch "shouldUpdateLinesState", ->
updateLinesState: ->
return unless @startRow? and @endRow? and @lineHeight?
visibleLineIds = {}
@@ -316,7 +345,7 @@ class TextEditorPresenter
top: row * @lineHeight
decorationClasses: @lineDecorationClassesForRow(row)
updateCursorsState: -> @batch "shouldUpdateCursorsState", ->
updateCursorsState: ->
@state.content.cursors = {}
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
return
@@ -334,7 +363,7 @@ class TextEditorPresenter
@emitDidUpdateState()
updateOverlaysState: -> @batch "shouldUpdateOverlaysState", ->
updateOverlaysState: ->
return unless @hasOverlayPositionRequirements()
visibleDecorationIds = {}
@@ -351,10 +380,9 @@ class TextEditorPresenter
pixelPosition = @pixelPositionForScreenPosition(screenPosition)
{scrollTop, scrollLeft} = @state.content
gutterWidth = @boundingClientRect.width - @contentFrameWidth
top = pixelPosition.top + @lineHeight - scrollTop
left = pixelPosition.left + gutterWidth - scrollLeft
left = pixelPosition.left + @gutterWidth - scrollLeft
if overlayDimensions = @overlayDimensions[decoration.id]
{itemWidth, itemHeight, contentMargin} = overlayDimensions
@@ -383,11 +411,11 @@ class TextEditorPresenter
return
updateLineNumberGutterState: -> @batch "shouldUpdateLineNumberGutterState", ->
@state.gutters.lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length
updateLineNumberGutterState: ->
@lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length
updateCommonGutterState: ->
@state.gutters.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
@sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
@gutterBackgroundColor
else
@backgroundColor
@@ -395,30 +423,45 @@ class TextEditorPresenter
didAddGutter: (gutter) ->
gutterDisposables = new CompositeDisposable
gutterDisposables.add gutter.onDidChangeVisible =>
@updateGutterOrderState()
@updateCustomGutterDecorationState()
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
gutterDisposables.add gutter.onDidDestroy =>
@disposables.remove(gutterDisposables)
gutterDisposables.dispose()
@updateGutterOrderState()
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
# It is not necessary to @updateCustomGutterDecorationState here.
# The destroyed gutter will be removed from the list of gutters in @state,
# and thus will be removed from the DOM.
@disposables.add(gutterDisposables)
@updateGutterOrderState()
@updateCustomGutterDecorationState()
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
updateGutterOrderState: ->
@batch "shouldUpdateGutterOrderState", ->
@state.gutters.sortedDescriptions = []
if @model.isMini()
return
for gutter in @model.getGutters()
isVisible = @gutterIsVisible(gutter)
@state.gutters.sortedDescriptions.push({gutter, visible: isVisible})
@state.gutters = []
if @model.isMini()
return
for gutter in @model.getGutters()
isVisible = @gutterIsVisible(gutter)
if gutter.name is 'line-number'
content = @lineNumberGutter
else
@customGutterDecorations[gutter.name] ?= {}
content = @customGutterDecorations[gutter.name]
@state.gutters.push({
gutter,
visible: isVisible,
styles: @sharedGutterStyles,
content,
})
# Updates the decoration state for the gutter with the given gutterName.
# @state.gutters.customDecorations is an {Object}, with the form:
# @customGutterDecorations is an {Object}, with the form:
# * gutterName : {
# decoration.id : {
# top: # of pixels from top
@@ -428,25 +471,44 @@ class TextEditorPresenter
# }
# }
updateCustomGutterDecorationState: ->
@batch 'shouldUpdateCustomGutterDecorationState', =>
return unless @startRow? and @endRow? and @lineHeight?
return unless @startRow? and @endRow? and @lineHeight?
@state.gutters.customDecorations = {}
return if @model.isMini()
if @model.isMini()
# Mini editors have no gutter decorations.
# We clear instead of reassigning to preserve the reference.
@clearAllCustomGutterDecorations()
for gutter in @model.getGutters()
gutterName = gutter.name
@state.gutters.customDecorations[gutterName] = {}
return if not @gutterIsVisible(gutter)
for gutter in @model.getGutters()
gutterName = gutter.name
gutterDecorations = @customGutterDecorations[gutterName]
if gutterDecorations
# Clear the gutter decorations; they are rebuilt.
# We clear instead of reassigning to preserve the reference.
@clearDecorationsForCustomGutterName(gutterName)
else
@customGutterDecorations[gutterName] = {}
return if not @gutterIsVisible(gutter)
relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
relevantDecorations.forEach (decoration) =>
decorationRange = decoration.getMarker().getScreenRange()
@state.gutters.customDecorations[gutterName][decoration.id] =
top: @lineHeight * decorationRange.start.row
height: @lineHeight * decorationRange.getRowCount()
item: decoration.getProperties().item
class: decoration.getProperties().class
relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
relevantDecorations.forEach (decoration) =>
decorationRange = decoration.getMarker().getScreenRange()
@customGutterDecorations[gutterName][decoration.id] =
top: @lineHeight * decorationRange.start.row
height: @lineHeight * decorationRange.getRowCount()
item: decoration.getProperties().item
class: decoration.getProperties().class
clearAllCustomGutterDecorations: ->
allGutterNames = Object.keys(@customGutterDecorations)
for gutterName in allGutterNames
@clearDecorationsForCustomGutterName(gutterName)
clearDecorationsForCustomGutterName: (gutterName) ->
gutterDecorations = @customGutterDecorations[gutterName]
if gutterDecorations
allDecorationIds = Object.keys(gutterDecorations)
for decorationId in allDecorationIds
delete gutterDecorations[decorationId]
gutterIsVisible: (gutterModel) ->
isVisible = gutterModel.isVisible()
@@ -454,7 +516,7 @@ class TextEditorPresenter
isVisible = isVisible and @showLineNumbers
isVisible
updateLineNumbersState: -> @batch "shouldUpdateLineNumbersState", ->
updateLineNumbersState: ->
return unless @startRow? and @endRow? and @lineHeight?
visibleLineNumberIds = {}
@@ -484,7 +546,7 @@ class TextEditorPresenter
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
@state.gutters.lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
@lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
visibleLineNumberIds[id] = true
if @mouseWheelScreenRow?
@@ -494,8 +556,8 @@ class TextEditorPresenter
id += '-' + wrapCount if wrapCount > 0
visibleLineNumberIds[id] = true
for id of @state.gutters.lineNumberGutter.lineNumbers
delete @state.gutters.lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id]
for id of @lineNumberGutter.lineNumbers
delete @lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id]
return
@@ -541,7 +603,8 @@ class TextEditorPresenter
if @baseCharacterWidth?
oldContentWidth = @contentWidth
@contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), Infinity]).left
clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped()
@contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left
@contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
if @contentHeight isnt oldContentHeight
@@ -670,8 +733,10 @@ class TextEditorPresenter
@startBlinkingCursors()
else
@stopBlinkingCursors(false)
@updateFocusedState()
@updateHiddenInputState()
@shouldUpdateFocusedState = true
@shouldUpdateHiddenInputState = true
@emitDidUpdateState()
setScrollTop: (scrollTop) ->
scrollTop = @constrainScrollTop(scrollTop)
@@ -682,14 +747,19 @@ class TextEditorPresenter
@updateStartRow()
@updateEndRow()
@didStartScrolling()
@updateVerticalScrollState()
@updateHiddenInputState()
@updateDecorations()
@updateLinesState()
@updateCursorsState()
@updateLineNumbersState()
@updateCustomGutterDecorationState()
@updateOverlaysState()
@shouldUpdateVerticalScrollState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
getScrollTop: ->
@scrollTop
didStartScrolling: ->
if @stoppedScrollingTimeoutId?
@@ -703,11 +773,11 @@ class TextEditorPresenter
@state.content.scrollingVertically = false
if @mouseWheelScreenRow?
@mouseWheelScreenRow = null
@updateLinesState()
@updateLineNumbersState()
@updateCustomGutterDecorationState()
else
@emitDidUpdateState()
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
setScrollLeft: (scrollLeft) ->
scrollLeft = @constrainScrollLeft(scrollLeft)
@@ -715,10 +785,15 @@ class TextEditorPresenter
oldScrollLeft = @scrollLeft
@scrollLeft = scrollLeft
@model.setScrollLeft(scrollLeft)
@updateHorizontalScrollState()
@updateHiddenInputState()
@updateCursorsState() unless oldScrollLeft?
@updateOverlaysState()
@shouldUpdateHorizontalScrollState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateCursorsState = true unless oldScrollLeft?
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
getScrollLeft: ->
@scrollLeft
setHorizontalScrollbarHeight: (horizontalScrollbarHeight) ->
unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight
@@ -726,10 +801,12 @@ class TextEditorPresenter
@measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
@model.setHorizontalScrollbarHeight(horizontalScrollbarHeight)
@updateScrollbarDimensions()
@updateScrollbarsState()
@updateVerticalScrollState()
@updateHorizontalScrollState()
@updateCursorsState() unless oldHorizontalScrollbarHeight?
@shouldUpdateScrollbarsState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateCursorsState = true unless oldHorizontalScrollbarHeight?
@emitDidUpdateState()
setVerticalScrollbarWidth: (verticalScrollbarWidth) ->
unless @measuredVerticalScrollbarWidth is verticalScrollbarWidth
@@ -737,28 +814,34 @@ class TextEditorPresenter
@measuredVerticalScrollbarWidth = verticalScrollbarWidth
@model.setVerticalScrollbarWidth(verticalScrollbarWidth)
@updateScrollbarDimensions()
@updateScrollbarsState()
@updateVerticalScrollState()
@updateHorizontalScrollState()
@updateCursorsState() unless oldVerticalScrollbarWidth?
@shouldUpdateScrollbarsState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateCursorsState = true unless oldVerticalScrollbarWidth?
@emitDidUpdateState()
setAutoHeight: (autoHeight) ->
unless @autoHeight is autoHeight
@autoHeight = autoHeight
@updateHeightState()
@shouldUpdateHeightState = true
@emitDidUpdateState()
setExplicitHeight: (explicitHeight) ->
unless @explicitHeight is explicitHeight
@explicitHeight = explicitHeight
@model.setHeight(explicitHeight)
@updateHeight()
@updateVerticalScrollState()
@updateScrollbarsState()
@updateDecorations()
@updateLinesState()
@updateCursorsState()
@updateLineNumbersState()
@updateCustomGutterDecorationState()
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
updateHeight: ->
height = @explicitHeight ? @contentHeight
@@ -776,18 +859,22 @@ class TextEditorPresenter
@model.setWidth(contentFrameWidth)
@updateScrollbarDimensions()
@updateClientWidth()
@updateVerticalScrollState()
@updateHorizontalScrollState()
@updateScrollbarsState()
@updateContentState()
@updateDecorations()
@updateLinesState()
@updateCursorsState() unless oldContentFrameWidth?
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true unless oldContentFrameWidth?
@emitDidUpdateState()
setBoundingClientRect: (boundingClientRect) ->
unless @clientRectsEqual(@boundingClientRect, boundingClientRect)
@boundingClientRect = boundingClientRect
@updateOverlaysState()
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
clientRectsEqual: (clientRectA, clientRectB) ->
clientRectA? and clientRectB? and
@@ -800,22 +887,33 @@ class TextEditorPresenter
if @windowWidth isnt width or @windowHeight isnt height
@windowWidth = width
@windowHeight = height
@updateOverlaysState()
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
setBackgroundColor: (backgroundColor) ->
unless @backgroundColor is backgroundColor
@backgroundColor = backgroundColor
@updateContentState()
@updateLineNumberGutterState()
@shouldUpdateContentState = true
@shouldUpdateLineNumberGutterState = true
@updateCommonGutterState()
@updateGutterOrderState()
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
setGutterBackgroundColor: (gutterBackgroundColor) ->
unless @gutterBackgroundColor is gutterBackgroundColor
@gutterBackgroundColor = gutterBackgroundColor
@updateLineNumberGutterState()
@shouldUpdateLineNumberGutterState = true
@updateCommonGutterState()
@updateGutterOrderState()
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
setGutterWidth: (gutterWidth) ->
if @gutterWidth isnt gutterWidth
@gutterWidth = gutterWidth
@updateOverlaysState()
setLineHeight: (lineHeight) ->
unless @lineHeight is lineHeight
@@ -826,17 +924,19 @@ class TextEditorPresenter
@updateHeight()
@updateStartRow()
@updateEndRow()
@updateHeightState()
@updateHorizontalScrollState()
@updateVerticalScrollState()
@updateScrollbarsState()
@updateHiddenInputState()
@updateDecorations()
@updateLinesState()
@updateCursorsState()
@updateLineNumbersState()
@updateCustomGutterDecorationState()
@updateOverlaysState()
@shouldUpdateHeightState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
setMouseWheelScreenRow: (mouseWheelScreenRow) ->
unless @mouseWheelScreenRow is mouseWheelScreenRow
@@ -876,15 +976,17 @@ class TextEditorPresenter
characterWidthsChanged: ->
@updateContentDimensions()
@updateHorizontalScrollState()
@updateVerticalScrollState()
@updateScrollbarsState()
@updateHiddenInputState()
@updateContentState()
@updateDecorations()
@updateLinesState()
@updateCursorsState()
@updateOverlaysState()
@shouldUpdateHorizontalScrollState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
clearScopedCharacterWidths: ->
@characterWidthsByScope = {}
@@ -972,11 +1074,11 @@ class TextEditorPresenter
intersectsVisibleRowRange = true
if intersectsVisibleRowRange
@updateLinesState() if decoration.isType('line')
@shouldUpdateLinesState = true if decoration.isType('line')
if decoration.isType('line-number')
@updateLineNumbersState()
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@updateCustomGutterDecorationState()
@shouldUpdateCustomGutterDecorationState = true
if decoration.isType('highlight')
return if change.textChanged
@@ -984,7 +1086,9 @@ class TextEditorPresenter
@updateHighlightState(decoration)
if decoration.isType('overlay')
@updateOverlaysState()
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
decorationPropertiesDidChange: (decoration, event) ->
{oldProperties} = event
@@ -995,29 +1099,33 @@ class TextEditorPresenter
decoration.getMarker().getScreenRange())
@addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
if decoration.isType('line') or Decoration.isType(oldProperties, 'line')
@updateLinesState()
@shouldUpdateLinesState = true
if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number')
@updateLineNumbersState()
@shouldUpdateLineNumbersState = true
if (decoration.isType('gutter') and not decoration.isType('line-number')) or
(Decoration.isType(oldProperties, 'gutter') and not Decoration.isType(oldProperties, 'line-number'))
@updateCustomGutterDecorationState()
@shouldUpdateCustomGutterDecorationState = true
else if decoration.isType('overlay')
@updateOverlaysState()
@shouldUpdateOverlaysState = true
else if decoration.isType('highlight')
@updateHighlightState(decoration, event)
@emitDidUpdateState()
didDestroyDecoration: (decoration) ->
if decoration.isType('line') or decoration.isType('gutter')
@removeFromLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
@updateLinesState() if decoration.isType('line')
@shouldUpdateLinesState = true if decoration.isType('line')
if decoration.isType('line-number')
@updateLineNumbersState()
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@updateCustomGutterDecorationState(decoration.getProperties().gutterName)
@shouldUpdateCustomGutterDecorationState = true
if decoration.isType('highlight')
@updateHighlightState(decoration)
if decoration.isType('overlay')
@updateOverlaysState()
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
highlightDidFlash: (decoration) ->
flash = decoration.consumeNextFlash()
@@ -1032,17 +1140,19 @@ class TextEditorPresenter
if decoration.isType('line') or decoration.isType('gutter')
@addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
@updateLinesState() if decoration.isType('line')
@shouldUpdateLinesState = true if decoration.isType('line')
if decoration.isType('line-number')
@updateLineNumbersState()
@shouldUpdateLineNumbersState = true
else if decoration.isType('gutter')
@updateCustomGutterDecorationState()
@shouldUpdateCustomGutterDecorationState = true
else if decoration.isType('highlight')
@updateHighlightState(decoration)
else if decoration.isType('overlay')
@updateOverlaysState()
@shouldUpdateOverlaysState = true
updateDecorations: -> @batch "shouldUpdateDecorations", ->
@emitDidUpdateState()
updateDecorations: ->
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterNameAndScreenRow = {}
@@ -1197,14 +1307,18 @@ class TextEditorPresenter
overlayState.itemWidth = itemWidth
overlayState.itemHeight = itemHeight
overlayState.contentMargin = contentMargin
@updateOverlaysState()
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
observeCursor: (cursor) ->
didChangePositionDisposable = cursor.onDidChangePosition =>
@updateHiddenInputState() if cursor.isLastCursor()
@shouldUpdateHiddenInputState = true if cursor.isLastCursor()
@pauseCursorBlinking()
@updateCursorState(cursor)
@emitDidUpdateState()
didChangeVisibilityDisposable = cursor.onDidChangeVisibility =>
@updateCursorState(cursor)
@@ -1212,19 +1326,23 @@ class TextEditorPresenter
@disposables.remove(didChangePositionDisposable)
@disposables.remove(didChangeVisibilityDisposable)
@disposables.remove(didDestroyDisposable)
@updateHiddenInputState()
@shouldUpdateHiddenInputState = true
@updateCursorState(cursor, true)
@emitDidUpdateState()
@disposables.add(didChangePositionDisposable)
@disposables.add(didChangeVisibilityDisposable)
@disposables.add(didDestroyDisposable)
didAddCursor: (cursor) ->
@observeCursor(cursor)
@updateHiddenInputState()
@shouldUpdateHiddenInputState = true
@pauseCursorBlinking()
@updateCursorState(cursor)
@emitDidUpdateState()
startBlinkingCursors: ->
unless @toggleCursorBlinkHandle
@state.content.cursorsVisible = true

View File

@@ -75,7 +75,7 @@ class TextEditor extends Model
'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
toProperty: 'languageMode'
constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible}) ->
constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible}={}) ->
super
@emitter = new Emitter
@@ -84,12 +84,10 @@ class TextEditor extends Model
@selections = []
buffer ?= new TextBuffer
@displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped})
@displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, ignoreInvisibles: @mini})
@buffer = @displayBuffer.buffer
@softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true
@updateInvisibles()
for marker in @findMarkers(@getSelectionMarkerAttributes())
marker.setProperties(preserveFolds: true)
@addSelection(marker)
@@ -170,21 +168,9 @@ class TextEditor extends Model
@subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration
@subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration
@subscribeToScopedConfigSettings()
subscribeToScopedConfigSettings: ->
@scopedConfigSubscriptions?.dispose()
@scopedConfigSubscriptions = subscriptions = new CompositeDisposable
scopeDescriptor = @getRootScopeDescriptor()
subscriptions.add atom.config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, => @updateInvisibles()
subscriptions.add atom.config.onDidChange 'editor.invisibles', scope: scopeDescriptor, => @updateInvisibles()
destroyed: ->
@unsubscribe() if includeDeprecatedAPIs
@disposables.dispose()
@scopedConfigSubscriptions.dispose()
selection.destroy() for selection in @getSelections()
@buffer.release()
@displayBuffer.destroy()
@@ -488,7 +474,7 @@ class TextEditor extends Model
setMini: (mini) ->
if mini isnt @mini
@mini = mini
@updateInvisibles()
@displayBuffer.setIgnoreInvisibles(@mini)
@emitter.emit 'did-change-mini', @mini
@mini
@@ -1057,6 +1043,16 @@ class TextEditor extends Model
deleteToBeginningOfWord: ->
@mutateSelectedText (selection) -> selection.deleteToBeginningOfWord()
# Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the
# previous word boundary.
deleteToPreviousWordBoundary: ->
@mutateSelectedText (selection) -> selection.deleteToPreviousWordBoundary()
# Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the
# next word boundary.
deleteToNextWordBoundary: ->
@mutateSelectedText (selection) -> selection.deleteToNextWordBoundary()
# Extended: For each selection, if the selection is empty, delete all characters
# of the containing line that precede the cursor. Otherwise delete the
# selected text.
@@ -2769,15 +2765,6 @@ class TextEditor extends Model
shouldAutoIndentOnPaste: ->
atom.config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor())
shouldShowInvisibles: ->
not @mini and atom.config.get('editor.showInvisibles', scope: @getRootScopeDescriptor())
updateInvisibles: ->
if @shouldShowInvisibles()
@displayBuffer.setInvisibles(atom.config.get('editor.invisibles', scope: @getRootScopeDescriptor()))
else
@displayBuffer.setInvisibles(null)
###
Section: Event Handlers
###
@@ -2786,8 +2773,6 @@ class TextEditor extends Model
@softTabs = @usesSoftTabs() ? @softTabs
handleGrammarChange: ->
@updateInvisibles()
@subscribeToScopedConfigSettings()
@unfoldAll()
@emit 'grammar-changed' if includeDeprecatedAPIs
@emitter.emit 'did-change-grammar', @getGrammar()

View File

@@ -20,8 +20,9 @@ class TokenizedBuffer extends Model
chunkSize: 50
invalidRows: null
visible: false
configSettings: null
constructor: ({@buffer, @tabLength, @invisibles}) ->
constructor: ({@buffer, @tabLength, @ignoreInvisibles}) ->
@emitter = new Emitter
@disposables = new CompositeDisposable
@@ -39,7 +40,7 @@ class TokenizedBuffer extends Model
serializeParams: ->
bufferPath: @buffer.getPath()
tabLength: @tabLength
invisibles: _.clone(@invisibles)
ignoreInvisibles: @ignoreInvisibles
deserializeParams: (params) ->
params.buffer = atom.project.bufferForPathSync(params.bufferPath)
@@ -76,13 +77,25 @@ class TokenizedBuffer extends Model
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
@disposables.add(@grammarUpdateDisposable)
@configSettings = tabLength: atom.config.get('editor.tabLength', scope: @rootScopeDescriptor)
scopeOptions = {scope: @rootScopeDescriptor}
@configSettings =
tabLength: atom.config.get('editor.tabLength', scopeOptions)
invisibles: atom.config.get('editor.invisibles', scopeOptions)
showInvisibles: atom.config.get('editor.showInvisibles', scopeOptions)
@grammarTabLengthSubscription?.dispose()
@grammarTabLengthSubscription = atom.config.onDidChange 'editor.tabLength', scope: @rootScopeDescriptor, ({newValue}) =>
if @configSubscriptions?
@configSubscriptions.dispose()
@disposables.remove(@configSubscriptions)
@configSubscriptions = new CompositeDisposable
@configSubscriptions.add atom.config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) =>
@configSettings.tabLength = newValue
@retokenizeLines()
@disposables.add(@grammarTabLengthSubscription)
['invisibles', 'showInvisibles'].forEach (key) =>
@configSubscriptions.add atom.config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) =>
oldInvisibles = @getInvisiblesToShow()
@configSettings[key] = newValue
@retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles)
@disposables.add(@configSubscriptions)
@retokenizeLines()
@@ -123,10 +136,11 @@ class TokenizedBuffer extends Model
@tabLength = tabLength
@retokenizeLines()
setInvisibles: (invisibles) ->
unless _.isEqual(invisibles, @invisibles)
@invisibles = invisibles
@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()
@@ -302,7 +316,7 @@ class TokenizedBuffer extends Model
tabLength = @getTabLength()
indentLevel = @indentLevelForRow(row)
lineEnding = @buffer.lineEndingForRow(row)
new TokenizedLine({tokens, tabLength, indentLevel, @invisibles, lineEnding})
new TokenizedLine({tokens, tabLength, indentLevel, invisibles: @getInvisiblesToShow(), lineEnding})
buildTokenizedLineForRow: (row, ruleStack) ->
@buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack)
@@ -312,7 +326,13 @@ class TokenizedBuffer extends Model
tabLength = @getTabLength()
indentLevel = @indentLevelForRow(row)
{tokens, ruleStack} = @grammar.tokenizeLine(line, ruleStack, row is 0)
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel, @invisibles})
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel, invisibles: @getInvisiblesToShow()})
getInvisiblesToShow: ->
if @configSettings.showInvisibles and not @ignoreInvisibles
@configSettings.invisibles
else
null
tokenizedLineForRow: (bufferRow) ->
@tokenizedLines[bufferRow]

View File

@@ -87,8 +87,9 @@ class TooltipManager
new Disposable ->
tooltip = $target.data('bs.tooltip')
tooltip.leave(currentTarget: target)
tooltip.hide()
if tooltip?
tooltip.leave(currentTarget: target)
tooltip.hide()
$target.tooltip('destroy')
humanizeKeystrokes = (keystroke) ->

View File

@@ -113,7 +113,10 @@ class WorkspaceElement extends HTMLElement
focusPaneViewOnRight: -> @paneContainer.focusPaneViewOnRight()
runPackageSpecs: ->
[projectPath] = atom.project.getPaths()
if activePath = atom.workspace.getActivePaneItem()?.getPath?()
[projectPath] = atom.project.relativizePath(activePath)
else
[projectPath] = atom.project.getPaths()
ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
atom.commands.add 'atom-workspace',

View File

@@ -75,6 +75,8 @@ class Workspace extends Model
atom.views.addViewProvider Panel, (model) ->
new PanelElement().initialize(model)
@subscribeToFontSize()
# Called by the Serializable mixin during deserialization
deserializeParams: (params) ->
for packageName in params.packagesWithActiveGrammars ? []
@@ -619,9 +621,14 @@ class Workspace extends Model
fontSize = atom.config.get("editor.fontSize")
atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1
# Restore to a default editor font size.
# Restore to the window's original editor font size.
resetFontSize: ->
atom.config.unset("editor.fontSize")
if @originalFontSize
atom.config.set("editor.fontSize", @originalFontSize)
subscribeToFontSize: ->
atom.config.onDidChange 'editor.fontSize', ({oldValue}) =>
@originalFontSize ?= oldValue
# Removes the item's uri from the list of potential items to reopen.
itemOpened: (item) ->

View File

@@ -1,8 +1,6 @@
<!DOCTYPE html>
<html style="background: #fff">
<html>
<head>
<title></title>
<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self'; style-src 'self' 'unsafe-inline';">
<script src="index.js"></script>

View File

@@ -1,6 +1,11 @@
(function() {
var fs = require('fs');
var path = require('path');
var loadSettings = null;
var loadSettingsError = null;
window.onload = function() {
try {
var startTime = Date.now();
@@ -12,66 +17,81 @@ window.onload = function() {
// Ensure ATOM_HOME is always set before anything else is required
setupAtomHome();
var cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache');
// Use separate compile cache when sudo'ing as root to avoid permission issues
if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) {
cacheDir = path.join(cacheDir, 'root');
}
var rawLoadSettings = decodeURIComponent(location.hash.substr(1));
var loadSettings;
try {
loadSettings = JSON.parse(rawLoadSettings);
} catch (error) {
console.error("Failed to parse load settings: " + rawLoadSettings);
throw error;
}
// Normalize to make sure drive letter case is consistent on Windows
process.resourcesPath = path.normalize(process.resourcesPath);
if (loadSettingsError) {
throw loadSettingsError;
}
var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep);
setupCoffeeCache(cacheDir);
ModuleCache = require('../src/module-cache');
ModuleCache.register(loadSettings);
ModuleCache.add(loadSettings.resourcePath);
require('grim').includeDeprecatedAPIs = !loadSettings.apiPreviewMode;
// Start the crash reporter before anything else.
require('crash-reporter').start({
productName: 'Atom',
companyName: 'GitHub',
// By explicitly passing the app version here, we could save the call
// of "require('remote').require('app').getVersion()".
extra: {_version: loadSettings.appVersion}
});
setupVmCompatibility();
setupCsonCache(cacheDir);
setupSourceMapCache(cacheDir);
setupBabel(cacheDir);
setupTypeScript(cacheDir);
require(loadSettings.bootstrapScript);
require('ipc').sendChannel('window-command', 'window:loaded');
if (global.atom) {
global.atom.loadTime = Date.now() - startTime;
console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms');
if (loadSettings.profileStartup) {
profileStartup(loadSettings, Date.now() - startTime);
} else {
setupWindow(loadSettings);
setLoadTime(Date.now() - startTime);
}
} catch (error) {
var currentWindow = require('remote').getCurrentWindow();
currentWindow.setSize(800, 600);
currentWindow.center();
currentWindow.show();
currentWindow.openDevTools();
console.error(error.stack || error);
handleSetupError(error);
}
}
var getCacheDirectory = function() {
var cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache');
// Use separate compile cache when sudo'ing as root to avoid permission issues
if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) {
cacheDir = path.join(cacheDir, 'root');
}
return cacheDir;
}
var setLoadTime = function(loadTime) {
if (global.atom) {
global.atom.loadTime = loadTime;
console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms');
}
}
var handleSetupError = function(error) {
var currentWindow = require('remote').getCurrentWindow();
currentWindow.setSize(800, 600);
currentWindow.center();
currentWindow.show();
currentWindow.openDevTools();
console.error(error.stack || error);
}
var setupWindow = function(loadSettings) {
var cacheDir = getCacheDirectory();
setupCoffeeCache(cacheDir);
ModuleCache = require('../src/module-cache');
ModuleCache.register(loadSettings);
ModuleCache.add(loadSettings.resourcePath);
require('grim').includeDeprecatedAPIs = !loadSettings.apiPreviewMode;
// Start the crash reporter before anything else.
require('crash-reporter').start({
productName: 'Atom',
companyName: 'GitHub',
// By explicitly passing the app version here, we could save the call
// of "require('remote').require('app').getVersion()".
extra: {_version: loadSettings.appVersion}
});
setupVmCompatibility();
setupCsonCache(cacheDir);
setupSourceMapCache(cacheDir);
setupBabel(cacheDir);
setupTypeScript(cacheDir);
require(loadSettings.bootstrapScript);
require('ipc').sendChannel('window-command', 'window:loaded');
}
var setupCoffeeCache = function(cacheDir) {
var CoffeeCache = require('coffee-cash');
CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee'));
@@ -118,6 +138,73 @@ var setupSourceMapCache = function(cacheDir) {
var setupVmCompatibility = function() {
var vm = require('vm');
if (!vm.Script.createContext)
if (!vm.Script.createContext) {
vm.Script.createContext = vm.createContext;
}
}
var profileStartup = function(loadSettings, initialTime) {
var profile = function() {
console.profile('startup');
try {
var startTime = Date.now()
setupWindow(loadSettings);
setLoadTime(Date.now() - startTime + initialTime);
} catch (error) {
handleSetupError(error);
} finally {
console.profileEnd('startup');
console.log("Switch to the Profiles tab to view the created startup profile")
}
};
var currentWindow = require('remote').getCurrentWindow();
if (currentWindow.devToolsWebContents) {
profile();
} else {
currentWindow.openDevTools();
currentWindow.once('devtools-opened', function() {
setTimeout(profile, 100);
});
}
}
var parseLoadSettings = function() {
var rawLoadSettings = decodeURIComponent(location.hash.substr(1));
try {
loadSettings = JSON.parse(rawLoadSettings);
} catch (error) {
console.error("Failed to parse load settings: " + rawLoadSettings);
loadSettingsError = error;
}
}
var setupWindowBackground = function() {
if (loadSettings && loadSettings.isSpec) {
return;
}
var backgroundColor = window.localStorage.getItem('atom:window-background-color');
if (!backgroundColor) {
return;
}
var backgroundStylesheet = document.createElement('style');
backgroundStylesheet.type = 'text/css';
backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }';
document.head.appendChild(backgroundStylesheet);
// Remove once the page loads
window.addEventListener("load", function loadWindow() {
window.removeEventListener("load", loadWindow, false);
setTimeout(function() {
backgroundStylesheet.remove();
backgroundStylesheet = null;
}, 1000);
}, false);
}
parseLoadSettings();
setupWindowBackground();
})();

View File

@@ -4,7 +4,7 @@
atom-text-editor {
display: block;
font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier;
font-family: Inconsolata, Monaco, Consolas, 'DejaVu Sans Mono', monospace;
line-height: 1.3;
}