mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge branch 'master' of https://github.com/atom/atom into tests
This commit is contained in:
5
.github/stale.yml
vendored
5
.github/stale.yml
vendored
@@ -1,10 +1,9 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
# Starting at two years of no activity
|
||||
daysUntilStale: 730
|
||||
daysUntilStale: 365
|
||||
# Number of days of inactivity before a stale Issue or Pull Request is closed
|
||||
daysUntilClose: 30
|
||||
daysUntilClose: 14
|
||||
# Issues or Pull Requests with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- regression
|
||||
|
||||
17
README.md
17
README.md
@@ -42,7 +42,7 @@ The `.zip` version will not automatically update.
|
||||
|
||||
Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom.
|
||||
|
||||
### Debian Linux (Ubuntu)
|
||||
### Debian based (Debian, Ubuntu, Linux Mint)
|
||||
|
||||
Atom is only available for 64-bit Linux systems.
|
||||
|
||||
@@ -53,23 +53,12 @@ Atom is only available for 64-bit Linux systems.
|
||||
The Linux version does not currently automatically update so you will need to
|
||||
repeat these steps to upgrade to future releases.
|
||||
|
||||
### Red Hat Linux (Fedora 21 and under, CentOS, Red Hat)
|
||||
### RPM based (Red Hat, openSUSE, Fedora, CentOS)
|
||||
|
||||
Atom is only available for 64-bit Linux systems.
|
||||
|
||||
1. Download `atom.x86_64.rpm` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
|
||||
2. Run `sudo yum localinstall atom.x86_64.rpm` on the downloaded package.
|
||||
3. Launch Atom using the installed `atom` command.
|
||||
|
||||
The Linux version does not currently automatically update so you will need to
|
||||
repeat these steps to upgrade to future releases.
|
||||
|
||||
### Fedora 22+
|
||||
|
||||
Atom is only available for 64-bit Linux systems.
|
||||
|
||||
1. Download `atom.x86_64.rpm` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
|
||||
2. Run `sudo dnf install ./atom.x86_64.rpm` on the downloaded package.
|
||||
2. Run `sudo rpm -i atom.x86_64.rpm` on the downloaded package.
|
||||
3. Launch Atom using the installed `atom` command.
|
||||
|
||||
The Linux version does not currently automatically update so you will need to
|
||||
|
||||
47
package.json
47
package.json
@@ -32,6 +32,8 @@
|
||||
"event-kit": "^2.3.0",
|
||||
"find-parent-dir": "^0.3.0",
|
||||
"first-mate": "7.0.7",
|
||||
"focus-trap": "^2.3.0",
|
||||
"fs-admin": "^0.1.5",
|
||||
"fs-plus": "^3.0.1",
|
||||
"fstream": "0.1.24",
|
||||
"fuzzaldrin": "^2.1",
|
||||
@@ -54,13 +56,12 @@
|
||||
"nsfw": "^1.0.15",
|
||||
"nslog": "^3",
|
||||
"oniguruma": "6.2.1",
|
||||
"pathwatcher": "8.0.0",
|
||||
"pathwatcher": "8.0.1",
|
||||
"postcss": "5.2.4",
|
||||
"postcss-selector-parser": "2.2.1",
|
||||
"property-accessors": "^1.1.3",
|
||||
"random-words": "0.0.1",
|
||||
"resolve": "^1.1.6",
|
||||
"runas": "^3.1",
|
||||
"scandal": "^3.1.0",
|
||||
"scoped-property-store": "^0.17.0",
|
||||
"scrollbar-style": "^3.2",
|
||||
@@ -69,7 +70,7 @@
|
||||
"service-hub": "^0.7.4",
|
||||
"sinon": "1.17.4",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "13.1.7",
|
||||
"text-buffer": "13.1.14",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@@ -82,17 +83,17 @@
|
||||
"atom-light-ui": "0.46.0",
|
||||
"base16-tomorrow-dark-theme": "1.5.0",
|
||||
"base16-tomorrow-light-theme": "1.5.0",
|
||||
"one-dark-ui": "1.10.7",
|
||||
"one-light-ui": "1.10.7",
|
||||
"one-dark-ui": "1.10.8",
|
||||
"one-light-ui": "1.10.8",
|
||||
"one-dark-syntax": "1.8.0",
|
||||
"one-light-syntax": "1.8.0",
|
||||
"solarized-dark-syntax": "1.1.2",
|
||||
"solarized-light-syntax": "1.1.2",
|
||||
"about": "1.7.6",
|
||||
"archive-view": "0.63.3",
|
||||
"autocomplete-atom-api": "0.10.2",
|
||||
"autocomplete-css": "0.17.2",
|
||||
"autocomplete-html": "0.8.0",
|
||||
"autocomplete-atom-api": "0.10.3",
|
||||
"autocomplete-css": "0.17.3",
|
||||
"autocomplete-html": "0.8.1",
|
||||
"autocomplete-plus": "2.35.8",
|
||||
"autocomplete-snippets": "1.11.1",
|
||||
"autoflow": "0.29.0",
|
||||
@@ -100,13 +101,13 @@
|
||||
"background-tips": "0.27.1",
|
||||
"bookmarks": "0.44.4",
|
||||
"bracket-matcher": "0.87.3",
|
||||
"command-palette": "0.40.4",
|
||||
"command-palette": "0.41.1",
|
||||
"dalek": "0.2.1",
|
||||
"deprecation-cop": "0.56.7",
|
||||
"dev-live-reload": "0.47.1",
|
||||
"encoding-selector": "0.23.4",
|
||||
"exception-reporting": "0.41.4",
|
||||
"find-and-replace": "0.210.0",
|
||||
"find-and-replace": "0.212.0",
|
||||
"fuzzy-finder": "1.5.8",
|
||||
"github": "0.5.0",
|
||||
"git-diff": "1.3.6",
|
||||
@@ -119,34 +120,34 @@
|
||||
"link": "0.31.3",
|
||||
"markdown-preview": "0.159.13",
|
||||
"metrics": "1.2.6",
|
||||
"notifications": "0.67.2",
|
||||
"notifications": "0.69.0",
|
||||
"open-on-github": "1.2.1",
|
||||
"package-generator": "1.1.1",
|
||||
"settings-view": "0.251.5",
|
||||
"snippets": "1.1.4",
|
||||
"spell-check": "0.72.0",
|
||||
"spell-check": "0.72.2",
|
||||
"status-bar": "1.8.11",
|
||||
"styleguide": "0.49.6",
|
||||
"symbols-view": "0.117.1",
|
||||
"tabs": "0.107.0",
|
||||
"tabs": "0.107.1",
|
||||
"timecop": "0.36.0",
|
||||
"tree-view": "0.217.7",
|
||||
"tree-view": "0.217.8",
|
||||
"update-package-dependencies": "0.12.0",
|
||||
"welcome": "0.36.5",
|
||||
"whitespace": "0.37.2",
|
||||
"wrap-guide": "0.40.2",
|
||||
"language-c": "0.58.1",
|
||||
"language-clojure": "0.22.4",
|
||||
"language-coffee-script": "0.48.9",
|
||||
"language-coffee-script": "0.49.0",
|
||||
"language-csharp": "0.14.2",
|
||||
"language-css": "0.42.4",
|
||||
"language-gfm": "0.90.0",
|
||||
"language-css": "0.42.5",
|
||||
"language-gfm": "0.90.1",
|
||||
"language-git": "0.19.1",
|
||||
"language-go": "0.44.2",
|
||||
"language-html": "0.47.3",
|
||||
"language-html": "0.47.7",
|
||||
"language-hyperlink": "0.16.2",
|
||||
"language-java": "0.27.3",
|
||||
"language-javascript": "0.127.2",
|
||||
"language-java": "0.27.4",
|
||||
"language-javascript": "0.127.3",
|
||||
"language-json": "0.19.1",
|
||||
"language-less": "0.33.0",
|
||||
"language-make": "0.22.3",
|
||||
@@ -158,8 +159,8 @@
|
||||
"language-python": "0.45.4",
|
||||
"language-ruby": "0.71.3",
|
||||
"language-ruby-on-rails": "0.25.2",
|
||||
"language-sass": "0.61.0",
|
||||
"language-shellscript": "0.25.2",
|
||||
"language-sass": "0.61.1",
|
||||
"language-shellscript": "0.25.3",
|
||||
"language-source": "0.9.0",
|
||||
"language-sql": "0.25.8",
|
||||
"language-text": "0.7.3",
|
||||
@@ -167,7 +168,7 @@
|
||||
"language-toml": "0.18.1",
|
||||
"language-typescript": "0.1.0",
|
||||
"language-xml": "0.35.2",
|
||||
"language-yaml": "0.30.1"
|
||||
"language-yaml": "0.30.2"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
@echo off
|
||||
|
||||
set USAGE=Usage: %0 source destination
|
||||
|
||||
if [%1] == [] (
|
||||
echo %USAGE%
|
||||
exit 1
|
||||
)
|
||||
if [%2] == [] (
|
||||
echo %USAGE%
|
||||
exit 2
|
||||
)
|
||||
|
||||
:: rm -rf %2
|
||||
if exist %2 rmdir %2 /s /q
|
||||
|
||||
:: cp -rf %1 %2
|
||||
(robocopy %1 %2 /e) ^& IF %ERRORLEVEL% LEQ 1 exit 0
|
||||
@@ -47,6 +47,7 @@ const EXCLUDE_REGEXPS_SOURCES = [
|
||||
escapeRegExp(path.join('build', 'Release', 'obj.target')),
|
||||
escapeRegExp(path.join('build', 'Release', 'obj')),
|
||||
escapeRegExp(path.join('build', 'Release', '.deps')),
|
||||
escapeRegExp(path.join('deps', 'libgit2')),
|
||||
escapeRegExp(path.join('vendor', 'apm')),
|
||||
|
||||
// These are only required in dev-mode, when pegjs grammars aren't precompiled
|
||||
@@ -54,7 +55,6 @@ const EXCLUDE_REGEXPS_SOURCES = [
|
||||
escapeRegExp(path.join('node_modules', 'pegjs')),
|
||||
escapeRegExp(path.join('node_modules', '.bin', 'pegjs')),
|
||||
escapeRegExp(path.join('node_modules', 'spellchecker', 'vendor', 'hunspell') + path.sep) + '.*',
|
||||
escapeRegExp(path.join('build', 'Release') + path.sep) + '.*\\.pdb',
|
||||
|
||||
// Ignore *.cc and *.h files from native modules
|
||||
escapeRegExp(path.sep) + '.+\\.(cc|h)$',
|
||||
@@ -64,10 +64,14 @@ const EXCLUDE_REGEXPS_SOURCES = [
|
||||
escapeRegExp(path.sep) + '.+\\.target.mk$',
|
||||
escapeRegExp(path.sep) + 'linker\\.lock$',
|
||||
escapeRegExp(path.join('build', 'Release') + path.sep) + '.+\\.node\\.dSYM',
|
||||
escapeRegExp(path.join('build', 'Release') + path.sep) + '.*\\.(pdb|lib|exp|map|ipdb|iobj)',
|
||||
|
||||
// Ignore test and example folders
|
||||
// Ignore node_module files we won't need at runtime
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + '_*te?sts?_*' + escapeRegExp(path.sep),
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + 'examples?' + escapeRegExp(path.sep)
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + 'examples?' + escapeRegExp(path.sep),
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.md$',
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.d\\.ts$',
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$'
|
||||
]
|
||||
|
||||
// Ignore spec directories in all bundled packages
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
const fs = require('fs-extra')
|
||||
const handleTilde = require('./handle-tilde')
|
||||
const path = require('path')
|
||||
const runas = require('runas')
|
||||
const template = require('lodash.template')
|
||||
|
||||
const CONFIG = require('../config')
|
||||
@@ -31,11 +30,12 @@ module.exports = function (packagedAppPath, installDir) {
|
||||
fs.copySync(packagedAppPath, installationDirPath)
|
||||
} catch (e) {
|
||||
console.log(`Administrator elevation required to install into "${installationDirPath}"`)
|
||||
const copyScriptPath = path.join(CONFIG.repositoryRootPath, 'script', 'copy-folder.cmd')
|
||||
const exitCode = runas('cmd', ['/c', copyScriptPath, packagedAppPath, installationDirPath], {admin: true})
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`Installation failed. "${copyScriptPath}" exited with status: ${exitCode}`)
|
||||
}
|
||||
const fsAdmin = require('fs-admin')
|
||||
return new Promise((resolve, reject) => {
|
||||
fsAdmin.recursiveCopy(packagedAppPath, installationDirPath, (error) => {
|
||||
error ? reject(error) : resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const atomExecutableName = CONFIG.channel === 'beta' ? 'atom-beta' : 'atom'
|
||||
@@ -95,4 +95,6 @@ module.exports = function (packagedAppPath, installDir) {
|
||||
console.log(`Changing permissions to 755 for "${installationDirPath}"`)
|
||||
fs.chmodSync(installationDirPath, '755')
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"electron-mksnapshot": "~1.6",
|
||||
"electron-packager": "7.3.0",
|
||||
"electron-winstaller": "2.6.2",
|
||||
"fs-admin": "^0.1.5",
|
||||
"fs-extra": "0.30.0",
|
||||
"glob": "7.0.3",
|
||||
"joanna": "0.0.9",
|
||||
@@ -25,7 +26,6 @@
|
||||
"npm": "5.3.0",
|
||||
"passwd-user": "2.1.0",
|
||||
"pegjs": "0.9.0",
|
||||
"runas": "3.1.1",
|
||||
"season": "5.3.0",
|
||||
"semver": "5.3.0",
|
||||
"standard": "8.4.0",
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs-plus'
|
||||
temp = require('temp').track()
|
||||
CommandInstaller = require '../src/command-installer'
|
||||
|
||||
describe "CommandInstaller on #darwin", ->
|
||||
[installer, resourcesPath, installationPath, atomBinPath, apmBinPath] = []
|
||||
|
||||
beforeEach ->
|
||||
installationPath = temp.mkdirSync("atom-bin")
|
||||
|
||||
resourcesPath = temp.mkdirSync('atom-app')
|
||||
atomBinPath = path.join(resourcesPath, 'app', 'atom.sh')
|
||||
apmBinPath = path.join(resourcesPath, 'app', 'apm', 'node_modules', '.bin', 'apm')
|
||||
fs.writeFileSync(atomBinPath, "")
|
||||
fs.writeFileSync(apmBinPath, "")
|
||||
fs.chmodSync(atomBinPath, '755')
|
||||
fs.chmodSync(apmBinPath, '755')
|
||||
|
||||
spyOn(CommandInstaller::, 'getResourcesDirectory').andReturn(resourcesPath)
|
||||
spyOn(CommandInstaller::, 'getInstallDirectory').andReturn(installationPath)
|
||||
|
||||
afterEach ->
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
it "shows an error dialog when installing commands interactively fails", ->
|
||||
appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"])
|
||||
installer = new CommandInstaller(appDelegate)
|
||||
installer.initialize("2.0.2")
|
||||
spyOn(installer, "installAtomCommand").andCallFake (__, callback) -> callback(new Error("an error"))
|
||||
|
||||
installer.installShellCommandsInteractively()
|
||||
|
||||
expect(appDelegate.confirm).toHaveBeenCalledWith({
|
||||
message: "Failed to install shell commands"
|
||||
detailedMessage: "an error"
|
||||
})
|
||||
|
||||
appDelegate.confirm.reset()
|
||||
installer.installAtomCommand.andCallFake (__, callback) -> callback()
|
||||
spyOn(installer, "installApmCommand").andCallFake (__, callback) -> callback(new Error("another error"))
|
||||
|
||||
installer.installShellCommandsInteractively()
|
||||
|
||||
expect(appDelegate.confirm).toHaveBeenCalledWith({
|
||||
message: "Failed to install shell commands"
|
||||
detailedMessage: "another error"
|
||||
})
|
||||
|
||||
it "shows a success dialog when installing commands interactively succeeds", ->
|
||||
appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"])
|
||||
installer = new CommandInstaller(appDelegate)
|
||||
installer.initialize("2.0.2")
|
||||
spyOn(installer, "installAtomCommand").andCallFake (__, callback) -> callback()
|
||||
spyOn(installer, "installApmCommand").andCallFake (__, callback) -> callback()
|
||||
|
||||
installer.installShellCommandsInteractively()
|
||||
|
||||
expect(appDelegate.confirm).toHaveBeenCalledWith({
|
||||
message: "Commands installed."
|
||||
detailedMessage: "The shell commands `atom` and `apm` are installed."
|
||||
})
|
||||
|
||||
describe "when using a stable version of atom", ->
|
||||
beforeEach ->
|
||||
installer = new CommandInstaller()
|
||||
installer.initialize("2.0.2")
|
||||
|
||||
it "symlinks the atom command as 'atom'", ->
|
||||
installedAtomPath = path.join(installationPath, 'atom')
|
||||
|
||||
expect(fs.isFileSync(installedAtomPath)).toBeFalsy()
|
||||
|
||||
waitsFor (done) ->
|
||||
installer.installAtomCommand(false, done)
|
||||
|
||||
runs ->
|
||||
expect(fs.realpathSync(installedAtomPath)).toBe fs.realpathSync(atomBinPath)
|
||||
expect(fs.isExecutableSync(installedAtomPath)).toBe true
|
||||
expect(fs.isFileSync(path.join(installationPath, 'atom-beta'))).toBe false
|
||||
|
||||
it "symlinks the apm command as 'apm'", ->
|
||||
installedApmPath = path.join(installationPath, 'apm')
|
||||
|
||||
expect(fs.isFileSync(installedApmPath)).toBeFalsy()
|
||||
|
||||
waitsFor (done) ->
|
||||
installer.installApmCommand(false, done)
|
||||
|
||||
runs ->
|
||||
expect(fs.realpathSync(installedApmPath)).toBe fs.realpathSync(apmBinPath)
|
||||
expect(fs.isExecutableSync(installedApmPath)).toBeTruthy()
|
||||
expect(fs.isFileSync(path.join(installationPath, 'apm-beta'))).toBe false
|
||||
|
||||
describe "when using a beta version of atom", ->
|
||||
beforeEach ->
|
||||
installer = new CommandInstaller()
|
||||
installer.initialize("2.2.0-beta.0")
|
||||
|
||||
it "symlinks the atom command as 'atom-beta'", ->
|
||||
installedAtomPath = path.join(installationPath, 'atom-beta')
|
||||
|
||||
expect(fs.isFileSync(installedAtomPath)).toBeFalsy()
|
||||
|
||||
waitsFor (done) ->
|
||||
installer.installAtomCommand(false, done)
|
||||
|
||||
runs ->
|
||||
expect(fs.realpathSync(installedAtomPath)).toBe fs.realpathSync(atomBinPath)
|
||||
expect(fs.isExecutableSync(installedAtomPath)).toBe true
|
||||
expect(fs.isFileSync(path.join(installationPath, 'atom'))).toBe false
|
||||
|
||||
it "symlinks the apm command as 'apm-beta'", ->
|
||||
installedApmPath = path.join(installationPath, 'apm-beta')
|
||||
|
||||
expect(fs.isFileSync(installedApmPath)).toBeFalsy()
|
||||
|
||||
waitsFor (done) ->
|
||||
installer.installApmCommand(false, done)
|
||||
|
||||
runs ->
|
||||
expect(fs.realpathSync(installedApmPath)).toBe fs.realpathSync(apmBinPath)
|
||||
expect(fs.isExecutableSync(installedApmPath)).toBeTruthy()
|
||||
expect(fs.isFileSync(path.join(installationPath, 'apm'))).toBe false
|
||||
143
spec/command-installer-spec.js
Normal file
143
spec/command-installer-spec.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
const temp = require('temp').track()
|
||||
const CommandInstaller = require('../src/command-installer')
|
||||
|
||||
describe('CommandInstaller on #darwin', () => {
|
||||
let installer, resourcesPath, installationPath, atomBinPath, apmBinPath
|
||||
|
||||
beforeEach(() => {
|
||||
installationPath = temp.mkdirSync('atom-bin')
|
||||
|
||||
resourcesPath = temp.mkdirSync('atom-app')
|
||||
atomBinPath = path.join(resourcesPath, 'app', 'atom.sh')
|
||||
apmBinPath = path.join(resourcesPath, 'app', 'apm', 'node_modules', '.bin', 'apm')
|
||||
fs.writeFileSync(atomBinPath, '')
|
||||
fs.writeFileSync(apmBinPath, '')
|
||||
fs.chmodSync(atomBinPath, '755')
|
||||
fs.chmodSync(apmBinPath, '755')
|
||||
|
||||
spyOn(CommandInstaller.prototype, 'getResourcesDirectory').andReturn(resourcesPath)
|
||||
spyOn(CommandInstaller.prototype, 'getInstallDirectory').andReturn(installationPath)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (error) {}
|
||||
})
|
||||
|
||||
it('shows an error dialog when installing commands interactively fails', () => {
|
||||
const appDelegate = jasmine.createSpyObj('appDelegate', ['confirm'])
|
||||
installer = new CommandInstaller(appDelegate)
|
||||
installer.initialize('2.0.2')
|
||||
spyOn(installer, 'installAtomCommand').andCallFake((__, callback) => callback(new Error('an error')))
|
||||
|
||||
installer.installShellCommandsInteractively()
|
||||
|
||||
expect(appDelegate.confirm).toHaveBeenCalledWith({
|
||||
message: 'Failed to install shell commands',
|
||||
detailedMessage: 'an error'
|
||||
})
|
||||
|
||||
appDelegate.confirm.reset()
|
||||
installer.installAtomCommand.andCallFake((__, callback) => callback())
|
||||
spyOn(installer, 'installApmCommand').andCallFake((__, callback) => callback(new Error('another error')))
|
||||
|
||||
installer.installShellCommandsInteractively()
|
||||
|
||||
expect(appDelegate.confirm).toHaveBeenCalledWith({
|
||||
message: 'Failed to install shell commands',
|
||||
detailedMessage: 'another error'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a success dialog when installing commands interactively succeeds', () => {
|
||||
const appDelegate = jasmine.createSpyObj('appDelegate', ['confirm'])
|
||||
installer = new CommandInstaller(appDelegate)
|
||||
installer.initialize('2.0.2')
|
||||
spyOn(installer, 'installAtomCommand').andCallFake((__, callback) => callback())
|
||||
spyOn(installer, 'installApmCommand').andCallFake((__, callback) => callback())
|
||||
|
||||
installer.installShellCommandsInteractively()
|
||||
|
||||
expect(appDelegate.confirm).toHaveBeenCalledWith({
|
||||
message: 'Commands installed.',
|
||||
detailedMessage: 'The shell commands `atom` and `apm` are installed.'
|
||||
})
|
||||
})
|
||||
|
||||
describe('when using a stable version of atom', () => {
|
||||
beforeEach(() => {
|
||||
installer = new CommandInstaller()
|
||||
installer.initialize('2.0.2')
|
||||
})
|
||||
|
||||
it("symlinks the atom command as 'atom'", () => {
|
||||
const installedAtomPath = path.join(installationPath, 'atom')
|
||||
expect(fs.isFileSync(installedAtomPath)).toBeFalsy()
|
||||
|
||||
waitsFor(done => {
|
||||
installer.installAtomCommand(false, error => {
|
||||
expect(error).toBeNull()
|
||||
expect(fs.realpathSync(installedAtomPath)).toBe(fs.realpathSync(atomBinPath))
|
||||
expect(fs.isExecutableSync(installedAtomPath)).toBe(true)
|
||||
expect(fs.isFileSync(path.join(installationPath, 'atom-beta'))).toBe(false)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("symlinks the apm command as 'apm'", () => {
|
||||
const installedApmPath = path.join(installationPath, 'apm')
|
||||
expect(fs.isFileSync(installedApmPath)).toBeFalsy()
|
||||
|
||||
waitsFor(done => {
|
||||
installer.installApmCommand(false, error => {
|
||||
expect(error).toBeNull()
|
||||
expect(fs.realpathSync(installedApmPath)).toBe(fs.realpathSync(apmBinPath))
|
||||
expect(fs.isExecutableSync(installedApmPath)).toBeTruthy()
|
||||
expect(fs.isFileSync(path.join(installationPath, 'apm-beta'))).toBe(false)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when using a beta version of atom', () => {
|
||||
beforeEach(() => {
|
||||
installer = new CommandInstaller()
|
||||
installer.initialize('2.2.0-beta.0')
|
||||
})
|
||||
|
||||
it("symlinks the atom command as 'atom-beta'", () => {
|
||||
const installedAtomPath = path.join(installationPath, 'atom-beta')
|
||||
expect(fs.isFileSync(installedAtomPath)).toBeFalsy()
|
||||
|
||||
waitsFor(done => {
|
||||
installer.installAtomCommand(false, error => {
|
||||
expect(error).toBeNull()
|
||||
expect(fs.realpathSync(installedAtomPath)).toBe(fs.realpathSync(atomBinPath))
|
||||
expect(fs.isExecutableSync(installedAtomPath)).toBe(true)
|
||||
expect(fs.isFileSync(path.join(installationPath, 'atom'))).toBe(false)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("symlinks the apm command as 'apm-beta'", () => {
|
||||
const installedApmPath = path.join(installationPath, 'apm-beta')
|
||||
expect(fs.isFileSync(installedApmPath)).toBeFalsy()
|
||||
|
||||
waitsFor(done => {
|
||||
installer.installApmCommand(false, error => {
|
||||
expect(error).toBeNull()
|
||||
expect(fs.realpathSync(installedApmPath)).toBe(fs.realpathSync(apmBinPath))
|
||||
expect(fs.isExecutableSync(installedApmPath)).toBeTruthy()
|
||||
expect(fs.isFileSync(path.join(installationPath, 'apm'))).toBe(false)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -224,33 +224,36 @@ describe("CommandRegistry", () => {
|
||||
expect(addError.message).toContain(badSelector);
|
||||
});
|
||||
|
||||
it("throws an error when called with a non-function callback and selector target", () => {
|
||||
it("throws an error when called with a null callback and selector target", () => {
|
||||
const badCallback = null;
|
||||
let addError = null;
|
||||
|
||||
try {
|
||||
expect(() => {
|
||||
registry.add('.selector', 'foo:bar', badCallback);
|
||||
} catch (error) {
|
||||
addError = error;
|
||||
}
|
||||
expect(addError.message).toContain("Can't register a command with non-function callback.");
|
||||
}).toThrow(new Error('Cannot register a command with a null listener.'));
|
||||
});
|
||||
|
||||
it("throws an error when called with an non-function callback and object target", () => {
|
||||
it("throws an error when called with a null callback and object target", () => {
|
||||
const badCallback = null;
|
||||
let addError = null;
|
||||
|
||||
try {
|
||||
expect(() => {
|
||||
registry.add(document.body, 'foo:bar', badCallback);
|
||||
} catch (error) {
|
||||
addError = error;
|
||||
}
|
||||
expect(addError.message).toContain("Can't register a command with non-function callback.");
|
||||
}).toThrow(new Error('Cannot register a command with a null listener.'));
|
||||
});
|
||||
|
||||
it("throws an error when called with an object listener without a didDispatch method", () => {
|
||||
const badListener = {
|
||||
title: 'a listener without a didDispatch callback',
|
||||
description: 'this should throw an error'
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
registry.add(document.body, 'foo:bar', badListener);
|
||||
}).toThrow(new Error('Listener must be a callback function or an object with a didDispatch method.'));
|
||||
});
|
||||
});
|
||||
|
||||
describe("::findCommands({target})", () =>
|
||||
it("returns commands that can be invoked on the target or its ancestors", () => {
|
||||
describe("::findCommands({target})", () => {
|
||||
it("returns command descriptors that can be invoked on the target or its ancestors", () => {
|
||||
registry.add('.parent', 'namespace:command-1', () => {});
|
||||
registry.add('.child', 'namespace:command-2', () => {});
|
||||
registry.add('.grandchild', 'namespace:command-3', () => {});
|
||||
@@ -268,8 +271,75 @@ describe("CommandRegistry", () => {
|
||||
{name: 'namespace:command-2', displayName: 'Namespace: Command 2'},
|
||||
{name: 'namespace:command-1', displayName: 'Namespace: Command 1'}
|
||||
]);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("returns command descriptors with arbitrary metadata if set in a listener object", () => {
|
||||
registry.add('.grandchild', 'namespace:command-1', () => {});
|
||||
registry.add('.grandchild', 'namespace:command-2', {
|
||||
displayName: 'Custom Command 2',
|
||||
metadata: {
|
||||
some: 'other',
|
||||
object: 'data'
|
||||
},
|
||||
didDispatch() {}
|
||||
});
|
||||
registry.add('.grandchild', 'namespace:command-3', {
|
||||
name: 'some:other:incorrect:commandname',
|
||||
displayName: 'Custom Command 3',
|
||||
metadata: {
|
||||
some: 'other',
|
||||
object: 'data'
|
||||
},
|
||||
didDispatch() {}
|
||||
});
|
||||
|
||||
const commands = registry.findCommands({target: grandchild});
|
||||
expect(commands).toEqual([
|
||||
{
|
||||
displayName: 'Namespace: Command 1',
|
||||
name: 'namespace:command-1'
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Command 2',
|
||||
metadata: {
|
||||
some : 'other',
|
||||
object : 'data'
|
||||
},
|
||||
name: 'namespace:command-2'
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Command 3',
|
||||
metadata: {
|
||||
some : 'other',
|
||||
object : 'data'
|
||||
},
|
||||
name: 'namespace:command-3'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns command descriptors with arbitrary metadata if set on a listener function", () => {
|
||||
function listener () {}
|
||||
listener.displayName = 'Custom Command 2'
|
||||
listener.metadata = {
|
||||
some: 'other',
|
||||
object: 'data'
|
||||
};
|
||||
|
||||
registry.add('.grandchild', 'namespace:command-2', listener);
|
||||
const commands = registry.findCommands({target: grandchild});
|
||||
expect(commands).toEqual([
|
||||
{
|
||||
displayName : 'Custom Command 2',
|
||||
metadata: {
|
||||
some: 'other',
|
||||
object: 'data'
|
||||
},
|
||||
name: 'namespace:command-2'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("::dispatch(target, commandName)", () => {
|
||||
it("simulates invocation of the given command ", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
path = require 'path'
|
||||
Package = require '../src/package'
|
||||
PackageManager = require '../src/package-manager'
|
||||
temp = require('temp').track()
|
||||
fs = require 'fs-plus'
|
||||
{Disposable} = require 'atom'
|
||||
@@ -20,6 +21,22 @@ describe "PackageManager", ->
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "initialize", ->
|
||||
it "adds regular package path", ->
|
||||
packageManger = new PackageManager({})
|
||||
configDirPath = path.join('~', 'someConfig')
|
||||
packageManger.initialize({configDirPath})
|
||||
expect(packageManger.packageDirPaths.length).toBe 1
|
||||
expect(packageManger.packageDirPaths[0]).toBe path.join(configDirPath, 'packages')
|
||||
|
||||
it "adds regular package path and dev package path in dev mode", ->
|
||||
packageManger = new PackageManager({})
|
||||
configDirPath = path.join('~', 'someConfig')
|
||||
packageManger.initialize({configDirPath, devMode: true})
|
||||
expect(packageManger.packageDirPaths.length).toBe 2
|
||||
expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'packages')
|
||||
expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'dev', 'packages')
|
||||
|
||||
describe "::getApmPath()", ->
|
||||
it "returns the path to the apm command", ->
|
||||
apmPath = path.join(process.resourcesPath, "app", "apm", "bin", "apm")
|
||||
|
||||
@@ -1056,7 +1056,7 @@ describe('Pane', () => {
|
||||
|
||||
describe('when `moveActiveItem: true` is passed in the params', () => {
|
||||
it('moves the active item', () => {
|
||||
const pane2 = pane1.splitLeft({moveActiveItem: true})
|
||||
const pane2 = pane1.splitRight({moveActiveItem: true})
|
||||
expect(pane2.getActiveItem()).toBe(item1)
|
||||
})
|
||||
})
|
||||
@@ -1092,7 +1092,7 @@ describe('Pane', () => {
|
||||
|
||||
describe('when `moveActiveItem: true` is passed in the params', () => {
|
||||
it('moves the active item', () => {
|
||||
const pane2 = pane1.splitLeft({moveActiveItem: true})
|
||||
const pane2 = pane1.splitUp({moveActiveItem: true})
|
||||
expect(pane2.getActiveItem()).toBe(item1)
|
||||
})
|
||||
})
|
||||
@@ -1128,7 +1128,7 @@ describe('Pane', () => {
|
||||
|
||||
describe('when `moveActiveItem: true` is passed in the params', () => {
|
||||
it('moves the active item', () => {
|
||||
const pane2 = pane1.splitLeft({moveActiveItem: true})
|
||||
const pane2 = pane1.splitDown({moveActiveItem: true})
|
||||
expect(pane2.getActiveItem()).toBe(item1)
|
||||
})
|
||||
})
|
||||
@@ -1152,6 +1152,32 @@ describe('Pane', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the pane is empty', () => {
|
||||
describe('when `moveActiveItem: true` is passed in the params', () => {
|
||||
it('gracefully ignores the moveActiveItem parameter', () => {
|
||||
pane1.destroyItem(item1)
|
||||
expect(pane1.getActiveItem()).toBe(undefined)
|
||||
|
||||
const pane2 = pane1.split('horizontal', 'before', {moveActiveItem: true})
|
||||
expect(container.root.children).toEqual([pane2, pane1])
|
||||
|
||||
expect(pane2.getActiveItem()).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when `copyActiveItem: true` is passed in the params', () => {
|
||||
it('gracefully ignores the copyActiveItem parameter', () => {
|
||||
pane1.destroyItem(item1)
|
||||
expect(pane1.getActiveItem()).toBe(undefined)
|
||||
|
||||
const pane2 = pane1.split('horizontal', 'before', {copyActiveItem: true})
|
||||
expect(container.root.children).toEqual([pane2, pane1])
|
||||
|
||||
expect(pane2.getActiveItem()).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('activates the new pane', () => {
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
const pane2 = pane1.splitRight()
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('PanelContainerElement', () => {
|
||||
this.model = model
|
||||
return this
|
||||
}
|
||||
focus() {}
|
||||
}
|
||||
|
||||
const TestPanelContainerItemElement = document.registerElement(
|
||||
@@ -159,5 +160,57 @@ describe('PanelContainerElement', () => {
|
||||
expect(panel1.getElement()).toHaveClass('overlay')
|
||||
expect(panel1.getElement()).toHaveClass('from-top')
|
||||
})
|
||||
|
||||
describe("autoFocus", () => {
|
||||
function createPanel() {
|
||||
const panel = new Panel(
|
||||
{
|
||||
item: new TestPanelContainerItem(),
|
||||
autoFocus: true,
|
||||
visible: false
|
||||
},
|
||||
atom.views
|
||||
)
|
||||
|
||||
container.addPanel(panel)
|
||||
return panel
|
||||
}
|
||||
|
||||
it("focuses the first tabbable item if available", () => {
|
||||
const panel = createPanel()
|
||||
const panelEl = panel.getElement()
|
||||
const inputEl = document.createElement('input')
|
||||
|
||||
panelEl.appendChild(inputEl)
|
||||
expect(document.activeElement).not.toBe(inputEl)
|
||||
|
||||
panel.show()
|
||||
expect(document.activeElement).toBe(inputEl)
|
||||
})
|
||||
|
||||
it("focuses the entire panel item when no tabbable item is available and the panel is focusable", () => {
|
||||
const panel = createPanel()
|
||||
const panelEl = panel.getElement()
|
||||
|
||||
spyOn(panelEl, 'focus')
|
||||
panel.show()
|
||||
expect(panelEl.focus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("returns focus to the original activeElement", () => {
|
||||
const panel = createPanel()
|
||||
const previousActiveElement = document.activeElement
|
||||
const panelEl = panel.getElement()
|
||||
panelEl.appendChild(document.createElement('input'))
|
||||
|
||||
panel.show()
|
||||
panel.hide()
|
||||
|
||||
waitsFor(() => document.activeElement === previousActiveElement)
|
||||
runs(() => {
|
||||
expect(document.activeElement).toBe(previousActiveElement)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -688,8 +688,8 @@ describe('TextEditorComponent', () => {
|
||||
await component.getNextUpdatePromise()
|
||||
element.style.width = component.getGutterContainerWidth() + component.getContentHeight() - 20 + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
expect(component.isHorizontalScrollbarVisible()).toBe(true)
|
||||
expect(component.isVerticalScrollbarVisible()).toBe(false)
|
||||
expect(component.canScrollHorizontally()).toBe(true)
|
||||
expect(component.canScrollVertically()).toBe(false)
|
||||
expect(element.offsetHeight).toBe(component.getContentHeight() + component.getHorizontalScrollbarHeight() + 2 * editorPadding)
|
||||
|
||||
// When a vertical scrollbar is visible, autoWidth accounts for it
|
||||
@@ -697,8 +697,8 @@ describe('TextEditorComponent', () => {
|
||||
await component.getNextUpdatePromise()
|
||||
element.style.height = component.getContentHeight() - 20
|
||||
await component.getNextUpdatePromise()
|
||||
expect(component.isHorizontalScrollbarVisible()).toBe(false)
|
||||
expect(component.isVerticalScrollbarVisible()).toBe(true)
|
||||
expect(component.canScrollHorizontally()).toBe(false)
|
||||
expect(component.canScrollVertically()).toBe(true)
|
||||
expect(element.offsetWidth).toBe(
|
||||
component.getGutterContainerWidth() +
|
||||
component.getContentWidth() +
|
||||
@@ -898,8 +898,8 @@ describe('TextEditorComponent', () => {
|
||||
editor.setText('x'.repeat(20) + 'y'.repeat(20))
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
expect(component.isHorizontalScrollbarVisible()).toBe(false)
|
||||
expect(component.isVerticalScrollbarVisible()).toBe(false)
|
||||
expect(component.canScrollVertically()).toBe(false)
|
||||
expect(component.canScrollHorizontally()).toBe(false)
|
||||
expect(component.refs.horizontalScrollbar).toBeUndefined()
|
||||
expect(component.refs.verticalScrollbar).toBeUndefined()
|
||||
})
|
||||
@@ -1209,7 +1209,6 @@ describe('TextEditorComponent', () => {
|
||||
}
|
||||
|
||||
{
|
||||
global.debug = true
|
||||
const expectedScrollTop = component.getScrollTop()
|
||||
const expectedScrollLeft = 20 * (scrollSensitivity / 100)
|
||||
component.didMouseWheel({deltaX: 20, deltaY: -10})
|
||||
@@ -1228,6 +1227,40 @@ describe('TextEditorComponent', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('always scrolls by a minimum of 1, even when the delta is small or the scroll sensitivity is low', () => {
|
||||
const scrollSensitivity = 10
|
||||
const {component, editor} = buildComponent({height: 50, width: 50, scrollSensitivity})
|
||||
|
||||
{
|
||||
component.didMouseWheel({deltaX: 0, deltaY: 3})
|
||||
expect(component.getScrollTop()).toBe(1)
|
||||
expect(component.getScrollLeft()).toBe(0)
|
||||
expect(component.refs.content.style.transform).toBe(`translate(0px, -1px)`)
|
||||
}
|
||||
|
||||
{
|
||||
component.didMouseWheel({deltaX: 4, deltaY: 0})
|
||||
expect(component.getScrollTop()).toBe(1)
|
||||
expect(component.getScrollLeft()).toBe(1)
|
||||
expect(component.refs.content.style.transform).toBe(`translate(-1px, -1px)`)
|
||||
}
|
||||
|
||||
editor.update({scrollSensitivity: 100})
|
||||
{
|
||||
component.didMouseWheel({deltaX: 0, deltaY: -0.3})
|
||||
expect(component.getScrollTop()).toBe(0)
|
||||
expect(component.getScrollLeft()).toBe(1)
|
||||
expect(component.refs.content.style.transform).toBe(`translate(-1px, 0px)`)
|
||||
}
|
||||
|
||||
{
|
||||
component.didMouseWheel({deltaX: -0.1, deltaY: 0})
|
||||
expect(component.getScrollTop()).toBe(0)
|
||||
expect(component.getScrollLeft()).toBe(0)
|
||||
expect(component.refs.content.style.transform).toBe(`translate(0px, 0px)`)
|
||||
}
|
||||
})
|
||||
|
||||
it('inverts deltaX and deltaY when holding shift on Windows and Linux', async () => {
|
||||
const scrollSensitivity = 50
|
||||
const {component, editor} = buildComponent({height: 50, width: 50, scrollSensitivity})
|
||||
@@ -1508,7 +1541,7 @@ describe('TextEditorComponent', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('renders multi-line highlights that span across tiles', async () => {
|
||||
it('renders multi-line highlights', async () => {
|
||||
const {component, element, editor} = buildComponent({rowsPerTile: 3})
|
||||
const marker = editor.markScreenRange([[2, 4], [3, 4]])
|
||||
editor.decorateMarker(marker, {type: 'highlight', class: 'a'})
|
||||
@@ -1516,9 +1549,7 @@ describe('TextEditorComponent', () => {
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
{
|
||||
// We have 2 top-level highlight divs due to the regions being split
|
||||
// across 2 different tiles
|
||||
expect(element.querySelectorAll('.highlight.a').length).toBe(2)
|
||||
expect(element.querySelectorAll('.highlight.a').length).toBe(1)
|
||||
|
||||
const regions = element.querySelectorAll('.highlight.a .region.a')
|
||||
expect(regions.length).toBe(2)
|
||||
@@ -1539,11 +1570,10 @@ describe('TextEditorComponent', () => {
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
{
|
||||
// Still split across 2 tiles
|
||||
expect(element.querySelectorAll('.highlight.a').length).toBe(2)
|
||||
expect(element.querySelectorAll('.highlight.a').length).toBe(1)
|
||||
|
||||
const regions = element.querySelectorAll('.highlight.a .region.a')
|
||||
expect(regions.length).toBe(4) // Each tile renders its
|
||||
expect(regions.length).toBe(3)
|
||||
|
||||
const region0Rect = regions[0].getBoundingClientRect()
|
||||
expect(region0Rect.top).toBe(lineNodeForScreenRow(component, 2).getBoundingClientRect().top)
|
||||
@@ -1553,21 +1583,15 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
const region1Rect = regions[1].getBoundingClientRect()
|
||||
expect(region1Rect.top).toBe(lineNodeForScreenRow(component, 3).getBoundingClientRect().top)
|
||||
expect(region1Rect.bottom).toBe(lineNodeForScreenRow(component, 4).getBoundingClientRect().top)
|
||||
expect(region1Rect.bottom).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().top)
|
||||
expect(Math.round(region1Rect.left)).toBe(component.refs.content.getBoundingClientRect().left)
|
||||
expect(Math.round(region1Rect.right)).toBe(component.refs.content.getBoundingClientRect().right)
|
||||
|
||||
const region2Rect = regions[2].getBoundingClientRect()
|
||||
expect(region2Rect.top).toBe(lineNodeForScreenRow(component, 4).getBoundingClientRect().top)
|
||||
expect(region2Rect.bottom).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().top)
|
||||
expect(region2Rect.top).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().top)
|
||||
expect(region2Rect.bottom).toBe(lineNodeForScreenRow(component, 6).getBoundingClientRect().top)
|
||||
expect(Math.round(region2Rect.left)).toBe(component.refs.content.getBoundingClientRect().left)
|
||||
expect(Math.round(region2Rect.right)).toBe(component.refs.content.getBoundingClientRect().right)
|
||||
|
||||
const region3Rect = regions[3].getBoundingClientRect()
|
||||
expect(region3Rect.top).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().top)
|
||||
expect(region3Rect.bottom).toBe(lineNodeForScreenRow(component, 5).getBoundingClientRect().bottom)
|
||||
expect(Math.round(region3Rect.left)).toBe(component.refs.content.getBoundingClientRect().left)
|
||||
expect(Math.round(region3Rect.right)).toBe(clientLeftForCharacter(component, 5, 4))
|
||||
expect(Math.round(region2Rect.right)).toBe(clientLeftForCharacter(component, 5, 4))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1580,20 +1604,15 @@ describe('TextEditorComponent', () => {
|
||||
// Flash on initial appearence of highlight
|
||||
await component.getNextUpdatePromise()
|
||||
const highlights = element.querySelectorAll('.highlight.a')
|
||||
expect(highlights.length).toBe(2) // split across 2 tiles
|
||||
expect(highlights.length).toBe(1)
|
||||
|
||||
expect(highlights[0].classList.contains('b')).toBe(true)
|
||||
expect(highlights[1].classList.contains('b')).toBe(true)
|
||||
|
||||
await conditionPromise(() =>
|
||||
!highlights[0].classList.contains('b') &&
|
||||
!highlights[1].classList.contains('b')
|
||||
)
|
||||
await conditionPromise(() => !highlights[0].classList.contains('b'))
|
||||
|
||||
// Don't flash on next update if another flash wasn't requested
|
||||
await setScrollTop(component, 100)
|
||||
expect(highlights[0].classList.contains('b')).toBe(false)
|
||||
expect(highlights[1].classList.contains('b')).toBe(false)
|
||||
|
||||
// Flashing the same class again before the first flash completes
|
||||
// removes the flash class and adds it back on the next frame to ensure
|
||||
@@ -1601,22 +1620,13 @@ describe('TextEditorComponent', () => {
|
||||
decoration.flash('e', 100)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(highlights[0].classList.contains('e')).toBe(true)
|
||||
expect(highlights[1].classList.contains('e')).toBe(true)
|
||||
|
||||
decoration.flash('e', 100)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(highlights[0].classList.contains('e')).toBe(false)
|
||||
expect(highlights[1].classList.contains('e')).toBe(false)
|
||||
|
||||
await conditionPromise(() =>
|
||||
highlights[0].classList.contains('e') &&
|
||||
highlights[1].classList.contains('e')
|
||||
)
|
||||
|
||||
await conditionPromise(() =>
|
||||
!highlights[0].classList.contains('e') &&
|
||||
!highlights[1].classList.contains('e')
|
||||
)
|
||||
await conditionPromise(() => highlights[0].classList.contains('e'))
|
||||
await conditionPromise(() => !highlights[0].classList.contains('e'))
|
||||
})
|
||||
|
||||
it("flashing a highlight decoration doesn't unflash other highlight decorations", async () => {
|
||||
@@ -1628,16 +1638,14 @@ describe('TextEditorComponent', () => {
|
||||
decoration.flash('c', 1000)
|
||||
await component.getNextUpdatePromise()
|
||||
const highlights = element.querySelectorAll('.highlight.a')
|
||||
expect(highlights.length).toBe(1)
|
||||
expect(highlights[0].classList.contains('c')).toBe(true)
|
||||
expect(highlights[1].classList.contains('c')).toBe(true)
|
||||
|
||||
// Flash another class while the previously-flashed class is still highlighted
|
||||
decoration.flash('d', 100)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(highlights[0].classList.contains('c')).toBe(true)
|
||||
expect(highlights[1].classList.contains('c')).toBe(true)
|
||||
expect(highlights[0].classList.contains('d')).toBe(true)
|
||||
expect(highlights[1].classList.contains('d')).toBe(true)
|
||||
})
|
||||
|
||||
it('supports layer decorations', async () => {
|
||||
@@ -2002,7 +2010,7 @@ describe('TextEditorComponent', () => {
|
||||
])
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
expect(queryOnScreenLineElements(element).length).toBe(9)
|
||||
expect(item1.previousSibling.className).toBe('highlights')
|
||||
expect(item1.previousSibling).toBeNull()
|
||||
expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1))
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2))
|
||||
@@ -2026,7 +2034,7 @@ describe('TextEditorComponent', () => {
|
||||
])
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
expect(queryOnScreenLineElements(element).length).toBe(9)
|
||||
expect(item1.previousSibling.className).toBe('highlights')
|
||||
expect(item1.previousSibling).toBeNull()
|
||||
expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1))
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2))
|
||||
@@ -2081,7 +2089,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(element.contains(item1)).toBe(false)
|
||||
expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1))
|
||||
expect(item3.previousSibling.className).toBe('highlights')
|
||||
expect(item3.previousSibling).toBeNull()
|
||||
expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
@@ -2104,9 +2112,9 @@ describe('TextEditorComponent', () => {
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
expect(queryOnScreenLineElements(element).length).toBe(9)
|
||||
expect(element.contains(item1)).toBe(false)
|
||||
expect(item2.previousSibling.className).toBe('highlights')
|
||||
expect(item2.previousSibling).toBeNull()
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3))
|
||||
expect(item3.previousSibling.className).toBe('highlights')
|
||||
expect(item3.previousSibling).toBeNull()
|
||||
expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(element.contains(item4)).toBe(false)
|
||||
expect(element.contains(item5)).toBe(false)
|
||||
@@ -2128,7 +2136,7 @@ describe('TextEditorComponent', () => {
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
expect(queryOnScreenLineElements(element).length).toBe(9)
|
||||
expect(element.contains(item1)).toBe(false)
|
||||
expect(item2.previousSibling.className).toBe('highlights')
|
||||
expect(item2.previousSibling).toBeNull()
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3))
|
||||
expect(element.contains(item3)).toBe(false)
|
||||
expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 9))
|
||||
@@ -2155,7 +2163,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(element.contains(item1)).toBe(false)
|
||||
expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1))
|
||||
expect(item3.previousSibling.className).toBe('highlights')
|
||||
expect(item3.previousSibling).toBeNull()
|
||||
expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
@@ -2185,7 +2193,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(element.contains(item1)).toBe(false)
|
||||
expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1))
|
||||
expect(item3.previousSibling.className).toBe('highlights')
|
||||
expect(item3.previousSibling).toBeNull()
|
||||
expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
@@ -2223,7 +2231,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(element.contains(item1)).toBe(false)
|
||||
expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1))
|
||||
expect(item3.previousSibling.className).toBe('highlights')
|
||||
expect(item3.previousSibling).toBeNull()
|
||||
expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
@@ -2250,7 +2258,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(element.contains(item1)).toBe(false)
|
||||
expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1))
|
||||
expect(item3.previousSibling.className).toBe('highlights')
|
||||
expect(item3.previousSibling).toBeNull()
|
||||
expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0))
|
||||
expect(item4.previousSibling).toBe(lineNodeForScreenRow(component, 6))
|
||||
expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7))
|
||||
@@ -2276,6 +2284,110 @@ describe('TextEditorComponent', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('removes block decorations whose markers are invalidated, and adds them back when they become valid again', async () => {
|
||||
const editor = buildEditor({rowsPerTile: 3, autoHeight: false})
|
||||
const {item, decoration, marker} = createBlockDecorationAtScreenRow(editor, 3, {height: 44, position: 'before', invalidate: 'touch'})
|
||||
const {component, element} = buildComponent({editor, rowsPerTile: 3})
|
||||
|
||||
// Invalidating the marker removes the block decoration.
|
||||
editor.getBuffer().deleteRows(2, 3)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.parentElement).toBeNull()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
|
||||
// Moving invalid markers is ignored.
|
||||
marker.setScreenRange([[2, 0], [2, 0]])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.parentElement).toBeNull()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
|
||||
// Making the marker valid again adds back the block decoration.
|
||||
marker.bufferMarker.valid = true
|
||||
marker.setScreenRange([[3, 0], [3, 0]])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 3))
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight() + 44},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
|
||||
// Destroying the decoration and invalidating the marker at the same time
|
||||
// removes the block decoration correctly.
|
||||
editor.getBuffer().deleteRows(2, 3)
|
||||
decoration.destroy()
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.parentElement).toBeNull()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
})
|
||||
|
||||
it('does not render block decorations when decorating invalid markers', async () => {
|
||||
const editor = buildEditor({rowsPerTile: 3, autoHeight: false})
|
||||
const {component, element} = buildComponent({editor, rowsPerTile: 3})
|
||||
|
||||
const marker = editor.markScreenPosition([3, 0], {invalidate: 'touch'})
|
||||
const item = document.createElement('div')
|
||||
item.style.height = 30 + 'px'
|
||||
item.style.width = 30 + 'px'
|
||||
editor.getBuffer().deleteRows(1, 4)
|
||||
|
||||
const decoration = editor.decorateMarker(marker, {type: 'block', item, position: 'before'})
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.parentElement).toBeNull()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
|
||||
// Making the marker valid again causes the corresponding block decoration
|
||||
// to be added to the editor.
|
||||
marker.bufferMarker.valid = true
|
||||
marker.setScreenRange([[2, 0], [2, 0]])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2))
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight() + 30},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
})
|
||||
|
||||
it('does not try to remeasure block decorations whose markers are invalid (regression)', async () => {
|
||||
const editor = buildEditor({rowsPerTile: 3, autoHeight: false})
|
||||
const {component, element} = buildComponent({editor, rowsPerTile: 3})
|
||||
const {decoration, marker} = createBlockDecorationAtScreenRow(editor, 2, {height: '12px', invalidate: 'touch'})
|
||||
editor.getBuffer().deleteRows(0, 3)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
// Trigger a re-measurement of all block decorations.
|
||||
await setEditorWidthInCharacters(component, 20)
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
})
|
||||
|
||||
it('measures block decorations correctly when they are added before the component width has been updated', async () => {
|
||||
{
|
||||
const {editor, component, element} = buildComponent({autoHeight: false, width: 500, attach: false})
|
||||
@@ -2343,8 +2455,8 @@ describe('TextEditorComponent', () => {
|
||||
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
|
||||
})
|
||||
|
||||
function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, marginTop, marginBottom, position}) {
|
||||
const marker = editor.markScreenPosition([screenRow, 0], {invalidate: 'never'})
|
||||
function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, marginTop, marginBottom, position, invalidate}) {
|
||||
const marker = editor.markScreenPosition([screenRow, 0], {invalidate: invalidate || 'never'})
|
||||
const item = document.createElement('div')
|
||||
item.style.height = height + 'px'
|
||||
if (margin != null) item.style.margin = margin + 'px'
|
||||
@@ -2352,7 +2464,7 @@ describe('TextEditorComponent', () => {
|
||||
if (marginBottom != null) item.style.marginBottom = marginBottom + 'px'
|
||||
item.style.width = 30 + 'px'
|
||||
const decoration = editor.decorateMarker(marker, {type: 'block', item, position})
|
||||
return {item, decoration}
|
||||
return {item, decoration, marker}
|
||||
}
|
||||
|
||||
function assertTilesAreSizedAndPositionedCorrectly (component, tiles) {
|
||||
@@ -4225,12 +4337,12 @@ function lineNumberNodeForScreenRow (component, row) {
|
||||
|
||||
function lineNodeForScreenRow (component, row) {
|
||||
const renderedScreenLine = component.renderedScreenLineForRow(row)
|
||||
return component.lineNodesByScreenLineId.get(renderedScreenLine.id)
|
||||
return component.lineComponentsByScreenLineId.get(renderedScreenLine.id).element
|
||||
}
|
||||
|
||||
function textNodesForScreenRow (component, row) {
|
||||
const screenLine = component.renderedScreenLineForRow(row)
|
||||
return component.textNodesByScreenLineId.get(screenLine.id)
|
||||
return component.lineComponentsByScreenLineId.get(screenLine.id).textNodes
|
||||
}
|
||||
|
||||
function setScrollTop (component, scrollTop) {
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs-plus'
|
||||
runas = null # defer until used
|
||||
|
||||
symlinkCommand = (sourcePath, destinationPath, callback) ->
|
||||
fs.unlink destinationPath, (error) ->
|
||||
if error? and error?.code isnt 'ENOENT'
|
||||
callback(error)
|
||||
else
|
||||
fs.makeTree path.dirname(destinationPath), (error) ->
|
||||
if error?
|
||||
callback(error)
|
||||
else
|
||||
fs.symlink sourcePath, destinationPath, callback
|
||||
|
||||
symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) ->
|
||||
runas ?= require 'runas'
|
||||
if runas('/bin/rm', ['-f', destinationPath], admin: true) isnt 0
|
||||
throw new Error("Failed to remove '#{destinationPath}'")
|
||||
|
||||
if runas('/bin/mkdir', ['-p', path.dirname(destinationPath)], admin: true) isnt 0
|
||||
throw new Error("Failed to create directory '#{destinationPath}'")
|
||||
|
||||
if runas('/bin/ln', ['-s', sourcePath, destinationPath], admin: true) isnt 0
|
||||
throw new Error("Failed to symlink '#{sourcePath}' to '#{destinationPath}'")
|
||||
|
||||
module.exports =
|
||||
class CommandInstaller
|
||||
constructor: (@applicationDelegate) ->
|
||||
|
||||
initialize: (@appVersion) ->
|
||||
|
||||
getInstallDirectory: ->
|
||||
"/usr/local/bin"
|
||||
|
||||
getResourcesDirectory: ->
|
||||
process.resourcesPath
|
||||
|
||||
installShellCommandsInteractively: ->
|
||||
showErrorDialog = (error) =>
|
||||
@applicationDelegate.confirm
|
||||
message: "Failed to install shell commands"
|
||||
detailedMessage: error.message
|
||||
|
||||
@installAtomCommand true, (error) =>
|
||||
if error?
|
||||
showErrorDialog(error)
|
||||
else
|
||||
@installApmCommand true, (error) =>
|
||||
if error?
|
||||
showErrorDialog(error)
|
||||
else
|
||||
@applicationDelegate.confirm
|
||||
message: "Commands installed."
|
||||
detailedMessage: "The shell commands `atom` and `apm` are installed."
|
||||
|
||||
installAtomCommand: (askForPrivilege, callback) ->
|
||||
programName = if @appVersion.includes("beta")
|
||||
"atom-beta"
|
||||
else
|
||||
"atom"
|
||||
|
||||
commandPath = path.join(@getResourcesDirectory(), 'app', 'atom.sh')
|
||||
@createSymlink commandPath, programName, askForPrivilege, callback
|
||||
|
||||
installApmCommand: (askForPrivilege, callback) ->
|
||||
programName = if @appVersion.includes("beta")
|
||||
"apm-beta"
|
||||
else
|
||||
"apm"
|
||||
|
||||
commandPath = path.join(@getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm')
|
||||
@createSymlink commandPath, programName, askForPrivilege, callback
|
||||
|
||||
createSymlink: (commandPath, commandName, askForPrivilege, callback) ->
|
||||
return unless process.platform is 'darwin'
|
||||
|
||||
destinationPath = path.join(@getInstallDirectory(), commandName)
|
||||
|
||||
fs.readlink destinationPath, (error, realpath) ->
|
||||
if realpath is commandPath
|
||||
callback()
|
||||
return
|
||||
|
||||
symlinkCommand commandPath, destinationPath, (error) ->
|
||||
if askForPrivilege and error?.code is 'EACCES'
|
||||
try
|
||||
error = null
|
||||
symlinkCommandWithPrivilegeSync(commandPath, destinationPath)
|
||||
catch err
|
||||
error = err
|
||||
|
||||
callback?(error)
|
||||
88
src/command-installer.js
Normal file
88
src/command-installer.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
|
||||
module.exports =
|
||||
class CommandInstaller {
|
||||
constructor (applicationDelegate) {
|
||||
this.applicationDelegate = applicationDelegate
|
||||
}
|
||||
|
||||
initialize (appVersion) {
|
||||
this.appVersion = appVersion
|
||||
}
|
||||
|
||||
getInstallDirectory () {
|
||||
return '/usr/local/bin'
|
||||
}
|
||||
|
||||
getResourcesDirectory () {
|
||||
return process.resourcesPath
|
||||
}
|
||||
|
||||
installShellCommandsInteractively () {
|
||||
const showErrorDialog = (error) => {
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Failed to install shell commands',
|
||||
detailedMessage: error.message
|
||||
})
|
||||
}
|
||||
|
||||
this.installAtomCommand(true, error => {
|
||||
if (error) return showErrorDialog(error)
|
||||
this.installApmCommand(true, error => {
|
||||
if (error) return showErrorDialog(error)
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Commands installed.',
|
||||
detailedMessage: 'The shell commands `atom` and `apm` are installed.'
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
installAtomCommand (askForPrivilege, callback) {
|
||||
this.installCommand(
|
||||
path.join(this.getResourcesDirectory(), 'app', 'atom.sh'),
|
||||
this.appVersion.includes('beta') ? 'atom-beta' : 'atom',
|
||||
askForPrivilege,
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
installApmCommand (askForPrivilege, callback) {
|
||||
this.installCommand(
|
||||
path.join(this.getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm'),
|
||||
this.appVersion.includes('beta') ? 'apm-beta' : 'apm',
|
||||
askForPrivilege,
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
installCommand (commandPath, commandName, askForPrivilege, callback) {
|
||||
if (process.platform !== 'darwin') return callback()
|
||||
|
||||
const destinationPath = path.join(this.getInstallDirectory(), commandName)
|
||||
|
||||
fs.readlink(destinationPath, (error, realpath) => {
|
||||
if (error && error.code !== 'ENOENT') return callback(error)
|
||||
if (realpath === commandPath) return callback()
|
||||
this.createSymlink(fs, commandPath, destinationPath, error => {
|
||||
if (error && error.code === 'EACCES' && askForPrivilege) {
|
||||
const fsAdmin = require('fs-admin')
|
||||
this.createSymlink(fsAdmin, commandPath, destinationPath, callback)
|
||||
} else {
|
||||
callback(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createSymlink (fs, sourcePath, destinationPath, callback) {
|
||||
fs.unlink(destinationPath, (error) => {
|
||||
if (error && error.code !== 'ENOENT') return callback(error)
|
||||
fs.makeTree(path.dirname(destinationPath), (error) => {
|
||||
if (error) return callback(error)
|
||||
fs.symlink(sourcePath, destinationPath, callback)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
/* global Event, CustomEvent */
|
||||
|
||||
const { Emitter, Disposable, CompositeDisposable } = require('event-kit')
|
||||
const { calculateSpecificity, validateSelector } = require('clear-cut')
|
||||
const _ = require('underscore-plus')
|
||||
@@ -91,11 +89,24 @@ module.exports = class CommandRegistry {
|
||||
// DOM element, the command will be associated with just that element.
|
||||
// * `commandName` A {String} containing the name of a command you want to
|
||||
// handle such as `user:insert-date`.
|
||||
// * `callback` A {Function} to call when the given command is invoked on an
|
||||
// element matching the selector. It will be called with `this` referencing
|
||||
// the matching DOM node.
|
||||
// * `event` A standard DOM event instance. Call `stopPropagation` or
|
||||
// `stopImmediatePropagation` to terminate bubbling early.
|
||||
// * `listener` A listener which handles the event. Either A {Function} to
|
||||
// call when the given command is invoked on an element matching the
|
||||
// selector, or an {Object} with a `didDispatch` property which is such a
|
||||
// function.
|
||||
//
|
||||
// The function (`listener` itself if it is a function, or the `didDispatch`
|
||||
// method if `listener` is an object) will be called with `this` referencing
|
||||
// the matching DOM node and the following argument:
|
||||
// * `event` A standard DOM event instance. Call `stopPropagation` or
|
||||
// `stopImmediatePropagation` to terminate bubbling early.
|
||||
//
|
||||
// Additionally, `listener` may have additional properties which are returned
|
||||
// to those who query using `atom.commands.findCommands`, as well as several
|
||||
// meaningful metadata properties:
|
||||
// * `displayName`: Overrides any generated `displayName` that would
|
||||
// otherwise be generated from the event name.
|
||||
// * `description`: Used by consumers to display detailed information about
|
||||
// the command.
|
||||
//
|
||||
// ## Arguments: Registering Multiple Commands
|
||||
//
|
||||
@@ -109,72 +120,79 @@ module.exports = class CommandRegistry {
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
// added command handler(s).
|
||||
add (target, commandName, callback, throwOnInvalidSelector = true) {
|
||||
add (target, commandName, listener, throwOnInvalidSelector = true) {
|
||||
if (typeof commandName === 'object') {
|
||||
const commands = commandName
|
||||
throwOnInvalidSelector = callback
|
||||
throwOnInvalidSelector = listener
|
||||
const disposable = new CompositeDisposable()
|
||||
for (commandName in commands) {
|
||||
callback = commands[commandName]
|
||||
disposable.add(
|
||||
this.add(target, commandName, callback, throwOnInvalidSelector)
|
||||
)
|
||||
listener = commands[commandName]
|
||||
disposable.add(this.add(target, commandName, listener, throwOnInvalidSelector))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error("Can't register a command with non-function callback.")
|
||||
if (listener == null) {
|
||||
throw new Error('Cannot register a command with a null listener.')
|
||||
}
|
||||
|
||||
// type Listener = ((e: CustomEvent) => void) | {
|
||||
// displayName?: string,
|
||||
// description?: string,
|
||||
// didDispatch(e: CustomEvent): void,
|
||||
// }
|
||||
if ((typeof listener !== 'function') && (typeof listener.didDispatch !== 'function')) {
|
||||
throw new Error('Listener must be a callback function or an object with a didDispatch method.')
|
||||
}
|
||||
|
||||
if (typeof target === 'string') {
|
||||
if (throwOnInvalidSelector) {
|
||||
validateSelector(target)
|
||||
}
|
||||
return this.addSelectorBasedListener(target, commandName, callback)
|
||||
return this.addSelectorBasedListener(target, commandName, listener)
|
||||
} else {
|
||||
return this.addInlineListener(target, commandName, callback)
|
||||
return this.addInlineListener(target, commandName, listener)
|
||||
}
|
||||
}
|
||||
|
||||
addSelectorBasedListener (selector, commandName, callback) {
|
||||
addSelectorBasedListener (selector, commandName, listener) {
|
||||
if (this.selectorBasedListenersByCommandName[commandName] == null) {
|
||||
this.selectorBasedListenersByCommandName[commandName] = []
|
||||
}
|
||||
const listenersForCommand = this.selectorBasedListenersByCommandName[commandName]
|
||||
const listener = new SelectorBasedListener(selector, callback)
|
||||
listenersForCommand.push(listener)
|
||||
const selectorListener = new SelectorBasedListener(selector, commandName, listener)
|
||||
listenersForCommand.push(selectorListener)
|
||||
|
||||
this.commandRegistered(commandName)
|
||||
|
||||
return new Disposable(() => {
|
||||
listenersForCommand.splice(listenersForCommand.indexOf(listener), 1)
|
||||
listenersForCommand.splice(listenersForCommand.indexOf(selectorListener), 1)
|
||||
if (listenersForCommand.length === 0) {
|
||||
return delete this.selectorBasedListenersByCommandName[commandName]
|
||||
delete this.selectorBasedListenersByCommandName[commandName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addInlineListener (element, commandName, callback) {
|
||||
addInlineListener (element, commandName, listener) {
|
||||
if (this.inlineListenersByCommandName[commandName] == null) {
|
||||
this.inlineListenersByCommandName[commandName] = new WeakMap()
|
||||
}
|
||||
|
||||
const listenersForCommand = this.inlineListenersByCommandName[commandName]
|
||||
let listenersForElement = listenersForCommand.get(element)
|
||||
if (listenersForElement == null) {
|
||||
if (!listenersForElement) {
|
||||
listenersForElement = []
|
||||
listenersForCommand.set(element, listenersForElement)
|
||||
}
|
||||
const listener = new InlineListener(callback)
|
||||
listenersForElement.push(listener)
|
||||
const inlineListener = new InlineListener(commandName, listener)
|
||||
listenersForElement.push(inlineListener)
|
||||
|
||||
this.commandRegistered(commandName)
|
||||
|
||||
return new Disposable(function () {
|
||||
listenersForElement.splice(listenersForElement.indexOf(listener), 1)
|
||||
return new Disposable(() => {
|
||||
listenersForElement.splice(listenersForElement.indexOf(inlineListener), 1)
|
||||
if (listenersForElement.length === 0) {
|
||||
return listenersForCommand.delete(element)
|
||||
listenersForCommand.delete(element)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -184,10 +202,17 @@ module.exports = class CommandRegistry {
|
||||
// * `params` An {Object} containing one or more of the following keys:
|
||||
// * `target` A DOM node that is the hypothetical target of a given command.
|
||||
//
|
||||
// Returns an {Array} of {Object}s containing the following keys:
|
||||
// Returns an {Array} of `CommandDescriptor` {Object}s containing the following keys:
|
||||
// * `name` The name of the command. For example, `user:insert-date`.
|
||||
// * `displayName` The display name of the command. For example,
|
||||
// `User: Insert Date`.
|
||||
// Additional metadata may also be present in the returned descriptor:
|
||||
// * `description` a {String} describing the function of the command in more
|
||||
// detail than the title
|
||||
// * `tags` an {Array} of {String}s that describe keywords related to the
|
||||
// command
|
||||
// Any additional nonstandard metadata provided when the command was `add`ed
|
||||
// may also be present in the returned descriptor.
|
||||
findCommands ({ target }) {
|
||||
const commandNames = new Set()
|
||||
const commands = []
|
||||
@@ -198,23 +223,20 @@ module.exports = class CommandRegistry {
|
||||
listeners = this.inlineListenersByCommandName[name]
|
||||
if (listeners.has(currentTarget) && !commandNames.has(name)) {
|
||||
commandNames.add(name)
|
||||
commands.push({ name, displayName: _.humanizeEventName(name) })
|
||||
const targetListeners = listeners.get(currentTarget)
|
||||
commands.push(
|
||||
...targetListeners.map(listener => listener.descriptor)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const commandName in this.selectorBasedListenersByCommandName) {
|
||||
listeners = this.selectorBasedListenersByCommandName[commandName]
|
||||
for (const listener of listeners) {
|
||||
if (
|
||||
currentTarget.webkitMatchesSelector &&
|
||||
currentTarget.webkitMatchesSelector(listener.selector)
|
||||
) {
|
||||
if (listener.matchesTarget(currentTarget)) {
|
||||
if (!commandNames.has(commandName)) {
|
||||
commandNames.add(commandName)
|
||||
commands.push({
|
||||
name: commandName,
|
||||
displayName: _.humanizeEventName(commandName)
|
||||
})
|
||||
commands.push(listener.descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,9 +361,7 @@ module.exports = class CommandRegistry {
|
||||
if (currentTarget.webkitMatchesSelector != null) {
|
||||
const selectorBasedListeners =
|
||||
(this.selectorBasedListenersByCommandName[event.type] || [])
|
||||
.filter(listener =>
|
||||
currentTarget.webkitMatchesSelector(listener.selector)
|
||||
)
|
||||
.filter(listener => listener.matchesTarget(currentTarget))
|
||||
.sort((a, b) => a.compare(b))
|
||||
listeners = selectorBasedListeners.concat(listeners)
|
||||
}
|
||||
@@ -358,7 +378,7 @@ module.exports = class CommandRegistry {
|
||||
if (immediatePropagationStopped) {
|
||||
break
|
||||
}
|
||||
listener.callback.call(currentTarget, dispatchedEvent)
|
||||
listener.didDispatch.call(currentTarget, dispatchedEvent)
|
||||
}
|
||||
|
||||
if (currentTarget === window) {
|
||||
@@ -383,10 +403,15 @@ module.exports = class CommandRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// type Listener = {
|
||||
// descriptor: CommandDescriptor,
|
||||
// extractDidDispatch: (e: CustomEvent) => void,
|
||||
// };
|
||||
class SelectorBasedListener {
|
||||
constructor (selector, callback) {
|
||||
constructor (selector, commandName, listener) {
|
||||
this.selector = selector
|
||||
this.callback = callback
|
||||
this.didDispatch = extractDidDispatch(listener)
|
||||
this.descriptor = extractDescriptor(commandName, listener)
|
||||
this.specificity = calculateSpecificity(this.selector)
|
||||
this.sequenceNumber = SequenceCount++
|
||||
}
|
||||
@@ -397,10 +422,33 @@ class SelectorBasedListener {
|
||||
this.sequenceNumber - other.sequenceNumber
|
||||
)
|
||||
}
|
||||
|
||||
matchesTarget (target) {
|
||||
return target.webkitMatchesSelector && target.webkitMatchesSelector(this.selector)
|
||||
}
|
||||
}
|
||||
|
||||
class InlineListener {
|
||||
constructor (callback) {
|
||||
this.callback = callback
|
||||
constructor (commandName, listener) {
|
||||
this.didDispatch = extractDidDispatch(listener)
|
||||
this.descriptor = extractDescriptor(commandName, listener)
|
||||
}
|
||||
}
|
||||
|
||||
// type CommandDescriptor = {
|
||||
// name: string,
|
||||
// displayName: string,
|
||||
// };
|
||||
function extractDescriptor (name, listener) {
|
||||
return Object.assign(
|
||||
_.omit(listener, 'didDispatch'),
|
||||
{
|
||||
name,
|
||||
displayName: listener.displayName ? listener.displayName : _.humanizeEventName(name)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function extractDidDispatch (listener) {
|
||||
return typeof listener === 'function' ? listener : listener.didDispatch
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class AtomWindow
|
||||
options =
|
||||
show: false
|
||||
title: 'Atom'
|
||||
tabbingIdentifier: 'atom'
|
||||
webPreferences:
|
||||
# Prevent specs from throttling when the window is in the background:
|
||||
# this should result in faster CI builds, and an improvement in the
|
||||
|
||||
@@ -1,652 +0,0 @@
|
||||
path = require 'path'
|
||||
normalizePackageData = null
|
||||
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
fs = require 'fs-plus'
|
||||
CSON = require 'season'
|
||||
|
||||
ServiceHub = require 'service-hub'
|
||||
Package = require './package'
|
||||
ThemePackage = require './theme-package'
|
||||
{isDeprecatedPackage, getDeprecatedPackageMetadata} = require './deprecated-packages'
|
||||
packageJSON = require('../package.json')
|
||||
|
||||
# Extended: Package manager for coordinating the lifecycle of Atom packages.
|
||||
#
|
||||
# An instance of this class is always available as the `atom.packages` global.
|
||||
#
|
||||
# Packages can be loaded, activated, and deactivated, and unloaded:
|
||||
# * Loading a package reads and parses the package's metadata and resources
|
||||
# such as keymaps, menus, stylesheets, etc.
|
||||
# * Activating a package registers the loaded resources and calls `activate()`
|
||||
# on the package's main module.
|
||||
# * Deactivating a package unregisters the package's resources and calls
|
||||
# `deactivate()` on the package's main module.
|
||||
# * Unloading a package removes it completely from the package manager.
|
||||
#
|
||||
# Packages can be enabled/disabled via the `core.disabledPackages` config
|
||||
# settings and also by calling `enablePackage()/disablePackage()`.
|
||||
module.exports =
|
||||
class PackageManager
|
||||
constructor: (params) ->
|
||||
{
|
||||
@config, @styleManager, @notificationManager, @keymapManager,
|
||||
@commandRegistry, @grammarRegistry, @deserializerManager, @viewRegistry
|
||||
} = params
|
||||
|
||||
@emitter = new Emitter
|
||||
@activationHookEmitter = new Emitter
|
||||
@packageDirPaths = []
|
||||
@deferredActivationHooks = []
|
||||
@triggeredActivationHooks = new Set()
|
||||
@packagesCache = packageJSON._atomPackages ? {}
|
||||
@packageDependencies = packageJSON.packageDependencies ? {}
|
||||
@initialPackagesLoaded = false
|
||||
@initialPackagesActivated = false
|
||||
@preloadedPackages = {}
|
||||
@loadedPackages = {}
|
||||
@activePackages = {}
|
||||
@activatingPackages = {}
|
||||
@packageStates = {}
|
||||
@serviceHub = new ServiceHub
|
||||
|
||||
@packageActivators = []
|
||||
@registerPackageActivator(this, ['atom', 'textmate'])
|
||||
|
||||
initialize: (params) ->
|
||||
{configDirPath, @devMode, safeMode, @resourcePath} = params
|
||||
if configDirPath? and not safeMode
|
||||
if @devMode
|
||||
@packageDirPaths.push(path.join(configDirPath, "dev", "packages"))
|
||||
@packageDirPaths.push(path.join(configDirPath, "packages"))
|
||||
|
||||
setContextMenuManager: (@contextMenuManager) ->
|
||||
|
||||
setMenuManager: (@menuManager) ->
|
||||
|
||||
setThemeManager: (@themeManager) ->
|
||||
|
||||
reset: ->
|
||||
@serviceHub.clear()
|
||||
@deactivatePackages()
|
||||
@loadedPackages = {}
|
||||
@preloadedPackages = {}
|
||||
@packageStates = {}
|
||||
@packagesCache = packageJSON._atomPackages ? {}
|
||||
@packageDependencies = packageJSON.packageDependencies ? {}
|
||||
@triggeredActivationHooks.clear()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Invoke the given callback when all packages have been loaded.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidLoadInitialPackages: (callback) ->
|
||||
@emitter.on 'did-load-initial-packages', callback
|
||||
|
||||
# Public: Invoke the given callback when all packages have been activated.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidActivateInitialPackages: (callback) ->
|
||||
@emitter.on 'did-activate-initial-packages', callback
|
||||
|
||||
# Public: Invoke the given callback when a package is activated.
|
||||
#
|
||||
# * `callback` A {Function} to be invoked when a package is activated.
|
||||
# * `package` The {Package} that was activated.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidActivatePackage: (callback) ->
|
||||
@emitter.on 'did-activate-package', callback
|
||||
|
||||
# Public: Invoke the given callback when a package is deactivated.
|
||||
#
|
||||
# * `callback` A {Function} to be invoked when a package is deactivated.
|
||||
# * `package` The {Package} that was deactivated.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDeactivatePackage: (callback) ->
|
||||
@emitter.on 'did-deactivate-package', callback
|
||||
|
||||
# Public: Invoke the given callback when a package is loaded.
|
||||
#
|
||||
# * `callback` A {Function} to be invoked when a package is loaded.
|
||||
# * `package` The {Package} that was loaded.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidLoadPackage: (callback) ->
|
||||
@emitter.on 'did-load-package', callback
|
||||
|
||||
# Public: Invoke the given callback when a package is unloaded.
|
||||
#
|
||||
# * `callback` A {Function} to be invoked when a package is unloaded.
|
||||
# * `package` The {Package} that was unloaded.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidUnloadPackage: (callback) ->
|
||||
@emitter.on 'did-unload-package', callback
|
||||
|
||||
###
|
||||
Section: Package system data
|
||||
###
|
||||
|
||||
# Public: Get the path to the apm command.
|
||||
#
|
||||
# Uses the value of the `core.apmPath` config setting if it exists.
|
||||
#
|
||||
# Return a {String} file path to apm.
|
||||
getApmPath: ->
|
||||
configPath = atom.config.get('core.apmPath')
|
||||
return configPath if configPath
|
||||
return @apmPath if @apmPath?
|
||||
|
||||
commandName = 'apm'
|
||||
commandName += '.cmd' if process.platform is 'win32'
|
||||
apmRoot = path.join(process.resourcesPath, 'app', 'apm')
|
||||
@apmPath = path.join(apmRoot, 'bin', commandName)
|
||||
unless fs.isFileSync(@apmPath)
|
||||
@apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName)
|
||||
@apmPath
|
||||
|
||||
# Public: Get the paths being used to look for packages.
|
||||
#
|
||||
# Returns an {Array} of {String} directory paths.
|
||||
getPackageDirPaths: ->
|
||||
_.clone(@packageDirPaths)
|
||||
|
||||
###
|
||||
Section: General package data
|
||||
###
|
||||
|
||||
# Public: Resolve the given package name to a path on disk.
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Return a {String} folder path or undefined if it could not be resolved.
|
||||
resolvePackagePath: (name) ->
|
||||
return name if fs.isDirectorySync(name)
|
||||
|
||||
packagePath = fs.resolve(@packageDirPaths..., name)
|
||||
return packagePath if fs.isDirectorySync(packagePath)
|
||||
|
||||
packagePath = path.join(@resourcePath, 'node_modules', name)
|
||||
return packagePath if @hasAtomEngine(packagePath)
|
||||
|
||||
# Public: Is the package with the given name bundled with Atom?
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isBundledPackage: (name) ->
|
||||
@getPackageDependencies().hasOwnProperty(name)
|
||||
|
||||
isDeprecatedPackage: (name, version) ->
|
||||
isDeprecatedPackage(name, version)
|
||||
|
||||
getDeprecatedPackageMetadata: (name) ->
|
||||
getDeprecatedPackageMetadata(name)
|
||||
|
||||
###
|
||||
Section: Enabling and disabling packages
|
||||
###
|
||||
|
||||
# Public: Enable the package with the given name.
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns the {Package} that was enabled or null if it isn't loaded.
|
||||
enablePackage: (name) ->
|
||||
pack = @loadPackage(name)
|
||||
pack?.enable()
|
||||
pack
|
||||
|
||||
# Public: Disable the package with the given name.
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns the {Package} that was disabled or null if it isn't loaded.
|
||||
disablePackage: (name) ->
|
||||
pack = @loadPackage(name)
|
||||
|
||||
unless @isPackageDisabled(name)
|
||||
pack?.disable()
|
||||
|
||||
pack
|
||||
|
||||
# Public: Is the package with the given name disabled?
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isPackageDisabled: (name) ->
|
||||
_.include(@config.get('core.disabledPackages') ? [], name)
|
||||
|
||||
###
|
||||
Section: Accessing active packages
|
||||
###
|
||||
|
||||
# Public: Get an {Array} of all the active {Package}s.
|
||||
getActivePackages: ->
|
||||
_.values(@activePackages)
|
||||
|
||||
# Public: Get the active {Package} with the given name.
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns a {Package} or undefined.
|
||||
getActivePackage: (name) ->
|
||||
@activePackages[name]
|
||||
|
||||
# Public: Is the {Package} with the given name active?
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isPackageActive: (name) ->
|
||||
@getActivePackage(name)?
|
||||
|
||||
# Public: Returns a {Boolean} indicating whether package activation has occurred.
|
||||
hasActivatedInitialPackages: -> @initialPackagesActivated
|
||||
|
||||
###
|
||||
Section: Accessing loaded packages
|
||||
###
|
||||
|
||||
# Public: Get an {Array} of all the loaded {Package}s
|
||||
getLoadedPackages: ->
|
||||
_.values(@loadedPackages)
|
||||
|
||||
# Get packages for a certain package type
|
||||
#
|
||||
# * `types` an {Array} of {String}s like ['atom', 'textmate'].
|
||||
getLoadedPackagesForTypes: (types) ->
|
||||
pack for pack in @getLoadedPackages() when pack.getType() in types
|
||||
|
||||
# Public: Get the loaded {Package} with the given name.
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns a {Package} or undefined.
|
||||
getLoadedPackage: (name) ->
|
||||
@loadedPackages[name]
|
||||
|
||||
# Public: Is the package with the given name loaded?
|
||||
#
|
||||
# * `name` - The {String} package name.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isPackageLoaded: (name) ->
|
||||
@getLoadedPackage(name)?
|
||||
|
||||
# Public: Returns a {Boolean} indicating whether package loading has occurred.
|
||||
hasLoadedInitialPackages: -> @initialPackagesLoaded
|
||||
|
||||
###
|
||||
Section: Accessing available packages
|
||||
###
|
||||
|
||||
# Public: Returns an {Array} of {String}s of all the available package paths.
|
||||
getAvailablePackagePaths: ->
|
||||
@getAvailablePackages().map((a) -> a.path)
|
||||
|
||||
# Public: Returns an {Array} of {String}s of all the available package names.
|
||||
getAvailablePackageNames: ->
|
||||
@getAvailablePackages().map((a) -> a.name)
|
||||
|
||||
# Public: Returns an {Array} of {String}s of all the available package metadata.
|
||||
getAvailablePackageMetadata: ->
|
||||
packages = []
|
||||
for pack in @getAvailablePackages()
|
||||
metadata = @getLoadedPackage(pack.name)?.metadata ? @loadPackageMetadata(pack, true)
|
||||
packages.push(metadata)
|
||||
packages
|
||||
|
||||
getAvailablePackages: ->
|
||||
packages = []
|
||||
packagesByName = new Set()
|
||||
|
||||
for packageDirPath in @packageDirPaths
|
||||
if fs.isDirectorySync(packageDirPath)
|
||||
for packagePath in fs.readdirSync(packageDirPath)
|
||||
packagePath = path.join(packageDirPath, packagePath)
|
||||
packageName = path.basename(packagePath)
|
||||
if not packageName.startsWith('.') and not packagesByName.has(packageName) and fs.isDirectorySync(packagePath)
|
||||
packages.push({
|
||||
name: packageName,
|
||||
path: packagePath,
|
||||
isBundled: false
|
||||
})
|
||||
packagesByName.add(packageName)
|
||||
|
||||
for packageName of @packageDependencies
|
||||
unless packagesByName.has(packageName)
|
||||
packages.push({
|
||||
name: packageName,
|
||||
path: path.join(@resourcePath, 'node_modules', packageName),
|
||||
isBundled: true
|
||||
})
|
||||
|
||||
packages.sort((a, b) -> a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
getPackageState: (name) ->
|
||||
@packageStates[name]
|
||||
|
||||
setPackageState: (name, state) ->
|
||||
@packageStates[name] = state
|
||||
|
||||
getPackageDependencies: ->
|
||||
@packageDependencies
|
||||
|
||||
hasAtomEngine: (packagePath) ->
|
||||
metadata = @loadPackageMetadata(packagePath, true)
|
||||
metadata?.engines?.atom?
|
||||
|
||||
unobserveDisabledPackages: ->
|
||||
@disabledPackagesSubscription?.dispose()
|
||||
@disabledPackagesSubscription = null
|
||||
|
||||
observeDisabledPackages: ->
|
||||
@disabledPackagesSubscription ?= @config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) =>
|
||||
packagesToEnable = _.difference(oldValue, newValue)
|
||||
packagesToDisable = _.difference(newValue, oldValue)
|
||||
|
||||
@deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName)
|
||||
@activatePackage(packageName) for packageName in packagesToEnable
|
||||
null
|
||||
|
||||
unobservePackagesWithKeymapsDisabled: ->
|
||||
@packagesWithKeymapsDisabledSubscription?.dispose()
|
||||
@packagesWithKeymapsDisabledSubscription = null
|
||||
|
||||
observePackagesWithKeymapsDisabled: ->
|
||||
@packagesWithKeymapsDisabledSubscription ?= @config.onDidChange 'core.packagesWithKeymapsDisabled', ({newValue, oldValue}) =>
|
||||
keymapsToEnable = _.difference(oldValue, newValue)
|
||||
keymapsToDisable = _.difference(newValue, oldValue)
|
||||
|
||||
disabledPackageNames = new Set(@config.get('core.disabledPackages'))
|
||||
for packageName in keymapsToDisable when not disabledPackageNames.has(packageName)
|
||||
@getLoadedPackage(packageName)?.deactivateKeymaps()
|
||||
for packageName in keymapsToEnable when not disabledPackageNames.has(packageName)
|
||||
@getLoadedPackage(packageName)?.activateKeymaps()
|
||||
null
|
||||
|
||||
preloadPackages: ->
|
||||
for packageName, pack of @packagesCache
|
||||
@preloadPackage(packageName, pack)
|
||||
|
||||
preloadPackage: (packageName, pack) ->
|
||||
metadata = pack.metadata ? {}
|
||||
unless typeof metadata.name is 'string' and metadata.name.length > 0
|
||||
metadata.name = packageName
|
||||
|
||||
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
|
||||
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
|
||||
|
||||
options = {
|
||||
path: pack.rootDirPath, name: packageName, preloadedPackage: true,
|
||||
bundledPackage: true, metadata, packageManager: this, @config,
|
||||
@styleManager, @commandRegistry, @keymapManager,
|
||||
@notificationManager, @grammarRegistry, @themeManager, @menuManager,
|
||||
@contextMenuManager, @deserializerManager, @viewRegistry
|
||||
}
|
||||
if metadata.theme
|
||||
pack = new ThemePackage(options)
|
||||
else
|
||||
pack = new Package(options)
|
||||
|
||||
pack.preload()
|
||||
@preloadedPackages[packageName] = pack
|
||||
|
||||
loadPackages: ->
|
||||
# Ensure atom exports is already in the require cache so the load time
|
||||
# of the first package isn't skewed by being the first to require atom
|
||||
require '../exports/atom'
|
||||
|
||||
disabledPackageNames = new Set(@config.get('core.disabledPackages'))
|
||||
@config.transact =>
|
||||
for pack in @getAvailablePackages()
|
||||
@loadAvailablePackage(pack, disabledPackageNames)
|
||||
return
|
||||
@initialPackagesLoaded = true
|
||||
@emitter.emit 'did-load-initial-packages'
|
||||
|
||||
loadPackage: (nameOrPath) ->
|
||||
if path.basename(nameOrPath)[0].match(/^\./) # primarily to skip .git folder
|
||||
null
|
||||
else if pack = @getLoadedPackage(nameOrPath)
|
||||
pack
|
||||
else if packagePath = @resolvePackagePath(nameOrPath)
|
||||
name = path.basename(nameOrPath)
|
||||
@loadAvailablePackage({name, path: packagePath, isBundled: @isBundledPackagePath(packagePath)})
|
||||
else
|
||||
console.warn "Could not resolve '#{nameOrPath}' to a package path"
|
||||
null
|
||||
|
||||
loadAvailablePackage: (availablePackage, disabledPackageNames) ->
|
||||
preloadedPackage = @preloadedPackages[availablePackage.name]
|
||||
|
||||
if disabledPackageNames?.has(availablePackage.name)
|
||||
if preloadedPackage?
|
||||
preloadedPackage.deactivate()
|
||||
delete preloadedPackage[availablePackage.name]
|
||||
else
|
||||
loadedPackage = @getLoadedPackage(availablePackage.name)
|
||||
if loadedPackage?
|
||||
loadedPackage
|
||||
else
|
||||
if preloadedPackage?
|
||||
if availablePackage.isBundled
|
||||
preloadedPackage.finishLoading()
|
||||
@loadedPackages[availablePackage.name] = preloadedPackage
|
||||
return preloadedPackage
|
||||
else
|
||||
preloadedPackage.deactivate()
|
||||
delete preloadedPackage[availablePackage.name]
|
||||
|
||||
try
|
||||
metadata = @loadPackageMetadata(availablePackage) ? {}
|
||||
catch error
|
||||
@handleMetadataError(error, availablePackage.path)
|
||||
return null
|
||||
|
||||
unless availablePackage.isBundled
|
||||
if @isDeprecatedPackage(metadata.name, metadata.version)
|
||||
console.warn "Could not load #{metadata.name}@#{metadata.version} because it uses deprecated APIs that have been removed."
|
||||
return null
|
||||
|
||||
options = {
|
||||
path: availablePackage.path, name: availablePackage.name, metadata,
|
||||
bundledPackage: availablePackage.isBundled, packageManager: this,
|
||||
@config, @styleManager, @commandRegistry, @keymapManager,
|
||||
@notificationManager, @grammarRegistry, @themeManager, @menuManager,
|
||||
@contextMenuManager, @deserializerManager, @viewRegistry
|
||||
}
|
||||
if metadata.theme
|
||||
pack = new ThemePackage(options)
|
||||
else
|
||||
pack = new Package(options)
|
||||
pack.load()
|
||||
@loadedPackages[pack.name] = pack
|
||||
@emitter.emit 'did-load-package', pack
|
||||
pack
|
||||
|
||||
unloadPackages: ->
|
||||
@unloadPackage(name) for name in _.keys(@loadedPackages)
|
||||
null
|
||||
|
||||
unloadPackage: (name) ->
|
||||
if @isPackageActive(name)
|
||||
throw new Error("Tried to unload active package '#{name}'")
|
||||
|
||||
if pack = @getLoadedPackage(name)
|
||||
delete @loadedPackages[pack.name]
|
||||
@emitter.emit 'did-unload-package', pack
|
||||
else
|
||||
throw new Error("No loaded package for name '#{name}'")
|
||||
|
||||
# Activate all the packages that should be activated.
|
||||
activate: ->
|
||||
promises = []
|
||||
for [activator, types] in @packageActivators
|
||||
packages = @getLoadedPackagesForTypes(types)
|
||||
promises = promises.concat(activator.activatePackages(packages))
|
||||
Promise.all(promises).then =>
|
||||
@triggerDeferredActivationHooks()
|
||||
@initialPackagesActivated = true
|
||||
@emitter.emit 'did-activate-initial-packages'
|
||||
|
||||
# another type of package manager can handle other package types.
|
||||
# See ThemeManager
|
||||
registerPackageActivator: (activator, types) ->
|
||||
@packageActivators.push([activator, types])
|
||||
|
||||
activatePackages: (packages) ->
|
||||
promises = []
|
||||
@config.transactAsync =>
|
||||
for pack in packages
|
||||
promise = @activatePackage(pack.name)
|
||||
promises.push(promise) unless pack.activationShouldBeDeferred()
|
||||
Promise.all(promises)
|
||||
@observeDisabledPackages()
|
||||
@observePackagesWithKeymapsDisabled()
|
||||
promises
|
||||
|
||||
# Activate a single package by name
|
||||
activatePackage: (name) ->
|
||||
if pack = @getActivePackage(name)
|
||||
Promise.resolve(pack)
|
||||
else if pack = @loadPackage(name)
|
||||
@activatingPackages[pack.name] = pack
|
||||
activationPromise = pack.activate().then =>
|
||||
if @activatingPackages[pack.name]?
|
||||
delete @activatingPackages[pack.name]
|
||||
@activePackages[pack.name] = pack
|
||||
@emitter.emit 'did-activate-package', pack
|
||||
pack
|
||||
|
||||
unless @deferredActivationHooks?
|
||||
@triggeredActivationHooks.forEach((hook) => @activationHookEmitter.emit(hook))
|
||||
|
||||
activationPromise
|
||||
else
|
||||
Promise.reject(new Error("Failed to load package '#{name}'"))
|
||||
|
||||
triggerDeferredActivationHooks: ->
|
||||
return unless @deferredActivationHooks?
|
||||
@activationHookEmitter.emit(hook) for hook in @deferredActivationHooks
|
||||
@deferredActivationHooks = null
|
||||
|
||||
triggerActivationHook: (hook) ->
|
||||
return new Error("Cannot trigger an empty activation hook") unless hook? and _.isString(hook) and hook.length > 0
|
||||
@triggeredActivationHooks.add(hook)
|
||||
if @deferredActivationHooks?
|
||||
@deferredActivationHooks.push hook
|
||||
else
|
||||
@activationHookEmitter.emit(hook)
|
||||
|
||||
onDidTriggerActivationHook: (hook, callback) ->
|
||||
return unless hook? and _.isString(hook) and hook.length > 0
|
||||
@activationHookEmitter.on(hook, callback)
|
||||
|
||||
serialize: ->
|
||||
for pack in @getActivePackages()
|
||||
@serializePackage(pack)
|
||||
@packageStates
|
||||
|
||||
serializePackage: (pack) ->
|
||||
@setPackageState(pack.name, state) if state = pack.serialize?()
|
||||
|
||||
# Deactivate all packages
|
||||
deactivatePackages: ->
|
||||
@config.transact =>
|
||||
@deactivatePackage(pack.name, true) for pack in @getLoadedPackages()
|
||||
return
|
||||
@unobserveDisabledPackages()
|
||||
@unobservePackagesWithKeymapsDisabled()
|
||||
|
||||
# Deactivate the package with the given name
|
||||
deactivatePackage: (name, suppressSerialization) ->
|
||||
pack = @getLoadedPackage(name)
|
||||
@serializePackage(pack) if not suppressSerialization and @isPackageActive(pack.name)
|
||||
pack.deactivate()
|
||||
delete @activePackages[pack.name]
|
||||
delete @activatingPackages[pack.name]
|
||||
@emitter.emit 'did-deactivate-package', pack
|
||||
|
||||
handleMetadataError: (error, packagePath) ->
|
||||
metadataPath = path.join(packagePath, 'package.json')
|
||||
detail = "#{error.message} in #{metadataPath}"
|
||||
stack = "#{error.stack}\n at #{metadataPath}:1:1"
|
||||
message = "Failed to load the #{path.basename(packagePath)} package"
|
||||
@notificationManager.addError(message, {stack, detail, packageName: path.basename(packagePath), dismissable: true})
|
||||
|
||||
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, ->
|
||||
|
||||
reloadActivePackageStyleSheets: ->
|
||||
for pack in @getActivePackages() when pack.getType() isnt 'theme'
|
||||
pack.reloadStylesheets?()
|
||||
return
|
||||
|
||||
isBundledPackagePath: (packagePath) ->
|
||||
if @devMode
|
||||
return false unless @resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
|
||||
|
||||
@resourcePathWithTrailingSlash ?= "#{@resourcePath}#{path.sep}"
|
||||
packagePath?.startsWith(@resourcePathWithTrailingSlash)
|
||||
|
||||
loadPackageMetadata: (packagePathOrAvailablePackage, ignoreErrors=false) ->
|
||||
if typeof packagePathOrAvailablePackage is 'object'
|
||||
availablePackage = packagePathOrAvailablePackage
|
||||
packageName = availablePackage.name
|
||||
packagePath = availablePackage.path
|
||||
isBundled = availablePackage.isBundled
|
||||
else
|
||||
packagePath = packagePathOrAvailablePackage
|
||||
packageName = path.basename(packagePath)
|
||||
isBundled = @isBundledPackagePath(packagePath)
|
||||
|
||||
if isBundled
|
||||
metadata = @packagesCache[packageName]?.metadata
|
||||
|
||||
unless metadata?
|
||||
if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
|
||||
try
|
||||
metadata = CSON.readFileSync(metadataPath)
|
||||
@normalizePackageMetadata(metadata)
|
||||
catch error
|
||||
throw error unless ignoreErrors
|
||||
|
||||
metadata ?= {}
|
||||
unless typeof metadata.name is 'string' and metadata.name.length > 0
|
||||
metadata.name = packageName
|
||||
|
||||
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
|
||||
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
|
||||
|
||||
metadata
|
||||
|
||||
normalizePackageMetadata: (metadata) ->
|
||||
unless metadata?._id
|
||||
normalizePackageData ?= require 'normalize-package-data'
|
||||
normalizePackageData(metadata)
|
||||
858
src/package-manager.js
Normal file
858
src/package-manager.js
Normal file
@@ -0,0 +1,858 @@
|
||||
const path = require('path')
|
||||
let normalizePackageData = null
|
||||
|
||||
const _ = require('underscore-plus')
|
||||
const {Emitter} = require('event-kit')
|
||||
const fs = require('fs-plus')
|
||||
const CSON = require('season')
|
||||
|
||||
const ServiceHub = require('service-hub')
|
||||
const Package = require('./package')
|
||||
const ThemePackage = require('./theme-package')
|
||||
const {isDeprecatedPackage, getDeprecatedPackageMetadata} = require('./deprecated-packages')
|
||||
const packageJSON = require('../package.json')
|
||||
|
||||
// Extended: Package manager for coordinating the lifecycle of Atom packages.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.packages` global.
|
||||
//
|
||||
// Packages can be loaded, activated, and deactivated, and unloaded:
|
||||
// * Loading a package reads and parses the package's metadata and resources
|
||||
// such as keymaps, menus, stylesheets, etc.
|
||||
// * Activating a package registers the loaded resources and calls `activate()`
|
||||
// on the package's main module.
|
||||
// * Deactivating a package unregisters the package's resources and calls
|
||||
// `deactivate()` on the package's main module.
|
||||
// * Unloading a package removes it completely from the package manager.
|
||||
//
|
||||
// Packages can be enabled/disabled via the `core.disabledPackages` config
|
||||
// settings and also by calling `enablePackage()/disablePackage()`.
|
||||
module.exports = class PackageManager {
|
||||
constructor (params) {
|
||||
({
|
||||
config: this.config, styleManager: this.styleManager, notificationManager: this.notificationManager, keymapManager: this.keymapManager,
|
||||
commandRegistry: this.commandRegistry, grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry
|
||||
} = params)
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.activationHookEmitter = new Emitter()
|
||||
this.packageDirPaths = []
|
||||
this.deferredActivationHooks = []
|
||||
this.triggeredActivationHooks = new Set()
|
||||
this.packagesCache = packageJSON._atomPackages != null ? packageJSON._atomPackages : {}
|
||||
this.packageDependencies = packageJSON.packageDependencies != null ? packageJSON.packageDependencies : {}
|
||||
this.initialPackagesLoaded = false
|
||||
this.initialPackagesActivated = false
|
||||
this.preloadedPackages = {}
|
||||
this.loadedPackages = {}
|
||||
this.activePackages = {}
|
||||
this.activatingPackages = {}
|
||||
this.packageStates = {}
|
||||
this.serviceHub = new ServiceHub()
|
||||
|
||||
this.packageActivators = []
|
||||
this.registerPackageActivator(this, ['atom', 'textmate'])
|
||||
}
|
||||
|
||||
initialize (params) {
|
||||
this.devMode = params.devMode
|
||||
this.resourcePath = params.resourcePath
|
||||
if (params.configDirPath != null && !params.safeMode) {
|
||||
if (this.devMode) {
|
||||
this.packageDirPaths.push(path.join(params.configDirPath, 'dev', 'packages'))
|
||||
}
|
||||
this.packageDirPaths.push(path.join(params.configDirPath, 'packages'))
|
||||
}
|
||||
}
|
||||
|
||||
setContextMenuManager (contextMenuManager) {
|
||||
this.contextMenuManager = contextMenuManager
|
||||
}
|
||||
|
||||
setMenuManager (menuManager) {
|
||||
this.menuManager = menuManager
|
||||
}
|
||||
|
||||
setThemeManager (themeManager) {
|
||||
this.themeManager = themeManager
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.serviceHub.clear()
|
||||
this.deactivatePackages()
|
||||
this.loadedPackages = {}
|
||||
this.preloadedPackages = {}
|
||||
this.packageStates = {}
|
||||
this.packagesCache = packageJSON._atomPackages != null ? packageJSON._atomPackages : {}
|
||||
this.packageDependencies = packageJSON.packageDependencies != null ? packageJSON.packageDependencies : {}
|
||||
this.triggeredActivationHooks.clear()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Public: Invoke the given callback when all packages have been loaded.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidLoadInitialPackages (callback) {
|
||||
return this.emitter.on('did-load-initial-packages', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when all packages have been activated.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidActivateInitialPackages (callback) {
|
||||
return this.emitter.on('did-activate-initial-packages', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when a package is activated.
|
||||
//
|
||||
// * `callback` A {Function} to be invoked when a package is activated.
|
||||
// * `package` The {Package} that was activated.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidActivatePackage (callback) {
|
||||
return this.emitter.on('did-activate-package', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when a package is deactivated.
|
||||
//
|
||||
// * `callback` A {Function} to be invoked when a package is deactivated.
|
||||
// * `package` The {Package} that was deactivated.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDeactivatePackage (callback) {
|
||||
return this.emitter.on('did-deactivate-package', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when a package is loaded.
|
||||
//
|
||||
// * `callback` A {Function} to be invoked when a package is loaded.
|
||||
// * `package` The {Package} that was loaded.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidLoadPackage (callback) {
|
||||
return this.emitter.on('did-load-package', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when a package is unloaded.
|
||||
//
|
||||
// * `callback` A {Function} to be invoked when a package is unloaded.
|
||||
// * `package` The {Package} that was unloaded.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidUnloadPackage (callback) {
|
||||
return this.emitter.on('did-unload-package', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Package system data
|
||||
*/
|
||||
|
||||
// Public: Get the path to the apm command.
|
||||
//
|
||||
// Uses the value of the `core.apmPath` config setting if it exists.
|
||||
//
|
||||
// Return a {String} file path to apm.
|
||||
getApmPath () {
|
||||
const configPath = atom.config.get('core.apmPath')
|
||||
if (configPath || this.apmPath) {
|
||||
return configPath || this.apmPath
|
||||
}
|
||||
|
||||
const commandName = process.platform === 'win32' ? 'apm.cmd' : 'apm'
|
||||
const apmRoot = path.join(process.resourcesPath, 'app', 'apm')
|
||||
this.apmPath = path.join(apmRoot, 'bin', commandName)
|
||||
if (!fs.isFileSync(this.apmPath)) {
|
||||
this.apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName)
|
||||
}
|
||||
return this.apmPath
|
||||
}
|
||||
|
||||
// Public: Get the paths being used to look for packages.
|
||||
//
|
||||
// Returns an {Array} of {String} directory paths.
|
||||
getPackageDirPaths () {
|
||||
return _.clone(this.packageDirPaths)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: General package data
|
||||
*/
|
||||
|
||||
// Public: Resolve the given package name to a path on disk.
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Return a {String} folder path or undefined if it could not be resolved.
|
||||
resolvePackagePath (name) {
|
||||
if (fs.isDirectorySync(name)) {
|
||||
return name
|
||||
}
|
||||
|
||||
let packagePath = fs.resolve(...this.packageDirPaths, name)
|
||||
if (fs.isDirectorySync(packagePath)) {
|
||||
return packagePath
|
||||
}
|
||||
|
||||
packagePath = path.join(this.resourcePath, 'node_modules', name)
|
||||
if (this.hasAtomEngine(packagePath)) {
|
||||
return packagePath
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Public: Is the package with the given name bundled with Atom?
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isBundledPackage (name) {
|
||||
return this.getPackageDependencies().hasOwnProperty(name)
|
||||
}
|
||||
|
||||
isDeprecatedPackage (name, version) {
|
||||
return isDeprecatedPackage(name, version)
|
||||
}
|
||||
|
||||
getDeprecatedPackageMetadata (name) {
|
||||
return getDeprecatedPackageMetadata(name)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Enabling and disabling packages
|
||||
*/
|
||||
|
||||
// Public: Enable the package with the given name.
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns the {Package} that was enabled or null if it isn't loaded.
|
||||
enablePackage (name) {
|
||||
const pack = this.loadPackage(name)
|
||||
if (pack != null) {
|
||||
pack.enable()
|
||||
}
|
||||
return pack
|
||||
}
|
||||
|
||||
// Public: Disable the package with the given name.
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns the {Package} that was disabled or null if it isn't loaded.
|
||||
disablePackage (name) {
|
||||
const pack = this.loadPackage(name)
|
||||
if (!this.isPackageDisabled(name) && pack != null) {
|
||||
pack.disable()
|
||||
}
|
||||
return pack
|
||||
}
|
||||
|
||||
// Public: Is the package with the given name disabled?
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isPackageDisabled (name) {
|
||||
return _.include(this.config.get('core.disabledPackages') || [], name)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Accessing active packages
|
||||
*/
|
||||
|
||||
// Public: Get an {Array} of all the active {Package}s.
|
||||
getActivePackages () {
|
||||
return _.values(this.activePackages)
|
||||
}
|
||||
|
||||
// Public: Get the active {Package} with the given name.
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns a {Package} or undefined.
|
||||
getActivePackage (name) {
|
||||
return this.activePackages[name]
|
||||
}
|
||||
|
||||
// Public: Is the {Package} with the given name active?
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isPackageActive (name) {
|
||||
return (this.getActivePackage(name) != null)
|
||||
}
|
||||
|
||||
// Public: Returns a {Boolean} indicating whether package activation has occurred.
|
||||
hasActivatedInitialPackages () {
|
||||
return this.initialPackagesActivated
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Accessing loaded packages
|
||||
*/
|
||||
|
||||
// Public: Get an {Array} of all the loaded {Package}s
|
||||
getLoadedPackages () {
|
||||
return _.values(this.loadedPackages)
|
||||
}
|
||||
|
||||
// Get packages for a certain package type
|
||||
//
|
||||
// * `types` an {Array} of {String}s like ['atom', 'textmate'].
|
||||
getLoadedPackagesForTypes (types) {
|
||||
return this.getLoadedPackages().filter(p => types.includes(p.getType()))
|
||||
}
|
||||
|
||||
// Public: Get the loaded {Package} with the given name.
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns a {Package} or undefined.
|
||||
getLoadedPackage (name) {
|
||||
return this.loadedPackages[name]
|
||||
}
|
||||
|
||||
// Public: Is the package with the given name loaded?
|
||||
//
|
||||
// * `name` - The {String} package name.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isPackageLoaded (name) {
|
||||
return this.getLoadedPackage(name) != null
|
||||
}
|
||||
|
||||
// Public: Returns a {Boolean} indicating whether package loading has occurred.
|
||||
hasLoadedInitialPackages () {
|
||||
return this.initialPackagesLoaded
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Accessing available packages
|
||||
*/
|
||||
|
||||
// Public: Returns an {Array} of {String}s of all the available package paths.
|
||||
getAvailablePackagePaths () {
|
||||
return this.getAvailablePackages().map(a => a.path)
|
||||
}
|
||||
|
||||
// Public: Returns an {Array} of {String}s of all the available package names.
|
||||
getAvailablePackageNames () {
|
||||
return this.getAvailablePackages().map(a => a.name)
|
||||
}
|
||||
|
||||
// Public: Returns an {Array} of {String}s of all the available package metadata.
|
||||
getAvailablePackageMetadata () {
|
||||
const packages = []
|
||||
for (const pack of this.getAvailablePackages()) {
|
||||
const loadedPackage = this.getLoadedPackage(pack.name)
|
||||
const metadata = loadedPackage != null ? loadedPackage.metadata : this.loadPackageMetadata(pack, true)
|
||||
packages.push(metadata)
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
getAvailablePackages () {
|
||||
const packages = []
|
||||
const packagesByName = new Set()
|
||||
|
||||
for (const packageDirPath of this.packageDirPaths) {
|
||||
if (fs.isDirectorySync(packageDirPath)) {
|
||||
for (let packagePath of fs.readdirSync(packageDirPath)) {
|
||||
packagePath = path.join(packageDirPath, packagePath)
|
||||
const packageName = path.basename(packagePath)
|
||||
if (!packageName.startsWith('.') && !packagesByName.has(packageName) && fs.isDirectorySync(packagePath)) {
|
||||
packages.push({
|
||||
name: packageName,
|
||||
path: packagePath,
|
||||
isBundled: false
|
||||
})
|
||||
packagesByName.add(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const packageName in this.packageDependencies) {
|
||||
if (!packagesByName.has(packageName)) {
|
||||
packages.push({
|
||||
name: packageName,
|
||||
path: path.join(this.resourcePath, 'node_modules', packageName),
|
||||
isBundled: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return packages.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private
|
||||
*/
|
||||
|
||||
getPackageState (name) {
|
||||
return this.packageStates[name]
|
||||
}
|
||||
|
||||
setPackageState (name, state) {
|
||||
this.packageStates[name] = state
|
||||
}
|
||||
|
||||
getPackageDependencies () {
|
||||
return this.packageDependencies
|
||||
}
|
||||
|
||||
hasAtomEngine (packagePath) {
|
||||
const metadata = this.loadPackageMetadata(packagePath, true)
|
||||
return metadata != null && metadata.engines != null && metadata.engines.atom != null
|
||||
}
|
||||
|
||||
unobserveDisabledPackages () {
|
||||
if (this.disabledPackagesSubscription != null) {
|
||||
this.disabledPackagesSubscription.dispose()
|
||||
}
|
||||
this.disabledPackagesSubscription = null
|
||||
}
|
||||
|
||||
observeDisabledPackages () {
|
||||
if (this.disabledPackagesSubscription != null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.disabledPackagesSubscription = this.config.onDidChange('core.disabledPackages', ({newValue, oldValue}) => {
|
||||
const packagesToEnable = _.difference(oldValue, newValue)
|
||||
const packagesToDisable = _.difference(newValue, oldValue)
|
||||
packagesToDisable.forEach(name => { if (this.getActivePackage(name)) this.deactivatePackage(name) })
|
||||
packagesToEnable.forEach(name => this.activatePackage(name))
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
unobservePackagesWithKeymapsDisabled () {
|
||||
if (this.packagesWithKeymapsDisabledSubscription != null) {
|
||||
this.packagesWithKeymapsDisabledSubscription.dispose()
|
||||
}
|
||||
this.packagesWithKeymapsDisabledSubscription = null
|
||||
}
|
||||
|
||||
observePackagesWithKeymapsDisabled () {
|
||||
if (this.packagesWithKeymapsDisabledSubscription != null) {
|
||||
return
|
||||
}
|
||||
|
||||
const performOnLoadedActivePackages = (packageNames, disabledPackageNames, action) => {
|
||||
for (const packageName of packageNames) {
|
||||
if (!disabledPackageNames.has(packageName)) {
|
||||
var pack = this.getLoadedPackage(packageName)
|
||||
if (pack != null) {
|
||||
action(pack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.packagesWithKeymapsDisabledSubscription = this.config.onDidChange('core.packagesWithKeymapsDisabled', ({newValue, oldValue}) => {
|
||||
const keymapsToEnable = _.difference(oldValue, newValue)
|
||||
const keymapsToDisable = _.difference(newValue, oldValue)
|
||||
|
||||
const disabledPackageNames = new Set(this.config.get('core.disabledPackages'))
|
||||
performOnLoadedActivePackages(keymapsToDisable, disabledPackageNames, p => p.deactivateKeymaps())
|
||||
performOnLoadedActivePackages(keymapsToEnable, disabledPackageNames, p => p.activateKeymaps())
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
preloadPackages () {
|
||||
const result = []
|
||||
for (const packageName in this.packagesCache) {
|
||||
result.push(this.preloadPackage(packageName, this.packagesCache[packageName]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
preloadPackage (packageName, pack) {
|
||||
const metadata = pack.metadata || {}
|
||||
if (typeof metadata.name !== 'string' || metadata.name.length < 1) {
|
||||
metadata.name = packageName
|
||||
}
|
||||
|
||||
if (metadata.repository != null && metadata.repository.type === 'git' && typeof metadata.repository.url === 'string') {
|
||||
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
|
||||
}
|
||||
|
||||
const options = {
|
||||
path: pack.rootDirPath,
|
||||
name: packageName,
|
||||
preloadedPackage: true,
|
||||
bundledPackage: true,
|
||||
metadata,
|
||||
packageManager: this,
|
||||
config: this.config,
|
||||
styleManager: this.styleManager,
|
||||
commandRegistry: this.commandRegistry,
|
||||
keymapManager: this.keymapManager,
|
||||
notificationManager: this.notificationManager,
|
||||
grammarRegistry: this.grammarRegistry,
|
||||
themeManager: this.themeManager,
|
||||
menuManager: this.menuManager,
|
||||
contextMenuManager: this.contextMenuManager,
|
||||
deserializerManager: this.deserializerManager,
|
||||
viewRegistry: this.viewRegistry
|
||||
}
|
||||
|
||||
pack = metadata.theme ? new ThemePackage(options) : new Package(options)
|
||||
pack.preload()
|
||||
this.preloadedPackages[packageName] = pack
|
||||
return pack
|
||||
}
|
||||
|
||||
loadPackages () {
|
||||
// Ensure atom exports is already in the require cache so the load time
|
||||
// of the first package isn't skewed by being the first to require atom
|
||||
require('../exports/atom')
|
||||
|
||||
const disabledPackageNames = new Set(this.config.get('core.disabledPackages'))
|
||||
this.config.transact(() => {
|
||||
for (const pack of this.getAvailablePackages()) {
|
||||
this.loadAvailablePackage(pack, disabledPackageNames)
|
||||
}
|
||||
})
|
||||
this.initialPackagesLoaded = true
|
||||
this.emitter.emit('did-load-initial-packages')
|
||||
}
|
||||
|
||||
loadPackage (nameOrPath) {
|
||||
if (path.basename(nameOrPath)[0].match(/^\./)) { // primarily to skip .git folder
|
||||
return null
|
||||
}
|
||||
|
||||
const pack = this.getLoadedPackage(nameOrPath)
|
||||
if (pack) {
|
||||
return pack
|
||||
}
|
||||
|
||||
const packagePath = this.resolvePackagePath(nameOrPath)
|
||||
if (packagePath) {
|
||||
const name = path.basename(nameOrPath)
|
||||
return this.loadAvailablePackage({name, path: packagePath, isBundled: this.isBundledPackagePath(packagePath)})
|
||||
}
|
||||
|
||||
console.warn(`Could not resolve '${nameOrPath}' to a package path`)
|
||||
return null
|
||||
}
|
||||
|
||||
loadAvailablePackage (availablePackage, disabledPackageNames) {
|
||||
const preloadedPackage = this.preloadedPackages[availablePackage.name]
|
||||
|
||||
if (disabledPackageNames != null && disabledPackageNames.has(availablePackage.name)) {
|
||||
if (preloadedPackage != null) {
|
||||
preloadedPackage.deactivate()
|
||||
delete preloadedPackage[availablePackage.name]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const loadedPackage = this.getLoadedPackage(availablePackage.name)
|
||||
if (loadedPackage != null) {
|
||||
return loadedPackage
|
||||
}
|
||||
|
||||
if (preloadedPackage != null) {
|
||||
if (availablePackage.isBundled) {
|
||||
preloadedPackage.finishLoading()
|
||||
this.loadedPackages[availablePackage.name] = preloadedPackage
|
||||
return preloadedPackage
|
||||
} else {
|
||||
preloadedPackage.deactivate()
|
||||
delete preloadedPackage[availablePackage.name]
|
||||
}
|
||||
}
|
||||
|
||||
let metadata
|
||||
try {
|
||||
metadata = this.loadPackageMetadata(availablePackage) || {}
|
||||
} catch (error) {
|
||||
this.handleMetadataError(error, availablePackage.path)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!availablePackage.isBundled && this.isDeprecatedPackage(metadata.name, metadata.version)) {
|
||||
console.warn(`Could not load ${metadata.name}@${metadata.version} because it uses deprecated APIs that have been removed.`)
|
||||
return null
|
||||
}
|
||||
|
||||
const options = {
|
||||
path: availablePackage.path,
|
||||
name: availablePackage.name,
|
||||
metadata,
|
||||
bundledPackage: availablePackage.isBundled,
|
||||
packageManager: this,
|
||||
config: this.config,
|
||||
styleManager: this.styleManager,
|
||||
commandRegistry: this.commandRegistry,
|
||||
keymapManager: this.keymapManager,
|
||||
notificationManager: this.notificationManager,
|
||||
grammarRegistry: this.grammarRegistry,
|
||||
themeManager: this.themeManager,
|
||||
menuManager: this.menuManager,
|
||||
contextMenuManager: this.contextMenuManager,
|
||||
deserializerManager: this.deserializerManager,
|
||||
viewRegistry: this.viewRegistry
|
||||
}
|
||||
|
||||
const pack = metadata.theme ? new ThemePackage(options) : new Package(options)
|
||||
pack.load()
|
||||
this.loadedPackages[pack.name] = pack
|
||||
this.emitter.emit('did-load-package', pack)
|
||||
return pack
|
||||
}
|
||||
|
||||
unloadPackages () {
|
||||
_.keys(this.loadedPackages).forEach(name => this.unloadPackage(name))
|
||||
}
|
||||
|
||||
unloadPackage (name) {
|
||||
if (this.isPackageActive(name)) {
|
||||
throw new Error(`Tried to unload active package '${name}'`)
|
||||
}
|
||||
|
||||
const pack = this.getLoadedPackage(name)
|
||||
if (pack) {
|
||||
delete this.loadedPackages[pack.name]
|
||||
this.emitter.emit('did-unload-package', pack)
|
||||
} else {
|
||||
throw new Error(`No loaded package for name '${name}'`)
|
||||
}
|
||||
}
|
||||
|
||||
// Activate all the packages that should be activated.
|
||||
activate () {
|
||||
let promises = []
|
||||
for (let [activator, types] of this.packageActivators) {
|
||||
const packages = this.getLoadedPackagesForTypes(types)
|
||||
promises = promises.concat(activator.activatePackages(packages))
|
||||
}
|
||||
return Promise.all(promises).then(() => {
|
||||
this.triggerDeferredActivationHooks()
|
||||
this.initialPackagesActivated = true
|
||||
this.emitter.emit('did-activate-initial-packages')
|
||||
})
|
||||
}
|
||||
|
||||
// another type of package manager can handle other package types.
|
||||
// See ThemeManager
|
||||
registerPackageActivator (activator, types) {
|
||||
this.packageActivators.push([activator, types])
|
||||
}
|
||||
|
||||
activatePackages (packages) {
|
||||
const promises = []
|
||||
this.config.transactAsync(() => {
|
||||
for (const pack of packages) {
|
||||
const promise = this.activatePackage(pack.name)
|
||||
if (!pack.activationShouldBeDeferred()) {
|
||||
promises.push(promise)
|
||||
}
|
||||
}
|
||||
return Promise.all(promises)
|
||||
})
|
||||
this.observeDisabledPackages()
|
||||
this.observePackagesWithKeymapsDisabled()
|
||||
return promises
|
||||
}
|
||||
|
||||
// Activate a single package by name
|
||||
activatePackage (name) {
|
||||
let pack = this.getActivePackage(name)
|
||||
if (pack) {
|
||||
return Promise.resolve(pack)
|
||||
}
|
||||
|
||||
pack = this.loadPackage(name)
|
||||
if (!pack) {
|
||||
return Promise.reject(new Error(`Failed to load package '${name}'`))
|
||||
}
|
||||
|
||||
this.activatingPackages[pack.name] = pack
|
||||
const activationPromise = pack.activate().then(() => {
|
||||
if (this.activatingPackages[pack.name] != null) {
|
||||
delete this.activatingPackages[pack.name]
|
||||
this.activePackages[pack.name] = pack
|
||||
this.emitter.emit('did-activate-package', pack)
|
||||
}
|
||||
return pack
|
||||
})
|
||||
|
||||
if (this.deferredActivationHooks == null) {
|
||||
this.triggeredActivationHooks.forEach(hook => this.activationHookEmitter.emit(hook))
|
||||
}
|
||||
|
||||
return activationPromise
|
||||
}
|
||||
|
||||
triggerDeferredActivationHooks () {
|
||||
if (this.deferredActivationHooks == null) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const hook of this.deferredActivationHooks) {
|
||||
this.activationHookEmitter.emit(hook)
|
||||
}
|
||||
|
||||
this.deferredActivationHooks = null
|
||||
}
|
||||
|
||||
triggerActivationHook (hook) {
|
||||
if (hook == null || !_.isString(hook) || hook.length <= 0) {
|
||||
return new Error('Cannot trigger an empty activation hook')
|
||||
}
|
||||
|
||||
this.triggeredActivationHooks.add(hook)
|
||||
if (this.deferredActivationHooks != null) {
|
||||
this.deferredActivationHooks.push(hook)
|
||||
} else {
|
||||
this.activationHookEmitter.emit(hook)
|
||||
}
|
||||
}
|
||||
|
||||
onDidTriggerActivationHook (hook, callback) {
|
||||
if (hook == null || !_.isString(hook) || hook.length <= 0) {
|
||||
return
|
||||
}
|
||||
return this.activationHookEmitter.on(hook, callback)
|
||||
}
|
||||
|
||||
serialize () {
|
||||
for (const pack of this.getActivePackages()) {
|
||||
this.serializePackage(pack)
|
||||
}
|
||||
return this.packageStates
|
||||
}
|
||||
|
||||
serializePackage (pack) {
|
||||
if (typeof pack.serialize === 'function') {
|
||||
this.setPackageState(pack.name, pack.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivate all packages
|
||||
deactivatePackages () {
|
||||
this.config.transact(() => {
|
||||
this.getLoadedPackages().forEach(pack => this.deactivatePackage(pack.name, true))
|
||||
})
|
||||
this.unobserveDisabledPackages()
|
||||
this.unobservePackagesWithKeymapsDisabled()
|
||||
}
|
||||
|
||||
// Deactivate the package with the given name
|
||||
deactivatePackage (name, suppressSerialization) {
|
||||
const pack = this.getLoadedPackage(name)
|
||||
if (!suppressSerialization && this.isPackageActive(pack.name)) {
|
||||
this.serializePackage(pack)
|
||||
}
|
||||
pack.deactivate()
|
||||
delete this.activePackages[pack.name]
|
||||
delete this.activatingPackages[pack.name]
|
||||
this.emitter.emit('did-deactivate-package', pack)
|
||||
}
|
||||
|
||||
handleMetadataError (error, packagePath) {
|
||||
const metadataPath = path.join(packagePath, 'package.json')
|
||||
const detail = `${error.message} in ${metadataPath}`
|
||||
const stack = `${error.stack}\n at ${metadataPath}:1:1`
|
||||
const message = `Failed to load the ${path.basename(packagePath)} package`
|
||||
this.notificationManager.addError(message, {stack, detail, packageName: path.basename(packagePath), dismissable: true})
|
||||
}
|
||||
|
||||
uninstallDirectory (directory) {
|
||||
const symlinkPromise = new Promise(resolve => fs.isSymbolicLink(directory, isSymLink => resolve(isSymLink)))
|
||||
const dirPromise = new Promise(resolve => fs.isDirectory(directory, isDir => resolve(isDir)))
|
||||
|
||||
return Promise.all([symlinkPromise, dirPromise]).then(values => {
|
||||
const [isSymLink, isDir] = values
|
||||
if (!isSymLink && isDir) {
|
||||
return fs.remove(directory, function () {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reloadActivePackageStyleSheets () {
|
||||
for (const pack of this.getActivePackages()) {
|
||||
if (pack.getType() !== 'theme' && typeof pack.reloadStylesheets === 'function') {
|
||||
pack.reloadStylesheets()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isBundledPackagePath (packagePath) {
|
||||
if (this.devMode && !this.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.resourcePathWithTrailingSlash == null) {
|
||||
this.resourcePathWithTrailingSlash = `${this.resourcePath}${path.sep}`
|
||||
}
|
||||
|
||||
return packagePath != null && packagePath.startsWith(this.resourcePathWithTrailingSlash)
|
||||
}
|
||||
|
||||
loadPackageMetadata (packagePathOrAvailablePackage, ignoreErrors = false) {
|
||||
let isBundled, packageName, packagePath
|
||||
if (typeof packagePathOrAvailablePackage === 'object') {
|
||||
const availablePackage = packagePathOrAvailablePackage
|
||||
packageName = availablePackage.name
|
||||
packagePath = availablePackage.path
|
||||
isBundled = availablePackage.isBundled
|
||||
} else {
|
||||
packagePath = packagePathOrAvailablePackage
|
||||
packageName = path.basename(packagePath)
|
||||
isBundled = this.isBundledPackagePath(packagePath)
|
||||
}
|
||||
|
||||
let metadata
|
||||
if (isBundled && this.packagesCache[packageName] != null) {
|
||||
metadata = this.packagesCache[packageName].metadata
|
||||
}
|
||||
|
||||
if (metadata == null) {
|
||||
const metadataPath = CSON.resolve(path.join(packagePath, 'package'))
|
||||
if (metadataPath) {
|
||||
try {
|
||||
metadata = CSON.readFileSync(metadataPath)
|
||||
this.normalizePackageMetadata(metadata)
|
||||
} catch (error) {
|
||||
if (!ignoreErrors) { throw error }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata == null) {
|
||||
metadata = {}
|
||||
}
|
||||
|
||||
if (typeof metadata.name !== 'string' || metadata.name.length <= 0) {
|
||||
metadata.name = packageName
|
||||
}
|
||||
|
||||
if (metadata.repository && metadata.repository.type === 'git' && typeof metadata.repository.url === 'string') {
|
||||
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
normalizePackageMetadata (metadata) {
|
||||
if (metadata != null) {
|
||||
normalizePackageData = normalizePackageData || require('normalize-package-data')
|
||||
normalizePackageData(metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -888,7 +888,7 @@ class Pane
|
||||
when 'before' then @parent.insertChildBefore(this, newPane)
|
||||
when 'after' then @parent.insertChildAfter(this, newPane)
|
||||
|
||||
@moveItemToPane(@activeItem, newPane) if params?.moveActiveItem
|
||||
@moveItemToPane(@activeItem, newPane) if params?.moveActiveItem and @activeItem
|
||||
|
||||
newPane.activate()
|
||||
newPane
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const focusTrap = require('focus-trap')
|
||||
const {CompositeDisposable} = require('event-kit')
|
||||
|
||||
class PanelContainerElement extends HTMLElement {
|
||||
@@ -52,6 +53,26 @@ class PanelContainerElement extends HTMLElement {
|
||||
this.subscriptions.add(panel.onDidChangeVisible(visible => {
|
||||
if (visible) { this.hideAllPanelsExcept(panel) }
|
||||
}))
|
||||
|
||||
if (panel.autoFocus) {
|
||||
const modalFocusTrap = focusTrap(panelElement, {
|
||||
// focus-trap will attempt to give focus to the first tabbable element
|
||||
// on activation. If there aren't any tabbable elements,
|
||||
// give focus to the panel element itself
|
||||
fallbackFocus: panelElement,
|
||||
// closing is handled by core Atom commands and this already deactivates
|
||||
// on visibility changes
|
||||
escapeDeactivates: false
|
||||
})
|
||||
|
||||
this.subscriptions.add(panel.onDidChangeVisible(visible => {
|
||||
if (visible) {
|
||||
modalFocusTrap.activate()
|
||||
} else {
|
||||
modalFocusTrap.deactivate()
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,16 +13,15 @@ class Panel {
|
||||
Section: Construction and Destruction
|
||||
*/
|
||||
|
||||
constructor ({item, visible, priority, className}, viewRegistry) {
|
||||
constructor ({item, autoFocus, visible, priority, className}, viewRegistry) {
|
||||
this.destroyed = false
|
||||
this.item = item
|
||||
this.visible = visible
|
||||
this.priority = priority
|
||||
this.autoFocus = autoFocus == null ? false : autoFocus
|
||||
this.visible = visible == null ? true : visible
|
||||
this.priority = priority == null ? 100 : priority
|
||||
this.className = className
|
||||
this.viewRegistry = viewRegistry
|
||||
this.emitter = new Emitter()
|
||||
if (this.visible == null) this.visible = true
|
||||
if (this.priority == null) this.priority = 100
|
||||
}
|
||||
|
||||
// Public: Destroy and remove this panel from the UI.
|
||||
|
||||
@@ -124,8 +124,7 @@ class TextEditorComponent {
|
||||
this.blockDecorationSentinel.style.height = '1px'
|
||||
this.heightsByBlockDecoration = new WeakMap()
|
||||
this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this))
|
||||
this.lineNodesByScreenLineId = new Map()
|
||||
this.textNodesByScreenLineId = new Map()
|
||||
this.lineComponentsByScreenLineId = new Map()
|
||||
this.overlayComponents = new Set()
|
||||
this.overlayDimensionsByElement = new WeakMap()
|
||||
this.shouldRenderDummyScrollbars = true
|
||||
@@ -157,7 +156,7 @@ class TextEditorComponent {
|
||||
this.decorationsToRender = {
|
||||
lineNumbers: null,
|
||||
lines: null,
|
||||
highlights: new Map(),
|
||||
highlights: [],
|
||||
cursors: [],
|
||||
overlays: [],
|
||||
customGutter: new Map(),
|
||||
@@ -165,7 +164,7 @@ class TextEditorComponent {
|
||||
text: []
|
||||
}
|
||||
this.decorationsToMeasure = {
|
||||
highlights: new Map(),
|
||||
highlights: [],
|
||||
cursors: new Map()
|
||||
}
|
||||
this.textDecorationsByMarker = new Map()
|
||||
@@ -287,7 +286,8 @@ class TextEditorComponent {
|
||||
const decorations = this.props.model.getDecorations()
|
||||
for (var i = 0; i < decorations.length; i++) {
|
||||
const decoration = decorations[i]
|
||||
if (decoration.getProperties().type === 'block') {
|
||||
const marker = decoration.getMarker()
|
||||
if (marker.isValid() && decoration.getProperties().type === 'block') {
|
||||
this.blockDecorationsToMeasure.add(decoration)
|
||||
}
|
||||
}
|
||||
@@ -382,7 +382,10 @@ class TextEditorComponent {
|
||||
this.measureGutterDimensions()
|
||||
this.remeasureGutterDimensions = false
|
||||
}
|
||||
const wasHorizontalScrollbarVisible = this.isHorizontalScrollbarVisible()
|
||||
const wasHorizontalScrollbarVisible = (
|
||||
this.canScrollHorizontally() &&
|
||||
this.getHorizontalScrollbarHeight() > 0
|
||||
)
|
||||
|
||||
this.measureLongestLineWidth()
|
||||
this.measureHorizontalPositions()
|
||||
@@ -392,7 +395,12 @@ class TextEditorComponent {
|
||||
this.derivedDimensionsCache = {}
|
||||
const {screenRange, options} = this.pendingAutoscroll
|
||||
this.autoscrollHorizontally(screenRange, options)
|
||||
if (!wasHorizontalScrollbarVisible && this.isHorizontalScrollbarVisible()) {
|
||||
|
||||
const isHorizontalScrollbarVisible = (
|
||||
this.canScrollHorizontally() &&
|
||||
this.getHorizontalScrollbarHeight() > 0
|
||||
)
|
||||
if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) {
|
||||
this.autoscrollVertically(screenRange, options)
|
||||
}
|
||||
this.pendingAutoscroll = null
|
||||
@@ -440,13 +448,13 @@ class TextEditorComponent {
|
||||
if (this.hasInitialMeasurements) {
|
||||
if (model.getAutoHeight()) {
|
||||
clientContainerHeight = this.getContentHeight()
|
||||
if (this.isHorizontalScrollbarVisible()) clientContainerHeight += this.getHorizontalScrollbarHeight()
|
||||
if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight()
|
||||
clientContainerHeight += 'px'
|
||||
}
|
||||
if (model.getAutoWidth()) {
|
||||
style.width = 'min-content'
|
||||
clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth()
|
||||
if (this.isVerticalScrollbarVisible()) clientContainerWidth += this.getVerticalScrollbarWidth()
|
||||
if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth()
|
||||
clientContainerWidth += 'px'
|
||||
} else {
|
||||
style.width = this.element.style.width
|
||||
@@ -547,7 +555,6 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
let children
|
||||
let style = {
|
||||
contain: 'strict',
|
||||
overflow: 'hidden',
|
||||
@@ -558,17 +565,6 @@ class TextEditorComponent {
|
||||
style.height = ceilToPhysicalPixelBoundary(this.getScrollHeight()) + 'px'
|
||||
style.willChange = 'transform'
|
||||
style.transform = `translate(${-roundToPhysicalPixelBoundary(this.getScrollLeft())}px, ${-roundToPhysicalPixelBoundary(this.getScrollTop())}px)`
|
||||
children = [
|
||||
this.renderLineTiles(),
|
||||
this.renderBlockDecorationMeasurementArea(),
|
||||
this.renderCharacterMeasurementLine()
|
||||
]
|
||||
} else {
|
||||
children = [
|
||||
this.renderLineTiles(),
|
||||
this.renderBlockDecorationMeasurementArea(),
|
||||
this.renderCharacterMeasurementLine()
|
||||
]
|
||||
}
|
||||
|
||||
return $.div(
|
||||
@@ -577,10 +573,23 @@ class TextEditorComponent {
|
||||
on: {mousedown: this.didMouseDownOnContent},
|
||||
style
|
||||
},
|
||||
children
|
||||
this.renderHighlightDecorations(),
|
||||
this.renderLineTiles(),
|
||||
this.renderBlockDecorationMeasurementArea(),
|
||||
this.renderCharacterMeasurementLine()
|
||||
)
|
||||
}
|
||||
|
||||
renderHighlightDecorations () {
|
||||
return $(HighlightsComponent, {
|
||||
hasInitialMeasurements: this.hasInitialMeasurements,
|
||||
highlightDecorations: this.decorationsToRender.highlights.slice(),
|
||||
width: this.getScrollWidth(),
|
||||
height: this.getScrollHeight(),
|
||||
lineHeight: this.getLineHeight()
|
||||
})
|
||||
}
|
||||
|
||||
renderLineTiles () {
|
||||
const children = []
|
||||
const style = {
|
||||
@@ -590,7 +599,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
if (this.hasInitialMeasurements) {
|
||||
const {lineNodesByScreenLineId, textNodesByScreenLineId} = this
|
||||
const {lineComponentsByScreenLineId} = this
|
||||
|
||||
const startRow = this.getRenderedStartRow()
|
||||
const endRow = this.getRenderedEndRow()
|
||||
@@ -616,11 +625,9 @@ class TextEditorComponent {
|
||||
lineDecorations: this.decorationsToRender.lines.slice(tileStartRow - startRow, tileEndRow - startRow),
|
||||
textDecorations: this.decorationsToRender.text.slice(tileStartRow - startRow, tileEndRow - startRow),
|
||||
blockDecorations: this.decorationsToRender.blocks.get(tileStartRow),
|
||||
highlightDecorations: this.decorationsToRender.highlights.get(tileStartRow),
|
||||
displayLayer: this.props.model.displayLayer,
|
||||
nodePool: this.lineNodesPool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
lineComponentsByScreenLineId
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -633,8 +640,7 @@ class TextEditorComponent {
|
||||
screenRow,
|
||||
displayLayer: this.props.model.displayLayer,
|
||||
nodePool: this.lineNodesPool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
lineComponentsByScreenLineId
|
||||
}))
|
||||
}
|
||||
})
|
||||
@@ -719,18 +725,21 @@ class TextEditorComponent {
|
||||
if (this.shouldRenderDummyScrollbars && !this.props.model.isMini()) {
|
||||
let scrollHeight, scrollTop, horizontalScrollbarHeight
|
||||
let scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible
|
||||
let canScrollHorizontally, canScrollVertically
|
||||
|
||||
if (this.hasInitialMeasurements) {
|
||||
scrollHeight = this.getScrollHeight()
|
||||
scrollWidth = this.getScrollWidth()
|
||||
scrollTop = this.getScrollTop()
|
||||
scrollLeft = this.getScrollLeft()
|
||||
canScrollHorizontally = this.canScrollHorizontally()
|
||||
canScrollVertically = this.canScrollVertically()
|
||||
horizontalScrollbarHeight =
|
||||
this.isHorizontalScrollbarVisible()
|
||||
canScrollHorizontally
|
||||
? this.getHorizontalScrollbarHeight()
|
||||
: 0
|
||||
verticalScrollbarWidth =
|
||||
this.isVerticalScrollbarVisible()
|
||||
canScrollVertically
|
||||
? this.getVerticalScrollbarWidth()
|
||||
: 0
|
||||
forceScrollbarVisible = this.remeasureScrollbars
|
||||
@@ -744,10 +753,10 @@ class TextEditorComponent {
|
||||
orientation: 'vertical',
|
||||
didScroll: this.didScrollDummyScrollbar,
|
||||
didMouseDown: this.didMouseDownOnContent,
|
||||
canScroll: canScrollVertically,
|
||||
scrollHeight,
|
||||
scrollTop,
|
||||
horizontalScrollbarHeight,
|
||||
verticalScrollbarWidth,
|
||||
forceScrollbarVisible
|
||||
}),
|
||||
$(DummyScrollbarComponent, {
|
||||
@@ -755,9 +764,9 @@ class TextEditorComponent {
|
||||
orientation: 'horizontal',
|
||||
didScroll: this.didScrollDummyScrollbar,
|
||||
didMouseDown: this.didMouseDownOnContent,
|
||||
canScroll: canScrollHorizontally,
|
||||
scrollWidth,
|
||||
scrollLeft,
|
||||
horizontalScrollbarHeight,
|
||||
verticalScrollbarWidth,
|
||||
forceScrollbarVisible
|
||||
})
|
||||
@@ -966,7 +975,7 @@ class TextEditorComponent {
|
||||
this.decorationsToRender.customGutter.clear()
|
||||
this.decorationsToRender.blocks = new Map()
|
||||
this.decorationsToRender.text = []
|
||||
this.decorationsToMeasure.highlights.clear()
|
||||
this.decorationsToMeasure.highlights.length = 0
|
||||
this.decorationsToMeasure.cursors.clear()
|
||||
this.textDecorationsByMarker.clear()
|
||||
this.textDecorationBoundaries.length = 0
|
||||
@@ -1064,34 +1073,16 @@ class TextEditorComponent {
|
||||
|
||||
const {class: className, flashRequested, flashClass, flashDuration} = decoration
|
||||
decoration.flashRequested = false
|
||||
|
||||
let tileStartRow = this.tileStartRowForRow(screenRange.start.row)
|
||||
const rowsPerTile = this.getRowsPerTile()
|
||||
|
||||
while (tileStartRow <= screenRange.end.row) {
|
||||
const tileEndRow = tileStartRow + rowsPerTile
|
||||
const screenRangeInTile = constrainRangeToRows(screenRange, tileStartRow, tileEndRow)
|
||||
|
||||
let tileHighlights = this.decorationsToMeasure.highlights.get(tileStartRow)
|
||||
if (!tileHighlights) {
|
||||
tileHighlights = []
|
||||
this.decorationsToMeasure.highlights.set(tileStartRow, tileHighlights)
|
||||
}
|
||||
|
||||
tileHighlights.push({
|
||||
screenRange: screenRangeInTile,
|
||||
key,
|
||||
className,
|
||||
flashRequested,
|
||||
flashClass,
|
||||
flashDuration
|
||||
})
|
||||
|
||||
this.requestHorizontalMeasurement(screenRangeInTile.start.row, screenRangeInTile.start.column)
|
||||
this.requestHorizontalMeasurement(screenRangeInTile.end.row, screenRangeInTile.end.column)
|
||||
|
||||
tileStartRow = tileStartRow + rowsPerTile
|
||||
}
|
||||
this.decorationsToMeasure.highlights.push({
|
||||
screenRange,
|
||||
key,
|
||||
className,
|
||||
flashRequested,
|
||||
flashClass,
|
||||
flashDuration
|
||||
})
|
||||
this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column)
|
||||
this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column)
|
||||
}
|
||||
|
||||
addCursorDecorationToMeasure (decoration, marker, screenRange, reversed) {
|
||||
@@ -1312,18 +1303,16 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
updateHighlightsToRender () {
|
||||
this.decorationsToRender.highlights.clear()
|
||||
this.decorationsToMeasure.highlights.forEach((highlights, tileRow) => {
|
||||
for (let i = 0, length = highlights.length; i < length; i++) {
|
||||
const highlight = highlights[i]
|
||||
const {start, end} = highlight.screenRange
|
||||
highlight.startPixelTop = this.pixelPositionAfterBlocksForRow(start.row)
|
||||
highlight.startPixelLeft = this.pixelLeftForRowAndColumn(start.row, start.column)
|
||||
highlight.endPixelTop = this.pixelPositionAfterBlocksForRow(end.row) + this.getLineHeight()
|
||||
highlight.endPixelLeft = this.pixelLeftForRowAndColumn(end.row, end.column)
|
||||
}
|
||||
this.decorationsToRender.highlights.set(tileRow, highlights)
|
||||
})
|
||||
this.decorationsToRender.highlights.length = 0
|
||||
for (let i = 0; i < this.decorationsToMeasure.highlights.length; i++) {
|
||||
const highlight = this.decorationsToMeasure.highlights[i]
|
||||
const {start, end} = highlight.screenRange
|
||||
highlight.startPixelTop = this.pixelPositionAfterBlocksForRow(start.row)
|
||||
highlight.startPixelLeft = this.pixelLeftForRowAndColumn(start.row, start.column)
|
||||
highlight.endPixelTop = this.pixelPositionAfterBlocksForRow(end.row) + this.getLineHeight()
|
||||
highlight.endPixelLeft = this.pixelLeftForRowAndColumn(end.row, end.column)
|
||||
this.decorationsToRender.highlights.push(highlight)
|
||||
}
|
||||
}
|
||||
|
||||
updateCursorsToRender () {
|
||||
@@ -1514,11 +1503,15 @@ class TextEditorComponent {
|
||||
let {deltaX, deltaY} = event
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
deltaX = deltaX * scrollSensitivity
|
||||
deltaX = (Math.sign(deltaX) === 1)
|
||||
? Math.max(1, deltaX * scrollSensitivity)
|
||||
: Math.min(-1, deltaX * scrollSensitivity)
|
||||
deltaY = 0
|
||||
} else {
|
||||
deltaX = 0
|
||||
deltaY = deltaY * scrollSensitivity
|
||||
deltaY = (Math.sign(deltaY) === 1)
|
||||
? Math.max(1, deltaY * scrollSensitivity)
|
||||
: Math.min(-1, deltaY * scrollSensitivity)
|
||||
}
|
||||
|
||||
if (this.getPlatform() !== 'darwin' && event.shiftKey) {
|
||||
@@ -2196,7 +2189,8 @@ class TextEditorComponent {
|
||||
|
||||
measureLongestLineWidth () {
|
||||
if (this.longestLineToMeasure) {
|
||||
this.measurements.longestLineWidth = this.lineNodesByScreenLineId.get(this.longestLineToMeasure.id).firstChild.offsetWidth
|
||||
const lineComponent = this.lineComponentsByScreenLineId.get(this.longestLineToMeasure.id)
|
||||
this.measurements.longestLineWidth = lineComponent.element.firstChild.offsetWidth
|
||||
this.longestLineToMeasure = null
|
||||
}
|
||||
}
|
||||
@@ -2226,15 +2220,25 @@ class TextEditorComponent {
|
||||
columnsToMeasure.sort((a, b) => a - b)
|
||||
|
||||
const screenLine = this.renderedScreenLineForRow(row)
|
||||
const lineNode = this.lineNodesByScreenLineId.get(screenLine.id)
|
||||
const lineComponent = this.lineComponentsByScreenLineId.get(screenLine.id)
|
||||
|
||||
if (!lineNode) {
|
||||
const error = new Error('Requested measurement of a line that is not currently rendered')
|
||||
error.metadata = {row, columnsToMeasure}
|
||||
if (!lineComponent) {
|
||||
const error = new Error('Requested measurement of a line component that is not currently rendered')
|
||||
error.metadata = {
|
||||
row,
|
||||
columnsToMeasure,
|
||||
renderedScreenLineIds: this.renderedScreenLines.map((line) => line.id),
|
||||
extraRenderedScreenLineIds: Array.from(this.extraRenderedScreenLines.keys()),
|
||||
lineComponentScreenLineIds: Array.from(this.lineComponentsByScreenLineId.keys()),
|
||||
renderedStartRow: this.getRenderedStartRow(),
|
||||
renderedEndRow: this.getRenderedEndRow(),
|
||||
requestedScreenLineId: screenLine.id
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const textNodes = this.textNodesByScreenLineId.get(screenLine.id)
|
||||
const lineNode = lineComponent.element
|
||||
const textNodes = lineComponent.textNodes
|
||||
let positionsForLine = this.horizontalPixelPositionsByScreenLineId.get(screenLine.id)
|
||||
if (positionsForLine == null) {
|
||||
positionsForLine = new Map()
|
||||
@@ -2349,7 +2353,7 @@ class TextEditorComponent {
|
||||
|
||||
const linesClientLeft = this.refs.lineTiles.getBoundingClientRect().left
|
||||
const targetClientLeft = linesClientLeft + Math.max(0, left)
|
||||
const textNodes = this.textNodesByScreenLineId.get(screenLine.id)
|
||||
const {textNodes} = this.lineComponentsByScreenLineId.get(screenLine.id)
|
||||
|
||||
let containingTextNodeIndex
|
||||
{
|
||||
@@ -2452,37 +2456,61 @@ class TextEditorComponent {
|
||||
const {model} = this.props
|
||||
const decorations = model.getDecorations({type: 'block'})
|
||||
for (let i = 0; i < decorations.length; i++) {
|
||||
this.didAddBlockDecoration(decorations[i])
|
||||
this.addBlockDecoration(decorations[i])
|
||||
}
|
||||
}
|
||||
|
||||
didAddBlockDecoration (decoration) {
|
||||
addBlockDecoration (decoration, subscribeToChanges = true) {
|
||||
const marker = decoration.getMarker()
|
||||
const {item, position} = decoration.getProperties()
|
||||
const element = TextEditor.viewForItem(item)
|
||||
const row = marker.getHeadScreenPosition().row
|
||||
this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after')
|
||||
|
||||
this.blockDecorationsToMeasure.add(decoration)
|
||||
this.blockDecorationsByElement.set(element, decoration)
|
||||
this.blockDecorationResizeObserver.observe(element)
|
||||
if (marker.isValid()) {
|
||||
const row = marker.getHeadScreenPosition().row
|
||||
this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after')
|
||||
this.blockDecorationsToMeasure.add(decoration)
|
||||
this.blockDecorationsByElement.set(element, decoration)
|
||||
this.blockDecorationResizeObserver.observe(element)
|
||||
|
||||
const didUpdateDisposable = marker.bufferMarker.onDidChange((e) => {
|
||||
if (!e.textChanged) {
|
||||
this.lineTopIndex.moveBlock(decoration, marker.getHeadScreenPosition().row)
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
})
|
||||
const didDestroyDisposable = decoration.onDidDestroy(() => {
|
||||
this.blockDecorationsToMeasure.delete(decoration)
|
||||
this.heightsByBlockDecoration.delete(decoration)
|
||||
this.blockDecorationsByElement.delete(element)
|
||||
this.blockDecorationResizeObserver.unobserve(element)
|
||||
this.lineTopIndex.removeBlock(decoration)
|
||||
didUpdateDisposable.dispose()
|
||||
didDestroyDisposable.dispose()
|
||||
this.scheduleUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
if (subscribeToChanges) {
|
||||
let wasValid = marker.isValid()
|
||||
|
||||
const didUpdateDisposable = marker.bufferMarker.onDidChange(({textChanged}) => {
|
||||
const isValid = marker.isValid()
|
||||
if (wasValid && !isValid) {
|
||||
wasValid = false
|
||||
this.blockDecorationsToMeasure.delete(decoration)
|
||||
this.heightsByBlockDecoration.delete(decoration)
|
||||
this.blockDecorationsByElement.delete(element)
|
||||
this.blockDecorationResizeObserver.unobserve(element)
|
||||
this.lineTopIndex.removeBlock(decoration)
|
||||
this.scheduleUpdate()
|
||||
} else if (!wasValid && isValid) {
|
||||
wasValid = true
|
||||
this.addBlockDecoration(decoration, false)
|
||||
} else if (isValid && !textChanged) {
|
||||
this.lineTopIndex.moveBlock(decoration, marker.getHeadScreenPosition().row)
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
})
|
||||
|
||||
const didDestroyDisposable = decoration.onDidDestroy(() => {
|
||||
didUpdateDisposable.dispose()
|
||||
didDestroyDisposable.dispose()
|
||||
|
||||
if (marker.isValid()) {
|
||||
this.blockDecorationsToMeasure.delete(decoration)
|
||||
this.heightsByBlockDecoration.delete(decoration)
|
||||
this.blockDecorationsByElement.delete(element)
|
||||
this.blockDecorationResizeObserver.unobserve(element)
|
||||
this.lineTopIndex.removeBlock(decoration)
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didResizeBlockDecorations (entries) {
|
||||
@@ -2560,7 +2588,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
getScrollContainerClientWidth () {
|
||||
if (this.isVerticalScrollbarVisible()) {
|
||||
if (this.canScrollVertically()) {
|
||||
return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()
|
||||
} else {
|
||||
return this.getScrollContainerWidth()
|
||||
@@ -2568,14 +2596,14 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
getScrollContainerClientHeight () {
|
||||
if (this.isHorizontalScrollbarVisible()) {
|
||||
if (this.canScrollHorizontally()) {
|
||||
return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()
|
||||
} else {
|
||||
return this.getScrollContainerHeight()
|
||||
}
|
||||
}
|
||||
|
||||
isVerticalScrollbarVisible () {
|
||||
canScrollVertically () {
|
||||
const {model} = this.props
|
||||
if (model.isMini()) return false
|
||||
if (model.getAutoHeight()) return false
|
||||
@@ -2586,7 +2614,7 @@ class TextEditorComponent {
|
||||
)
|
||||
}
|
||||
|
||||
isHorizontalScrollbarVisible () {
|
||||
canScrollHorizontally () {
|
||||
const {model} = this.props
|
||||
if (model.isMini()) return false
|
||||
if (model.getAutoWidth()) return false
|
||||
@@ -2935,15 +2963,18 @@ class DummyScrollbarComponent {
|
||||
render () {
|
||||
const {
|
||||
orientation, scrollWidth, scrollHeight,
|
||||
verticalScrollbarWidth, horizontalScrollbarHeight, forceScrollbarVisible,
|
||||
didScroll, didMouseDown
|
||||
verticalScrollbarWidth, horizontalScrollbarHeight,
|
||||
canScroll, forceScrollbarVisible, didScroll
|
||||
} = this.props
|
||||
|
||||
const outerStyle = {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
zIndex: 1
|
||||
zIndex: 1,
|
||||
willChange: 'transform'
|
||||
}
|
||||
if (!canScroll) outerStyle.visibility = 'hidden'
|
||||
|
||||
const innerStyle = {}
|
||||
if (orientation === 'horizontal') {
|
||||
let right = (verticalScrollbarWidth || 0)
|
||||
@@ -2954,9 +2985,6 @@ class DummyScrollbarComponent {
|
||||
outerStyle.overflowY = 'hidden'
|
||||
outerStyle.overflowX = forceScrollbarVisible ? 'scroll' : 'auto'
|
||||
outerStyle.cursor = 'default'
|
||||
if (horizontalScrollbarHeight === 0) {
|
||||
outerStyle.visibility = 'hidden'
|
||||
}
|
||||
innerStyle.height = '15px'
|
||||
innerStyle.width = (scrollWidth || 0) + 'px'
|
||||
} else {
|
||||
@@ -2968,9 +2996,6 @@ class DummyScrollbarComponent {
|
||||
outerStyle.overflowX = 'hidden'
|
||||
outerStyle.overflowY = forceScrollbarVisible ? 'scroll' : 'auto'
|
||||
outerStyle.cursor = 'default'
|
||||
if (verticalScrollbarWidth === 0) {
|
||||
outerStyle.visibility = 'hidden'
|
||||
}
|
||||
innerStyle.width = '15px'
|
||||
innerStyle.height = (scrollHeight || 0) + 'px'
|
||||
}
|
||||
@@ -2981,7 +3006,7 @@ class DummyScrollbarComponent {
|
||||
style: outerStyle,
|
||||
on: {
|
||||
scroll: didScroll,
|
||||
mousedown: didMouseDown
|
||||
mousedown: this.didMouseDown
|
||||
}
|
||||
},
|
||||
$.div({style: innerStyle})
|
||||
@@ -3525,10 +3550,8 @@ class CursorsAndInputComponent {
|
||||
|
||||
class LinesTileComponent {
|
||||
constructor (props) {
|
||||
this.highlightComponentsByKey = new Map()
|
||||
this.props = props
|
||||
etch.initialize(this)
|
||||
this.updateHighlights()
|
||||
this.createLines()
|
||||
this.updateBlockDecorations({}, props)
|
||||
}
|
||||
@@ -3542,16 +3565,10 @@ class LinesTileComponent {
|
||||
this.updateLines(oldProps, newProps)
|
||||
this.updateBlockDecorations(oldProps, newProps)
|
||||
}
|
||||
this.updateHighlights()
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.highlightComponentsByKey.forEach((highlightComponent) => {
|
||||
highlightComponent.destroy()
|
||||
})
|
||||
this.highlightComponentsByKey.clear()
|
||||
|
||||
for (let i = 0; i < this.lineComponents.length; i++) {
|
||||
this.lineComponents[i].destroy()
|
||||
}
|
||||
@@ -3572,12 +3589,7 @@ class LinesTileComponent {
|
||||
width: width + 'px',
|
||||
transform: `translateY(${top}px)`
|
||||
}
|
||||
},
|
||||
$.div({
|
||||
ref: 'highlights',
|
||||
className: 'highlights',
|
||||
style: {layout: 'contain'}
|
||||
})
|
||||
}
|
||||
// Lines and block decorations will be manually inserted here for efficiency
|
||||
)
|
||||
}
|
||||
@@ -3585,7 +3597,7 @@ class LinesTileComponent {
|
||||
createLines () {
|
||||
const {
|
||||
tileStartRow, screenLines, lineDecorations, textDecorations,
|
||||
nodePool, displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId
|
||||
nodePool, displayLayer, lineComponentsByScreenLineId
|
||||
} = this.props
|
||||
|
||||
this.lineComponents = []
|
||||
@@ -3597,8 +3609,7 @@ class LinesTileComponent {
|
||||
textDecorations: textDecorations[i],
|
||||
displayLayer,
|
||||
nodePool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
lineComponentsByScreenLineId
|
||||
})
|
||||
this.element.appendChild(component.element)
|
||||
this.lineComponents.push(component)
|
||||
@@ -3608,7 +3619,7 @@ class LinesTileComponent {
|
||||
updateLines (oldProps, newProps) {
|
||||
var {
|
||||
screenLines, tileStartRow, lineDecorations, textDecorations,
|
||||
nodePool, displayLayer, lineNodesByScreenLineId, textNodesByScreenLineId
|
||||
nodePool, displayLayer, lineComponentsByScreenLineId
|
||||
} = newProps
|
||||
|
||||
var oldScreenLines = oldProps.screenLines
|
||||
@@ -3631,8 +3642,7 @@ class LinesTileComponent {
|
||||
textDecorations: textDecorations[newScreenLineIndex],
|
||||
displayLayer,
|
||||
nodePool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
lineComponentsByScreenLineId
|
||||
})
|
||||
this.element.appendChild(newScreenLineComponent.element)
|
||||
this.lineComponents.push(newScreenLineComponent)
|
||||
@@ -3668,8 +3678,7 @@ class LinesTileComponent {
|
||||
textDecorations: textDecorations[newScreenLineIndex],
|
||||
displayLayer,
|
||||
nodePool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
lineComponentsByScreenLineId
|
||||
})
|
||||
this.element.insertBefore(newScreenLineComponent.element, this.getFirstElementForScreenLine(oldProps, oldScreenLine))
|
||||
newScreenLineComponents.push(newScreenLineComponent)
|
||||
@@ -3695,8 +3704,7 @@ class LinesTileComponent {
|
||||
textDecorations: textDecorations[newScreenLineIndex],
|
||||
displayLayer,
|
||||
nodePool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
lineComponentsByScreenLineId
|
||||
})
|
||||
this.element.insertBefore(newScreenLineComponent.element, oldScreenLineComponent.element)
|
||||
oldScreenLineComponent.destroy()
|
||||
@@ -3731,11 +3739,11 @@ class LinesTileComponent {
|
||||
}
|
||||
}
|
||||
|
||||
return oldProps.lineNodesByScreenLineId.get(screenLine.id)
|
||||
return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element
|
||||
}
|
||||
|
||||
updateBlockDecorations (oldProps, newProps) {
|
||||
var {blockDecorations, lineNodesByScreenLineId} = newProps
|
||||
var {blockDecorations, lineComponentsByScreenLineId} = newProps
|
||||
|
||||
if (oldProps.blockDecorations) {
|
||||
oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => {
|
||||
@@ -3760,7 +3768,7 @@ class LinesTileComponent {
|
||||
if (oldDecorations && oldDecorations.includes(newDecoration)) continue
|
||||
|
||||
var element = TextEditor.viewForItem(newDecoration.item)
|
||||
var lineNode = lineNodesByScreenLineId.get(screenLineId)
|
||||
var lineNode = lineComponentsByScreenLineId.get(screenLineId).element
|
||||
if (newDecoration.position === 'after') {
|
||||
this.element.insertBefore(element, lineNode.nextSibling)
|
||||
} else {
|
||||
@@ -3771,40 +3779,6 @@ class LinesTileComponent {
|
||||
}
|
||||
}
|
||||
|
||||
updateHighlights () {
|
||||
const {top, lineHeight, highlightDecorations} = this.props
|
||||
|
||||
const visibleHighlightDecorations = new Set()
|
||||
if (highlightDecorations) {
|
||||
for (let i = 0; i < highlightDecorations.length; i++) {
|
||||
const highlightDecoration = highlightDecorations[i]
|
||||
|
||||
const highlightProps = Object.assign(
|
||||
{parentTileTop: top, lineHeight},
|
||||
highlightDecorations[i]
|
||||
)
|
||||
let highlightComponent = this.highlightComponentsByKey.get(highlightDecoration.key)
|
||||
if (highlightComponent) {
|
||||
highlightComponent.update(highlightProps)
|
||||
} else {
|
||||
highlightComponent = new HighlightComponent(highlightProps)
|
||||
this.refs.highlights.appendChild(highlightComponent.element)
|
||||
this.highlightComponentsByKey.set(highlightDecoration.key, highlightComponent)
|
||||
}
|
||||
|
||||
highlightDecorations[i].flashRequested = false
|
||||
visibleHighlightDecorations.add(highlightDecoration.key)
|
||||
}
|
||||
}
|
||||
|
||||
this.highlightComponentsByKey.forEach((highlightComponent, key) => {
|
||||
if (!visibleHighlightDecorations.has(key)) {
|
||||
highlightComponent.destroy()
|
||||
this.highlightComponentsByKey.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shouldUpdate (newProps) {
|
||||
const oldProps = this.props
|
||||
if (oldProps.top !== newProps.top) return true
|
||||
@@ -3816,25 +3790,6 @@ class LinesTileComponent {
|
||||
if (!arraysEqual(oldProps.screenLines, newProps.screenLines)) return true
|
||||
if (!arraysEqual(oldProps.lineDecorations, newProps.lineDecorations)) return true
|
||||
|
||||
if (!oldProps.highlightDecorations && newProps.highlightDecorations) return true
|
||||
if (oldProps.highlightDecorations && !newProps.highlightDecorations) return true
|
||||
|
||||
if (oldProps.highlightDecorations && newProps.highlightDecorations) {
|
||||
if (oldProps.highlightDecorations.length !== newProps.highlightDecorations.length) return true
|
||||
|
||||
for (let i = 0, length = oldProps.highlightDecorations.length; i < length; i++) {
|
||||
const oldHighlight = oldProps.highlightDecorations[i]
|
||||
const newHighlight = newProps.highlightDecorations[i]
|
||||
if (oldHighlight.className !== newHighlight.className) return true
|
||||
if (newHighlight.flashRequested) return true
|
||||
if (oldHighlight.startPixelTop !== newHighlight.startPixelTop) return true
|
||||
if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true
|
||||
if (oldHighlight.endPixelTop !== newHighlight.endPixelTop) return true
|
||||
if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true
|
||||
if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true
|
||||
}
|
||||
}
|
||||
|
||||
if (oldProps.blockDecorations && newProps.blockDecorations) {
|
||||
if (oldProps.blockDecorations.size !== newProps.blockDecorations.size) return true
|
||||
|
||||
@@ -3872,11 +3827,11 @@ class LinesTileComponent {
|
||||
|
||||
class LineComponent {
|
||||
constructor (props) {
|
||||
const {nodePool, screenRow, screenLine, lineNodesByScreenLineId, offScreen} = props
|
||||
const {nodePool, screenRow, screenLine, lineComponentsByScreenLineId, offScreen} = props
|
||||
this.props = props
|
||||
this.element = nodePool.getElement('DIV', this.buildClassName(), null)
|
||||
this.element.dataset.screenRow = screenRow
|
||||
lineNodesByScreenLineId.set(screenLine.id, this.element)
|
||||
this.textNodes = []
|
||||
|
||||
if (offScreen) {
|
||||
this.element.style.position = 'absolute'
|
||||
@@ -3885,6 +3840,7 @@ class LineComponent {
|
||||
}
|
||||
|
||||
this.appendContents()
|
||||
lineComponentsByScreenLineId.set(screenLine.id, this)
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
@@ -3906,10 +3862,10 @@ class LineComponent {
|
||||
}
|
||||
|
||||
destroy () {
|
||||
const {nodePool, lineNodesByScreenLineId, textNodesByScreenLineId, screenLine} = this.props
|
||||
if (lineNodesByScreenLineId.get(screenLine.id) === this.element) {
|
||||
lineNodesByScreenLineId.delete(screenLine.id)
|
||||
textNodesByScreenLineId.delete(screenLine.id)
|
||||
const {nodePool, lineComponentsByScreenLineId, screenLine} = this.props
|
||||
|
||||
if (lineComponentsByScreenLineId.get(screenLine.id) === this) {
|
||||
lineComponentsByScreenLineId.delete(screenLine.id)
|
||||
}
|
||||
|
||||
this.element.remove()
|
||||
@@ -3917,10 +3873,9 @@ class LineComponent {
|
||||
}
|
||||
|
||||
appendContents () {
|
||||
const {displayLayer, nodePool, screenLine, textDecorations, textNodesByScreenLineId} = this.props
|
||||
const {displayLayer, nodePool, screenLine, textDecorations} = this.props
|
||||
|
||||
const textNodes = []
|
||||
textNodesByScreenLineId.set(screenLine.id, textNodes)
|
||||
this.textNodes.length = 0
|
||||
|
||||
const {lineText, tags} = screenLine
|
||||
let openScopeNode = nodePool.getElement('SPAN', null, null)
|
||||
@@ -3951,7 +3906,7 @@ class LineComponent {
|
||||
const nextTokenColumn = column + tag
|
||||
while (nextDecoration && nextDecoration.column <= nextTokenColumn) {
|
||||
const text = lineText.substring(column, nextDecoration.column)
|
||||
this.appendTextNode(textNodes, openScopeNode, text, activeClassName, activeStyle)
|
||||
this.appendTextNode(openScopeNode, text, activeClassName, activeStyle)
|
||||
column = nextDecoration.column
|
||||
activeClassName = nextDecoration.className
|
||||
activeStyle = nextDecoration.style
|
||||
@@ -3960,7 +3915,7 @@ class LineComponent {
|
||||
|
||||
if (column < nextTokenColumn) {
|
||||
const text = lineText.substring(column, nextTokenColumn)
|
||||
this.appendTextNode(textNodes, openScopeNode, text, activeClassName, activeStyle)
|
||||
this.appendTextNode(openScopeNode, text, activeClassName, activeStyle)
|
||||
column = nextTokenColumn
|
||||
}
|
||||
}
|
||||
@@ -3970,7 +3925,7 @@ class LineComponent {
|
||||
if (column === 0) {
|
||||
const textNode = nodePool.getTextNode(' ')
|
||||
this.element.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
this.textNodes.push(textNode)
|
||||
}
|
||||
|
||||
if (lineText.endsWith(displayLayer.foldCharacter)) {
|
||||
@@ -3979,11 +3934,11 @@ class LineComponent {
|
||||
// measurements when such marker is the last character on the line.
|
||||
const textNode = nodePool.getTextNode(ZERO_WIDTH_NBSP_CHARACTER)
|
||||
this.element.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
this.textNodes.push(textNode)
|
||||
}
|
||||
}
|
||||
|
||||
appendTextNode (textNodes, openScopeNode, text, activeClassName, activeStyle) {
|
||||
appendTextNode (openScopeNode, text, activeClassName, activeStyle) {
|
||||
const {nodePool} = this.props
|
||||
|
||||
if (activeClassName || activeStyle) {
|
||||
@@ -3994,7 +3949,7 @@ class LineComponent {
|
||||
|
||||
const textNode = nodePool.getTextNode(text)
|
||||
openScopeNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
this.textNodes.push(textNode)
|
||||
}
|
||||
|
||||
buildClassName () {
|
||||
@@ -4005,6 +3960,90 @@ class LineComponent {
|
||||
}
|
||||
}
|
||||
|
||||
class HighlightsComponent {
|
||||
constructor (props) {
|
||||
this.props = {}
|
||||
this.element = document.createElement('div')
|
||||
this.element.className = 'highlights'
|
||||
this.element.style.contain = 'strict'
|
||||
this.element.style.position = 'absolute'
|
||||
this.element.style.overflow = 'hidden'
|
||||
this.highlightComponentsByKey = new Map()
|
||||
this.update(props)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.highlightComponentsByKey.forEach((highlightComponent) => {
|
||||
highlightComponent.destroy()
|
||||
})
|
||||
this.highlightComponentsByKey.clear()
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
if (this.shouldUpdate(newProps)) {
|
||||
this.props = newProps
|
||||
const {height, width, lineHeight, highlightDecorations} = this.props
|
||||
|
||||
this.element.style.height = height + 'px'
|
||||
this.element.style.width = width + 'px'
|
||||
|
||||
const visibleHighlightDecorations = new Set()
|
||||
if (highlightDecorations) {
|
||||
for (let i = 0; i < highlightDecorations.length; i++) {
|
||||
const highlightDecoration = highlightDecorations[i]
|
||||
const highlightProps = Object.assign({lineHeight}, highlightDecorations[i])
|
||||
|
||||
let highlightComponent = this.highlightComponentsByKey.get(highlightDecoration.key)
|
||||
if (highlightComponent) {
|
||||
highlightComponent.update(highlightProps)
|
||||
} else {
|
||||
highlightComponent = new HighlightComponent(highlightProps)
|
||||
this.element.appendChild(highlightComponent.element)
|
||||
this.highlightComponentsByKey.set(highlightDecoration.key, highlightComponent)
|
||||
}
|
||||
|
||||
highlightDecorations[i].flashRequested = false
|
||||
visibleHighlightDecorations.add(highlightDecoration.key)
|
||||
}
|
||||
}
|
||||
|
||||
this.highlightComponentsByKey.forEach((highlightComponent, key) => {
|
||||
if (!visibleHighlightDecorations.has(key)) {
|
||||
highlightComponent.destroy()
|
||||
this.highlightComponentsByKey.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
shouldUpdate (newProps) {
|
||||
const oldProps = this.props
|
||||
|
||||
if (!newProps.hasInitialMeasurements) return false
|
||||
|
||||
if (oldProps.width !== newProps.width) return true
|
||||
if (oldProps.height !== newProps.height) return true
|
||||
if (oldProps.lineHeight !== newProps.lineHeight) return true
|
||||
if (!oldProps.highlightDecorations && newProps.highlightDecorations) return true
|
||||
if (oldProps.highlightDecorations && !newProps.highlightDecorations) return true
|
||||
if (oldProps.highlightDecorations && newProps.highlightDecorations) {
|
||||
if (oldProps.highlightDecorations.length !== newProps.highlightDecorations.length) return true
|
||||
|
||||
for (let i = 0, length = oldProps.highlightDecorations.length; i < length; i++) {
|
||||
const oldHighlight = oldProps.highlightDecorations[i]
|
||||
const newHighlight = newProps.highlightDecorations[i]
|
||||
if (oldHighlight.className !== newHighlight.className) return true
|
||||
if (newHighlight.flashRequested) return true
|
||||
if (oldHighlight.startPixelTop !== newHighlight.startPixelTop) return true
|
||||
if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true
|
||||
if (oldHighlight.endPixelTop !== newHighlight.endPixelTop) return true
|
||||
if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true
|
||||
if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HighlightComponent {
|
||||
constructor (props) {
|
||||
this.props = props
|
||||
@@ -4050,15 +4089,12 @@ class HighlightComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
let {startPixelTop, endPixelTop} = this.props
|
||||
const {
|
||||
className, screenRange, parentTileTop, lineHeight,
|
||||
startPixelLeft, endPixelLeft
|
||||
className, screenRange, lineHeight,
|
||||
startPixelTop, startPixelLeft, endPixelTop, endPixelLeft
|
||||
} = this.props
|
||||
startPixelTop -= parentTileTop
|
||||
endPixelTop -= parentTileTop
|
||||
const regionClassName = 'region ' + className
|
||||
|
||||
let regionClassName = 'region ' + className
|
||||
let children
|
||||
if (screenRange.start.row === screenRange.end.row) {
|
||||
children = $.div({
|
||||
|
||||
@@ -738,7 +738,7 @@ class TextEditor extends Model
|
||||
# Called by DecorationManager when a decoration is added.
|
||||
didAddDecoration: (decoration) ->
|
||||
if decoration.isType('block')
|
||||
@component?.didAddBlockDecoration(decoration)
|
||||
@component?.addBlockDecoration(decoration)
|
||||
|
||||
# Extended: Calls your `callback` when the placeholder text is changed.
|
||||
#
|
||||
|
||||
@@ -1752,6 +1752,11 @@ module.exports = class Workspace extends Model {
|
||||
// (default: true)
|
||||
// * `priority` (optional) {Number} Determines stacking order. Lower priority items are
|
||||
// forced closer to the edges of the window. (default: 100)
|
||||
// * `autoFocus` (optional) {Boolean} true if you want modal focus managed for you by Atom.
|
||||
// Atom will automatically focus your modal panel's first tabbable element when the modal
|
||||
// opens and will restore the previously selected element when the modal closes. Atom will
|
||||
// also automatically restrict user tab focus within your modal while it is open.
|
||||
// (default: false)
|
||||
//
|
||||
// Returns a {Panel}
|
||||
addModalPanel (options = {}) {
|
||||
|
||||
@@ -115,6 +115,9 @@ atom-dock {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
// Promote to own layer, fixes rendering issue atom/atom#14915
|
||||
will-change: transform;
|
||||
|
||||
&.right { left: 0; }
|
||||
&.bottom { top: 0; }
|
||||
&.left { right: 0; }
|
||||
|
||||
@@ -70,10 +70,6 @@ atom-text-editor {
|
||||
}
|
||||
}
|
||||
|
||||
.lines {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: none;
|
||||
padding: 0;
|
||||
|
||||
Reference in New Issue
Block a user