Merge branch 'master' of https://github.com/atom/atom into tests

This commit is contained in:
Steven Hobson-Campbell
2017-09-05 17:58:56 -07:00
28 changed files with 1879 additions and 1296 deletions

5
.github/stale.yml vendored
View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()
})
})
})
})
})

View File

@@ -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 ", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -70,10 +70,6 @@ atom-text-editor {
}
}
.lines {
background-color: inherit;
}
.highlight {
background: none;
padding: 0;