Merge commit '1d9a4cafcf6cc288d675512db8fd984e13aab869' into dw-windows-separate-channels

This commit is contained in:
Rafael Oleza 2019-06-01 00:28:37 +02:00
commit 79f6836349
300 changed files with 57696 additions and 45988 deletions

View File

@ -1,4 +1,4 @@
spec/fixtures/**/*.js **/spec/fixtures/**/*.js
node_modules node_modules
/vendor/ /vendor/
/out/ /out/

View File

@ -25,7 +25,7 @@
"rules": { "rules": {
"standard/no-callback-literal": ["off"], "standard/no-callback-literal": ["off"],
"node/no-deprecated-api": ["off"], "node/no-deprecated-api": ["off"],
"prettier/prettier": ["off"] // disable prettier rules for now. "prettier/prettier": ["error"]
}, },
"overrides": [ "overrides": [
{ {
@ -41,4 +41,4 @@
} }
} }
] ]
} }

View File

@ -1,4 +1,3 @@
{ {
"semi": false,
"singleQuote": true "singleQuote": true
} }

View File

@ -1,72 +1,75 @@
const Chart = require('chart.js') const Chart = require('chart.js');
const glob = require('glob') const glob = require('glob');
const fs = require('fs-plus') const fs = require('fs-plus');
const path = require('path') const path = require('path');
module.exports = async ({test, benchmarkPaths}) => { module.exports = async ({ test, benchmarkPaths }) => {
document.body.style.backgroundColor = '#ffffff' document.body.style.backgroundColor = '#ffffff';
document.body.style.overflow = 'auto' document.body.style.overflow = 'auto';
let paths = [] let paths = [];
for (const benchmarkPath of benchmarkPaths) { for (const benchmarkPath of benchmarkPaths) {
if (fs.isDirectorySync(benchmarkPath)) { if (fs.isDirectorySync(benchmarkPath)) {
paths = paths.concat(glob.sync(path.join(benchmarkPath, '**', '*.bench.js'))) paths = paths.concat(
glob.sync(path.join(benchmarkPath, '**', '*.bench.js'))
);
} else { } else {
paths.push(benchmarkPath) paths.push(benchmarkPath);
} }
} }
while (paths.length > 0) { while (paths.length > 0) {
const benchmark = require(paths.shift())({test}) const benchmark = require(paths.shift())({ test });
let results let results;
if (benchmark instanceof Promise) { if (benchmark instanceof Promise) {
results = await benchmark results = await benchmark;
} else { } else {
results = benchmark results = benchmark;
} }
const dataByBenchmarkName = {} const dataByBenchmarkName = {};
for (const {name, duration, x} of results) { for (const { name, duration, x } of results) {
dataByBenchmarkName[name] = dataByBenchmarkName[name] || {points: []} dataByBenchmarkName[name] = dataByBenchmarkName[name] || { points: [] };
dataByBenchmarkName[name].points.push({x, y: duration}) dataByBenchmarkName[name].points.push({ x, y: duration });
} }
const benchmarkContainer = document.createElement('div') const benchmarkContainer = document.createElement('div');
document.body.appendChild(benchmarkContainer) document.body.appendChild(benchmarkContainer);
for (const key in dataByBenchmarkName) { for (const key in dataByBenchmarkName) {
const data = dataByBenchmarkName[key] const data = dataByBenchmarkName[key];
if (data.points.length > 1) { if (data.points.length > 1) {
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas');
benchmarkContainer.appendChild(canvas) benchmarkContainer.appendChild(canvas);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Chart(canvas, { new Chart(canvas, {
type: 'line', type: 'line',
data: { data: {
datasets: [{label: key, fill: false, data: data.points}] datasets: [{ label: key, fill: false, data: data.points }]
}, },
options: { options: {
showLines: false, showLines: false,
scales: {xAxes: [{type: 'linear', position: 'bottom'}]} scales: { xAxes: [{ type: 'linear', position: 'bottom' }] }
} }
}) });
const textualOutput = `${key}:\n\n` + data.points.map((p) => `${p.x}\t${p.y}`).join('\n') const textualOutput =
console.log(textualOutput) `${key}:\n\n` + data.points.map(p => `${p.x}\t${p.y}`).join('\n');
console.log(textualOutput);
} else { } else {
const title = document.createElement('h2') const title = document.createElement('h2');
title.textContent = key title.textContent = key;
benchmarkContainer.appendChild(title) benchmarkContainer.appendChild(title);
const duration = document.createElement('p') const duration = document.createElement('p');
duration.textContent = `${data.points[0].y}ms` duration.textContent = `${data.points[0].y}ms`;
benchmarkContainer.appendChild(duration) benchmarkContainer.appendChild(duration);
const textualOutput = `${key}: ${data.points[0].y}` const textualOutput = `${key}: ${data.points[0].y}`;
console.log(textualOutput) console.log(textualOutput);
} }
await global.atom.reset() await global.atom.reset();
} }
} }
return 0 return 0;
} };

View File

@ -1,88 +1,100 @@
const {TextEditor, TextBuffer} = require('atom') const { TextEditor, TextBuffer } = require('atom');
const MIN_SIZE_IN_KB = 0 * 1024 const MIN_SIZE_IN_KB = 0 * 1024;
const MAX_SIZE_IN_KB = 10 * 1024 const MAX_SIZE_IN_KB = 10 * 1024;
const SIZE_STEP_IN_KB = 1024 const SIZE_STEP_IN_KB = 1024;
const LINE_TEXT = 'Lorem ipsum dolor sit amet\n' const LINE_TEXT = 'Lorem ipsum dolor sit amet\n';
const TEXT = LINE_TEXT.repeat(Math.ceil(MAX_SIZE_IN_KB * 1024 / LINE_TEXT.length)) const TEXT = LINE_TEXT.repeat(
Math.ceil((MAX_SIZE_IN_KB * 1024) / LINE_TEXT.length)
);
module.exports = async ({test}) => { module.exports = async ({ test }) => {
const data = [] const data = [];
document.body.appendChild(atom.workspace.getElement()) document.body.appendChild(atom.workspace.getElement());
atom.packages.loadPackages() atom.packages.loadPackages();
await atom.packages.activate() await atom.packages.activate();
for (let pane of atom.workspace.getPanes()) { for (let pane of atom.workspace.getPanes()) {
pane.destroy() pane.destroy();
} }
for (let sizeInKB = MIN_SIZE_IN_KB; sizeInKB < MAX_SIZE_IN_KB; sizeInKB += SIZE_STEP_IN_KB) { for (
const text = TEXT.slice(0, sizeInKB * 1024) let sizeInKB = MIN_SIZE_IN_KB;
console.log(text.length / 1024) sizeInKB < MAX_SIZE_IN_KB;
sizeInKB += SIZE_STEP_IN_KB
) {
const text = TEXT.slice(0, sizeInKB * 1024);
console.log(text.length / 1024);
let t0 = window.performance.now() let t0 = window.performance.now();
const buffer = new TextBuffer({text}) const buffer = new TextBuffer({ text });
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true}) const editor = new TextEditor({
atom.grammars.autoAssignLanguageMode(buffer) buffer,
atom.workspace.getActivePane().activateItem(editor) autoHeight: false,
let t1 = window.performance.now() largeFileMode: true
});
atom.grammars.autoAssignLanguageMode(buffer);
atom.workspace.getActivePane().activateItem(editor);
let t1 = window.performance.now();
data.push({ data.push({
name: 'Opening a large file', name: 'Opening a large file',
x: sizeInKB, x: sizeInKB,
duration: t1 - t0 duration: t1 - t0
}) });
const tickDurations = [] const tickDurations = [];
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
await timeout(50) await timeout(50);
t0 = window.performance.now() t0 = window.performance.now();
await timeout(0) await timeout(0);
t1 = window.performance.now() t1 = window.performance.now();
tickDurations[i] = t1 - t0 tickDurations[i] = t1 - t0;
} }
data.push({ data.push({
name: 'Max time event loop was blocked after opening a large file', name: 'Max time event loop was blocked after opening a large file',
x: sizeInKB, x: sizeInKB,
duration: Math.max(...tickDurations) duration: Math.max(...tickDurations)
}) });
t0 = window.performance.now() t0 = window.performance.now();
editor.setCursorScreenPosition(editor.element.screenPositionForPixelPosition({ editor.setCursorScreenPosition(
top: 100, editor.element.screenPositionForPixelPosition({
left: 30 top: 100,
})) left: 30
t1 = window.performance.now() })
);
t1 = window.performance.now();
data.push({ data.push({
name: 'Clicking the editor after opening a large file', name: 'Clicking the editor after opening a large file',
x: sizeInKB, x: sizeInKB,
duration: t1 - t0 duration: t1 - t0
}) });
t0 = window.performance.now() t0 = window.performance.now();
editor.element.setScrollTop(editor.element.getScrollTop() + 100) editor.element.setScrollTop(editor.element.getScrollTop() + 100);
t1 = window.performance.now() t1 = window.performance.now();
data.push({ data.push({
name: 'Scrolling down after opening a large file', name: 'Scrolling down after opening a large file',
x: sizeInKB, x: sizeInKB,
duration: t1 - t0 duration: t1 - t0
}) });
editor.destroy() editor.destroy();
buffer.destroy() buffer.destroy();
await timeout(10000) await timeout(10000);
} }
atom.workspace.getElement().remove() atom.workspace.getElement().remove();
return data return data;
} };
function timeout (duration) { function timeout(duration) {
return new Promise((resolve) => setTimeout(resolve, duration)) return new Promise(resolve => setTimeout(resolve, duration));
} }

View File

@ -1,95 +1,105 @@
const path = require('path') const path = require('path');
const fs = require('fs') const fs = require('fs');
const {TextEditor, TextBuffer} = require('atom') const { TextEditor, TextBuffer } = require('atom');
const SIZES_IN_KB = [ const SIZES_IN_KB = [512, 1024, 2048];
512, const REPEATED_TEXT = fs
1024, .readFileSync(
2048 path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'),
] 'utf8'
const REPEATED_TEXT = fs.readFileSync(path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8').replace(/\n/g, '') )
const TEXT = REPEATED_TEXT.repeat(Math.ceil(SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024 / REPEATED_TEXT.length)) .replace(/\n/g, '');
const TEXT = REPEATED_TEXT.repeat(
Math.ceil((SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024) / REPEATED_TEXT.length)
);
module.exports = async ({test}) => { module.exports = async ({ test }) => {
const data = [] const data = [];
const workspaceElement = atom.workspace.getElement() const workspaceElement = atom.workspace.getElement();
document.body.appendChild(workspaceElement) document.body.appendChild(workspaceElement);
atom.packages.loadPackages() atom.packages.loadPackages();
await atom.packages.activate() await atom.packages.activate();
console.log(atom.getLoadSettings().resourcePath); console.log(atom.getLoadSettings().resourcePath);
for (let pane of atom.workspace.getPanes()) { for (let pane of atom.workspace.getPanes()) {
pane.destroy() pane.destroy();
} }
for (const sizeInKB of SIZES_IN_KB) { for (const sizeInKB of SIZES_IN_KB) {
const text = TEXT.slice(0, sizeInKB * 1024) const text = TEXT.slice(0, sizeInKB * 1024);
console.log(text.length / 1024) console.log(text.length / 1024);
let t0 = window.performance.now() let t0 = window.performance.now();
const buffer = new TextBuffer({text}) const buffer = new TextBuffer({ text });
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true}) const editor = new TextEditor({
atom.grammars.assignLanguageMode(buffer, 'source.js') buffer,
atom.workspace.getActivePane().activateItem(editor) autoHeight: false,
let t1 = window.performance.now() largeFileMode: true
});
atom.grammars.assignLanguageMode(buffer, 'source.js');
atom.workspace.getActivePane().activateItem(editor);
let t1 = window.performance.now();
data.push({ data.push({
name: 'Opening a large single-line file', name: 'Opening a large single-line file',
x: sizeInKB, x: sizeInKB,
duration: t1 - t0 duration: t1 - t0
}) });
const tickDurations = [] const tickDurations = [];
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
await timeout(50) await timeout(50);
t0 = window.performance.now() t0 = window.performance.now();
await timeout(0) await timeout(0);
t1 = window.performance.now() t1 = window.performance.now();
tickDurations[i] = t1 - t0 tickDurations[i] = t1 - t0;
} }
data.push({ data.push({
name: 'Max time event loop was blocked after opening a large single-line file', name:
'Max time event loop was blocked after opening a large single-line file',
x: sizeInKB, x: sizeInKB,
duration: Math.max(...tickDurations) duration: Math.max(...tickDurations)
}) });
t0 = window.performance.now() t0 = window.performance.now();
editor.setCursorScreenPosition(editor.element.screenPositionForPixelPosition({ editor.setCursorScreenPosition(
top: 100, editor.element.screenPositionForPixelPosition({
left: 30 top: 100,
})) left: 30
t1 = window.performance.now() })
);
t1 = window.performance.now();
data.push({ data.push({
name: 'Clicking the editor after opening a large single-line file', name: 'Clicking the editor after opening a large single-line file',
x: sizeInKB, x: sizeInKB,
duration: t1 - t0 duration: t1 - t0
}) });
t0 = window.performance.now() t0 = window.performance.now();
editor.element.setScrollTop(editor.element.getScrollTop() + 100) editor.element.setScrollTop(editor.element.getScrollTop() + 100);
t1 = window.performance.now() t1 = window.performance.now();
data.push({ data.push({
name: 'Scrolling down after opening a large single-line file', name: 'Scrolling down after opening a large single-line file',
x: sizeInKB, x: sizeInKB,
duration: t1 - t0 duration: t1 - t0
}) });
editor.destroy() editor.destroy();
buffer.destroy() buffer.destroy();
await timeout(10000) await timeout(10000);
} }
workspaceElement.remove() workspaceElement.remove();
return data return data;
} };
function timeout (duration) { function timeout(duration) {
return new Promise((resolve) => setTimeout(resolve, duration)) return new Promise(resolve => setTimeout(resolve, duration));
} }

View File

@ -1,12 +1,12 @@
const TextBuffer = require('text-buffer') const TextBuffer = require('text-buffer');
const {Point, Range} = TextBuffer const { Point, Range } = TextBuffer;
const {File, Directory} = require('pathwatcher') const { File, Directory } = require('pathwatcher');
const {Emitter, Disposable, CompositeDisposable} = require('event-kit') const { Emitter, Disposable, CompositeDisposable } = require('event-kit');
const BufferedNodeProcess = require('../src/buffered-node-process') const BufferedNodeProcess = require('../src/buffered-node-process');
const BufferedProcess = require('../src/buffered-process') const BufferedProcess = require('../src/buffered-process');
const GitRepository = require('../src/git-repository') const GitRepository = require('../src/git-repository');
const Notification = require('../src/notification') const Notification = require('../src/notification');
const {watchPath} = require('../src/path-watcher') const { watchPath } = require('../src/path-watcher');
const atomExport = { const atomExport = {
BufferedNodeProcess, BufferedNodeProcess,
@ -22,23 +22,23 @@ const atomExport = {
Disposable, Disposable,
CompositeDisposable, CompositeDisposable,
watchPath watchPath
} };
// Shell integration is required by both Squirrel and Settings-View // Shell integration is required by both Squirrel and Settings-View
if (process.platform === 'win32') { if (process.platform === 'win32') {
Object.defineProperty(atomExport, 'WinShell', { Object.defineProperty(atomExport, 'WinShell', {
enumerable: true, enumerable: true,
get () { get() {
return require('../src/main-process/win-shell') return require('../src/main-process/win-shell');
} }
}) });
} }
// The following classes can't be used from a Task handler and should therefore // The following classes can't be used from a Task handler and should therefore
// only be exported when not running as a child node process // only be exported when not running as a child node process
if (process.type === 'renderer') { if (process.type === 'renderer') {
atomExport.Task = require('../src/task') atomExport.Task = require('../src/task');
atomExport.TextEditor = require('../src/text-editor') atomExport.TextEditor = require('../src/text-editor');
} }
module.exports = atomExport module.exports = atomExport;

View File

@ -1,7 +1,9 @@
module.exports = require('electron').clipboard module.exports = require('electron').clipboard;
const Grim = require('grim') const Grim = require('grim');
Grim.deprecate('Use `require("electron").clipboard` instead of `require("clipboard")`') Grim.deprecate(
'Use `require("electron").clipboard` instead of `require("clipboard")`'
);
// Ensure each package that requires this shim causes a deprecation warning // Ensure each package that requires this shim causes a deprecation warning
delete require.cache[__filename] delete require.cache[__filename];

View File

@ -1,7 +1,9 @@
module.exports = require('electron').ipcRenderer module.exports = require('electron').ipcRenderer;
const Grim = require('grim') const Grim = require('grim');
Grim.deprecate('Use `require("electron").ipcRenderer` instead of `require("ipc")`') Grim.deprecate(
'Use `require("electron").ipcRenderer` instead of `require("ipc")`'
);
// Ensure each package that requires this shim causes a deprecation warning // Ensure each package that requires this shim causes a deprecation warning
delete require.cache[__filename] delete require.cache[__filename];

View File

@ -1,7 +1,9 @@
module.exports = require('electron').remote module.exports = require('electron').remote;
const Grim = require('grim') const Grim = require('grim');
Grim.deprecate('Use `require("electron").remote` instead of `require("remote")`') Grim.deprecate(
'Use `require("electron").remote` instead of `require("remote")`'
);
// Ensure each package that requires this shim causes a deprecation warning // Ensure each package that requires this shim causes a deprecation warning
delete require.cache[__filename] delete require.cache[__filename];

View File

@ -1,7 +1,7 @@
module.exports = require('electron').shell module.exports = require('electron').shell;
const Grim = require('grim') const Grim = require('grim');
Grim.deprecate('Use `require("electron").shell` instead of `require("shell")`') Grim.deprecate('Use `require("electron").shell` instead of `require("shell")`');
// Ensure each package that requires this shim causes a deprecation warning // Ensure each package that requires this shim causes a deprecation warning
delete require.cache[__filename] delete require.cache[__filename];

View File

@ -1,7 +1,9 @@
module.exports = require('electron').webFrame module.exports = require('electron').webFrame;
const Grim = require('grim') const Grim = require('grim');
Grim.deprecate('Use `require("electron").webFrame` instead of `require("web-frame")`') Grim.deprecate(
'Use `require("electron").webFrame` instead of `require("web-frame")`'
);
// Ensure each package that requires this shim causes a deprecation warning // Ensure each package that requires this shim causes a deprecation warning
delete require.cache[__filename] delete require.cache[__filename];

View File

@ -1,67 +1,67 @@
const { CompositeDisposable, Emitter } = require('atom') const { CompositeDisposable, Emitter } = require('atom');
const AboutView = require('./components/about-view') const AboutView = require('./components/about-view');
// Deferred requires // Deferred requires
let shell let shell;
module.exports = class About { module.exports = class About {
constructor (initialState) { constructor(initialState) {
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
this.emitter = new Emitter() this.emitter = new Emitter();
this.state = initialState this.state = initialState;
this.views = { this.views = {
aboutView: null aboutView: null
} };
this.subscriptions.add( this.subscriptions.add(
atom.workspace.addOpener(uriToOpen => { atom.workspace.addOpener(uriToOpen => {
if (uriToOpen === this.state.uri) { if (uriToOpen === this.state.uri) {
return this.deserialize() return this.deserialize();
} }
}) })
) );
this.subscriptions.add( this.subscriptions.add(
atom.commands.add('atom-workspace', 'about:view-release-notes', () => { atom.commands.add('atom-workspace', 'about:view-release-notes', () => {
shell = shell || require('electron').shell shell = shell || require('electron').shell;
shell.openExternal( shell.openExternal(
this.state.updateManager.getReleaseNotesURLForCurrentVersion() this.state.updateManager.getReleaseNotesURLForCurrentVersion()
) );
}) })
) );
} }
destroy () { destroy() {
if (this.views.aboutView) this.views.aboutView.destroy() if (this.views.aboutView) this.views.aboutView.destroy();
this.views.aboutView = null this.views.aboutView = null;
if (this.state.updateManager) this.state.updateManager.dispose() if (this.state.updateManager) this.state.updateManager.dispose();
this.setState({ updateManager: null }) this.setState({ updateManager: null });
this.subscriptions.dispose() this.subscriptions.dispose();
} }
setState (newState) { setState(newState) {
if (newState && typeof newState === 'object') { if (newState && typeof newState === 'object') {
let { state } = this let { state } = this;
this.state = Object.assign({}, state, newState) this.state = Object.assign({}, state, newState);
this.didChange() this.didChange();
} }
} }
didChange () { didChange() {
this.emitter.emit('did-change') this.emitter.emit('did-change');
} }
onDidChange (callback) { onDidChange(callback) {
this.emitter.on('did-change', callback) this.emitter.on('did-change', callback);
} }
deserialize (state) { deserialize(state) {
if (!this.views.aboutView) { if (!this.views.aboutView) {
this.setState(state) this.setState(state);
this.views.aboutView = new AboutView({ this.views.aboutView = new AboutView({
uri: this.state.uri, uri: this.state.uri,
@ -71,14 +71,14 @@ module.exports = class About {
currentChromeVersion: this.state.currentChromeVersion, currentChromeVersion: this.state.currentChromeVersion,
currentNodeVersion: this.state.currentNodeVersion, currentNodeVersion: this.state.currentNodeVersion,
availableVersion: this.state.updateManager.getAvailableVersion() availableVersion: this.state.updateManager.getAvailableVersion()
}) });
this.handleStateChanges() this.handleStateChanges();
} }
return this.views.aboutView return this.views.aboutView;
} }
handleStateChanges () { handleStateChanges() {
this.onDidChange(() => { this.onDidChange(() => {
if (this.views.aboutView) { if (this.views.aboutView) {
this.views.aboutView.update({ this.views.aboutView.update({
@ -88,12 +88,12 @@ module.exports = class About {
currentChromeVersion: this.state.currentChromeVersion, currentChromeVersion: this.state.currentChromeVersion,
currentNodeVersion: this.state.currentNodeVersion, currentNodeVersion: this.state.currentNodeVersion,
availableVersion: this.state.updateManager.getAvailableVersion() availableVersion: this.state.updateManager.getAvailableVersion()
}) });
} }
}) });
this.state.updateManager.onDidChange(() => { this.state.updateManager.onDidChange(() => {
this.didChange() this.didChange();
}) });
} }
} };

View File

@ -1,38 +1,38 @@
const { CompositeDisposable } = require('atom') const { CompositeDisposable } = require('atom');
const etch = require('etch') const etch = require('etch');
const EtchComponent = require('../etch-component') const EtchComponent = require('../etch-component');
const $ = etch.dom const $ = etch.dom;
module.exports = class AboutStatusBar extends EtchComponent { module.exports = class AboutStatusBar extends EtchComponent {
constructor () { constructor() {
super() super();
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
this.subscriptions.add( this.subscriptions.add(
atom.tooltips.add(this.element, { atom.tooltips.add(this.element, {
title: title:
'An update will be installed the next time Atom is relaunched.<br/><br/>Click the squirrel icon for more information.' 'An update will be installed the next time Atom is relaunched.<br/><br/>Click the squirrel icon for more information.'
}) })
) );
} }
handleClick () { handleClick() {
atom.workspace.open('atom://about') atom.workspace.open('atom://about');
} }
render () { render() {
return $.div( return $.div(
{ {
className: 'about-release-notes inline-block', className: 'about-release-notes inline-block',
onclick: this.handleClick.bind(this) onclick: this.handleClick.bind(this)
}, },
$.span({ type: 'button', className: 'icon icon-squirrel' }) $.span({ type: 'button', className: 'icon icon-squirrel' })
) );
} }
destroy () { destroy() {
super.destroy() super.destroy();
this.subscriptions.dispose() this.subscriptions.dispose();
} }
} };

View File

@ -1,77 +1,77 @@
const { Disposable } = require('atom') const { Disposable } = require('atom');
const etch = require('etch') const etch = require('etch');
const shell = require('shell') const shell = require('shell');
const AtomLogo = require('./atom-logo') const AtomLogo = require('./atom-logo');
const EtchComponent = require('../etch-component') const EtchComponent = require('../etch-component');
const UpdateView = require('./update-view') const UpdateView = require('./update-view');
const $ = etch.dom const $ = etch.dom;
module.exports = class AboutView extends EtchComponent { module.exports = class AboutView extends EtchComponent {
handleAtomVersionClick (e) { handleAtomVersionClick(e) {
e.preventDefault() e.preventDefault();
atom.clipboard.write(this.props.currentAtomVersion) atom.clipboard.write(this.props.currentAtomVersion);
} }
handleElectronVersionClick (e) { handleElectronVersionClick(e) {
e.preventDefault() e.preventDefault();
atom.clipboard.write(this.props.currentElectronVersion) atom.clipboard.write(this.props.currentElectronVersion);
} }
handleChromeVersionClick (e) { handleChromeVersionClick(e) {
e.preventDefault() e.preventDefault();
atom.clipboard.write(this.props.currentChromeVersion) atom.clipboard.write(this.props.currentChromeVersion);
} }
handleNodeVersionClick (e) { handleNodeVersionClick(e) {
e.preventDefault() e.preventDefault();
atom.clipboard.write(this.props.currentNodeVersion) atom.clipboard.write(this.props.currentNodeVersion);
} }
handleReleaseNotesClick (e) { handleReleaseNotesClick(e) {
e.preventDefault() e.preventDefault();
shell.openExternal( shell.openExternal(
this.props.updateManager.getReleaseNotesURLForAvailableVersion() this.props.updateManager.getReleaseNotesURLForAvailableVersion()
) );
} }
handleLicenseClick (e) { handleLicenseClick(e) {
e.preventDefault() e.preventDefault();
atom.commands.dispatch( atom.commands.dispatch(
atom.views.getView(atom.workspace), atom.views.getView(atom.workspace),
'application:open-license' 'application:open-license'
) );
} }
handleTermsOfUseClick (e) { handleTermsOfUseClick(e) {
e.preventDefault() e.preventDefault();
shell.openExternal('https://atom.io/terms') shell.openExternal('https://atom.io/terms');
} }
handleHowToUpdateClick (e) { handleHowToUpdateClick(e) {
e.preventDefault() e.preventDefault();
shell.openExternal( shell.openExternal(
'https://flight-manual.atom.io/getting-started/sections/installing-atom/' 'https://flight-manual.atom.io/getting-started/sections/installing-atom/'
) );
} }
handleShowMoreClick (e) { handleShowMoreClick(e) {
e.preventDefault() e.preventDefault();
var showMoreDiv = document.querySelector('.show-more') var showMoreDiv = document.querySelector('.show-more');
var showMoreText = document.querySelector('.about-more-expand') var showMoreText = document.querySelector('.about-more-expand');
switch (showMoreText.textContent) { switch (showMoreText.textContent) {
case 'Show more': case 'Show more':
showMoreDiv.classList.toggle('hidden') showMoreDiv.classList.toggle('hidden');
showMoreText.textContent = 'Hide' showMoreText.textContent = 'Hide';
break break;
case 'Hide': case 'Hide':
showMoreDiv.classList.toggle('hidden') showMoreDiv.classList.toggle('hidden');
showMoreText.textContent = 'Show more' showMoreText.textContent = 'Show more';
break break;
} }
} }
render () { render() {
return $.div( return $.div(
{ className: 'pane-item native-key-bindings about' }, { className: 'pane-item native-key-bindings about' },
$.div( $.div(
@ -204,29 +204,29 @@ module.exports = class AboutView extends EtchComponent {
'Atom Community' 'Atom Community'
) )
) )
) );
} }
serialize () { serialize() {
return { return {
deserializer: this.constructor.name, deserializer: this.constructor.name,
uri: this.props.uri uri: this.props.uri
} };
} }
onDidChangeTitle () { onDidChangeTitle() {
return new Disposable() return new Disposable();
} }
onDidChangeModified () { onDidChangeModified() {
return new Disposable() return new Disposable();
} }
getTitle () { getTitle() {
return 'About' return 'About';
} }
getIconName () { getIconName() {
return 'info' return 'info';
} }
} };

View File

@ -1,10 +1,10 @@
const etch = require('etch') const etch = require('etch');
const EtchComponent = require('../etch-component') const EtchComponent = require('../etch-component');
const $ = etch.dom const $ = etch.dom;
module.exports = class AtomLogo extends EtchComponent { module.exports = class AtomLogo extends EtchComponent {
render () { render() {
return $.svg( return $.svg(
{ {
className: 'about-logo', className: 'about-logo',
@ -74,6 +74,6 @@ module.exports = class AtomLogo extends EtchComponent {
) )
) )
) )
) );
} }
} };

View File

@ -1,46 +1,46 @@
const etch = require('etch') const etch = require('etch');
const EtchComponent = require('../etch-component') const EtchComponent = require('../etch-component');
const UpdateManager = require('../update-manager') const UpdateManager = require('../update-manager');
const $ = etch.dom const $ = etch.dom;
module.exports = class UpdateView extends EtchComponent { module.exports = class UpdateView extends EtchComponent {
constructor (props) { constructor(props) {
super(props) super(props);
if ( if (
this.props.updateManager.getAutoUpdatesEnabled() && this.props.updateManager.getAutoUpdatesEnabled() &&
this.props.updateManager.getState() === UpdateManager.State.Idle this.props.updateManager.getState() === UpdateManager.State.Idle
) { ) {
this.props.updateManager.checkForUpdate() this.props.updateManager.checkForUpdate();
} }
} }
handleAutoUpdateCheckbox (e) { handleAutoUpdateCheckbox(e) {
atom.config.set('core.automaticallyUpdate', e.target.checked) atom.config.set('core.automaticallyUpdate', e.target.checked);
} }
shouldUpdateActionButtonBeDisabled () { shouldUpdateActionButtonBeDisabled() {
let { state } = this.props.updateManager let { state } = this.props.updateManager;
return ( return (
state === UpdateManager.State.CheckingForUpdate || state === UpdateManager.State.CheckingForUpdate ||
state === UpdateManager.State.DownloadingUpdate state === UpdateManager.State.DownloadingUpdate
) );
} }
executeUpdateAction () { executeUpdateAction() {
if ( if (
this.props.updateManager.state === this.props.updateManager.state ===
UpdateManager.State.UpdateAvailableToInstall UpdateManager.State.UpdateAvailableToInstall
) { ) {
this.props.updateManager.restartAndInstallUpdate() this.props.updateManager.restartAndInstallUpdate();
} else { } else {
this.props.updateManager.checkForUpdate() this.props.updateManager.checkForUpdate();
} }
} }
renderUpdateStatus () { renderUpdateStatus() {
let updateStatus = '' let updateStatus = '';
switch (this.props.updateManager.state) { switch (this.props.updateManager.state) {
case UpdateManager.State.Idle: case UpdateManager.State.Idle:
@ -52,8 +52,8 @@ module.exports = class UpdateView extends EtchComponent {
this.props.updateManager.getAutoUpdatesEnabled() this.props.updateManager.getAutoUpdatesEnabled()
? 'Atom will check for updates automatically' ? 'Atom will check for updates automatically'
: 'Automatic updates are disabled please check manually' : 'Automatic updates are disabled please check manually'
) );
break break;
case UpdateManager.State.CheckingForUpdate: case UpdateManager.State.CheckingForUpdate:
updateStatus = $.div( updateStatus = $.div(
{ className: 'about-updates-item app-checking-for-updates' }, { className: 'about-updates-item app-checking-for-updates' },
@ -61,15 +61,15 @@ module.exports = class UpdateView extends EtchComponent {
{ className: 'about-updates-label icon icon-search' }, { className: 'about-updates-label icon icon-search' },
'Checking for updates...' 'Checking for updates...'
) )
) );
break break;
case UpdateManager.State.DownloadingUpdate: case UpdateManager.State.DownloadingUpdate:
updateStatus = $.div( updateStatus = $.div(
{ className: 'about-updates-item app-downloading-update' }, { className: 'about-updates-item app-downloading-update' },
$.span({ className: 'loading loading-spinner-tiny inline-block' }), $.span({ className: 'loading loading-spinner-tiny inline-block' }),
$.span({ className: 'about-updates-label' }, 'Downloading update') $.span({ className: 'about-updates-label' }, 'Downloading update')
) );
break break;
case UpdateManager.State.UpdateAvailableToInstall: case UpdateManager.State.UpdateAvailableToInstall:
updateStatus = $.div( updateStatus = $.div(
{ className: 'about-updates-item app-update-available-to-install' }, { className: 'about-updates-item app-update-available-to-install' },
@ -88,8 +88,8 @@ module.exports = class UpdateView extends EtchComponent {
}, },
'Release Notes' 'Release Notes'
) )
) );
break break;
case UpdateManager.State.UpToDate: case UpdateManager.State.UpToDate:
updateStatus = $.div( updateStatus = $.div(
{ className: 'about-updates-item app-up-to-date' }, { className: 'about-updates-item app-up-to-date' },
@ -98,8 +98,8 @@ module.exports = class UpdateView extends EtchComponent {
{ className: 'about-updates-label is-strong' }, { className: 'about-updates-label is-strong' },
'Atom is up to date!' 'Atom is up to date!'
) )
) );
break break;
case UpdateManager.State.Unsupported: case UpdateManager.State.Unsupported:
updateStatus = $.div( updateStatus = $.div(
{ className: 'about-updates-item app-unsupported' }, { className: 'about-updates-item app-unsupported' },
@ -114,8 +114,8 @@ module.exports = class UpdateView extends EtchComponent {
}, },
'How to update' 'How to update'
) )
) );
break break;
case UpdateManager.State.Error: case UpdateManager.State.Error:
updateStatus = $.div( updateStatus = $.div(
{ className: 'about-updates-item app-update-error' }, { className: 'about-updates-item app-update-error' },
@ -124,14 +124,14 @@ module.exports = class UpdateView extends EtchComponent {
{ className: 'about-updates-label app-error-message is-strong' }, { className: 'about-updates-label app-error-message is-strong' },
this.props.updateManager.getErrorMessage() this.props.updateManager.getErrorMessage()
) )
) );
break break;
} }
return updateStatus return updateStatus;
} }
render () { render() {
return $.div( return $.div(
{ className: 'about-updates group-start' }, { className: 'about-updates group-start' },
$.div( $.div(
@ -176,6 +176,6 @@ module.exports = class UpdateView extends EtchComponent {
$.span({}, 'Automatically download updates') $.span({}, 'Automatically download updates')
) )
) )
) );
} }
} };

View File

@ -1,15 +1,15 @@
const etch = require('etch') const etch = require('etch');
/* /*
Public: Abstract class for handling the initialization Public: Abstract class for handling the initialization
boilerplate of an Etch component. boilerplate of an Etch component.
*/ */
module.exports = class EtchComponent { module.exports = class EtchComponent {
constructor (props) { constructor(props) {
this.props = props this.props = props;
etch.initialize(this) etch.initialize(this);
EtchComponent.setScheduler(atom.views) EtchComponent.setScheduler(atom.views);
} }
/* /*
@ -17,8 +17,8 @@ module.exports = class EtchComponent {
Returns a {Scheduler} Returns a {Scheduler}
*/ */
static getScheduler () { static getScheduler() {
return etch.getScheduler() return etch.getScheduler();
} }
/* /*
@ -26,8 +26,8 @@ module.exports = class EtchComponent {
* `scheduler` {Scheduler} * `scheduler` {Scheduler}
*/ */
static setScheduler (scheduler) { static setScheduler(scheduler) {
etch.setScheduler(scheduler) etch.setScheduler(scheduler);
} }
/* /*
@ -37,20 +37,20 @@ module.exports = class EtchComponent {
* `props` an {Object} representing the properties you want to update * `props` an {Object} representing the properties you want to update
*/ */
update (props) { update(props) {
let oldProps = this.props let oldProps = this.props;
this.props = Object.assign({}, oldProps, props) this.props = Object.assign({}, oldProps, props);
return etch.update(this) return etch.update(this);
} }
/* /*
Public: Destroys the component, removing it from the DOM. Public: Destroys the component, removing it from the DOM.
*/ */
destroy () { destroy() {
etch.destroy(this) etch.destroy(this);
} }
render () { render() {
throw new Error('Etch components must implement a `render` method') throw new Error('Etch components must implement a `render` method');
} }
} };

View File

@ -1,26 +1,26 @@
const { CompositeDisposable } = require('atom') const { CompositeDisposable } = require('atom');
const semver = require('semver') const semver = require('semver');
const UpdateManager = require('./update-manager') const UpdateManager = require('./update-manager');
const About = require('./about') const About = require('./about');
const StatusBarView = require('./components/about-status-bar') const StatusBarView = require('./components/about-status-bar');
let updateManager let updateManager;
// The local storage key for the available update version. // The local storage key for the available update version.
const AvailableUpdateVersion = 'about:version-available' const AvailableUpdateVersion = 'about:version-available';
const AboutURI = 'atom://about' const AboutURI = 'atom://about';
module.exports = { module.exports = {
activate () { activate() {
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
this.createModel() this.createModel();
let availableVersion = window.localStorage.getItem(AvailableUpdateVersion) let availableVersion = window.localStorage.getItem(AvailableUpdateVersion);
if ( if (
atom.getReleaseChannel() === 'dev' || atom.getReleaseChannel() === 'dev' ||
(availableVersion && semver.lte(availableVersion, atom.getVersion())) (availableVersion && semver.lte(availableVersion, atom.getVersion()))
) { ) {
this.clearUpdateState() this.clearUpdateState();
} }
this.subscriptions.add( this.subscriptions.add(
@ -32,48 +32,48 @@ module.exports = {
window.localStorage.setItem( window.localStorage.setItem(
AvailableUpdateVersion, AvailableUpdateVersion,
updateManager.getAvailableVersion() updateManager.getAvailableVersion()
) );
this.showStatusBarIfNeeded() this.showStatusBarIfNeeded();
} }
}) })
) );
this.subscriptions.add( this.subscriptions.add(
atom.commands.add('atom-workspace', 'about:clear-update-state', () => { atom.commands.add('atom-workspace', 'about:clear-update-state', () => {
this.clearUpdateState() this.clearUpdateState();
}) })
) );
}, },
deactivate () { deactivate() {
this.model.destroy() this.model.destroy();
if (this.statusBarTile) this.statusBarTile.destroy() if (this.statusBarTile) this.statusBarTile.destroy();
if (updateManager) { if (updateManager) {
updateManager.dispose() updateManager.dispose();
updateManager = undefined updateManager = undefined;
} }
}, },
clearUpdateState () { clearUpdateState() {
window.localStorage.removeItem(AvailableUpdateVersion) window.localStorage.removeItem(AvailableUpdateVersion);
}, },
consumeStatusBar (statusBar) { consumeStatusBar(statusBar) {
this.statusBar = statusBar this.statusBar = statusBar;
this.showStatusBarIfNeeded() this.showStatusBarIfNeeded();
}, },
deserializeAboutView (state) { deserializeAboutView(state) {
if (!this.model) { if (!this.model) {
this.createModel() this.createModel();
} }
return this.model.deserialize(state) return this.model.deserialize(state);
}, },
createModel () { createModel() {
updateManager = updateManager || new UpdateManager() updateManager = updateManager || new UpdateManager();
this.model = new About({ this.model = new About({
uri: AboutURI, uri: AboutURI,
@ -82,28 +82,28 @@ module.exports = {
currentChromeVersion: process.versions.chrome, currentChromeVersion: process.versions.chrome,
currentNodeVersion: process.version, currentNodeVersion: process.version,
updateManager: updateManager updateManager: updateManager
}) });
}, },
isUpdateAvailable () { isUpdateAvailable() {
let availableVersion = window.localStorage.getItem(AvailableUpdateVersion) let availableVersion = window.localStorage.getItem(AvailableUpdateVersion);
return availableVersion && semver.gt(availableVersion, atom.getVersion()) return availableVersion && semver.gt(availableVersion, atom.getVersion());
}, },
showStatusBarIfNeeded () { showStatusBarIfNeeded() {
if (this.isUpdateAvailable() && this.statusBar) { if (this.isUpdateAvailable() && this.statusBar) {
let statusBarView = new StatusBarView() let statusBarView = new StatusBarView();
if (this.statusBarTile) { if (this.statusBarTile) {
this.statusBarTile.destroy() this.statusBarTile.destroy();
} }
this.statusBarTile = this.statusBar.addRightTile({ this.statusBarTile = this.statusBar.addRightTile({
item: statusBarView, item: statusBarView,
priority: -100 priority: -100
}) });
return this.statusBarTile return this.statusBarTile;
} }
} }
} };

View File

@ -1,46 +1,46 @@
const { Emitter, CompositeDisposable } = require('atom') const { Emitter, CompositeDisposable } = require('atom');
const Unsupported = 'unsupported' const Unsupported = 'unsupported';
const Idle = 'idle' const Idle = 'idle';
const CheckingForUpdate = 'checking' const CheckingForUpdate = 'checking';
const DownloadingUpdate = 'downloading' const DownloadingUpdate = 'downloading';
const UpdateAvailableToInstall = 'update-available' const UpdateAvailableToInstall = 'update-available';
const UpToDate = 'no-update-available' const UpToDate = 'no-update-available';
const ErrorState = 'error' const ErrorState = 'error';
let UpdateManager = class UpdateManager { let UpdateManager = class UpdateManager {
constructor () { constructor() {
this.emitter = new Emitter() this.emitter = new Emitter();
this.currentVersion = atom.getVersion() this.currentVersion = atom.getVersion();
this.availableVersion = atom.getVersion() this.availableVersion = atom.getVersion();
this.resetState() this.resetState();
this.listenForAtomEvents() this.listenForAtomEvents();
} }
listenForAtomEvents () { listenForAtomEvents() {
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
this.subscriptions.add( this.subscriptions.add(
atom.autoUpdater.onDidBeginCheckingForUpdate(() => { atom.autoUpdater.onDidBeginCheckingForUpdate(() => {
this.setState(CheckingForUpdate) this.setState(CheckingForUpdate);
}), }),
atom.autoUpdater.onDidBeginDownloadingUpdate(() => { atom.autoUpdater.onDidBeginDownloadingUpdate(() => {
this.setState(DownloadingUpdate) this.setState(DownloadingUpdate);
}), }),
atom.autoUpdater.onDidCompleteDownloadingUpdate(({ releaseVersion }) => { atom.autoUpdater.onDidCompleteDownloadingUpdate(({ releaseVersion }) => {
this.setAvailableVersion(releaseVersion) this.setAvailableVersion(releaseVersion);
}), }),
atom.autoUpdater.onUpdateNotAvailable(() => { atom.autoUpdater.onUpdateNotAvailable(() => {
this.setState(UpToDate) this.setState(UpToDate);
}), }),
atom.autoUpdater.onUpdateError(() => { atom.autoUpdater.onUpdateError(() => {
this.setState(ErrorState) this.setState(ErrorState);
}), }),
atom.config.observe('core.automaticallyUpdate', value => { atom.config.observe('core.automaticallyUpdate', value => {
this.autoUpdatesEnabled = value this.autoUpdatesEnabled = value;
this.emitDidChange() this.emitDidChange();
}) })
) );
// TODO: When https://github.com/atom/electron/issues/4587 is closed we can add this support. // TODO: When https://github.com/atom/electron/issues/4587 is closed we can add this support.
// atom.autoUpdater.onUpdateAvailable => // atom.autoUpdater.onUpdateAvailable =>
@ -48,95 +48,95 @@ let UpdateManager = class UpdateManager {
// @updateAvailable.addClass('is-shown') // @updateAvailable.addClass('is-shown')
} }
dispose () { dispose() {
this.subscriptions.dispose() this.subscriptions.dispose();
} }
onDidChange (callback) { onDidChange(callback) {
return this.emitter.on('did-change', callback) return this.emitter.on('did-change', callback);
} }
emitDidChange () { emitDidChange() {
this.emitter.emit('did-change') this.emitter.emit('did-change');
} }
getAutoUpdatesEnabled () { getAutoUpdatesEnabled() {
return ( return (
this.autoUpdatesEnabled && this.state !== UpdateManager.State.Unsupported this.autoUpdatesEnabled && this.state !== UpdateManager.State.Unsupported
) );
} }
setAutoUpdatesEnabled (enabled) { setAutoUpdatesEnabled(enabled) {
return atom.config.set('core.automaticallyUpdate', enabled) return atom.config.set('core.automaticallyUpdate', enabled);
} }
getErrorMessage () { getErrorMessage() {
return atom.autoUpdater.getErrorMessage() return atom.autoUpdater.getErrorMessage();
} }
getState () { getState() {
return this.state return this.state;
} }
setState (state) { setState(state) {
this.state = state this.state = state;
this.emitDidChange() this.emitDidChange();
} }
resetState () { resetState() {
this.state = atom.autoUpdater.platformSupportsUpdates() this.state = atom.autoUpdater.platformSupportsUpdates()
? atom.autoUpdater.getState() ? atom.autoUpdater.getState()
: Unsupported : Unsupported;
this.emitDidChange() this.emitDidChange();
} }
getAvailableVersion () { getAvailableVersion() {
return this.availableVersion return this.availableVersion;
} }
setAvailableVersion (version) { setAvailableVersion(version) {
this.availableVersion = version this.availableVersion = version;
if (this.availableVersion !== this.currentVersion) { if (this.availableVersion !== this.currentVersion) {
this.state = UpdateAvailableToInstall this.state = UpdateAvailableToInstall;
} else { } else {
this.state = UpToDate this.state = UpToDate;
} }
this.emitDidChange() this.emitDidChange();
} }
checkForUpdate () { checkForUpdate() {
atom.autoUpdater.checkForUpdate() atom.autoUpdater.checkForUpdate();
} }
restartAndInstallUpdate () { restartAndInstallUpdate() {
atom.autoUpdater.restartAndInstallUpdate() atom.autoUpdater.restartAndInstallUpdate();
} }
getReleaseNotesURLForCurrentVersion () { getReleaseNotesURLForCurrentVersion() {
return this.getReleaseNotesURLForVersion(this.currentVersion) return this.getReleaseNotesURLForVersion(this.currentVersion);
} }
getReleaseNotesURLForAvailableVersion () { getReleaseNotesURLForAvailableVersion() {
return this.getReleaseNotesURLForVersion(this.availableVersion) return this.getReleaseNotesURLForVersion(this.availableVersion);
} }
getReleaseNotesURLForVersion (appVersion) { getReleaseNotesURLForVersion(appVersion) {
// Dev versions will not have a releases page // Dev versions will not have a releases page
if (appVersion.indexOf('dev') > -1) { if (appVersion.indexOf('dev') > -1) {
return 'https://atom.io/releases' return 'https://atom.io/releases';
} }
if (!appVersion.startsWith('v')) { if (!appVersion.startsWith('v')) {
appVersion = `v${appVersion}` appVersion = `v${appVersion}`;
} }
const releaseRepo = const releaseRepo =
appVersion.indexOf('nightly') > -1 ? 'atom-nightly-releases' : 'atom' appVersion.indexOf('nightly') > -1 ? 'atom-nightly-releases' : 'atom';
return `https://github.com/atom/${releaseRepo}/releases/tag/${appVersion}` return `https://github.com/atom/${releaseRepo}/releases/tag/${appVersion}`;
} }
} };
UpdateManager.State = { UpdateManager.State = {
Unsupported: Unsupported, Unsupported: Unsupported,
@ -146,6 +146,6 @@ UpdateManager.State = {
UpdateAvailableToInstall: UpdateAvailableToInstall, UpdateAvailableToInstall: UpdateAvailableToInstall,
UpToDate: UpToDate, UpToDate: UpToDate,
Error: ErrorState Error: ErrorState
} };
module.exports = UpdateManager module.exports = UpdateManager;

View File

@ -1,28 +1,28 @@
describe('About', () => { describe('About', () => {
let workspaceElement let workspaceElement;
beforeEach(async () => { beforeEach(async () => {
let storage = {} let storage = {};
spyOn(window.localStorage, 'setItem').andCallFake((key, value) => { spyOn(window.localStorage, 'setItem').andCallFake((key, value) => {
storage[key] = value storage[key] = value;
}) });
spyOn(window.localStorage, 'getItem').andCallFake(key => { spyOn(window.localStorage, 'getItem').andCallFake(key => {
return storage[key] return storage[key];
}) });
workspaceElement = atom.views.getView(atom.workspace) workspaceElement = atom.views.getView(atom.workspace);
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
}) });
it('deserializes correctly', () => { it('deserializes correctly', () => {
let deserializedAboutView = atom.deserializers.deserialize({ let deserializedAboutView = atom.deserializers.deserialize({
deserializer: 'AboutView', deserializer: 'AboutView',
uri: 'atom://about' uri: 'atom://about'
}) });
expect(deserializedAboutView).toBeTruthy() expect(deserializedAboutView).toBeTruthy();
}) });
describe('when the about:about-atom command is triggered', () => { describe('when the about:about-atom command is triggered', () => {
it('shows the About Atom view', async () => { it('shows the About Atom view', async () => {
@ -30,70 +30,70 @@ describe('About', () => {
// `toBeVisible()` matchers to work. Anything testing visibility or focus // `toBeVisible()` matchers to work. Anything testing visibility or focus
// requires that the workspaceElement is on the DOM. Tests that attach the // requires that the workspaceElement is on the DOM. Tests that attach the
// workspaceElement to the DOM are generally slower than those off DOM. // workspaceElement to the DOM are generally slower than those off DOM.
jasmine.attachToDOM(workspaceElement) jasmine.attachToDOM(workspaceElement);
expect(workspaceElement.querySelector('.about')).not.toExist() expect(workspaceElement.querySelector('.about')).not.toExist();
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
let aboutElement = workspaceElement.querySelector('.about') let aboutElement = workspaceElement.querySelector('.about');
expect(aboutElement).toBeVisible() expect(aboutElement).toBeVisible();
}) });
}) });
describe('when the Atom version number is clicked', () => { describe('when the Atom version number is clicked', () => {
it('copies the version number to the clipboard', async () => { it('copies the version number to the clipboard', async () => {
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
let aboutElement = workspaceElement.querySelector('.about') let aboutElement = workspaceElement.querySelector('.about');
let versionContainer = aboutElement.querySelector('.atom') let versionContainer = aboutElement.querySelector('.atom');
versionContainer.click() versionContainer.click();
expect(atom.clipboard.read()).toBe(atom.getVersion()) expect(atom.clipboard.read()).toBe(atom.getVersion());
}) });
}) });
describe('when the show more link is clicked', () => { describe('when the show more link is clicked', () => {
it('expands to show additional version numbers', async () => { it('expands to show additional version numbers', async () => {
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
jasmine.attachToDOM(workspaceElement) jasmine.attachToDOM(workspaceElement);
let aboutElement = workspaceElement.querySelector('.about') let aboutElement = workspaceElement.querySelector('.about');
let showMoreElement = aboutElement.querySelector('.show-more-expand') let showMoreElement = aboutElement.querySelector('.show-more-expand');
let moreInfoElement = workspaceElement.querySelector('.show-more') let moreInfoElement = workspaceElement.querySelector('.show-more');
showMoreElement.click() showMoreElement.click();
expect(moreInfoElement).toBeVisible() expect(moreInfoElement).toBeVisible();
}) });
}) });
describe('when the Electron version number is clicked', () => { describe('when the Electron version number is clicked', () => {
it('copies the version number to the clipboard', async () => { it('copies the version number to the clipboard', async () => {
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
let aboutElement = workspaceElement.querySelector('.about') let aboutElement = workspaceElement.querySelector('.about');
let versionContainer = aboutElement.querySelector('.electron') let versionContainer = aboutElement.querySelector('.electron');
versionContainer.click() versionContainer.click();
expect(atom.clipboard.read()).toBe(process.versions.electron) expect(atom.clipboard.read()).toBe(process.versions.electron);
}) });
}) });
describe('when the Chrome version number is clicked', () => { describe('when the Chrome version number is clicked', () => {
it('copies the version number to the clipboard', async () => { it('copies the version number to the clipboard', async () => {
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
let aboutElement = workspaceElement.querySelector('.about') let aboutElement = workspaceElement.querySelector('.about');
let versionContainer = aboutElement.querySelector('.chrome') let versionContainer = aboutElement.querySelector('.chrome');
versionContainer.click() versionContainer.click();
expect(atom.clipboard.read()).toBe(process.versions.chrome) expect(atom.clipboard.read()).toBe(process.versions.chrome);
}) });
}) });
describe('when the Node version number is clicked', () => { describe('when the Node version number is clicked', () => {
it('copies the version number to the clipboard', async () => { it('copies the version number to the clipboard', async () => {
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
let aboutElement = workspaceElement.querySelector('.about') let aboutElement = workspaceElement.querySelector('.about');
let versionContainer = aboutElement.querySelector('.node') let versionContainer = aboutElement.querySelector('.node');
versionContainer.click() versionContainer.click();
expect(atom.clipboard.read()).toBe(process.version) expect(atom.clipboard.read()).toBe(process.version);
}) });
}) });
}) });

View File

@ -1,179 +1,183 @@
const { conditionPromise } = require('./helpers/async-spec-helpers') const { conditionPromise } = require('./helpers/async-spec-helpers');
const MockUpdater = require('./mocks/updater') const MockUpdater = require('./mocks/updater');
describe('the status bar', () => { describe('the status bar', () => {
let atomVersion let atomVersion;
let workspaceElement let workspaceElement;
beforeEach(async () => { beforeEach(async () => {
let storage = {} let storage = {};
spyOn(window.localStorage, 'setItem').andCallFake((key, value) => { spyOn(window.localStorage, 'setItem').andCallFake((key, value) => {
storage[key] = value storage[key] = value;
}) });
spyOn(window.localStorage, 'getItem').andCallFake(key => { spyOn(window.localStorage, 'getItem').andCallFake(key => {
return storage[key] return storage[key];
}) });
spyOn(atom, 'getVersion').andCallFake(() => { spyOn(atom, 'getVersion').andCallFake(() => {
return atomVersion return atomVersion;
}) });
workspaceElement = atom.views.getView(atom.workspace) workspaceElement = atom.views.getView(atom.workspace);
await atom.packages.activatePackage('status-bar') await atom.packages.activatePackage('status-bar');
await atom.workspace.open('sample.js') await atom.workspace.open('sample.js');
}) });
afterEach(async () => { afterEach(async () => {
await atom.packages.deactivatePackage('about') await atom.packages.deactivatePackage('about');
await atom.packages.deactivatePackage('status-bar') await atom.packages.deactivatePackage('status-bar');
}) });
describe('on a stable version', function () { describe('on a stable version', function() {
beforeEach(async () => { beforeEach(async () => {
atomVersion = '1.2.3' atomVersion = '1.2.3';
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
}) });
describe('with no update', () => { describe('with no update', () => {
it('does not show the view', () => { it('does not show the view', () => {
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
}) });
describe('with an update', () => { describe('with an update', () => {
it('shows the view when the update finishes downloading', () => { it('shows the view when the update finishes downloading', () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
expect(workspaceElement).toContain('.about-release-notes') expect(workspaceElement).toContain('.about-release-notes');
}) });
describe('clicking on the status', () => { describe('clicking on the status', () => {
it('opens the about page', async () => { it('opens the about page', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
workspaceElement.querySelector('.about-release-notes').click() workspaceElement.querySelector('.about-release-notes').click();
await conditionPromise(() => workspaceElement.querySelector('.about')) await conditionPromise(() =>
expect(workspaceElement.querySelector('.about')).toExist() workspaceElement.querySelector('.about')
}) );
}) expect(workspaceElement.querySelector('.about')).toExist();
});
});
it('continues to show the squirrel until Atom is updated to the new version', async () => { it('continues to show the squirrel until Atom is updated to the new version', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
expect(workspaceElement).toContain('.about-release-notes') expect(workspaceElement).toContain('.about-release-notes');
await atom.packages.deactivatePackage('about') await atom.packages.deactivatePackage('about');
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
await Promise.resolve() // Service consumption hooks are deferred until the next tick await Promise.resolve(); // Service consumption hooks are deferred until the next tick
expect(workspaceElement).toContain('.about-release-notes') expect(workspaceElement).toContain('.about-release-notes');
await atom.packages.deactivatePackage('about') await atom.packages.deactivatePackage('about');
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
atomVersion = '42.0.0' atomVersion = '42.0.0';
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
await Promise.resolve() // Service consumption hooks are deferred until the next tick await Promise.resolve(); // Service consumption hooks are deferred until the next tick
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
it('does not show the view if Atom is updated to a newer version than notified', async () => { it('does not show the view if Atom is updated to a newer version than notified', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
await atom.packages.deactivatePackage('about') await atom.packages.deactivatePackage('about');
atomVersion = '43.0.0' atomVersion = '43.0.0';
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
await Promise.resolve() // Service consumption hooks are deferred until the next tick await Promise.resolve(); // Service consumption hooks are deferred until the next tick
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
}) });
}) });
describe('on a beta version', function () { describe('on a beta version', function() {
beforeEach(async () => { beforeEach(async () => {
atomVersion = '1.2.3-beta4' atomVersion = '1.2.3-beta4';
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
}) });
describe('with no update', () => { describe('with no update', () => {
it('does not show the view', () => { it('does not show the view', () => {
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
}) });
describe('with an update', () => { describe('with an update', () => {
it('shows the view when the update finishes downloading', () => { it('shows the view when the update finishes downloading', () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
expect(workspaceElement).toContain('.about-release-notes') expect(workspaceElement).toContain('.about-release-notes');
}) });
describe('clicking on the status', () => { describe('clicking on the status', () => {
it('opens the about page', async () => { it('opens the about page', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
workspaceElement.querySelector('.about-release-notes').click() workspaceElement.querySelector('.about-release-notes').click();
await conditionPromise(() => workspaceElement.querySelector('.about')) await conditionPromise(() =>
expect(workspaceElement.querySelector('.about')).toExist() workspaceElement.querySelector('.about')
}) );
}) expect(workspaceElement.querySelector('.about')).toExist();
});
});
it('continues to show the squirrel until Atom is updated to the new version', async () => { it('continues to show the squirrel until Atom is updated to the new version', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
expect(workspaceElement).toContain('.about-release-notes') expect(workspaceElement).toContain('.about-release-notes');
await atom.packages.deactivatePackage('about') await atom.packages.deactivatePackage('about');
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
await Promise.resolve() // Service consumption hooks are deferred until the next tick await Promise.resolve(); // Service consumption hooks are deferred until the next tick
expect(workspaceElement).toContain('.about-release-notes') expect(workspaceElement).toContain('.about-release-notes');
await atom.packages.deactivatePackage('about') await atom.packages.deactivatePackage('about');
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
atomVersion = '42.0.0' atomVersion = '42.0.0';
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
await Promise.resolve() // Service consumption hooks are deferred until the next tick await Promise.resolve(); // Service consumption hooks are deferred until the next tick
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
it('does not show the view if Atom is updated to a newer version than notified', async () => { it('does not show the view if Atom is updated to a newer version than notified', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
await atom.packages.deactivatePackage('about') await atom.packages.deactivatePackage('about');
atomVersion = '43.0.0' atomVersion = '43.0.0';
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
await Promise.resolve() // Service consumption hooks are deferred until the next tick await Promise.resolve(); // Service consumption hooks are deferred until the next tick
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
}) });
}) });
describe('on a development version', function () { describe('on a development version', function() {
beforeEach(async () => { beforeEach(async () => {
atomVersion = '1.2.3-dev-0123abcd' atomVersion = '1.2.3-dev-0123abcd';
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
}) });
describe('with no update', () => { describe('with no update', () => {
it('does not show the view', () => { it('does not show the view', () => {
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
}) });
describe('with a previously downloaded update', () => { describe('with a previously downloaded update', () => {
it('does not show the view', () => { it('does not show the view', () => {
window.localStorage.setItem('about:version-available', '42.0.0') window.localStorage.setItem('about:version-available', '42.0.0');
expect(workspaceElement).not.toContain('.about-release-notes') expect(workspaceElement).not.toContain('.about-release-notes');
}) });
}) });
}) });
}) });

View File

@ -1,26 +1,26 @@
/** @babel */ /** @babel */
const { now } = Date const { now } = Date;
const { setTimeout } = global const { setTimeout } = global;
export async function conditionPromise (condition) { export async function conditionPromise(condition) {
const startTime = now() const startTime = now();
while (true) { while (true) {
await timeoutPromise(100) await timeoutPromise(100);
if (await condition()) { if (await condition()) {
return return;
} }
if (now() - startTime > 5000) { if (now() - startTime > 5000) {
throw new Error('Timed out waiting on condition') throw new Error('Timed out waiting on condition');
} }
} }
} }
export function timeoutPromise (timeout) { export function timeoutPromise(timeout) {
return new Promise(function (resolve) { return new Promise(function(resolve) {
setTimeout(resolve, timeout) setTimeout(resolve, timeout);
}) });
} }

View File

@ -1,23 +1,23 @@
module.exports = { module.exports = {
updateError () { updateError() {
atom.autoUpdater.emitter.emit('update-error') atom.autoUpdater.emitter.emit('update-error');
}, },
checkForUpdate () { checkForUpdate() {
atom.autoUpdater.emitter.emit('did-begin-checking-for-update') atom.autoUpdater.emitter.emit('did-begin-checking-for-update');
}, },
updateNotAvailable () { updateNotAvailable() {
atom.autoUpdater.emitter.emit('update-not-available') atom.autoUpdater.emitter.emit('update-not-available');
}, },
downloadUpdate () { downloadUpdate() {
atom.autoUpdater.emitter.emit('did-begin-downloading-update') atom.autoUpdater.emitter.emit('did-begin-downloading-update');
}, },
finishDownloadingUpdate (releaseVersion) { finishDownloadingUpdate(releaseVersion) {
atom.autoUpdater.emitter.emit('did-complete-downloading-update', { atom.autoUpdater.emitter.emit('did-complete-downloading-update', {
releaseVersion releaseVersion
}) });
} }
} };

View File

@ -1,32 +1,32 @@
const UpdateManager = require('../lib/update-manager') const UpdateManager = require('../lib/update-manager');
describe('UpdateManager', () => { describe('UpdateManager', () => {
let updateManager let updateManager;
beforeEach(() => { beforeEach(() => {
updateManager = new UpdateManager() updateManager = new UpdateManager();
}) });
describe('::getReleaseNotesURLForVersion', () => { describe('::getReleaseNotesURLForVersion', () => {
it('returns atom.io releases when dev version', () => { it('returns atom.io releases when dev version', () => {
expect( expect(
updateManager.getReleaseNotesURLForVersion('1.7.0-dev-e44b57d') updateManager.getReleaseNotesURLForVersion('1.7.0-dev-e44b57d')
).toContain('atom.io/releases') ).toContain('atom.io/releases');
}) });
it('returns the page for the release when not a dev version', () => { it('returns the page for the release when not a dev version', () => {
expect(updateManager.getReleaseNotesURLForVersion('1.7.0')).toContain( expect(updateManager.getReleaseNotesURLForVersion('1.7.0')).toContain(
'atom/atom/releases/tag/v1.7.0' 'atom/atom/releases/tag/v1.7.0'
) );
expect(updateManager.getReleaseNotesURLForVersion('v1.7.0')).toContain( expect(updateManager.getReleaseNotesURLForVersion('v1.7.0')).toContain(
'atom/atom/releases/tag/v1.7.0' 'atom/atom/releases/tag/v1.7.0'
) );
expect( expect(
updateManager.getReleaseNotesURLForVersion('1.7.0-beta10') updateManager.getReleaseNotesURLForVersion('1.7.0-beta10')
).toContain('atom/atom/releases/tag/v1.7.0-beta10') ).toContain('atom/atom/releases/tag/v1.7.0-beta10');
expect( expect(
updateManager.getReleaseNotesURLForVersion('1.7.0-nightly10') updateManager.getReleaseNotesURLForVersion('1.7.0-nightly10')
).toContain('atom/atom-nightly-releases/releases/tag/v1.7.0-nightly10') ).toContain('atom/atom-nightly-releases/releases/tag/v1.7.0-nightly10');
}) });
}) });
}) });

View File

@ -1,385 +1,387 @@
const { shell } = require('electron') const { shell } = require('electron');
const main = require('../lib/main') const main = require('../lib/main');
const AboutView = require('../lib/components/about-view') const AboutView = require('../lib/components/about-view');
const UpdateView = require('../lib/components/update-view') const UpdateView = require('../lib/components/update-view');
const MockUpdater = require('./mocks/updater') const MockUpdater = require('./mocks/updater');
describe('UpdateView', () => { describe('UpdateView', () => {
let aboutElement let aboutElement;
let updateManager let updateManager;
let workspaceElement let workspaceElement;
let scheduler let scheduler;
beforeEach(async () => { beforeEach(async () => {
let storage = {} let storage = {};
spyOn(window.localStorage, 'setItem').andCallFake((key, value) => { spyOn(window.localStorage, 'setItem').andCallFake((key, value) => {
storage[key] = value storage[key] = value;
}) });
spyOn(window.localStorage, 'getItem').andCallFake(key => { spyOn(window.localStorage, 'getItem').andCallFake(key => {
return storage[key] return storage[key];
}) });
workspaceElement = atom.views.getView(atom.workspace) workspaceElement = atom.views.getView(atom.workspace);
await atom.packages.activatePackage('about') await atom.packages.activatePackage('about');
spyOn(atom.autoUpdater, 'getState').andReturn('idle') spyOn(atom.autoUpdater, 'getState').andReturn('idle');
spyOn(atom.autoUpdater, 'checkForUpdate') spyOn(atom.autoUpdater, 'checkForUpdate');
spyOn(atom.autoUpdater, 'platformSupportsUpdates').andReturn(true) spyOn(atom.autoUpdater, 'platformSupportsUpdates').andReturn(true);
}) });
describe('when the About page is open', () => { describe('when the About page is open', () => {
beforeEach(async () => { beforeEach(async () => {
jasmine.attachToDOM(workspaceElement) jasmine.attachToDOM(workspaceElement);
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
aboutElement = workspaceElement.querySelector('.about') aboutElement = workspaceElement.querySelector('.about');
updateManager = main.model.state.updateManager updateManager = main.model.state.updateManager;
scheduler = AboutView.getScheduler() scheduler = AboutView.getScheduler();
}) });
describe('when the updates are not supported by the platform', () => { describe('when the updates are not supported by the platform', () => {
beforeEach(async () => { beforeEach(async () => {
atom.autoUpdater.platformSupportsUpdates.andReturn(false) atom.autoUpdater.platformSupportsUpdates.andReturn(false);
updateManager.resetState() updateManager.resetState();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
}) });
it('hides the auto update UI and shows the update instructions link', async () => { it('hides the auto update UI and shows the update instructions link', async () => {
expect( expect(
aboutElement.querySelector('.about-update-action-button') aboutElement.querySelector('.about-update-action-button')
).not.toBeVisible() ).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.about-auto-updates') aboutElement.querySelector('.about-auto-updates')
).not.toBeVisible() ).not.toBeVisible();
}) });
it('opens the update instructions page when the instructions link is clicked', async () => { it('opens the update instructions page when the instructions link is clicked', async () => {
spyOn(shell, 'openExternal') spyOn(shell, 'openExternal');
let link = aboutElement.querySelector( let link = aboutElement.querySelector(
'.app-unsupported .about-updates-instructions' '.app-unsupported .about-updates-instructions'
) );
link.click() link.click();
let args = shell.openExternal.mostRecentCall.args let args = shell.openExternal.mostRecentCall.args;
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(args[0]).toContain('installing-atom') expect(args[0]).toContain('installing-atom');
}) });
}) });
describe('when updates are supported by the platform', () => { describe('when updates are supported by the platform', () => {
beforeEach(async () => { beforeEach(async () => {
atom.autoUpdater.platformSupportsUpdates.andReturn(true) atom.autoUpdater.platformSupportsUpdates.andReturn(true);
updateManager.resetState() updateManager.resetState();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
}) });
it('shows the auto update UI', () => { it('shows the auto update UI', () => {
expect(aboutElement.querySelector('.about-updates')).toBeVisible() expect(aboutElement.querySelector('.about-updates')).toBeVisible();
}) });
it('shows the correct panels when the app checks for updates and there is no update available', async () => { it('shows the correct panels when the app checks for updates and there is no update available', async () => {
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
).toBeVisible() ).toBeVisible();
MockUpdater.checkForUpdate() MockUpdater.checkForUpdate();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible() expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.app-checking-for-updates') aboutElement.querySelector('.app-checking-for-updates')
).toBeVisible() ).toBeVisible();
MockUpdater.updateNotAvailable() MockUpdater.updateNotAvailable();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect(aboutElement.querySelector('.app-up-to-date')).toBeVisible() expect(aboutElement.querySelector('.app-up-to-date')).toBeVisible();
expect( expect(
aboutElement.querySelector('.app-checking-for-updates') aboutElement.querySelector('.app-checking-for-updates')
).not.toBeVisible() ).not.toBeVisible();
}) });
it('shows the correct panels when the app checks for updates and encounters an error', async () => { it('shows the correct panels when the app checks for updates and encounters an error', async () => {
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
).toBeVisible() ).toBeVisible();
MockUpdater.checkForUpdate() MockUpdater.checkForUpdate();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible() expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.app-checking-for-updates') aboutElement.querySelector('.app-checking-for-updates')
).toBeVisible() ).toBeVisible();
spyOn(atom.autoUpdater, 'getErrorMessage').andReturn('an error message') spyOn(atom.autoUpdater, 'getErrorMessage').andReturn(
MockUpdater.updateError() 'an error message'
await scheduler.getNextUpdatePromise() );
expect(aboutElement.querySelector('.app-update-error')).toBeVisible() MockUpdater.updateError();
await scheduler.getNextUpdatePromise();
expect(aboutElement.querySelector('.app-update-error')).toBeVisible();
expect( expect(
aboutElement.querySelector('.app-error-message').textContent aboutElement.querySelector('.app-error-message').textContent
).toBe('an error message') ).toBe('an error message');
expect( expect(
aboutElement.querySelector('.app-checking-for-updates') aboutElement.querySelector('.app-checking-for-updates')
).not.toBeVisible() ).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.about-update-action-button').disabled aboutElement.querySelector('.about-update-action-button').disabled
).toBe(false) ).toBe(false);
expect( expect(
aboutElement.querySelector('.about-update-action-button').textContent aboutElement.querySelector('.about-update-action-button').textContent
).toBe('Check now') ).toBe('Check now');
}) });
it('shows the correct panels and button states when the app checks for updates and an update is downloaded', async () => { it('shows the correct panels and button states when the app checks for updates and an update is downloaded', async () => {
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector('.about-update-action-button').disabled aboutElement.querySelector('.about-update-action-button').disabled
).toBe(false) ).toBe(false);
expect( expect(
aboutElement.querySelector('.about-update-action-button').textContent aboutElement.querySelector('.about-update-action-button').textContent
).toBe('Check now') ).toBe('Check now');
MockUpdater.checkForUpdate() MockUpdater.checkForUpdate();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible() expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.app-checking-for-updates') aboutElement.querySelector('.app-checking-for-updates')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector('.about-update-action-button').disabled aboutElement.querySelector('.about-update-action-button').disabled
).toBe(true) ).toBe(true);
expect( expect(
aboutElement.querySelector('.about-update-action-button').textContent aboutElement.querySelector('.about-update-action-button').textContent
).toBe('Check now') ).toBe('Check now');
MockUpdater.downloadUpdate() MockUpdater.downloadUpdate();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect( expect(
aboutElement.querySelector('.app-checking-for-updates') aboutElement.querySelector('.app-checking-for-updates')
).not.toBeVisible() ).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.app-downloading-update') aboutElement.querySelector('.app-downloading-update')
).toBeVisible() ).toBeVisible();
// TODO: at some point it would be nice to be able to cancel an update download, and then this would be a cancel button // TODO: at some point it would be nice to be able to cancel an update download, and then this would be a cancel button
expect( expect(
aboutElement.querySelector('.about-update-action-button').disabled aboutElement.querySelector('.about-update-action-button').disabled
).toBe(true) ).toBe(true);
expect( expect(
aboutElement.querySelector('.about-update-action-button').textContent aboutElement.querySelector('.about-update-action-button').textContent
).toBe('Check now') ).toBe('Check now');
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect( expect(
aboutElement.querySelector('.app-downloading-update') aboutElement.querySelector('.app-downloading-update')
).not.toBeVisible() ).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.app-update-available-to-install') aboutElement.querySelector('.app-update-available-to-install')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector( aboutElement.querySelector(
'.app-update-available-to-install .about-updates-version' '.app-update-available-to-install .about-updates-version'
).textContent ).textContent
).toBe('42.0.0') ).toBe('42.0.0');
expect( expect(
aboutElement.querySelector('.about-update-action-button').disabled aboutElement.querySelector('.about-update-action-button').disabled
).toBe(false) ).toBe(false);
expect( expect(
aboutElement.querySelector('.about-update-action-button').textContent aboutElement.querySelector('.about-update-action-button').textContent
).toBe('Restart and install') ).toBe('Restart and install');
}) });
it('opens the release notes for the downloaded release when the release notes link are clicked', async () => { it('opens the release notes for the downloaded release when the release notes link are clicked', async () => {
MockUpdater.finishDownloadingUpdate('1.2.3') MockUpdater.finishDownloadingUpdate('1.2.3');
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
spyOn(shell, 'openExternal') spyOn(shell, 'openExternal');
let link = aboutElement.querySelector( let link = aboutElement.querySelector(
'.app-update-available-to-install .about-updates-release-notes' '.app-update-available-to-install .about-updates-release-notes'
) );
link.click() link.click();
let args = shell.openExternal.mostRecentCall.args let args = shell.openExternal.mostRecentCall.args;
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(args[0]).toContain('/v1.2.3') expect(args[0]).toContain('/v1.2.3');
}) });
it('executes checkForUpdate() when the check for update button is clicked', () => { it('executes checkForUpdate() when the check for update button is clicked', () => {
let button = aboutElement.querySelector('.about-update-action-button') let button = aboutElement.querySelector('.about-update-action-button');
button.click() button.click();
expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled() expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled();
}) });
it('executes restartAndInstallUpdate() when the restart and install button is clicked', async () => { it('executes restartAndInstallUpdate() when the restart and install button is clicked', async () => {
spyOn(atom.autoUpdater, 'restartAndInstallUpdate') spyOn(atom.autoUpdater, 'restartAndInstallUpdate');
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
let button = aboutElement.querySelector('.about-update-action-button') let button = aboutElement.querySelector('.about-update-action-button');
button.click() button.click();
expect(atom.autoUpdater.restartAndInstallUpdate).toHaveBeenCalled() expect(atom.autoUpdater.restartAndInstallUpdate).toHaveBeenCalled();
}) });
it("starts in the same state as atom's AutoUpdateManager", async () => { it("starts in the same state as atom's AutoUpdateManager", async () => {
atom.autoUpdater.getState.andReturn('downloading') atom.autoUpdater.getState.andReturn('downloading');
updateManager.resetState() updateManager.resetState();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect( expect(
aboutElement.querySelector('.app-checking-for-updates') aboutElement.querySelector('.app-checking-for-updates')
).not.toBeVisible() ).not.toBeVisible();
expect( expect(
aboutElement.querySelector('.app-downloading-update') aboutElement.querySelector('.app-downloading-update')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector('.about-update-action-button').disabled aboutElement.querySelector('.about-update-action-button').disabled
).toBe(true) ).toBe(true);
expect( expect(
aboutElement.querySelector('.about-update-action-button').textContent aboutElement.querySelector('.about-update-action-button').textContent
).toBe('Check now') ).toBe('Check now');
}) });
describe('when core.automaticallyUpdate is toggled', () => { describe('when core.automaticallyUpdate is toggled', () => {
beforeEach(async () => { beforeEach(async () => {
expect(atom.config.get('core.automaticallyUpdate')).toBe(true) expect(atom.config.get('core.automaticallyUpdate')).toBe(true);
atom.autoUpdater.checkForUpdate.reset() atom.autoUpdater.checkForUpdate.reset();
}) });
it('shows the auto update UI', async () => { it('shows the auto update UI', async () => {
expect( expect(
aboutElement.querySelector('.about-auto-updates input').checked aboutElement.querySelector('.about-auto-updates input').checked
).toBe(true) ).toBe(true);
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
.textContent .textContent
).toBe('Atom will check for updates automatically') ).toBe('Atom will check for updates automatically');
atom.config.set('core.automaticallyUpdate', false) atom.config.set('core.automaticallyUpdate', false);
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect( expect(
aboutElement.querySelector('.about-auto-updates input').checked aboutElement.querySelector('.about-auto-updates input').checked
).toBe(false) ).toBe(false);
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
.textContent .textContent
).toBe('Automatic updates are disabled please check manually') ).toBe('Automatic updates are disabled please check manually');
}) });
it('updates config and the UI when the checkbox is used to toggle', async () => { it('updates config and the UI when the checkbox is used to toggle', async () => {
expect( expect(
aboutElement.querySelector('.about-auto-updates input').checked aboutElement.querySelector('.about-auto-updates input').checked
).toBe(true) ).toBe(true);
aboutElement.querySelector('.about-auto-updates input').click() aboutElement.querySelector('.about-auto-updates input').click();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect(atom.config.get('core.automaticallyUpdate')).toBe(false) expect(atom.config.get('core.automaticallyUpdate')).toBe(false);
expect( expect(
aboutElement.querySelector('.about-auto-updates input').checked aboutElement.querySelector('.about-auto-updates input').checked
).toBe(false) ).toBe(false);
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
.textContent .textContent
).toBe('Automatic updates are disabled please check manually') ).toBe('Automatic updates are disabled please check manually');
aboutElement.querySelector('.about-auto-updates input').click() aboutElement.querySelector('.about-auto-updates input').click();
await scheduler.getNextUpdatePromise() await scheduler.getNextUpdatePromise();
expect(atom.config.get('core.automaticallyUpdate')).toBe(true) expect(atom.config.get('core.automaticallyUpdate')).toBe(true);
expect( expect(
aboutElement.querySelector('.about-auto-updates input').checked aboutElement.querySelector('.about-auto-updates input').checked
).toBe(true) ).toBe(true);
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector('.about-default-update-message') aboutElement.querySelector('.about-default-update-message')
.textContent .textContent
).toBe('Atom will check for updates automatically') ).toBe('Atom will check for updates automatically');
}) });
describe('checking for updates', function () { describe('checking for updates', function() {
afterEach(() => { afterEach(() => {
this.updateView = null this.updateView = null;
}) });
it('checks for update when the about page is shown', () => { it('checks for update when the about page is shown', () => {
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled() expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
this.updateView = new UpdateView({ this.updateView = new UpdateView({
updateManager: updateManager, updateManager: updateManager,
availableVersion: '9999.0.0', availableVersion: '9999.0.0',
viewUpdateReleaseNotes: () => {} viewUpdateReleaseNotes: () => {}
}) });
expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled() expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled();
}) });
it('does not check for update when the about page is shown and the update manager is not in the idle state', () => { it('does not check for update when the about page is shown and the update manager is not in the idle state', () => {
atom.autoUpdater.getState.andReturn('downloading') atom.autoUpdater.getState.andReturn('downloading');
updateManager.resetState() updateManager.resetState();
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled() expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
this.updateView = new UpdateView({ this.updateView = new UpdateView({
updateManager: updateManager, updateManager: updateManager,
availableVersion: '9999.0.0', availableVersion: '9999.0.0',
viewUpdateReleaseNotes: () => {} viewUpdateReleaseNotes: () => {}
}) });
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled() expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
}) });
it('does not check for update when the about page is shown and auto updates are turned off', () => { it('does not check for update when the about page is shown and auto updates are turned off', () => {
atom.config.set('core.automaticallyUpdate', false) atom.config.set('core.automaticallyUpdate', false);
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled() expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
this.updateView = new UpdateView({ this.updateView = new UpdateView({
updateManager: updateManager, updateManager: updateManager,
availableVersion: '9999.0.0', availableVersion: '9999.0.0',
viewUpdateReleaseNotes: () => {} viewUpdateReleaseNotes: () => {}
}) });
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled() expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
}) });
}) });
}) });
}) });
}) });
describe('when the About page is not open and an update is downloaded', () => { describe('when the About page is not open and an update is downloaded', () => {
it('should display the new version when it is opened', async () => { it('should display the new version when it is opened', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0') MockUpdater.finishDownloadingUpdate('42.0.0');
jasmine.attachToDOM(workspaceElement) jasmine.attachToDOM(workspaceElement);
await atom.workspace.open('atom://about') await atom.workspace.open('atom://about');
aboutElement = workspaceElement.querySelector('.about') aboutElement = workspaceElement.querySelector('.about');
updateManager = main.model.state.updateManager updateManager = main.model.state.updateManager;
scheduler = AboutView.getScheduler() scheduler = AboutView.getScheduler();
expect( expect(
aboutElement.querySelector('.app-update-available-to-install') aboutElement.querySelector('.app-update-available-to-install')
).toBeVisible() ).toBeVisible();
expect( expect(
aboutElement.querySelector( aboutElement.querySelector(
'.app-update-available-to-install .about-updates-version' '.app-update-available-to-install .about-updates-version'
).textContent ).textContent
).toBe('42.0.0') ).toBe('42.0.0');
expect( expect(
aboutElement.querySelector('.about-update-action-button').disabled aboutElement.querySelector('.about-update-action-button').disabled
).toBe(false) ).toBe(false);
expect( expect(
aboutElement.querySelector('.about-update-action-button').textContent aboutElement.querySelector('.about-update-action-button').textContent
).toBe('Restart and install') ).toBe('Restart and install');
}) });
}) });
}) });

View File

@ -1,56 +1,56 @@
/** @babel */ /** @babel */
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
module.exports = { module.exports = {
async enumerate () { async enumerate() {
if (atom.inDevMode()) { if (atom.inDevMode()) {
return [] return [];
} }
const duplicatePackages = [] const duplicatePackages = [];
const names = atom.packages.getAvailablePackageNames() const names = atom.packages.getAvailablePackageNames();
for (let name of names) { for (let name of names) {
if (atom.packages.isBundledPackage(name)) { if (atom.packages.isBundledPackage(name)) {
const isDuplicatedPackage = await this.isInstalledAsCommunityPackage( const isDuplicatedPackage = await this.isInstalledAsCommunityPackage(
name name
) );
if (isDuplicatedPackage) { if (isDuplicatedPackage) {
duplicatePackages.push(name) duplicatePackages.push(name);
} }
} }
} }
return duplicatePackages return duplicatePackages;
}, },
async isInstalledAsCommunityPackage (name) { async isInstalledAsCommunityPackage(name) {
const availablePackagePaths = atom.packages.getPackageDirPaths() const availablePackagePaths = atom.packages.getPackageDirPaths();
for (let packagePath of availablePackagePaths) { for (let packagePath of availablePackagePaths) {
const candidate = path.join(packagePath, name) const candidate = path.join(packagePath, name);
if (fs.existsSync(candidate)) { if (fs.existsSync(candidate)) {
const realPath = await this.realpath(candidate) const realPath = await this.realpath(candidate);
if (realPath === candidate) { if (realPath === candidate) {
return true return true;
} }
} }
} }
return false return false;
}, },
realpath (path) { realpath(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.realpath(path, function (error, realpath) { fs.realpath(path, function(error, realpath) {
if (error) { if (error) {
reject(error) reject(error);
} else { } else {
resolve(realpath) resolve(realpath);
} }
}) });
}) });
} }
} };

View File

@ -1,19 +1,19 @@
/** @babel */ /** @babel */
const dalek = require('./dalek') const dalek = require('./dalek');
const Grim = require('grim') const Grim = require('grim');
module.exports = { module.exports = {
activate () { activate() {
atom.packages.onDidActivateInitialPackages(async () => { atom.packages.onDidActivateInitialPackages(async () => {
const duplicates = await dalek.enumerate() const duplicates = await dalek.enumerate();
for (let i = 0; i < duplicates.length; i++) { for (let i = 0; i < duplicates.length; i++) {
const duplicate = duplicates[i] const duplicate = duplicates[i];
Grim.deprecate( Grim.deprecate(
`You have the core package "${duplicate}" installed as a community package. See https://github.com/atom/atom/blob/master/packages/dalek/README.md for how this causes problems and instructions on how to correct the situation.`, `You have the core package "${duplicate}" installed as a community package. See https://github.com/atom/atom/blob/master/packages/dalek/README.md for how this causes problems and instructions on how to correct the situation.`,
{ packageName: duplicate } { packageName: duplicate }
) );
} }
}) });
} }
} };

View File

@ -1,21 +1,21 @@
/** @babel */ /** @babel */
const assert = require('assert') const assert = require('assert');
const fs = require('fs') const fs = require('fs');
const sinon = require('sinon') const sinon = require('sinon');
const path = require('path') const path = require('path');
const dalek = require('../lib/dalek') const dalek = require('../lib/dalek');
describe('dalek', function () { describe('dalek', function() {
describe('enumerate', function () { describe('enumerate', function() {
let availablePackages = {} let availablePackages = {};
let realPaths = {} let realPaths = {};
let bundledPackages = [] let bundledPackages = [];
let packageDirPaths = [] let packageDirPaths = [];
let sandbox = null let sandbox = null;
beforeEach(function () { beforeEach(function() {
availablePackages = { availablePackages = {
'an-unduplicated-installed-package': path.join( 'an-unduplicated-installed-package': path.join(
'Users', 'Users',
@ -36,66 +36,68 @@ describe('dalek', function () {
'node_modules', 'node_modules',
'unduplicated-package' 'unduplicated-package'
) )
} };
atom.devMode = false atom.devMode = false;
bundledPackages = ['duplicated-package', 'unduplicated-package'] bundledPackages = ['duplicated-package', 'unduplicated-package'];
packageDirPaths = [path.join('Users', 'username', '.atom', 'packages')] packageDirPaths = [path.join('Users', 'username', '.atom', 'packages')];
sandbox = sinon.sandbox.create() sandbox = sinon.sandbox.create();
sandbox sandbox
.stub(dalek, 'realpath') .stub(dalek, 'realpath')
.callsFake(filePath => Promise.resolve(realPaths[filePath] || filePath)) .callsFake(filePath =>
Promise.resolve(realPaths[filePath] || filePath)
);
sandbox.stub(atom.packages, 'isBundledPackage').callsFake(packageName => { sandbox.stub(atom.packages, 'isBundledPackage').callsFake(packageName => {
return bundledPackages.includes(packageName) return bundledPackages.includes(packageName);
}) });
sandbox sandbox
.stub(atom.packages, 'getAvailablePackageNames') .stub(atom.packages, 'getAvailablePackageNames')
.callsFake(() => Object.keys(availablePackages)) .callsFake(() => Object.keys(availablePackages));
sandbox.stub(atom.packages, 'getPackageDirPaths').callsFake(() => { sandbox.stub(atom.packages, 'getPackageDirPaths').callsFake(() => {
return packageDirPaths return packageDirPaths;
}) });
sandbox.stub(fs, 'existsSync').callsFake(candidate => { sandbox.stub(fs, 'existsSync').callsFake(candidate => {
return ( return (
Object.values(availablePackages).includes(candidate) && Object.values(availablePackages).includes(candidate) &&
!candidate.includes(atom.getLoadSettings().resourcePath) !candidate.includes(atom.getLoadSettings().resourcePath)
) );
}) });
}) });
afterEach(function () { afterEach(function() {
sandbox.restore() sandbox.restore();
}) });
it('returns a list of duplicate names', async function () { it('returns a list of duplicate names', async function() {
assert.deepEqual(await dalek.enumerate(), ['duplicated-package']) assert.deepEqual(await dalek.enumerate(), ['duplicated-package']);
}) });
describe('when in dev mode', function () { describe('when in dev mode', function() {
beforeEach(function () { beforeEach(function() {
atom.devMode = true atom.devMode = true;
}) });
it('always returns an empty list', async function () { it('always returns an empty list', async function() {
assert.deepEqual(await dalek.enumerate(), []) assert.deepEqual(await dalek.enumerate(), []);
}) });
}) });
describe('when a package is symlinked into the package directory', async function () { describe('when a package is symlinked into the package directory', async function() {
beforeEach(function () { beforeEach(function() {
const realPath = path.join('Users', 'username', 'duplicated-package') const realPath = path.join('Users', 'username', 'duplicated-package');
const packagePath = path.join( const packagePath = path.join(
'Users', 'Users',
'username', 'username',
'.atom', '.atom',
'packages', 'packages',
'duplicated-package' 'duplicated-package'
) );
realPaths[packagePath] = realPath realPaths[packagePath] = realPath;
}) });
it('is not included in the list of duplicate names', async function () { it('is not included in the list of duplicate names', async function() {
assert.deepEqual(await dalek.enumerate(), []) assert.deepEqual(await dalek.enumerate(), []);
}) });
}) });
}) });
}) });

View File

@ -1,2 +1,2 @@
const createRunner = require('atom-mocha-test-runner').createRunner const createRunner = require('atom-mocha-test-runner').createRunner;
module.exports = createRunner({ testSuffixes: ['test.js'] }) module.exports = createRunner({ testSuffixes: ['test.js'] });

View File

@ -1,88 +1,88 @@
/** @babel */ /** @babel */
/** @jsx etch.dom */ /** @jsx etch.dom */
import _ from 'underscore-plus' import _ from 'underscore-plus';
import { CompositeDisposable } from 'atom' import { CompositeDisposable } from 'atom';
import etch from 'etch' import etch from 'etch';
import fs from 'fs-plus' import fs from 'fs-plus';
import Grim from 'grim' import Grim from 'grim';
import marked from 'marked' import marked from 'marked';
import path from 'path' import path from 'path';
import shell from 'shell' import shell from 'shell';
export default class DeprecationCopView { export default class DeprecationCopView {
constructor ({ uri }) { constructor({ uri }) {
this.uri = uri this.uri = uri;
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
this.subscriptions.add( this.subscriptions.add(
Grim.on('updated', () => { Grim.on('updated', () => {
etch.update(this) etch.update(this);
}) })
) );
// TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. // TODO: Remove conditional when the new StyleManager deprecation APIs reach stable.
if (atom.styles.onDidUpdateDeprecations) { if (atom.styles.onDidUpdateDeprecations) {
this.subscriptions.add( this.subscriptions.add(
atom.styles.onDidUpdateDeprecations(() => { atom.styles.onDidUpdateDeprecations(() => {
etch.update(this) etch.update(this);
}) })
) );
} }
etch.initialize(this) etch.initialize(this);
this.subscriptions.add( this.subscriptions.add(
atom.commands.add(this.element, { atom.commands.add(this.element, {
'core:move-up': () => { 'core:move-up': () => {
this.scrollUp() this.scrollUp();
}, },
'core:move-down': () => { 'core:move-down': () => {
this.scrollDown() this.scrollDown();
}, },
'core:page-up': () => { 'core:page-up': () => {
this.pageUp() this.pageUp();
}, },
'core:page-down': () => { 'core:page-down': () => {
this.pageDown() this.pageDown();
}, },
'core:move-to-top': () => { 'core:move-to-top': () => {
this.scrollToTop() this.scrollToTop();
}, },
'core:move-to-bottom': () => { 'core:move-to-bottom': () => {
this.scrollToBottom() this.scrollToBottom();
} }
}) })
) );
} }
serialize () { serialize() {
return { return {
deserializer: this.constructor.name, deserializer: this.constructor.name,
uri: this.getURI(), uri: this.getURI(),
version: 1 version: 1
} };
} }
destroy () { destroy() {
this.subscriptions.dispose() this.subscriptions.dispose();
return etch.destroy(this) return etch.destroy(this);
} }
update () { update() {
return etch.update(this) return etch.update(this);
} }
render () { render() {
return ( return (
<div <div
className='deprecation-cop pane-item native-key-bindings' className="deprecation-cop pane-item native-key-bindings"
tabIndex='-1' tabIndex="-1"
> >
<div className='panel'> <div className="panel">
<div className='padded deprecation-overview'> <div className="padded deprecation-overview">
<div className='pull-right btn-group'> <div className="pull-right btn-group">
<button <button
className='btn btn-primary check-for-update' className="btn btn-primary check-for-update"
onclick={event => { onclick={event => {
event.preventDefault() event.preventDefault();
this.checkForUpdates() this.checkForUpdates();
}} }}
> >
Check for Updates Check for Updates
@ -90,53 +90,53 @@ export default class DeprecationCopView {
</div> </div>
</div> </div>
<div className='panel-heading'> <div className="panel-heading">
<span>Deprecated calls</span> <span>Deprecated calls</span>
</div> </div>
<ul className='list-tree has-collapsable-children'> <ul className="list-tree has-collapsable-children">
{this.renderDeprecatedCalls()} {this.renderDeprecatedCalls()}
</ul> </ul>
<div className='panel-heading'> <div className="panel-heading">
<span>Deprecated selectors</span> <span>Deprecated selectors</span>
</div> </div>
<ul className='selectors list-tree has-collapsable-children'> <ul className="selectors list-tree has-collapsable-children">
{this.renderDeprecatedSelectors()} {this.renderDeprecatedSelectors()}
</ul> </ul>
</div> </div>
</div> </div>
) );
} }
renderDeprecatedCalls () { renderDeprecatedCalls() {
const deprecationsByPackageName = this.getDeprecatedCallsByPackageName() const deprecationsByPackageName = this.getDeprecatedCallsByPackageName();
const packageNames = Object.keys(deprecationsByPackageName) const packageNames = Object.keys(deprecationsByPackageName);
if (packageNames.length === 0) { if (packageNames.length === 0) {
return <li className='list-item'>No deprecated calls</li> return <li className="list-item">No deprecated calls</li>;
} else { } else {
return packageNames.sort().map(packageName => ( return packageNames.sort().map(packageName => (
<li className='deprecation list-nested-item collapsed'> <li className="deprecation list-nested-item collapsed">
<div <div
className='deprecation-info list-item' className="deprecation-info list-item"
onclick={event => onclick={event =>
event.target.parentElement.classList.toggle('collapsed') event.target.parentElement.classList.toggle('collapsed')
} }
> >
<span className='text-highlight'>{packageName || 'atom core'}</span> <span className="text-highlight">{packageName || 'atom core'}</span>
<span>{` (${_.pluralize( <span>{` (${_.pluralize(
deprecationsByPackageName[packageName].length, deprecationsByPackageName[packageName].length,
'deprecation' 'deprecation'
)})`}</span> )})`}</span>
</div> </div>
<ul className='list'> <ul className="list">
{this.renderPackageActionsIfNeeded(packageName)} {this.renderPackageActionsIfNeeded(packageName)}
{deprecationsByPackageName[packageName].map( {deprecationsByPackageName[packageName].map(
({ deprecation, stack }) => ( ({ deprecation, stack }) => (
<li className='list-item deprecation-detail'> <li className="list-item deprecation-detail">
<span className='text-warning icon icon-alert' /> <span className="text-warning icon icon-alert" />
<div <div
className='list-item deprecation-message' className="list-item deprecation-message"
innerHTML={marked(deprecation.getMessage())} innerHTML={marked(deprecation.getMessage())}
/> />
{this.renderIssueURLIfNeeded( {this.renderIssueURLIfNeeded(
@ -144,17 +144,17 @@ export default class DeprecationCopView {
deprecation, deprecation,
this.buildIssueURL(packageName, deprecation, stack) this.buildIssueURL(packageName, deprecation, stack)
)} )}
<div className='stack-trace'> <div className="stack-trace">
{stack.map(({ functionName, location }) => ( {stack.map(({ functionName, location }) => (
<div className='stack-line'> <div className="stack-line">
<span>{functionName}</span> <span>{functionName}</span>
<span> - </span> <span> - </span>
<a <a
className='stack-line-location' className="stack-line-location"
href={location} href={location}
onclick={event => { onclick={event => {
event.preventDefault() event.preventDefault();
this.openLocation(location) this.openLocation(location);
}} }}
> >
{location} {location}
@ -167,56 +167,56 @@ export default class DeprecationCopView {
)} )}
</ul> </ul>
</li> </li>
)) ));
} }
} }
renderDeprecatedSelectors () { renderDeprecatedSelectors() {
const deprecationsByPackageName = this.getDeprecatedSelectorsByPackageName() const deprecationsByPackageName = this.getDeprecatedSelectorsByPackageName();
const packageNames = Object.keys(deprecationsByPackageName) const packageNames = Object.keys(deprecationsByPackageName);
if (packageNames.length === 0) { if (packageNames.length === 0) {
return <li className='list-item'>No deprecated selectors</li> return <li className="list-item">No deprecated selectors</li>;
} else { } else {
return packageNames.map(packageName => ( return packageNames.map(packageName => (
<li className='deprecation list-nested-item collapsed'> <li className="deprecation list-nested-item collapsed">
<div <div
className='deprecation-info list-item' className="deprecation-info list-item"
onclick={event => onclick={event =>
event.target.parentElement.classList.toggle('collapsed') event.target.parentElement.classList.toggle('collapsed')
} }
> >
<span className='text-highlight'>{packageName}</span> <span className="text-highlight">{packageName}</span>
</div> </div>
<ul className='list'> <ul className="list">
{this.renderPackageActionsIfNeeded(packageName)} {this.renderPackageActionsIfNeeded(packageName)}
{deprecationsByPackageName[packageName].map( {deprecationsByPackageName[packageName].map(
({ packagePath, sourcePath, deprecation }) => { ({ packagePath, sourcePath, deprecation }) => {
const relativeSourcePath = path.relative( const relativeSourcePath = path.relative(
packagePath, packagePath,
sourcePath sourcePath
) );
const issueTitle = `Deprecated selector in \`${relativeSourcePath}\`` const issueTitle = `Deprecated selector in \`${relativeSourcePath}\``;
const issueBody = `In \`${relativeSourcePath}\`: \n\n${ const issueBody = `In \`${relativeSourcePath}\`: \n\n${
deprecation.message deprecation.message
}` }`;
return ( return (
<li className='list-item source-file'> <li className="list-item source-file">
<a <a
className='source-url' className="source-url"
href={sourcePath} href={sourcePath}
onclick={event => { onclick={event => {
event.preventDefault() event.preventDefault();
this.openLocation(sourcePath) this.openLocation(sourcePath);
}} }}
> >
{relativeSourcePath} {relativeSourcePath}
</a> </a>
<ul className='list'> <ul className="list">
<li className='list-item deprecation-detail'> <li className="list-item deprecation-detail">
<span className='text-warning icon icon-alert' /> <span className="text-warning icon icon-alert" />
<div <div
className='list-item deprecation-message' className="list-item deprecation-message"
innerHTML={marked(deprecation.message)} innerHTML={marked(deprecation.message)}
/> />
{this.renderSelectorIssueURLIfNeeded( {this.renderSelectorIssueURLIfNeeded(
@ -227,138 +227,138 @@ export default class DeprecationCopView {
</li> </li>
</ul> </ul>
</li> </li>
) );
} }
)} )}
</ul> </ul>
</li> </li>
)) ));
} }
} }
renderPackageActionsIfNeeded (packageName) { renderPackageActionsIfNeeded(packageName) {
if (packageName && atom.packages.getLoadedPackage(packageName)) { if (packageName && atom.packages.getLoadedPackage(packageName)) {
return ( return (
<div className='padded'> <div className="padded">
<div className='btn-group'> <div className="btn-group">
<button <button
className='btn check-for-update' className="btn check-for-update"
onclick={event => { onclick={event => {
event.preventDefault() event.preventDefault();
this.checkForUpdates() this.checkForUpdates();
}} }}
> >
Check for Update Check for Update
</button> </button>
<button <button
className='btn disable-package' className="btn disable-package"
data-package-name={packageName} data-package-name={packageName}
onclick={event => { onclick={event => {
event.preventDefault() event.preventDefault();
this.disablePackage(packageName) this.disablePackage(packageName);
}} }}
> >
Disable Package Disable Package
</button> </button>
</div> </div>
</div> </div>
) );
} else { } else {
return '' return '';
} }
} }
encodeURI (str) { encodeURI(str) {
return encodeURI(str) return encodeURI(str)
.replace(/#/g, '%23') .replace(/#/g, '%23')
.replace(/;/g, '%3B') .replace(/;/g, '%3B')
.replace(/%20/g, '+') .replace(/%20/g, '+');
} }
renderSelectorIssueURLIfNeeded (packageName, issueTitle, issueBody) { renderSelectorIssueURLIfNeeded(packageName, issueTitle, issueBody) {
const repoURL = this.getRepoURL(packageName) const repoURL = this.getRepoURL(packageName);
if (repoURL) { if (repoURL) {
const issueURL = `${repoURL}/issues/new?title=${this.encodeURI( const issueURL = `${repoURL}/issues/new?title=${this.encodeURI(
issueTitle issueTitle
)}&body=${this.encodeURI(issueBody)}` )}&body=${this.encodeURI(issueBody)}`;
return ( return (
<div className='btn-toolbar'> <div className="btn-toolbar">
<button <button
className='btn issue-url' className="btn issue-url"
data-issue-title={issueTitle} data-issue-title={issueTitle}
data-repo-url={repoURL} data-repo-url={repoURL}
data-issue-url={issueURL} data-issue-url={issueURL}
onclick={event => { onclick={event => {
event.preventDefault() event.preventDefault();
this.openIssueURL(repoURL, issueURL, issueTitle) this.openIssueURL(repoURL, issueURL, issueTitle);
}} }}
> >
Report Issue Report Issue
</button> </button>
</div> </div>
) );
} else { } else {
return '' return '';
} }
} }
renderIssueURLIfNeeded (packageName, deprecation, issueURL) { renderIssueURLIfNeeded(packageName, deprecation, issueURL) {
if (packageName && issueURL) { if (packageName && issueURL) {
const repoURL = this.getRepoURL(packageName) const repoURL = this.getRepoURL(packageName);
const issueTitle = `${deprecation.getOriginName()} is deprecated.` const issueTitle = `${deprecation.getOriginName()} is deprecated.`;
return ( return (
<div className='btn-toolbar'> <div className="btn-toolbar">
<button <button
className='btn issue-url' className="btn issue-url"
data-issue-title={issueTitle} data-issue-title={issueTitle}
data-repo-url={repoURL} data-repo-url={repoURL}
data-issue-url={issueURL} data-issue-url={issueURL}
onclick={event => { onclick={event => {
event.preventDefault() event.preventDefault();
this.openIssueURL(repoURL, issueURL, issueTitle) this.openIssueURL(repoURL, issueURL, issueTitle);
}} }}
> >
Report Issue Report Issue
</button> </button>
</div> </div>
) );
} else { } else {
return '' return '';
} }
} }
buildIssueURL (packageName, deprecation, stack) { buildIssueURL(packageName, deprecation, stack) {
const repoURL = this.getRepoURL(packageName) const repoURL = this.getRepoURL(packageName);
if (repoURL) { if (repoURL) {
const title = `${deprecation.getOriginName()} is deprecated.` const title = `${deprecation.getOriginName()} is deprecated.`;
const stacktrace = stack const stacktrace = stack
.map(({ functionName, location }) => `${functionName} (${location})`) .map(({ functionName, location }) => `${functionName} (${location})`)
.join('\n') .join('\n');
const body = `${deprecation.getMessage()}\n\`\`\`\n${stacktrace}\n\`\`\`` const body = `${deprecation.getMessage()}\n\`\`\`\n${stacktrace}\n\`\`\``;
return `${repoURL}/issues/new?title=${encodeURI(title)}&body=${encodeURI( return `${repoURL}/issues/new?title=${encodeURI(title)}&body=${encodeURI(
body body
)}` )}`;
} else { } else {
return null return null;
} }
} }
async openIssueURL (repoURL, issueURL, issueTitle) { async openIssueURL(repoURL, issueURL, issueTitle) {
const issue = await this.findSimilarIssue(repoURL, issueTitle) const issue = await this.findSimilarIssue(repoURL, issueTitle);
if (issue) { if (issue) {
shell.openExternal(issue.html_url) shell.openExternal(issue.html_url);
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
// Windows will not launch URLs greater than ~2000 bytes so we need to shrink it // Windows will not launch URLs greater than ~2000 bytes so we need to shrink it
shell.openExternal((await this.shortenURL(issueURL)) || issueURL) shell.openExternal((await this.shortenURL(issueURL)) || issueURL);
} else { } else {
shell.openExternal(issueURL) shell.openExternal(issueURL);
} }
} }
async findSimilarIssue (repoURL, issueTitle) { async findSimilarIssue(repoURL, issueTitle) {
const url = 'https://api.github.com/search/issues' const url = 'https://api.github.com/search/issues';
const repo = repoURL.replace(/http(s)?:\/\/(\d+\.)?github.com\//gi, '') const repo = repoURL.replace(/http(s)?:\/\/(\d+\.)?github.com\//gi, '');
const query = `${issueTitle} repo:${repo}` const query = `${issueTitle} repo:${repo}`;
const response = await window.fetch( const response = await window.fetch(
`${url}?q=${encodeURI(query)}&sort=created`, `${url}?q=${encodeURI(query)}&sort=created`,
{ {
@ -368,45 +368,45 @@ export default class DeprecationCopView {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
} }
) );
if (response.ok) { if (response.ok) {
const data = await response.json() const data = await response.json();
if (data.items) { if (data.items) {
const issues = {} const issues = {};
for (const issue of data.items) { for (const issue of data.items) {
if (issue.title.includes(issueTitle) && !issues[issue.state]) { if (issue.title.includes(issueTitle) && !issues[issue.state]) {
issues[issue.state] = issue issues[issue.state] = issue;
} }
} }
return issues.open || issues.closed return issues.open || issues.closed;
} }
} }
} }
async shortenURL (url) { async shortenURL(url) {
let encodedUrl = encodeURIComponent(url).substr(0, 5000) // is.gd has 5000 char limit let encodedUrl = encodeURIComponent(url).substr(0, 5000); // is.gd has 5000 char limit
let incompletePercentEncoding = encodedUrl.indexOf( let incompletePercentEncoding = encodedUrl.indexOf(
'%', '%',
encodedUrl.length - 2 encodedUrl.length - 2
) );
if (incompletePercentEncoding >= 0) { if (incompletePercentEncoding >= 0) {
// Handle an incomplete % encoding cut-off // Handle an incomplete % encoding cut-off
encodedUrl = encodedUrl.substr(0, incompletePercentEncoding) encodedUrl = encodedUrl.substr(0, incompletePercentEncoding);
} }
let result = await fetch('https://is.gd/create.php?format=simple', { let result = await fetch('https://is.gd/create.php?format=simple', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `url=${encodedUrl}` body: `url=${encodedUrl}`
}) });
return result.text() return result.text();
} }
getRepoURL (packageName) { getRepoURL(packageName) {
const loadedPackage = atom.packages.getLoadedPackage(packageName) const loadedPackage = atom.packages.getLoadedPackage(packageName);
if ( if (
loadedPackage && loadedPackage &&
loadedPackage.metadata && loadedPackage.metadata &&
@ -414,171 +414,171 @@ export default class DeprecationCopView {
) { ) {
const url = const url =
loadedPackage.metadata.repository.url || loadedPackage.metadata.repository.url ||
loadedPackage.metadata.repository loadedPackage.metadata.repository;
return url.replace(/\.git$/, '') return url.replace(/\.git$/, '');
} else { } else {
return null return null;
} }
} }
getDeprecatedCallsByPackageName () { getDeprecatedCallsByPackageName() {
const deprecatedCalls = Grim.getDeprecations() const deprecatedCalls = Grim.getDeprecations();
deprecatedCalls.sort((a, b) => b.getCallCount() - a.getCallCount()) deprecatedCalls.sort((a, b) => b.getCallCount() - a.getCallCount());
const deprecatedCallsByPackageName = {} const deprecatedCallsByPackageName = {};
for (const deprecation of deprecatedCalls) { for (const deprecation of deprecatedCalls) {
const stacks = deprecation.getStacks() const stacks = deprecation.getStacks();
stacks.sort((a, b) => b.callCount - a.callCount) stacks.sort((a, b) => b.callCount - a.callCount);
for (const stack of stacks) { for (const stack of stacks) {
let packageName = null let packageName = null;
if (stack.metadata && stack.metadata.packageName) { if (stack.metadata && stack.metadata.packageName) {
packageName = stack.metadata.packageName packageName = stack.metadata.packageName;
} else { } else {
packageName = (this.getPackageName(stack) || '').toLowerCase() packageName = (this.getPackageName(stack) || '').toLowerCase();
} }
deprecatedCallsByPackageName[packageName] = deprecatedCallsByPackageName[packageName] =
deprecatedCallsByPackageName[packageName] || [] deprecatedCallsByPackageName[packageName] || [];
deprecatedCallsByPackageName[packageName].push({ deprecation, stack }) deprecatedCallsByPackageName[packageName].push({ deprecation, stack });
} }
} }
return deprecatedCallsByPackageName return deprecatedCallsByPackageName;
} }
getDeprecatedSelectorsByPackageName () { getDeprecatedSelectorsByPackageName() {
const deprecatedSelectorsByPackageName = {} const deprecatedSelectorsByPackageName = {};
if (atom.styles.getDeprecations) { if (atom.styles.getDeprecations) {
const deprecatedSelectorsBySourcePath = atom.styles.getDeprecations() const deprecatedSelectorsBySourcePath = atom.styles.getDeprecations();
for (const sourcePath of Object.keys(deprecatedSelectorsBySourcePath)) { for (const sourcePath of Object.keys(deprecatedSelectorsBySourcePath)) {
const deprecation = deprecatedSelectorsBySourcePath[sourcePath] const deprecation = deprecatedSelectorsBySourcePath[sourcePath];
const components = sourcePath.split(path.sep) const components = sourcePath.split(path.sep);
const packagesComponentIndex = components.indexOf('packages') const packagesComponentIndex = components.indexOf('packages');
let packageName = null let packageName = null;
let packagePath = null let packagePath = null;
if (packagesComponentIndex === -1) { if (packagesComponentIndex === -1) {
packageName = 'Other' // could be Atom Core or the personal style sheet packageName = 'Other'; // could be Atom Core or the personal style sheet
packagePath = '' packagePath = '';
} else { } else {
packageName = components[packagesComponentIndex + 1] packageName = components[packagesComponentIndex + 1];
packagePath = components packagePath = components
.slice(0, packagesComponentIndex + 1) .slice(0, packagesComponentIndex + 1)
.join(path.sep) .join(path.sep);
} }
deprecatedSelectorsByPackageName[packageName] = deprecatedSelectorsByPackageName[packageName] =
deprecatedSelectorsByPackageName[packageName] || [] deprecatedSelectorsByPackageName[packageName] || [];
deprecatedSelectorsByPackageName[packageName].push({ deprecatedSelectorsByPackageName[packageName].push({
packagePath, packagePath,
sourcePath, sourcePath,
deprecation deprecation
}) });
} }
} }
return deprecatedSelectorsByPackageName return deprecatedSelectorsByPackageName;
} }
getPackageName (stack) { getPackageName(stack) {
const packagePaths = this.getPackagePathsByPackageName() const packagePaths = this.getPackagePathsByPackageName();
for (const [packageName, packagePath] of packagePaths) { for (const [packageName, packagePath] of packagePaths) {
if ( if (
packagePath.includes('.atom/dev/packages') || packagePath.includes('.atom/dev/packages') ||
packagePath.includes('.atom/packages') packagePath.includes('.atom/packages')
) { ) {
packagePaths.set(packageName, fs.absolute(packagePath)) packagePaths.set(packageName, fs.absolute(packagePath));
} }
} }
for (let i = 1; i < stack.length; i++) { for (let i = 1; i < stack.length; i++) {
const { fileName } = stack[i] const { fileName } = stack[i];
// Empty when it was run from the dev console // Empty when it was run from the dev console
if (!fileName) { if (!fileName) {
return null return null;
} }
// Continue to next stack entry if call is in node_modules // Continue to next stack entry if call is in node_modules
if (fileName.includes(`${path.sep}node_modules${path.sep}`)) { if (fileName.includes(`${path.sep}node_modules${path.sep}`)) {
continue continue;
} }
for (const [packageName, packagePath] of packagePaths) { for (const [packageName, packagePath] of packagePaths) {
const relativePath = path.relative(packagePath, fileName) const relativePath = path.relative(packagePath, fileName);
if (!/^\.\./.test(relativePath)) { if (!/^\.\./.test(relativePath)) {
return packageName return packageName;
} }
} }
if (atom.getUserInitScriptPath() === fileName) { if (atom.getUserInitScriptPath() === fileName) {
return `Your local ${path.basename(fileName)} file` return `Your local ${path.basename(fileName)} file`;
} }
} }
return null return null;
} }
getPackagePathsByPackageName () { getPackagePathsByPackageName() {
if (this.packagePathsByPackageName) { if (this.packagePathsByPackageName) {
return this.packagePathsByPackageName return this.packagePathsByPackageName;
} else { } else {
this.packagePathsByPackageName = new Map() this.packagePathsByPackageName = new Map();
for (const pack of atom.packages.getLoadedPackages()) { for (const pack of atom.packages.getLoadedPackages()) {
this.packagePathsByPackageName.set(pack.name, pack.path) this.packagePathsByPackageName.set(pack.name, pack.path);
} }
return this.packagePathsByPackageName return this.packagePathsByPackageName;
} }
} }
checkForUpdates () { checkForUpdates() {
atom.workspace.open('atom://config/updates') atom.workspace.open('atom://config/updates');
} }
disablePackage (packageName) { disablePackage(packageName) {
if (packageName) { if (packageName) {
atom.packages.disablePackage(packageName) atom.packages.disablePackage(packageName);
} }
} }
openLocation (location) { openLocation(location) {
let pathToOpen = location.replace('file://', '') let pathToOpen = location.replace('file://', '');
if (process.platform === 'win32') { if (process.platform === 'win32') {
pathToOpen = pathToOpen.replace(/^\//, '') pathToOpen = pathToOpen.replace(/^\//, '');
} }
atom.open({ pathsToOpen: [pathToOpen] }) atom.open({ pathsToOpen: [pathToOpen] });
} }
getURI () { getURI() {
return this.uri return this.uri;
} }
getTitle () { getTitle() {
return 'Deprecation Cop' return 'Deprecation Cop';
} }
getIconName () { getIconName() {
return 'alert' return 'alert';
} }
scrollUp () { scrollUp() {
this.element.scrollTop -= document.body.offsetHeight / 20 this.element.scrollTop -= document.body.offsetHeight / 20;
} }
scrollDown () { scrollDown() {
this.element.scrollTop += document.body.offsetHeight / 20 this.element.scrollTop += document.body.offsetHeight / 20;
} }
pageUp () { pageUp() {
this.element.scrollTop -= this.element.offsetHeight this.element.scrollTop -= this.element.offsetHeight;
} }
pageDown () { pageDown() {
this.element.scrollTop += this.element.offsetHeight this.element.scrollTop += this.element.offsetHeight;
} }
scrollToTop () { scrollToTop() {
this.element.scrollTop = 0 this.element.scrollTop = 0;
} }
scrollToBottom () { scrollToBottom() {
this.element.scrollTop = this.element.scrollHeight this.element.scrollTop = this.element.scrollHeight;
} }
} }

View File

@ -1,54 +1,54 @@
const { Disposable, CompositeDisposable } = require('atom') const { Disposable, CompositeDisposable } = require('atom');
const DeprecationCopView = require('./deprecation-cop-view') const DeprecationCopView = require('./deprecation-cop-view');
const DeprecationCopStatusBarView = require('./deprecation-cop-status-bar-view') const DeprecationCopStatusBarView = require('./deprecation-cop-status-bar-view');
const ViewURI = 'atom://deprecation-cop' const ViewURI = 'atom://deprecation-cop';
class DeprecationCopPackage { class DeprecationCopPackage {
activate () { activate() {
this.disposables = new CompositeDisposable() this.disposables = new CompositeDisposable();
this.disposables.add( this.disposables.add(
atom.workspace.addOpener(uri => { atom.workspace.addOpener(uri => {
if (uri === ViewURI) { if (uri === ViewURI) {
return this.deserializeDeprecationCopView({ uri }) return this.deserializeDeprecationCopView({ uri });
} }
}) })
) );
this.disposables.add( this.disposables.add(
atom.commands.add('atom-workspace', 'deprecation-cop:view', () => { atom.commands.add('atom-workspace', 'deprecation-cop:view', () => {
atom.workspace.open(ViewURI) atom.workspace.open(ViewURI);
}) })
) );
} }
deactivate () { deactivate() {
this.disposables.dispose() this.disposables.dispose();
const pane = atom.workspace.paneForURI(ViewURI) const pane = atom.workspace.paneForURI(ViewURI);
if (pane) { if (pane) {
pane.destroyItem(pane.itemForURI(ViewURI)) pane.destroyItem(pane.itemForURI(ViewURI));
} }
} }
deserializeDeprecationCopView (state) { deserializeDeprecationCopView(state) {
return new DeprecationCopView(state) return new DeprecationCopView(state);
} }
consumeStatusBar (statusBar) { consumeStatusBar(statusBar) {
const statusBarView = new DeprecationCopStatusBarView() const statusBarView = new DeprecationCopStatusBarView();
const statusBarTile = statusBar.addRightTile({ const statusBarTile = statusBar.addRightTile({
item: statusBarView, item: statusBarView,
priority: 150 priority: 150
}) });
this.disposables.add( this.disposables.add(
new Disposable(() => { new Disposable(() => {
statusBarView.destroy() statusBarView.destroy();
}) })
) );
this.disposables.add( this.disposables.add(
new Disposable(() => { new Disposable(() => {
statusBarTile.destroy() statusBarTile.destroy();
}) })
) );
} }
} }
module.exports = new DeprecationCopPackage() module.exports = new DeprecationCopPackage();

View File

@ -1,31 +1,31 @@
const fs = require('fs-plus') const fs = require('fs-plus');
const path = require('path') const path = require('path');
const Watcher = require('./watcher') const Watcher = require('./watcher');
module.exports = class BaseThemeWatcher extends Watcher { module.exports = class BaseThemeWatcher extends Watcher {
constructor () { constructor() {
super() super();
this.stylesheetsPath = path.dirname( this.stylesheetsPath = path.dirname(
atom.themes.resolveStylesheet('../static/atom.less') atom.themes.resolveStylesheet('../static/atom.less')
) );
this.watch() this.watch();
} }
watch () { watch() {
const filePaths = fs const filePaths = fs
.readdirSync(this.stylesheetsPath) .readdirSync(this.stylesheetsPath)
.filter(filePath => path.extname(filePath).includes('less')) .filter(filePath => path.extname(filePath).includes('less'));
for (const filePath of filePaths) { for (const filePath of filePaths) {
this.watchFile(path.join(this.stylesheetsPath, filePath)) this.watchFile(path.join(this.stylesheetsPath, filePath));
} }
} }
loadStylesheet () { loadStylesheet() {
this.loadAllStylesheets() this.loadAllStylesheets();
} }
loadAllStylesheets () { loadAllStylesheets() {
atom.themes.reloadBaseStylesheets() atom.themes.reloadBaseStylesheets();
} }
} };

View File

@ -1,30 +1,30 @@
module.exports = { module.exports = {
activate (state) { activate(state) {
if (!atom.inDevMode() || atom.inSpecMode()) return if (!atom.inDevMode() || atom.inSpecMode()) return;
if (atom.packages.hasActivatedInitialPackages()) { if (atom.packages.hasActivatedInitialPackages()) {
this.startWatching() this.startWatching();
} else { } else {
this.activatedDisposable = atom.packages.onDidActivateInitialPackages( this.activatedDisposable = atom.packages.onDidActivateInitialPackages(
() => this.startWatching() () => this.startWatching()
) );
} }
}, },
deactivate () { deactivate() {
if (this.activatedDisposable) this.activatedDisposable.dispose() if (this.activatedDisposable) this.activatedDisposable.dispose();
if (this.commandDisposable) this.commandDisposable.dispose() if (this.commandDisposable) this.commandDisposable.dispose();
if (this.uiWatcher) this.uiWatcher.destroy() if (this.uiWatcher) this.uiWatcher.destroy();
}, },
startWatching () { startWatching() {
const UIWatcher = require('./ui-watcher') const UIWatcher = require('./ui-watcher');
this.uiWatcher = new UIWatcher({ themeManager: atom.themes }) this.uiWatcher = new UIWatcher({ themeManager: atom.themes });
this.commandDisposable = atom.commands.add( this.commandDisposable = atom.commands.add(
'atom-workspace', 'atom-workspace',
'dev-live-reload:reload-all', 'dev-live-reload:reload-all',
() => this.uiWatcher.reloadAll() () => this.uiWatcher.reloadAll()
) );
if (this.activatedDisposable) this.activatedDisposable.dispose() if (this.activatedDisposable) this.activatedDisposable.dispose();
} }
} };

View File

@ -1,49 +1,50 @@
const fs = require('fs-plus') const fs = require('fs-plus');
const Watcher = require('./watcher') const Watcher = require('./watcher');
module.exports = class PackageWatcher extends Watcher { module.exports = class PackageWatcher extends Watcher {
static supportsPackage (pack, type) { static supportsPackage(pack, type) {
if (pack.getType() === type && pack.getStylesheetPaths().length) return true if (pack.getType() === type && pack.getStylesheetPaths().length)
return false return true;
return false;
} }
constructor (pack) { constructor(pack) {
super() super();
this.pack = pack this.pack = pack;
this.watch() this.watch();
} }
watch () { watch() {
const watchedPaths = [] const watchedPaths = [];
const watchPath = stylesheet => { const watchPath = stylesheet => {
if (!watchedPaths.includes(stylesheet)) this.watchFile(stylesheet) if (!watchedPaths.includes(stylesheet)) this.watchFile(stylesheet);
watchedPaths.push(stylesheet) watchedPaths.push(stylesheet);
} };
const stylesheetsPath = this.pack.getStylesheetsPath() const stylesheetsPath = this.pack.getStylesheetsPath();
if (fs.isDirectorySync(stylesheetsPath)) { if (fs.isDirectorySync(stylesheetsPath)) {
this.watchDirectory(stylesheetsPath) this.watchDirectory(stylesheetsPath);
} }
const stylesheetPaths = new Set(this.pack.getStylesheetPaths()) const stylesheetPaths = new Set(this.pack.getStylesheetPaths());
const onFile = stylesheetPath => stylesheetPaths.add(stylesheetPath) const onFile = stylesheetPath => stylesheetPaths.add(stylesheetPath);
const onFolder = () => true const onFolder = () => true;
fs.traverseTreeSync(stylesheetsPath, onFile, onFolder) fs.traverseTreeSync(stylesheetsPath, onFile, onFolder);
for (let stylesheet of stylesheetPaths) { for (let stylesheet of stylesheetPaths) {
watchPath(stylesheet) watchPath(stylesheet);
} }
} }
loadStylesheet (pathName) { loadStylesheet(pathName) {
if (pathName.includes('variables')) this.emitGlobalsChanged() if (pathName.includes('variables')) this.emitGlobalsChanged();
this.loadAllStylesheets() this.loadAllStylesheets();
} }
loadAllStylesheets () { loadAllStylesheets() {
console.log('Reloading package', this.pack.name) console.log('Reloading package', this.pack.name);
this.pack.reloadStylesheets() this.pack.reloadStylesheets();
} }
} };

View File

@ -1,114 +1,114 @@
const { CompositeDisposable } = require('atom') const { CompositeDisposable } = require('atom');
const BaseThemeWatcher = require('./base-theme-watcher') const BaseThemeWatcher = require('./base-theme-watcher');
const PackageWatcher = require('./package-watcher') const PackageWatcher = require('./package-watcher');
module.exports = class UIWatcher { module.exports = class UIWatcher {
constructor () { constructor() {
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
this.reloadAll = this.reloadAll.bind(this) this.reloadAll = this.reloadAll.bind(this);
this.watchers = [] this.watchers = [];
this.baseTheme = this.createWatcher(new BaseThemeWatcher()) this.baseTheme = this.createWatcher(new BaseThemeWatcher());
this.watchPackages() this.watchPackages();
} }
watchPackages () { watchPackages() {
this.watchedThemes = new Map() this.watchedThemes = new Map();
this.watchedPackages = new Map() this.watchedPackages = new Map();
for (const theme of atom.themes.getActiveThemes()) { for (const theme of atom.themes.getActiveThemes()) {
this.watchTheme(theme) this.watchTheme(theme);
} }
for (const pack of atom.packages.getActivePackages()) { for (const pack of atom.packages.getActivePackages()) {
this.watchPackage(pack) this.watchPackage(pack);
} }
this.watchForPackageChanges() this.watchForPackageChanges();
} }
watchForPackageChanges () { watchForPackageChanges() {
this.subscriptions.add( this.subscriptions.add(
atom.themes.onDidChangeActiveThemes(() => { atom.themes.onDidChangeActiveThemes(() => {
// We need to destroy all theme watchers as all theme packages are destroyed // We need to destroy all theme watchers as all theme packages are destroyed
// when a theme changes. // when a theme changes.
for (const theme of this.watchedThemes.values()) { for (const theme of this.watchedThemes.values()) {
theme.destroy() theme.destroy();
} }
this.watchedThemes.clear() this.watchedThemes.clear();
// Rewatch everything! // Rewatch everything!
for (const theme of atom.themes.getActiveThemes()) { for (const theme of atom.themes.getActiveThemes()) {
this.watchTheme(theme) this.watchTheme(theme);
} }
}) })
) );
this.subscriptions.add( this.subscriptions.add(
atom.packages.onDidActivatePackage(pack => this.watchPackage(pack)) atom.packages.onDidActivatePackage(pack => this.watchPackage(pack))
) );
this.subscriptions.add( this.subscriptions.add(
atom.packages.onDidDeactivatePackage(pack => { atom.packages.onDidDeactivatePackage(pack => {
// This only handles packages - onDidChangeActiveThemes handles themes // This only handles packages - onDidChangeActiveThemes handles themes
const watcher = this.watchedPackages.get(pack.name) const watcher = this.watchedPackages.get(pack.name);
if (watcher) watcher.destroy() if (watcher) watcher.destroy();
this.watchedPackages.delete(pack.name) this.watchedPackages.delete(pack.name);
}) })
) );
} }
watchTheme (theme) { watchTheme(theme) {
if (PackageWatcher.supportsPackage(theme, 'theme')) { if (PackageWatcher.supportsPackage(theme, 'theme')) {
this.watchedThemes.set( this.watchedThemes.set(
theme.name, theme.name,
this.createWatcher(new PackageWatcher(theme)) this.createWatcher(new PackageWatcher(theme))
) );
} }
} }
watchPackage (pack) { watchPackage(pack) {
if (PackageWatcher.supportsPackage(pack, 'atom')) { if (PackageWatcher.supportsPackage(pack, 'atom')) {
this.watchedPackages.set( this.watchedPackages.set(
pack.name, pack.name,
this.createWatcher(new PackageWatcher(pack)) this.createWatcher(new PackageWatcher(pack))
) );
} }
} }
createWatcher (watcher) { createWatcher(watcher) {
watcher.onDidChangeGlobals(() => { watcher.onDidChangeGlobals(() => {
console.log('Global changed, reloading all styles') console.log('Global changed, reloading all styles');
this.reloadAll() this.reloadAll();
}) });
watcher.onDidDestroy(() => watcher.onDidDestroy(() =>
this.watchers.splice(this.watchers.indexOf(watcher), 1) this.watchers.splice(this.watchers.indexOf(watcher), 1)
) );
this.watchers.push(watcher) this.watchers.push(watcher);
return watcher return watcher;
} }
reloadAll () { reloadAll() {
this.baseTheme.loadAllStylesheets() this.baseTheme.loadAllStylesheets();
for (const pack of atom.packages.getActivePackages()) { for (const pack of atom.packages.getActivePackages()) {
if (PackageWatcher.supportsPackage(pack, 'atom')) { if (PackageWatcher.supportsPackage(pack, 'atom')) {
pack.reloadStylesheets() pack.reloadStylesheets();
} }
} }
for (const theme of atom.themes.getActiveThemes()) { for (const theme of atom.themes.getActiveThemes()) {
if (PackageWatcher.supportsPackage(theme, 'theme')) { if (PackageWatcher.supportsPackage(theme, 'theme')) {
theme.reloadStylesheets() theme.reloadStylesheets();
} }
} }
} }
destroy () { destroy() {
this.subscriptions.dispose() this.subscriptions.dispose();
this.baseTheme.destroy() this.baseTheme.destroy();
for (const pack of this.watchedPackages.values()) { for (const pack of this.watchedPackages.values()) {
pack.destroy() pack.destroy();
} }
for (const theme of this.watchedThemes.values()) { for (const theme of this.watchedThemes.values()) {
theme.destroy() theme.destroy();
} }
} }
} };

View File

@ -1,74 +1,74 @@
const { CompositeDisposable, File, Directory, Emitter } = require('atom') const { CompositeDisposable, File, Directory, Emitter } = require('atom');
const path = require('path') const path = require('path');
module.exports = class Watcher { module.exports = class Watcher {
constructor () { constructor() {
this.destroy = this.destroy.bind(this) this.destroy = this.destroy.bind(this);
this.emitter = new Emitter() this.emitter = new Emitter();
this.disposables = new CompositeDisposable() this.disposables = new CompositeDisposable();
this.entities = [] // Used for specs this.entities = []; // Used for specs
} }
onDidDestroy (callback) { onDidDestroy(callback) {
this.emitter.on('did-destroy', callback) this.emitter.on('did-destroy', callback);
} }
onDidChangeGlobals (callback) { onDidChangeGlobals(callback) {
this.emitter.on('did-change-globals', callback) this.emitter.on('did-change-globals', callback);
} }
destroy () { destroy() {
this.disposables.dispose() this.disposables.dispose();
this.entities = null this.entities = null;
this.emitter.emit('did-destroy') this.emitter.emit('did-destroy');
this.emitter.dispose() this.emitter.dispose();
} }
watch () { watch() {
// override me // override me
} }
loadStylesheet (stylesheetPath) { loadStylesheet(stylesheetPath) {
// override me // override me
} }
loadAllStylesheets () { loadAllStylesheets() {
// override me // override me
} }
emitGlobalsChanged () { emitGlobalsChanged() {
this.emitter.emit('did-change-globals') this.emitter.emit('did-change-globals');
} }
watchDirectory (directoryPath) { watchDirectory(directoryPath) {
if (this.isInAsarArchive(directoryPath)) return if (this.isInAsarArchive(directoryPath)) return;
const entity = new Directory(directoryPath) const entity = new Directory(directoryPath);
this.disposables.add(entity.onDidChange(() => this.loadAllStylesheets())) this.disposables.add(entity.onDidChange(() => this.loadAllStylesheets()));
this.entities.push(entity) this.entities.push(entity);
} }
watchGlobalFile (filePath) { watchGlobalFile(filePath) {
const entity = new File(filePath) const entity = new File(filePath);
this.disposables.add(entity.onDidChange(() => this.emitGlobalsChanged())) this.disposables.add(entity.onDidChange(() => this.emitGlobalsChanged()));
this.entities.push(entity) this.entities.push(entity);
} }
watchFile (filePath) { watchFile(filePath) {
if (this.isInAsarArchive(filePath)) return if (this.isInAsarArchive(filePath)) return;
const reloadFn = () => this.loadStylesheet(entity.getPath()) const reloadFn = () => this.loadStylesheet(entity.getPath());
const entity = new File(filePath) const entity = new File(filePath);
this.disposables.add(entity.onDidChange(reloadFn)) this.disposables.add(entity.onDidChange(reloadFn));
this.disposables.add(entity.onDidDelete(reloadFn)) this.disposables.add(entity.onDidDelete(reloadFn));
this.disposables.add(entity.onDidRename(reloadFn)) this.disposables.add(entity.onDidRename(reloadFn));
this.entities.push(entity) this.entities.push(entity);
} }
isInAsarArchive (pathToCheck) { isInAsarArchive(pathToCheck) {
const { resourcePath } = atom.getLoadSettings() const { resourcePath } = atom.getLoadSettings();
return ( return (
pathToCheck.startsWith(`${resourcePath}${path.sep}`) && pathToCheck.startsWith(`${resourcePath}${path.sep}`) &&
path.extname(resourcePath) === '.asar' path.extname(resourcePath) === '.asar'
) );
} }
} };

View File

@ -1,26 +1,26 @@
/** @babel */ /** @babel */
export async function conditionPromise ( export async function conditionPromise(
condition, condition,
description = 'anonymous condition' description = 'anonymous condition'
) { ) {
const startTime = Date.now() const startTime = Date.now();
while (true) { while (true) {
await timeoutPromise(100) await timeoutPromise(100);
if (await condition()) { if (await condition()) {
return return;
} }
if (Date.now() - startTime > 5000) { if (Date.now() - startTime > 5000) {
throw new Error('Timed out waiting on ' + description) throw new Error('Timed out waiting on ' + description);
} }
} }
} }
export function timeoutPromise (timeout) { export function timeoutPromise(timeout) {
return new Promise(function (resolve) { return new Promise(function(resolve) {
global.setTimeout(resolve, timeout) global.setTimeout(resolve, timeout);
}) });
} }

View File

@ -1,128 +1,128 @@
describe('Dev Live Reload', () => { describe('Dev Live Reload', () => {
describe('package activation', () => { describe('package activation', () => {
let [pack, mainModule] = [] let [pack, mainModule] = [];
beforeEach(() => { beforeEach(() => {
pack = atom.packages.loadPackage('dev-live-reload') pack = atom.packages.loadPackage('dev-live-reload');
pack.requireMainModule() pack.requireMainModule();
mainModule = pack.mainModule mainModule = pack.mainModule;
spyOn(mainModule, 'startWatching') spyOn(mainModule, 'startWatching');
}) });
describe('when the window is not in dev mode', () => { describe('when the window is not in dev mode', () => {
beforeEach(() => spyOn(atom, 'inDevMode').andReturn(false)) beforeEach(() => spyOn(atom, 'inDevMode').andReturn(false));
it('does not watch files', async () => { it('does not watch files', async () => {
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true) spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true);
await atom.packages.activatePackage('dev-live-reload') await atom.packages.activatePackage('dev-live-reload');
expect(mainModule.startWatching).not.toHaveBeenCalled() expect(mainModule.startWatching).not.toHaveBeenCalled();
}) });
}) });
describe('when the window is in spec mode', () => { describe('when the window is in spec mode', () => {
beforeEach(() => spyOn(atom, 'inSpecMode').andReturn(true)) beforeEach(() => spyOn(atom, 'inSpecMode').andReturn(true));
it('does not watch files', async () => { it('does not watch files', async () => {
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true) spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true);
await atom.packages.activatePackage('dev-live-reload') await atom.packages.activatePackage('dev-live-reload');
expect(mainModule.startWatching).not.toHaveBeenCalled() expect(mainModule.startWatching).not.toHaveBeenCalled();
}) });
}) });
describe('when the window is in dev mode', () => { describe('when the window is in dev mode', () => {
beforeEach(() => { beforeEach(() => {
spyOn(atom, 'inDevMode').andReturn(true) spyOn(atom, 'inDevMode').andReturn(true);
spyOn(atom, 'inSpecMode').andReturn(false) spyOn(atom, 'inSpecMode').andReturn(false);
}) });
it('watches files', async () => { it('watches files', async () => {
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true) spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true);
await atom.packages.activatePackage('dev-live-reload') await atom.packages.activatePackage('dev-live-reload');
expect(mainModule.startWatching).toHaveBeenCalled() expect(mainModule.startWatching).toHaveBeenCalled();
}) });
}) });
describe('when the window is in both dev mode and spec mode', () => { describe('when the window is in both dev mode and spec mode', () => {
beforeEach(() => { beforeEach(() => {
spyOn(atom, 'inDevMode').andReturn(true) spyOn(atom, 'inDevMode').andReturn(true);
spyOn(atom, 'inSpecMode').andReturn(true) spyOn(atom, 'inSpecMode').andReturn(true);
}) });
it('does not watch files', async () => { it('does not watch files', async () => {
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true) spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true);
await atom.packages.activatePackage('dev-live-reload') await atom.packages.activatePackage('dev-live-reload');
expect(mainModule.startWatching).not.toHaveBeenCalled() expect(mainModule.startWatching).not.toHaveBeenCalled();
}) });
}) });
describe('when the package is activated before initial packages have been activated', () => { describe('when the package is activated before initial packages have been activated', () => {
beforeEach(() => { beforeEach(() => {
spyOn(atom, 'inDevMode').andReturn(true) spyOn(atom, 'inDevMode').andReturn(true);
spyOn(atom, 'inSpecMode').andReturn(false) spyOn(atom, 'inSpecMode').andReturn(false);
}) });
it('waits until all initial packages have been activated before watching files', async () => { it('waits until all initial packages have been activated before watching files', async () => {
await atom.packages.activatePackage('dev-live-reload') await atom.packages.activatePackage('dev-live-reload');
expect(mainModule.startWatching).not.toHaveBeenCalled() expect(mainModule.startWatching).not.toHaveBeenCalled();
atom.packages.emitter.emit('did-activate-initial-packages') atom.packages.emitter.emit('did-activate-initial-packages');
expect(mainModule.startWatching).toHaveBeenCalled() expect(mainModule.startWatching).toHaveBeenCalled();
}) });
}) });
}) });
describe('package deactivation', () => { describe('package deactivation', () => {
beforeEach(() => { beforeEach(() => {
spyOn(atom, 'inDevMode').andReturn(true) spyOn(atom, 'inDevMode').andReturn(true);
spyOn(atom, 'inSpecMode').andReturn(false) spyOn(atom, 'inSpecMode').andReturn(false);
}) });
it('stops watching all files', async () => { it('stops watching all files', async () => {
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true) spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true);
const { mainModule } = await atom.packages.activatePackage( const { mainModule } = await atom.packages.activatePackage(
'dev-live-reload' 'dev-live-reload'
) );
expect(mainModule.uiWatcher).not.toBeNull() expect(mainModule.uiWatcher).not.toBeNull();
spyOn(mainModule.uiWatcher, 'destroy') spyOn(mainModule.uiWatcher, 'destroy');
await atom.packages.deactivatePackage('dev-live-reload') await atom.packages.deactivatePackage('dev-live-reload');
expect(mainModule.uiWatcher.destroy).toHaveBeenCalled() expect(mainModule.uiWatcher.destroy).toHaveBeenCalled();
}) });
it('unsubscribes from the onDidActivateInitialPackages subscription if it is disabled before all initial packages are activated', async () => { it('unsubscribes from the onDidActivateInitialPackages subscription if it is disabled before all initial packages are activated', async () => {
const { mainModule } = await atom.packages.activatePackage( const { mainModule } = await atom.packages.activatePackage(
'dev-live-reload' 'dev-live-reload'
) );
expect(mainModule.activatedDisposable.disposed).toBe(false) expect(mainModule.activatedDisposable.disposed).toBe(false);
await atom.packages.deactivatePackage('dev-live-reload') await atom.packages.deactivatePackage('dev-live-reload');
expect(mainModule.activatedDisposable.disposed).toBe(true) expect(mainModule.activatedDisposable.disposed).toBe(true);
spyOn(mainModule, 'startWatching') spyOn(mainModule, 'startWatching');
atom.packages.emitter.emit('did-activate-initial-packages') atom.packages.emitter.emit('did-activate-initial-packages');
expect(mainModule.startWatching).not.toHaveBeenCalled() expect(mainModule.startWatching).not.toHaveBeenCalled();
}) });
it('removes its commands', async () => { it('removes its commands', async () => {
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true) spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true);
await atom.packages.activatePackage('dev-live-reload') await atom.packages.activatePackage('dev-live-reload');
expect( expect(
atom.commands atom.commands
.findCommands({ target: atom.views.getView(atom.workspace) }) .findCommands({ target: atom.views.getView(atom.workspace) })
.filter(command => command.name.startsWith('dev-live-reload')).length .filter(command => command.name.startsWith('dev-live-reload')).length
).toBeGreaterThan(0) ).toBeGreaterThan(0);
await atom.packages.deactivatePackage('dev-live-reload') await atom.packages.deactivatePackage('dev-live-reload');
expect( expect(
atom.commands atom.commands
.findCommands({ target: atom.views.getView(atom.workspace) }) .findCommands({ target: atom.views.getView(atom.workspace) })
.filter(command => command.name.startsWith('dev-live-reload')).length .filter(command => command.name.startsWith('dev-live-reload')).length
).toBe(0) ).toBe(0);
}) });
}) });
}) });

View File

@ -1,260 +1,262 @@
const path = require('path') const path = require('path');
const UIWatcher = require('../lib/ui-watcher') const UIWatcher = require('../lib/ui-watcher');
const { conditionPromise } = require('./async-spec-helpers') const { conditionPromise } = require('./async-spec-helpers');
describe('UIWatcher', () => { describe('UIWatcher', () => {
let uiWatcher = null let uiWatcher = null;
beforeEach(() => beforeEach(() =>
atom.packages.packageDirPaths.push(path.join(__dirname, 'fixtures')) atom.packages.packageDirPaths.push(path.join(__dirname, 'fixtures'))
) );
afterEach(() => uiWatcher && uiWatcher.destroy()) afterEach(() => uiWatcher && uiWatcher.destroy());
describe("when a base theme's file changes", () => { describe("when a base theme's file changes", () => {
beforeEach(() => { beforeEach(() => {
spyOn(atom.themes, 'resolveStylesheet').andReturn( spyOn(atom.themes, 'resolveStylesheet').andReturn(
path.join(__dirname, 'fixtures', 'static', 'atom.less') path.join(__dirname, 'fixtures', 'static', 'atom.less')
) );
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
}) });
it('reloads all the base styles', () => { it('reloads all the base styles', () => {
spyOn(atom.themes, 'reloadBaseStylesheets') spyOn(atom.themes, 'reloadBaseStylesheets');
expect(uiWatcher.baseTheme.entities[0].getPath()).toContain( expect(uiWatcher.baseTheme.entities[0].getPath()).toContain(
`${path.sep}static${path.sep}` `${path.sep}static${path.sep}`
) );
uiWatcher.baseTheme.entities[0].emitter.emit('did-change') uiWatcher.baseTheme.entities[0].emitter.emit('did-change');
expect(atom.themes.reloadBaseStylesheets).toHaveBeenCalled() expect(atom.themes.reloadBaseStylesheets).toHaveBeenCalled();
}) });
}) });
it("watches all the style sheets in the theme's styles folder", async () => { it("watches all the style sheets in the theme's styles folder", async () => {
const packagePath = path.join( const packagePath = path.join(
__dirname, __dirname,
'fixtures', 'fixtures',
'package-with-styles-folder' 'package-with-styles-folder'
) );
await atom.packages.activatePackage(packagePath) await atom.packages.activatePackage(packagePath);
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
const lastWatcher = uiWatcher.watchers[uiWatcher.watchers.length - 1] const lastWatcher = uiWatcher.watchers[uiWatcher.watchers.length - 1];
expect(lastWatcher.entities.length).toBe(4) expect(lastWatcher.entities.length).toBe(4);
expect(lastWatcher.entities[0].getPath()).toBe( expect(lastWatcher.entities[0].getPath()).toBe(
path.join(packagePath, 'styles') path.join(packagePath, 'styles')
) );
expect(lastWatcher.entities[1].getPath()).toBe( expect(lastWatcher.entities[1].getPath()).toBe(
path.join(packagePath, 'styles', '3.css') path.join(packagePath, 'styles', '3.css')
) );
expect(lastWatcher.entities[2].getPath()).toBe( expect(lastWatcher.entities[2].getPath()).toBe(
path.join(packagePath, 'styles', 'sub', '1.css') path.join(packagePath, 'styles', 'sub', '1.css')
) );
expect(lastWatcher.entities[3].getPath()).toBe( expect(lastWatcher.entities[3].getPath()).toBe(
path.join(packagePath, 'styles', 'sub', '2.less') path.join(packagePath, 'styles', 'sub', '2.less')
) );
}) });
describe('when a package stylesheet file changes', async () => { describe('when a package stylesheet file changes', async () => {
beforeEach(async () => { beforeEach(async () => {
await atom.packages.activatePackage( await atom.packages.activatePackage(
path.join(__dirname, 'fixtures', 'package-with-styles-manifest') path.join(__dirname, 'fixtures', 'package-with-styles-manifest')
) );
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
}) });
it('reloads all package styles', () => { it('reloads all package styles', () => {
const pack = atom.packages.getActivePackages()[0] const pack = atom.packages.getActivePackages()[0];
spyOn(pack, 'reloadStylesheets') spyOn(pack, 'reloadStylesheets');
uiWatcher.watchers[uiWatcher.watchers.length - 1].entities[1].emitter.emit('did-change') uiWatcher.watchers[
uiWatcher.watchers.length - 1
].entities[1].emitter.emit('did-change');
expect(pack.reloadStylesheets).toHaveBeenCalled() expect(pack.reloadStylesheets).toHaveBeenCalled();
}) });
}) });
describe('when a package does not have a stylesheet', () => { describe('when a package does not have a stylesheet', () => {
beforeEach(async () => { beforeEach(async () => {
await atom.packages.activatePackage('package-with-index') await atom.packages.activatePackage('package-with-index');
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
}) });
it('does not create a PackageWatcher', () => { it('does not create a PackageWatcher', () => {
expect(uiWatcher.watchedPackages['package-with-index']).toBeUndefined() expect(uiWatcher.watchedPackages['package-with-index']).toBeUndefined();
}) });
}) });
describe('when a package global file changes', () => { describe('when a package global file changes', () => {
beforeEach(async () => { beforeEach(async () => {
atom.config.set('core.themes', [ atom.config.set('core.themes', [
'theme-with-ui-variables', 'theme-with-ui-variables',
'theme-with-multiple-imported-files' 'theme-with-multiple-imported-files'
]) ]);
await atom.themes.activateThemes() await atom.themes.activateThemes();
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
}) });
afterEach(() => atom.themes.deactivateThemes()) afterEach(() => atom.themes.deactivateThemes());
it('reloads every package when the variables file changes', () => { it('reloads every package when the variables file changes', () => {
let varEntity let varEntity;
for (const theme of atom.themes.getActiveThemes()) { for (const theme of atom.themes.getActiveThemes()) {
spyOn(theme, 'reloadStylesheets') spyOn(theme, 'reloadStylesheets');
} }
for (const entity of uiWatcher.watchedThemes.get( for (const entity of uiWatcher.watchedThemes.get(
'theme-with-multiple-imported-files' 'theme-with-multiple-imported-files'
).entities) { ).entities) {
if (entity.getPath().indexOf('variables') > -1) varEntity = entity if (entity.getPath().indexOf('variables') > -1) varEntity = entity;
} }
varEntity.emitter.emit('did-change') varEntity.emitter.emit('did-change');
for (const theme of atom.themes.getActiveThemes()) { for (const theme of atom.themes.getActiveThemes()) {
expect(theme.reloadStylesheets).toHaveBeenCalled() expect(theme.reloadStylesheets).toHaveBeenCalled();
} }
}) });
}) });
describe('watcher lifecycle', () => { describe('watcher lifecycle', () => {
it('starts watching a package if it is activated after initial startup', async () => { it('starts watching a package if it is activated after initial startup', async () => {
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
expect(uiWatcher.watchedPackages.size).toBe(0) expect(uiWatcher.watchedPackages.size).toBe(0);
await atom.packages.activatePackage( await atom.packages.activatePackage(
path.join(__dirname, 'fixtures', 'package-with-styles-folder') path.join(__dirname, 'fixtures', 'package-with-styles-folder')
) );
expect( expect(
uiWatcher.watchedPackages.get('package-with-styles-folder') uiWatcher.watchedPackages.get('package-with-styles-folder')
).not.toBeUndefined() ).not.toBeUndefined();
}) });
it('unwatches a package after it is deactivated', async () => { it('unwatches a package after it is deactivated', async () => {
await atom.packages.activatePackage( await atom.packages.activatePackage(
path.join(__dirname, 'fixtures', 'package-with-styles-folder') path.join(__dirname, 'fixtures', 'package-with-styles-folder')
) );
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
const watcher = uiWatcher.watchedPackages.get( const watcher = uiWatcher.watchedPackages.get(
'package-with-styles-folder' 'package-with-styles-folder'
) );
expect(watcher).not.toBeUndefined() expect(watcher).not.toBeUndefined();
const watcherDestructionSpy = jasmine.createSpy('watcher-on-did-destroy') const watcherDestructionSpy = jasmine.createSpy('watcher-on-did-destroy');
watcher.onDidDestroy(watcherDestructionSpy) watcher.onDidDestroy(watcherDestructionSpy);
await atom.packages.deactivatePackage('package-with-styles-folder') await atom.packages.deactivatePackage('package-with-styles-folder');
expect( expect(
uiWatcher.watchedPackages.get('package-with-styles-folder') uiWatcher.watchedPackages.get('package-with-styles-folder')
).toBeUndefined() ).toBeUndefined();
expect(uiWatcher.watchedPackages.size).toBe(0) expect(uiWatcher.watchedPackages.size).toBe(0);
expect(watcherDestructionSpy).toHaveBeenCalled() expect(watcherDestructionSpy).toHaveBeenCalled();
}) });
it('does not watch activated packages after the UI watcher has been destroyed', async () => { it('does not watch activated packages after the UI watcher has been destroyed', async () => {
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
uiWatcher.destroy() uiWatcher.destroy();
await atom.packages.activatePackage( await atom.packages.activatePackage(
path.join(__dirname, 'fixtures', 'package-with-styles-folder') path.join(__dirname, 'fixtures', 'package-with-styles-folder')
) );
expect(uiWatcher.watchedPackages.size).toBe(0) expect(uiWatcher.watchedPackages.size).toBe(0);
}) });
}) });
describe('minimal theme packages', () => { describe('minimal theme packages', () => {
let pack = null let pack = null;
beforeEach(async () => { beforeEach(async () => {
atom.config.set('core.themes', [ atom.config.set('core.themes', [
'theme-with-syntax-variables', 'theme-with-syntax-variables',
'theme-with-index-less' 'theme-with-index-less'
]) ]);
await atom.themes.activateThemes() await atom.themes.activateThemes();
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
pack = atom.themes.getActiveThemes()[0] pack = atom.themes.getActiveThemes()[0];
}) });
afterEach(() => atom.themes.deactivateThemes()) afterEach(() => atom.themes.deactivateThemes());
it('watches themes without a styles directory', () => { it('watches themes without a styles directory', () => {
spyOn(pack, 'reloadStylesheets') spyOn(pack, 'reloadStylesheets');
spyOn(atom.themes, 'reloadBaseStylesheets') spyOn(atom.themes, 'reloadBaseStylesheets');
const watcher = uiWatcher.watchedThemes.get('theme-with-index-less') const watcher = uiWatcher.watchedThemes.get('theme-with-index-less');
expect(watcher.entities.length).toBe(1) expect(watcher.entities.length).toBe(1);
watcher.entities[0].emitter.emit('did-change') watcher.entities[0].emitter.emit('did-change');
expect(pack.reloadStylesheets).toHaveBeenCalled() expect(pack.reloadStylesheets).toHaveBeenCalled();
expect(atom.themes.reloadBaseStylesheets).not.toHaveBeenCalled() expect(atom.themes.reloadBaseStylesheets).not.toHaveBeenCalled();
}) });
}) });
describe('theme packages', () => { describe('theme packages', () => {
let pack = null let pack = null;
beforeEach(async () => { beforeEach(async () => {
atom.config.set('core.themes', [ atom.config.set('core.themes', [
'theme-with-syntax-variables', 'theme-with-syntax-variables',
'theme-with-multiple-imported-files' 'theme-with-multiple-imported-files'
]) ]);
await atom.themes.activateThemes() await atom.themes.activateThemes();
uiWatcher = new UIWatcher() uiWatcher = new UIWatcher();
pack = atom.themes.getActiveThemes()[0] pack = atom.themes.getActiveThemes()[0];
}) });
afterEach(() => atom.themes.deactivateThemes()) afterEach(() => atom.themes.deactivateThemes());
it('reloads the theme when anything within the theme changes', () => { it('reloads the theme when anything within the theme changes', () => {
spyOn(pack, 'reloadStylesheets') spyOn(pack, 'reloadStylesheets');
spyOn(atom.themes, 'reloadBaseStylesheets') spyOn(atom.themes, 'reloadBaseStylesheets');
const watcher = uiWatcher.watchedThemes.get( const watcher = uiWatcher.watchedThemes.get(
'theme-with-multiple-imported-files' 'theme-with-multiple-imported-files'
) );
expect(watcher.entities.length).toBe(6) expect(watcher.entities.length).toBe(6);
watcher.entities[2].emitter.emit('did-change') watcher.entities[2].emitter.emit('did-change');
expect(pack.reloadStylesheets).toHaveBeenCalled() expect(pack.reloadStylesheets).toHaveBeenCalled();
expect(atom.themes.reloadBaseStylesheets).not.toHaveBeenCalled() expect(atom.themes.reloadBaseStylesheets).not.toHaveBeenCalled();
watcher.entities[watcher.entities.length - 1].emitter.emit('did-change') watcher.entities[watcher.entities.length - 1].emitter.emit('did-change');
expect(atom.themes.reloadBaseStylesheets).toHaveBeenCalled() expect(atom.themes.reloadBaseStylesheets).toHaveBeenCalled();
}) });
it('unwatches when a theme is deactivated', async () => { it('unwatches when a theme is deactivated', async () => {
jasmine.useRealClock() jasmine.useRealClock();
atom.config.set('core.themes', []) atom.config.set('core.themes', []);
await conditionPromise( await conditionPromise(
() => !uiWatcher.watchedThemes['theme-with-multiple-imported-files'] () => !uiWatcher.watchedThemes['theme-with-multiple-imported-files']
) );
}) });
it('watches a new theme when it is deactivated', async () => { it('watches a new theme when it is deactivated', async () => {
jasmine.useRealClock() jasmine.useRealClock();
atom.config.set('core.themes', [ atom.config.set('core.themes', [
'theme-with-syntax-variables', 'theme-with-syntax-variables',
'theme-with-package-file' 'theme-with-package-file'
]) ]);
await conditionPromise(() => await conditionPromise(() =>
uiWatcher.watchedThemes.get('theme-with-package-file') uiWatcher.watchedThemes.get('theme-with-package-file')
) );
pack = atom.themes.getActiveThemes()[0] pack = atom.themes.getActiveThemes()[0];
spyOn(pack, 'reloadStylesheets') spyOn(pack, 'reloadStylesheets');
expect(pack.name).toBe('theme-with-package-file') expect(pack.name).toBe('theme-with-package-file');
const watcher = uiWatcher.watchedThemes.get('theme-with-package-file') const watcher = uiWatcher.watchedThemes.get('theme-with-package-file');
watcher.entities[2].emitter.emit('did-change') watcher.entities[2].emitter.emit('did-change');
expect(pack.reloadStylesheets).toHaveBeenCalled() expect(pack.reloadStylesheets).toHaveBeenCalled();
}) });
}) });
}) });

View File

@ -1,57 +1,57 @@
/** @babel */ /** @babel */
import { CompositeDisposable } from 'atom' import { CompositeDisposable } from 'atom';
let reporter let reporter;
function getReporter () { function getReporter() {
if (!reporter) { if (!reporter) {
const Reporter = require('./reporter') const Reporter = require('./reporter');
reporter = new Reporter() reporter = new Reporter();
} }
return reporter return reporter;
} }
export default { export default {
activate () { activate() {
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
if (!atom.config.get('exception-reporting.userId')) { if (!atom.config.get('exception-reporting.userId')) {
atom.config.set('exception-reporting.userId', require('node-uuid').v4()) atom.config.set('exception-reporting.userId', require('node-uuid').v4());
} }
this.subscriptions.add( this.subscriptions.add(
atom.onDidThrowError(({ message, url, line, column, originalError }) => { atom.onDidThrowError(({ message, url, line, column, originalError }) => {
try { try {
getReporter().reportUncaughtException(originalError) getReporter().reportUncaughtException(originalError);
} catch (secondaryException) { } catch (secondaryException) {
try { try {
console.error( console.error(
'Error reporting uncaught exception', 'Error reporting uncaught exception',
secondaryException secondaryException
) );
getReporter().reportUncaughtException(secondaryException) getReporter().reportUncaughtException(secondaryException);
} catch (error) {} } catch (error) {}
} }
}) })
) );
if (atom.onDidFailAssertion != null) { if (atom.onDidFailAssertion != null) {
this.subscriptions.add( this.subscriptions.add(
atom.onDidFailAssertion(error => { atom.onDidFailAssertion(error => {
try { try {
getReporter().reportFailedAssertion(error) getReporter().reportFailedAssertion(error);
} catch (secondaryException) { } catch (secondaryException) {
try { try {
console.error( console.error(
'Error reporting assertion failure', 'Error reporting assertion failure',
secondaryException secondaryException
) );
getReporter().reportUncaughtException(secondaryException) getReporter().reportUncaughtException(secondaryException);
} catch (error) {} } catch (error) {}
} }
}) })
) );
} }
} }
} };

View File

@ -1,31 +1,31 @@
/** @babel */ /** @babel */
import os from 'os' import os from 'os';
import stackTrace from 'stack-trace' import stackTrace from 'stack-trace';
import fs from 'fs-plus' import fs from 'fs-plus';
import path from 'path' import path from 'path';
const API_KEY = '7ddca14cb60cbd1cd12d1b252473b076' const API_KEY = '7ddca14cb60cbd1cd12d1b252473b076';
const LIB_VERSION = require('../package.json')['version'] const LIB_VERSION = require('../package.json')['version'];
const StackTraceCache = new WeakMap() const StackTraceCache = new WeakMap();
export default class Reporter { export default class Reporter {
constructor (params = {}) { constructor(params = {}) {
this.request = params.request || window.fetch this.request = params.request || window.fetch;
this.alwaysReport = params.hasOwnProperty('alwaysReport') this.alwaysReport = params.hasOwnProperty('alwaysReport')
? params.alwaysReport ? params.alwaysReport
: false : false;
this.reportPreviousErrors = params.hasOwnProperty('reportPreviousErrors') this.reportPreviousErrors = params.hasOwnProperty('reportPreviousErrors')
? params.reportPreviousErrors ? params.reportPreviousErrors
: true : true;
this.resourcePath = this.normalizePath( this.resourcePath = this.normalizePath(
params.resourcePath || process.resourcesPath params.resourcePath || process.resourcesPath
) );
this.reportedErrors = [] this.reportedErrors = [];
this.reportedAssertionFailures = [] this.reportedAssertionFailures = [];
} }
buildNotificationJSON (error, params) { buildNotificationJSON(error, params) {
return { return {
apiKey: API_KEY, apiKey: API_KEY,
notifier: { notifier: {
@ -51,18 +51,18 @@ export default class Reporter {
metaData: error.metadata metaData: error.metadata
} }
] ]
} };
} }
buildExceptionJSON (error, projectRoot) { buildExceptionJSON(error, projectRoot) {
return { return {
errorClass: error.constructor.name, errorClass: error.constructor.name,
message: error.message, message: error.message,
stacktrace: this.buildStackTraceJSON(error, projectRoot) stacktrace: this.buildStackTraceJSON(error, projectRoot)
} };
} }
buildStackTraceJSON (error, projectRoot) { buildStackTraceJSON(error, projectRoot) {
return this.parseStackTrace(error).map(callSite => { return this.parseStackTrace(error).map(callSite => {
return { return {
file: this.scrubPath(callSite.getFileName()), file: this.scrubPath(callSite.getFileName()),
@ -71,110 +71,110 @@ export default class Reporter {
lineNumber: callSite.getLineNumber(), lineNumber: callSite.getLineNumber(),
columnNumber: callSite.getColumnNumber(), columnNumber: callSite.getColumnNumber(),
inProject: !/node_modules/.test(callSite.getFileName()) inProject: !/node_modules/.test(callSite.getFileName())
} };
}) });
} }
normalizePath (pathToNormalize) { normalizePath(pathToNormalize) {
return pathToNormalize return pathToNormalize
.replace('file:///', '') // Sometimes it's a uri .replace('file:///', '') // Sometimes it's a uri
.replace(/\\/g, '/') // Unify path separators across Win/macOS/Linux .replace(/\\/g, '/'); // Unify path separators across Win/macOS/Linux
} }
scrubPath (pathToScrub) { scrubPath(pathToScrub) {
const absolutePath = this.normalizePath(pathToScrub) const absolutePath = this.normalizePath(pathToScrub);
if (this.isBundledFile(absolutePath)) { if (this.isBundledFile(absolutePath)) {
return this.normalizePath(path.relative(this.resourcePath, absolutePath)) return this.normalizePath(path.relative(this.resourcePath, absolutePath));
} else { } else {
return absolutePath return absolutePath
.replace(this.normalizePath(fs.getHomeDirectory()), '~') // Remove users home dir .replace(this.normalizePath(fs.getHomeDirectory()), '~') // Remove users home dir
.replace(/.*(\/packages\/.*)/, '$1') // Remove everything before app.asar or packages .replace(/.*(\/packages\/.*)/, '$1'); // Remove everything before app.asar or packages
} }
} }
getDefaultNotificationParams () { getDefaultNotificationParams() {
return { return {
userId: atom.config.get('exception-reporting.userId'), userId: atom.config.get('exception-reporting.userId'),
appVersion: atom.getVersion(), appVersion: atom.getVersion(),
releaseStage: this.getReleaseChannel(atom.getVersion()), releaseStage: this.getReleaseChannel(atom.getVersion()),
projectRoot: atom.getLoadSettings().resourcePath, projectRoot: atom.getLoadSettings().resourcePath,
osVersion: `${os.platform()}-${os.arch()}-${os.release()}` osVersion: `${os.platform()}-${os.arch()}-${os.release()}`
} };
} }
getReleaseChannel (version) { getReleaseChannel(version) {
return version.indexOf('beta') > -1 return version.indexOf('beta') > -1
? 'beta' ? 'beta'
: version.indexOf('dev') > -1 : version.indexOf('dev') > -1
? 'dev' ? 'dev'
: 'stable' : 'stable';
} }
performRequest (json) { performRequest(json) {
this.request.call(null, 'https://notify.bugsnag.com', { this.request.call(null, 'https://notify.bugsnag.com', {
method: 'POST', method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }), headers: new Headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify(json) body: JSON.stringify(json)
}) });
} }
shouldReport (error) { shouldReport(error) {
if (this.alwaysReport) return true // Used in specs if (this.alwaysReport) return true; // Used in specs
if (atom.config.get('core.telemetryConsent') !== 'limited') return false if (atom.config.get('core.telemetryConsent') !== 'limited') return false;
if (atom.inDevMode()) return false if (atom.inDevMode()) return false;
const topFrame = this.parseStackTrace(error)[0] const topFrame = this.parseStackTrace(error)[0];
const fileName = topFrame ? topFrame.getFileName() : null const fileName = topFrame ? topFrame.getFileName() : null;
return ( return (
fileName && fileName &&
(this.isBundledFile(fileName) || this.isTeletypeFile(fileName)) (this.isBundledFile(fileName) || this.isTeletypeFile(fileName))
) );
} }
parseStackTrace (error) { parseStackTrace(error) {
let callSites = StackTraceCache.get(error) let callSites = StackTraceCache.get(error);
if (callSites) { if (callSites) {
return callSites return callSites;
} else { } else {
callSites = stackTrace.parse(error) callSites = stackTrace.parse(error);
StackTraceCache.set(error, callSites) StackTraceCache.set(error, callSites);
return callSites return callSites;
} }
} }
requestPrivateMetadataConsent (error, message, reportFn) { requestPrivateMetadataConsent(error, message, reportFn) {
let notification, dismissSubscription let notification, dismissSubscription;
function reportWithoutPrivateMetadata () { function reportWithoutPrivateMetadata() {
if (dismissSubscription) { if (dismissSubscription) {
dismissSubscription.dispose() dismissSubscription.dispose();
} }
delete error.privateMetadata delete error.privateMetadata;
delete error.privateMetadataDescription delete error.privateMetadataDescription;
reportFn(error) reportFn(error);
if (notification) { if (notification) {
notification.dismiss() notification.dismiss();
} }
} }
function reportWithPrivateMetadata () { function reportWithPrivateMetadata() {
if (error.metadata == null) { if (error.metadata == null) {
error.metadata = {} error.metadata = {};
} }
for (let key in error.privateMetadata) { for (let key in error.privateMetadata) {
let value = error.privateMetadata[key] let value = error.privateMetadata[key];
error.metadata[key] = value error.metadata[key] = value;
} }
reportWithoutPrivateMetadata() reportWithoutPrivateMetadata();
} }
const name = error.privateMetadataRequestName const name = error.privateMetadataRequestName;
if (name != null) { if (name != null) {
if (localStorage.getItem(`private-metadata-request:${name}`)) { if (localStorage.getItem(`private-metadata-request:${name}`)) {
return reportWithoutPrivateMetadata(error) return reportWithoutPrivateMetadata(error);
} else { } else {
localStorage.setItem(`private-metadata-request:${name}`, true) localStorage.setItem(`private-metadata-request:${name}`, true);
} }
} }
@ -193,51 +193,51 @@ export default class Reporter {
onDidClick: reportWithPrivateMetadata onDidClick: reportWithPrivateMetadata
} }
] ]
}) });
dismissSubscription = notification.onDidDismiss( dismissSubscription = notification.onDidDismiss(
reportWithoutPrivateMetadata reportWithoutPrivateMetadata
) );
} }
addPackageMetadata (error) { addPackageMetadata(error) {
let activePackages = atom.packages.getActivePackages() let activePackages = atom.packages.getActivePackages();
const availablePackagePaths = atom.packages.getPackageDirPaths() const availablePackagePaths = atom.packages.getPackageDirPaths();
if (activePackages.length > 0) { if (activePackages.length > 0) {
let userPackages = {} let userPackages = {};
let bundledPackages = {} let bundledPackages = {};
for (let pack of atom.packages.getActivePackages()) { for (let pack of atom.packages.getActivePackages()) {
if (availablePackagePaths.includes(path.dirname(pack.path))) { if (availablePackagePaths.includes(path.dirname(pack.path))) {
userPackages[pack.name] = pack.metadata.version userPackages[pack.name] = pack.metadata.version;
} else { } else {
bundledPackages[pack.name] = pack.metadata.version bundledPackages[pack.name] = pack.metadata.version;
} }
} }
if (error.metadata == null) { if (error.metadata == null) {
error.metadata = {} error.metadata = {};
} }
error.metadata.bundledPackages = bundledPackages error.metadata.bundledPackages = bundledPackages;
error.metadata.userPackages = userPackages error.metadata.userPackages = userPackages;
} }
} }
addPreviousErrorsMetadata (error) { addPreviousErrorsMetadata(error) {
if (!this.reportPreviousErrors) return if (!this.reportPreviousErrors) return;
if (!error.metadata) error.metadata = {} if (!error.metadata) error.metadata = {};
error.metadata.previousErrors = this.reportedErrors.map( error.metadata.previousErrors = this.reportedErrors.map(
error => error.message error => error.message
) );
error.metadata.previousAssertionFailures = this.reportedAssertionFailures.map( error.metadata.previousAssertionFailures = this.reportedAssertionFailures.map(
error => error.message error => error.message
) );
} }
reportUncaughtException (error) { reportUncaughtException(error) {
if (!this.shouldReport(error)) return if (!this.shouldReport(error)) return;
this.addPackageMetadata(error) this.addPackageMetadata(error);
this.addPreviousErrorsMetadata(error) this.addPreviousErrorsMetadata(error);
if ( if (
error.privateMetadata != null && error.privateMetadata != null &&
@ -247,21 +247,21 @@ export default class Reporter {
error, error,
'The Atom team would like to collect the following information to resolve this error:', 'The Atom team would like to collect the following information to resolve this error:',
error => this.reportUncaughtException(error) error => this.reportUncaughtException(error)
) );
return return;
} }
let params = this.getDefaultNotificationParams() let params = this.getDefaultNotificationParams();
params.severity = 'error' params.severity = 'error';
this.performRequest(this.buildNotificationJSON(error, params)) this.performRequest(this.buildNotificationJSON(error, params));
this.reportedErrors.push(error) this.reportedErrors.push(error);
} }
reportFailedAssertion (error) { reportFailedAssertion(error) {
if (!this.shouldReport(error)) return if (!this.shouldReport(error)) return;
this.addPackageMetadata(error) this.addPackageMetadata(error);
this.addPreviousErrorsMetadata(error) this.addPreviousErrorsMetadata(error);
if ( if (
error.privateMetadata != null && error.privateMetadata != null &&
@ -271,32 +271,32 @@ export default class Reporter {
error, error,
'The Atom team would like to collect some information to resolve an unexpected condition:', 'The Atom team would like to collect some information to resolve an unexpected condition:',
error => this.reportFailedAssertion(error) error => this.reportFailedAssertion(error)
) );
return return;
} }
let params = this.getDefaultNotificationParams() let params = this.getDefaultNotificationParams();
params.severity = 'warning' params.severity = 'warning';
this.performRequest(this.buildNotificationJSON(error, params)) this.performRequest(this.buildNotificationJSON(error, params));
this.reportedAssertionFailures.push(error) this.reportedAssertionFailures.push(error);
} }
// Used in specs // Used in specs
setRequestFunction (requestFunction) { setRequestFunction(requestFunction) {
this.request = requestFunction this.request = requestFunction;
} }
isBundledFile (fileName) { isBundledFile(fileName) {
return this.normalizePath(fileName).indexOf(this.resourcePath) === 0 return this.normalizePath(fileName).indexOf(this.resourcePath) === 0;
} }
isTeletypeFile (fileName) { isTeletypeFile(fileName) {
const teletypePath = atom.packages.resolvePackagePath('teletype') const teletypePath = atom.packages.resolvePackagePath('teletype');
return ( return (
teletypePath && this.normalizePath(fileName).indexOf(teletypePath) === 0 teletypePath && this.normalizePath(fileName).indexOf(teletypePath) === 0
) );
} }
} }
Reporter.API_KEY = API_KEY Reporter.API_KEY = API_KEY;
Reporter.LIB_VERSION = LIB_VERSION Reporter.LIB_VERSION = LIB_VERSION;

View File

@ -1,76 +1,76 @@
const Reporter = require('../lib/reporter') const Reporter = require('../lib/reporter');
const semver = require('semver') const semver = require('semver');
const os = require('os') const os = require('os');
const path = require('path') const path = require('path');
const fs = require('fs-plus') const fs = require('fs-plus');
let osVersion = `${os.platform()}-${os.arch()}-${os.release()}` let osVersion = `${os.platform()}-${os.arch()}-${os.release()}`;
let getReleaseChannel = version => { let getReleaseChannel = version => {
return version.indexOf('beta') > -1 return version.indexOf('beta') > -1
? 'beta' ? 'beta'
: version.indexOf('dev') > -1 : version.indexOf('dev') > -1
? 'dev' ? 'dev'
: 'stable' : 'stable';
} };
describe('Reporter', () => { describe('Reporter', () => {
let reporter, let reporter,
requests, requests,
initialStackTraceLimit, initialStackTraceLimit,
initialFsGetHomeDirectory, initialFsGetHomeDirectory,
mockActivePackages mockActivePackages;
beforeEach(() => { beforeEach(() => {
reporter = new Reporter({ reporter = new Reporter({
request: (url, options) => requests.push(Object.assign({ url }, options)), request: (url, options) => requests.push(Object.assign({ url }, options)),
alwaysReport: true, alwaysReport: true,
reportPreviousErrors: false reportPreviousErrors: false
}) });
requests = [] requests = [];
mockActivePackages = [] mockActivePackages = [];
spyOn(atom.packages, 'getActivePackages').andCallFake( spyOn(atom.packages, 'getActivePackages').andCallFake(
() => mockActivePackages () => mockActivePackages
) );
initialStackTraceLimit = Error.stackTraceLimit initialStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 1 Error.stackTraceLimit = 1;
initialFsGetHomeDirectory = fs.getHomeDirectory initialFsGetHomeDirectory = fs.getHomeDirectory;
}) });
afterEach(() => { afterEach(() => {
fs.getHomeDirectory = initialFsGetHomeDirectory fs.getHomeDirectory = initialFsGetHomeDirectory;
Error.stackTraceLimit = initialStackTraceLimit Error.stackTraceLimit = initialStackTraceLimit;
}) });
describe('.reportUncaughtException(error)', () => { describe('.reportUncaughtException(error)', () => {
it('posts errors originated inside Atom Core to BugSnag', () => { it('posts errors originated inside Atom Core to BugSnag', () => {
const repositoryRootPath = path.join(__dirname, '..') const repositoryRootPath = path.join(__dirname, '..');
reporter = new Reporter({ reporter = new Reporter({
request: (url, options) => request: (url, options) =>
requests.push(Object.assign({ url }, options)), requests.push(Object.assign({ url }, options)),
alwaysReport: true, alwaysReport: true,
reportPreviousErrors: false, reportPreviousErrors: false,
resourcePath: repositoryRootPath resourcePath: repositoryRootPath
}) });
let error = new Error() let error = new Error();
Error.captureStackTrace(error) Error.captureStackTrace(error);
reporter.reportUncaughtException(error) reporter.reportUncaughtException(error);
let [lineNumber, columnNumber] = error.stack let [lineNumber, columnNumber] = error.stack
.match(/.js:(\d+):(\d+)/) .match(/.js:(\d+):(\d+)/)
.slice(1) .slice(1)
.map(s => parseInt(s)) .map(s => parseInt(s));
expect(requests.length).toBe(1) expect(requests.length).toBe(1);
let [request] = requests let [request] = requests;
expect(request.method).toBe('POST') expect(request.method).toBe('POST');
expect(request.url).toBe('https://notify.bugsnag.com') expect(request.url).toBe('https://notify.bugsnag.com');
expect(request.headers.get('Content-Type')).toBe('application/json') expect(request.headers.get('Content-Type')).toBe('application/json');
let body = JSON.parse(request.body) let body = JSON.parse(request.body);
// Delete `inProject` field because tests may fail when run as part of Atom core // Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`) // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject delete body.events[0].exceptions[0].stacktrace[0].inProject;
expect(body).toEqual({ expect(body).toEqual({
apiKey: Reporter.API_KEY, apiKey: Reporter.API_KEY,
@ -109,29 +109,29 @@ describe('Reporter', () => {
} }
} }
] ]
}) });
}) });
it('posts errors originated outside Atom Core to BugSnag', () => { it('posts errors originated outside Atom Core to BugSnag', () => {
fs.getHomeDirectory = () => path.join(__dirname, '..', '..') fs.getHomeDirectory = () => path.join(__dirname, '..', '..');
let error = new Error() let error = new Error();
Error.captureStackTrace(error) Error.captureStackTrace(error);
reporter.reportUncaughtException(error) reporter.reportUncaughtException(error);
let [lineNumber, columnNumber] = error.stack let [lineNumber, columnNumber] = error.stack
.match(/.js:(\d+):(\d+)/) .match(/.js:(\d+):(\d+)/)
.slice(1) .slice(1)
.map(s => parseInt(s)) .map(s => parseInt(s));
expect(requests.length).toBe(1) expect(requests.length).toBe(1);
let [request] = requests let [request] = requests;
expect(request.method).toBe('POST') expect(request.method).toBe('POST');
expect(request.url).toBe('https://notify.bugsnag.com') expect(request.url).toBe('https://notify.bugsnag.com');
expect(request.headers.get('Content-Type')).toBe('application/json') expect(request.headers.get('Content-Type')).toBe('application/json');
let body = JSON.parse(request.body) let body = JSON.parse(request.body);
// Delete `inProject` field because tests may fail when run as part of Atom core // Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`) // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject delete body.events[0].exceptions[0].stacktrace[0].inProject;
expect(body).toEqual({ expect(body).toEqual({
apiKey: Reporter.API_KEY, apiKey: Reporter.API_KEY,
@ -170,84 +170,84 @@ describe('Reporter', () => {
} }
} }
] ]
}) });
}) });
describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => { describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => {
let [error, notification] = [] let [error, notification] = [];
beforeEach(() => { beforeEach(() => {
atom.notifications.clear() atom.notifications.clear();
spyOn(atom.notifications, 'addInfo').andCallThrough() spyOn(atom.notifications, 'addInfo').andCallThrough();
error = new Error() error = new Error();
Error.captureStackTrace(error) Error.captureStackTrace(error);
error.metadata = { foo: 'bar' } error.metadata = { foo: 'bar' };
error.privateMetadata = { baz: 'quux' } error.privateMetadata = { baz: 'quux' };
error.privateMetadataDescription = 'The contents of baz' error.privateMetadataDescription = 'The contents of baz';
}) });
it('posts a notification asking for consent', () => { it('posts a notification asking for consent', () => {
reporter.reportUncaughtException(error) reporter.reportUncaughtException(error);
expect(atom.notifications.addInfo).toHaveBeenCalled() expect(atom.notifications.addInfo).toHaveBeenCalled();
}) });
it('submits the error with the private metadata if the user consents', () => { it('submits the error with the private metadata if the user consents', () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough() spyOn(reporter, 'reportUncaughtException').andCallThrough();
reporter.reportUncaughtException(error) reporter.reportUncaughtException(error);
reporter.reportUncaughtException.reset() reporter.reportUncaughtException.reset();
notification = atom.notifications.getNotifications()[0] notification = atom.notifications.getNotifications()[0];
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1] let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
expect(notificationOptions.buttons[1].text).toMatch(/Yes/) expect(notificationOptions.buttons[1].text).toMatch(/Yes/);
notificationOptions.buttons[1].onDidClick() notificationOptions.buttons[1].onDidClick();
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error) expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error);
expect(reporter.reportUncaughtException.callCount).toBe(1) expect(reporter.reportUncaughtException.callCount).toBe(1);
expect(error.privateMetadata).toBeUndefined() expect(error.privateMetadata).toBeUndefined();
expect(error.privateMetadataDescription).toBeUndefined() expect(error.privateMetadataDescription).toBeUndefined();
expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' }) expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' });
expect(notification.isDismissed()).toBe(true) expect(notification.isDismissed()).toBe(true);
}) });
it('submits the error without the private metadata if the user does not consent', () => { it('submits the error without the private metadata if the user does not consent', () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough() spyOn(reporter, 'reportUncaughtException').andCallThrough();
reporter.reportUncaughtException(error) reporter.reportUncaughtException(error);
reporter.reportUncaughtException.reset() reporter.reportUncaughtException.reset();
notification = atom.notifications.getNotifications()[0] notification = atom.notifications.getNotifications()[0];
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1] let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
expect(notificationOptions.buttons[0].text).toMatch(/No/) expect(notificationOptions.buttons[0].text).toMatch(/No/);
notificationOptions.buttons[0].onDidClick() notificationOptions.buttons[0].onDidClick();
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error) expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error);
expect(reporter.reportUncaughtException.callCount).toBe(1) expect(reporter.reportUncaughtException.callCount).toBe(1);
expect(error.privateMetadata).toBeUndefined() expect(error.privateMetadata).toBeUndefined();
expect(error.privateMetadataDescription).toBeUndefined() expect(error.privateMetadataDescription).toBeUndefined();
expect(error.metadata).toEqual({ foo: 'bar' }) expect(error.metadata).toEqual({ foo: 'bar' });
expect(notification.isDismissed()).toBe(true) expect(notification.isDismissed()).toBe(true);
}) });
it('submits the error without the private metadata if the user dismisses the notification', () => { it('submits the error without the private metadata if the user dismisses the notification', () => {
spyOn(reporter, 'reportUncaughtException').andCallThrough() spyOn(reporter, 'reportUncaughtException').andCallThrough();
reporter.reportUncaughtException(error) reporter.reportUncaughtException(error);
reporter.reportUncaughtException.reset() reporter.reportUncaughtException.reset();
notification = atom.notifications.getNotifications()[0] notification = atom.notifications.getNotifications()[0];
notification.dismiss() notification.dismiss();
expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error) expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error);
expect(reporter.reportUncaughtException.callCount).toBe(1) expect(reporter.reportUncaughtException.callCount).toBe(1);
expect(error.privateMetadata).toBeUndefined() expect(error.privateMetadata).toBeUndefined();
expect(error.privateMetadataDescription).toBeUndefined() expect(error.privateMetadataDescription).toBeUndefined();
expect(error.metadata).toEqual({ foo: 'bar' }) expect(error.metadata).toEqual({ foo: 'bar' });
}) });
}) });
it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => { it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
mockActivePackages = [ mockActivePackages = [
@ -273,71 +273,71 @@ describe('Reporter', () => {
'/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2',
metadata: { version: '1.2.0' } metadata: { version: '1.2.0' }
} }
] ];
const packageDirPaths = ['/Users/user/.atom/packages'] const packageDirPaths = ['/Users/user/.atom/packages'];
spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths) spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths);
let error = new Error() let error = new Error();
Error.captureStackTrace(error) Error.captureStackTrace(error);
reporter.reportUncaughtException(error) reporter.reportUncaughtException(error);
expect(error.metadata.userPackages).toEqual({ expect(error.metadata.userPackages).toEqual({
'user-1': '1.0.0', 'user-1': '1.0.0',
'user-2': '1.2.0' 'user-2': '1.2.0'
}) });
expect(error.metadata.bundledPackages).toEqual({ expect(error.metadata.bundledPackages).toEqual({
'bundled-1': '1.0.0', 'bundled-1': '1.0.0',
'bundled-2': '1.2.0' 'bundled-2': '1.2.0'
}) });
}) });
it('adds previous error messages and assertion failures to the reported metadata', () => { it('adds previous error messages and assertion failures to the reported metadata', () => {
reporter.reportPreviousErrors = true reporter.reportPreviousErrors = true;
reporter.reportUncaughtException(new Error('A')) reporter.reportUncaughtException(new Error('A'));
reporter.reportUncaughtException(new Error('B')) reporter.reportUncaughtException(new Error('B'));
reporter.reportFailedAssertion(new Error('X')) reporter.reportFailedAssertion(new Error('X'));
reporter.reportFailedAssertion(new Error('Y')) reporter.reportFailedAssertion(new Error('Y'));
reporter.reportUncaughtException(new Error('C')) reporter.reportUncaughtException(new Error('C'));
expect(requests.length).toBe(5) expect(requests.length).toBe(5);
const lastRequest = requests[requests.length - 1] const lastRequest = requests[requests.length - 1];
const body = JSON.parse(lastRequest.body) const body = JSON.parse(lastRequest.body);
console.log(body) console.log(body);
expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']) expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']);
expect(body.events[0].metaData.previousAssertionFailures).toEqual([ expect(body.events[0].metaData.previousAssertionFailures).toEqual([
'X', 'X',
'Y' 'Y'
]) ]);
}) });
}) });
describe('.reportFailedAssertion(error)', () => { describe('.reportFailedAssertion(error)', () => {
it('posts warnings to bugsnag', () => { it('posts warnings to bugsnag', () => {
fs.getHomeDirectory = () => path.join(__dirname, '..', '..') fs.getHomeDirectory = () => path.join(__dirname, '..', '..');
let error = new Error() let error = new Error();
Error.captureStackTrace(error) Error.captureStackTrace(error);
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
let [lineNumber, columnNumber] = error.stack let [lineNumber, columnNumber] = error.stack
.match(/.js:(\d+):(\d+)/) .match(/.js:(\d+):(\d+)/)
.slice(1) .slice(1)
.map(s => parseInt(s)) .map(s => parseInt(s));
expect(requests.length).toBe(1) expect(requests.length).toBe(1);
let [request] = requests let [request] = requests;
expect(request.method).toBe('POST') expect(request.method).toBe('POST');
expect(request.url).toBe('https://notify.bugsnag.com') expect(request.url).toBe('https://notify.bugsnag.com');
expect(request.headers.get('Content-Type')).toBe('application/json') expect(request.headers.get('Content-Type')).toBe('application/json');
let body = JSON.parse(request.body) let body = JSON.parse(request.body);
// Delete `inProject` field because tests may fail when run as part of Atom core // Delete `inProject` field because tests may fail when run as part of Atom core
// (i.e. when this test file will be located under `node_modules/exception-reporting/spec`) // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`)
delete body.events[0].exceptions[0].stacktrace[0].inProject delete body.events[0].exceptions[0].stacktrace[0].inProject;
expect(body).toEqual({ expect(body).toEqual({
apiKey: Reporter.API_KEY, apiKey: Reporter.API_KEY,
@ -376,112 +376,112 @@ describe('Reporter', () => {
} }
} }
] ]
}) });
}) });
describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => { describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => {
let [error, notification] = [] let [error, notification] = [];
beforeEach(() => { beforeEach(() => {
atom.notifications.clear() atom.notifications.clear();
spyOn(atom.notifications, 'addInfo').andCallThrough() spyOn(atom.notifications, 'addInfo').andCallThrough();
error = new Error() error = new Error();
Error.captureStackTrace(error) Error.captureStackTrace(error);
error.metadata = { foo: 'bar' } error.metadata = { foo: 'bar' };
error.privateMetadata = { baz: 'quux' } error.privateMetadata = { baz: 'quux' };
error.privateMetadataDescription = 'The contents of baz' error.privateMetadataDescription = 'The contents of baz';
}) });
it('posts a notification asking for consent', () => { it('posts a notification asking for consent', () => {
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
expect(atom.notifications.addInfo).toHaveBeenCalled() expect(atom.notifications.addInfo).toHaveBeenCalled();
}) });
it('submits the error with the private metadata if the user consents', () => { it('submits the error with the private metadata if the user consents', () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough() spyOn(reporter, 'reportFailedAssertion').andCallThrough();
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
reporter.reportFailedAssertion.reset() reporter.reportFailedAssertion.reset();
notification = atom.notifications.getNotifications()[0] notification = atom.notifications.getNotifications()[0];
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1] let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
expect(notificationOptions.buttons[1].text).toMatch(/Yes/) expect(notificationOptions.buttons[1].text).toMatch(/Yes/);
notificationOptions.buttons[1].onDidClick() notificationOptions.buttons[1].onDidClick();
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error) expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error);
expect(reporter.reportFailedAssertion.callCount).toBe(1) expect(reporter.reportFailedAssertion.callCount).toBe(1);
expect(error.privateMetadata).toBeUndefined() expect(error.privateMetadata).toBeUndefined();
expect(error.privateMetadataDescription).toBeUndefined() expect(error.privateMetadataDescription).toBeUndefined();
expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' }) expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' });
expect(notification.isDismissed()).toBe(true) expect(notification.isDismissed()).toBe(true);
}) });
it('submits the error without the private metadata if the user does not consent', () => { it('submits the error without the private metadata if the user does not consent', () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough() spyOn(reporter, 'reportFailedAssertion').andCallThrough();
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
reporter.reportFailedAssertion.reset() reporter.reportFailedAssertion.reset();
notification = atom.notifications.getNotifications()[0] notification = atom.notifications.getNotifications()[0];
let notificationOptions = atom.notifications.addInfo.argsForCall[0][1] let notificationOptions = atom.notifications.addInfo.argsForCall[0][1];
expect(notificationOptions.buttons[0].text).toMatch(/No/) expect(notificationOptions.buttons[0].text).toMatch(/No/);
notificationOptions.buttons[0].onDidClick() notificationOptions.buttons[0].onDidClick();
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error) expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error);
expect(reporter.reportFailedAssertion.callCount).toBe(1) expect(reporter.reportFailedAssertion.callCount).toBe(1);
expect(error.privateMetadata).toBeUndefined() expect(error.privateMetadata).toBeUndefined();
expect(error.privateMetadataDescription).toBeUndefined() expect(error.privateMetadataDescription).toBeUndefined();
expect(error.metadata).toEqual({ foo: 'bar' }) expect(error.metadata).toEqual({ foo: 'bar' });
expect(notification.isDismissed()).toBe(true) expect(notification.isDismissed()).toBe(true);
}) });
it('submits the error without the private metadata if the user dismisses the notification', () => { it('submits the error without the private metadata if the user dismisses the notification', () => {
spyOn(reporter, 'reportFailedAssertion').andCallThrough() spyOn(reporter, 'reportFailedAssertion').andCallThrough();
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
reporter.reportFailedAssertion.reset() reporter.reportFailedAssertion.reset();
notification = atom.notifications.getNotifications()[0] notification = atom.notifications.getNotifications()[0];
notification.dismiss() notification.dismiss();
expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error) expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error);
expect(reporter.reportFailedAssertion.callCount).toBe(1) expect(reporter.reportFailedAssertion.callCount).toBe(1);
expect(error.privateMetadata).toBeUndefined() expect(error.privateMetadata).toBeUndefined();
expect(error.privateMetadataDescription).toBeUndefined() expect(error.privateMetadataDescription).toBeUndefined();
expect(error.metadata).toEqual({ foo: 'bar' }) expect(error.metadata).toEqual({ foo: 'bar' });
}) });
it("only notifies the user once for a given 'privateMetadataRequestName'", () => { it("only notifies the user once for a given 'privateMetadataRequestName'", () => {
let fakeStorage = {} let fakeStorage = {};
spyOn(global.localStorage, 'setItem').andCallFake( spyOn(global.localStorage, 'setItem').andCallFake(
(key, value) => (fakeStorage[key] = value) (key, value) => (fakeStorage[key] = value)
) );
spyOn(global.localStorage, 'getItem').andCallFake( spyOn(global.localStorage, 'getItem').andCallFake(
key => fakeStorage[key] key => fakeStorage[key]
) );
error.privateMetadataRequestName = 'foo' error.privateMetadataRequestName = 'foo';
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
expect(atom.notifications.addInfo).toHaveBeenCalled() expect(atom.notifications.addInfo).toHaveBeenCalled();
atom.notifications.addInfo.reset() atom.notifications.addInfo.reset();
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
expect(atom.notifications.addInfo).not.toHaveBeenCalled() expect(atom.notifications.addInfo).not.toHaveBeenCalled();
let error2 = new Error() let error2 = new Error();
Error.captureStackTrace(error2) Error.captureStackTrace(error2);
error2.privateMetadataDescription = 'Something about you' error2.privateMetadataDescription = 'Something about you';
error2.privateMetadata = { baz: 'quux' } error2.privateMetadata = { baz: 'quux' };
error2.privateMetadataRequestName = 'bar' error2.privateMetadataRequestName = 'bar';
reporter.reportFailedAssertion(error2) reporter.reportFailedAssertion(error2);
expect(atom.notifications.addInfo).toHaveBeenCalled() expect(atom.notifications.addInfo).toHaveBeenCalled();
}) });
}) });
it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => { it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => {
mockActivePackages = [ mockActivePackages = [
@ -507,46 +507,46 @@ describe('Reporter', () => {
'/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2',
metadata: { version: '1.2.0' } metadata: { version: '1.2.0' }
} }
] ];
const packageDirPaths = ['/Users/user/.atom/packages'] const packageDirPaths = ['/Users/user/.atom/packages'];
spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths) spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths);
let error = new Error() let error = new Error();
Error.captureStackTrace(error) Error.captureStackTrace(error);
reporter.reportFailedAssertion(error) reporter.reportFailedAssertion(error);
expect(error.metadata.userPackages).toEqual({ expect(error.metadata.userPackages).toEqual({
'user-1': '1.0.0', 'user-1': '1.0.0',
'user-2': '1.2.0' 'user-2': '1.2.0'
}) });
expect(error.metadata.bundledPackages).toEqual({ expect(error.metadata.bundledPackages).toEqual({
'bundled-1': '1.0.0', 'bundled-1': '1.0.0',
'bundled-2': '1.2.0' 'bundled-2': '1.2.0'
}) });
}) });
it('adds previous error messages and assertion failures to the reported metadata', () => { it('adds previous error messages and assertion failures to the reported metadata', () => {
reporter.reportPreviousErrors = true reporter.reportPreviousErrors = true;
reporter.reportUncaughtException(new Error('A')) reporter.reportUncaughtException(new Error('A'));
reporter.reportUncaughtException(new Error('B')) reporter.reportUncaughtException(new Error('B'));
reporter.reportFailedAssertion(new Error('X')) reporter.reportFailedAssertion(new Error('X'));
reporter.reportFailedAssertion(new Error('Y')) reporter.reportFailedAssertion(new Error('Y'));
reporter.reportFailedAssertion(new Error('C')) reporter.reportFailedAssertion(new Error('C'));
expect(requests.length).toBe(5) expect(requests.length).toBe(5);
const lastRequest = requests[requests.length - 1] const lastRequest = requests[requests.length - 1];
const body = JSON.parse(lastRequest.body) const body = JSON.parse(lastRequest.body);
expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']) expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']);
expect(body.events[0].metaData.previousAssertionFailures).toEqual([ expect(body.events[0].metaData.previousAssertionFailures).toEqual([
'X', 'X',
'Y' 'Y'
]) ]);
}) });
}) });
}) });

View File

@ -1,89 +1,89 @@
const SelectListView = require('atom-select-list') const SelectListView = require('atom-select-list');
const { repositoryForPath } = require('./helpers') const { repositoryForPath } = require('./helpers');
module.exports = class DiffListView { module.exports = class DiffListView {
constructor () { constructor() {
this.selectListView = new SelectListView({ this.selectListView = new SelectListView({
emptyMessage: 'No diffs in file', emptyMessage: 'No diffs in file',
items: [], items: [],
filterKeyForItem: diff => diff.lineText, filterKeyForItem: diff => diff.lineText,
elementForItem: diff => { elementForItem: diff => {
const li = document.createElement('li') const li = document.createElement('li');
li.classList.add('two-lines') li.classList.add('two-lines');
const primaryLine = document.createElement('div') const primaryLine = document.createElement('div');
primaryLine.classList.add('primary-line') primaryLine.classList.add('primary-line');
primaryLine.textContent = diff.lineText primaryLine.textContent = diff.lineText;
li.appendChild(primaryLine) li.appendChild(primaryLine);
const secondaryLine = document.createElement('div') const secondaryLine = document.createElement('div');
secondaryLine.classList.add('secondary-line') secondaryLine.classList.add('secondary-line');
secondaryLine.textContent = `-${diff.oldStart},${diff.oldLines} +${ secondaryLine.textContent = `-${diff.oldStart},${diff.oldLines} +${
diff.newStart diff.newStart
},${diff.newLines}` },${diff.newLines}`;
li.appendChild(secondaryLine) li.appendChild(secondaryLine);
return li return li;
}, },
didConfirmSelection: diff => { didConfirmSelection: diff => {
this.cancel() this.cancel();
const bufferRow = diff.newStart > 0 ? diff.newStart - 1 : diff.newStart const bufferRow = diff.newStart > 0 ? diff.newStart - 1 : diff.newStart;
this.editor.setCursorBufferPosition([bufferRow, 0], { this.editor.setCursorBufferPosition([bufferRow, 0], {
autoscroll: true autoscroll: true
}) });
this.editor.moveToFirstCharacterOfLine() this.editor.moveToFirstCharacterOfLine();
}, },
didCancelSelection: () => { didCancelSelection: () => {
this.cancel() this.cancel();
} }
}) });
this.selectListView.element.classList.add('diff-list-view') this.selectListView.element.classList.add('diff-list-view');
this.panel = atom.workspace.addModalPanel({ this.panel = atom.workspace.addModalPanel({
item: this.selectListView, item: this.selectListView,
visible: false visible: false
}) });
} }
attach () { attach() {
this.previouslyFocusedElement = document.activeElement this.previouslyFocusedElement = document.activeElement;
this.selectListView.reset() this.selectListView.reset();
this.panel.show() this.panel.show();
this.selectListView.focus() this.selectListView.focus();
} }
cancel () { cancel() {
this.panel.hide() this.panel.hide();
if (this.previouslyFocusedElement) { if (this.previouslyFocusedElement) {
this.previouslyFocusedElement.focus() this.previouslyFocusedElement.focus();
this.previouslyFocusedElement = null this.previouslyFocusedElement = null;
} }
} }
destroy () { destroy() {
this.cancel() this.cancel();
this.panel.destroy() this.panel.destroy();
return this.selectListView.destroy() return this.selectListView.destroy();
} }
async toggle () { async toggle() {
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
if (this.panel.isVisible()) { if (this.panel.isVisible()) {
this.cancel() this.cancel();
} else if (editor) { } else if (editor) {
this.editor = editor this.editor = editor;
const repository = repositoryForPath(this.editor.getPath()) const repository = repositoryForPath(this.editor.getPath());
let diffs = repository let diffs = repository
? repository.getLineDiffs(this.editor.getPath(), this.editor.getText()) ? repository.getLineDiffs(this.editor.getPath(), this.editor.getText())
: [] : [];
if (!diffs) diffs = [] if (!diffs) diffs = [];
for (let diff of diffs) { for (let diff of diffs) {
const bufferRow = diff.newStart > 0 ? diff.newStart - 1 : diff.newStart const bufferRow = diff.newStart > 0 ? diff.newStart - 1 : diff.newStart;
const lineText = this.editor.lineTextForBufferRow(bufferRow) const lineText = this.editor.lineTextForBufferRow(bufferRow);
diff.lineText = lineText ? lineText.trim() : '' diff.lineText = lineText ? lineText.trim() : '';
} }
await this.selectListView.update({ items: diffs }) await this.selectListView.update({ items: diffs });
this.attach() this.attach();
} }
} }
} };

View File

@ -1,21 +1,21 @@
const { CompositeDisposable } = require('atom') const { CompositeDisposable } = require('atom');
const { repositoryForPath } = require('./helpers') const { repositoryForPath } = require('./helpers');
const MAX_BUFFER_LENGTH_TO_DIFF = 2 * 1024 * 1024 const MAX_BUFFER_LENGTH_TO_DIFF = 2 * 1024 * 1024;
module.exports = class GitDiffView { module.exports = class GitDiffView {
constructor (editor) { constructor(editor) {
this.updateDiffs = this.updateDiffs.bind(this) this.updateDiffs = this.updateDiffs.bind(this);
this.editor = editor this.editor = editor;
this.subscriptions = new CompositeDisposable() this.subscriptions = new CompositeDisposable();
this.decorations = {} this.decorations = {};
this.markers = [] this.markers = [];
} }
start () { start() {
const editorElement = this.editor.getElement() const editorElement = this.editor.getElement();
this.subscribeToRepository() this.subscribeToRepository();
this.subscriptions.add( this.subscriptions.add(
this.editor.onDidStopChanging(this.updateDiffs), this.editor.onDidStopChanging(this.updateDiffs),
@ -35,29 +35,29 @@ module.exports = class GitDiffView {
), ),
editorElement.onDidAttach(() => this.updateIconDecoration()), editorElement.onDidAttach(() => this.updateIconDecoration()),
this.editor.onDidDestroy(() => { this.editor.onDidDestroy(() => {
this.cancelUpdate() this.cancelUpdate();
this.removeDecorations() this.removeDecorations();
this.subscriptions.dispose() this.subscriptions.dispose();
}) })
) );
this.updateIconDecoration() this.updateIconDecoration();
this.scheduleUpdate() this.scheduleUpdate();
} }
moveToNextDiff () { moveToNextDiff() {
const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1 const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1;
let nextDiffLineNumber = null let nextDiffLineNumber = null;
let firstDiffLineNumber = null let firstDiffLineNumber = null;
if (this.diffs) { if (this.diffs) {
for (const { newStart } of this.diffs) { for (const { newStart } of this.diffs) {
if (newStart > cursorLineNumber) { if (newStart > cursorLineNumber) {
if (nextDiffLineNumber == null) nextDiffLineNumber = newStart - 1 if (nextDiffLineNumber == null) nextDiffLineNumber = newStart - 1;
nextDiffLineNumber = Math.min(newStart - 1, nextDiffLineNumber) nextDiffLineNumber = Math.min(newStart - 1, nextDiffLineNumber);
} }
if (firstDiffLineNumber == null) firstDiffLineNumber = newStart - 1 if (firstDiffLineNumber == null) firstDiffLineNumber = newStart - 1;
firstDiffLineNumber = Math.min(newStart - 1, firstDiffLineNumber) firstDiffLineNumber = Math.min(newStart - 1, firstDiffLineNumber);
} }
} }
@ -66,39 +66,39 @@ module.exports = class GitDiffView {
atom.config.get('git-diff.wrapAroundOnMoveToDiff') && atom.config.get('git-diff.wrapAroundOnMoveToDiff') &&
nextDiffLineNumber == null nextDiffLineNumber == null
) { ) {
nextDiffLineNumber = firstDiffLineNumber nextDiffLineNumber = firstDiffLineNumber;
} }
this.moveToLineNumber(nextDiffLineNumber) this.moveToLineNumber(nextDiffLineNumber);
} }
updateIconDecoration () { updateIconDecoration() {
const gutter = this.editor.getElement().querySelector('.gutter') const gutter = this.editor.getElement().querySelector('.gutter');
if (gutter) { if (gutter) {
if ( if (
atom.config.get('editor.showLineNumbers') && atom.config.get('editor.showLineNumbers') &&
atom.config.get('git-diff.showIconsInEditorGutter') atom.config.get('git-diff.showIconsInEditorGutter')
) { ) {
gutter.classList.add('git-diff-icon') gutter.classList.add('git-diff-icon');
} else { } else {
gutter.classList.remove('git-diff-icon') gutter.classList.remove('git-diff-icon');
} }
} }
} }
moveToPreviousDiff () { moveToPreviousDiff() {
const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1 const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1;
let previousDiffLineNumber = -1 let previousDiffLineNumber = -1;
let lastDiffLineNumber = -1 let lastDiffLineNumber = -1;
if (this.diffs) { if (this.diffs) {
for (const { newStart } of this.diffs) { for (const { newStart } of this.diffs) {
if (newStart < cursorLineNumber) { if (newStart < cursorLineNumber) {
previousDiffLineNumber = Math.max( previousDiffLineNumber = Math.max(
newStart - 1, newStart - 1,
previousDiffLineNumber previousDiffLineNumber
) );
} }
lastDiffLineNumber = Math.max(newStart - 1, lastDiffLineNumber) lastDiffLineNumber = Math.max(newStart - 1, lastDiffLineNumber);
} }
} }
@ -107,87 +107,87 @@ module.exports = class GitDiffView {
atom.config.get('git-diff.wrapAroundOnMoveToDiff') && atom.config.get('git-diff.wrapAroundOnMoveToDiff') &&
previousDiffLineNumber === -1 previousDiffLineNumber === -1
) { ) {
previousDiffLineNumber = lastDiffLineNumber previousDiffLineNumber = lastDiffLineNumber;
} }
this.moveToLineNumber(previousDiffLineNumber) this.moveToLineNumber(previousDiffLineNumber);
} }
moveToLineNumber (lineNumber) { moveToLineNumber(lineNumber) {
if (lineNumber != null && lineNumber >= 0) { if (lineNumber != null && lineNumber >= 0) {
this.editor.setCursorBufferPosition([lineNumber, 0]) this.editor.setCursorBufferPosition([lineNumber, 0]);
this.editor.moveToFirstCharacterOfLine() this.editor.moveToFirstCharacterOfLine();
} }
} }
subscribeToRepository () { subscribeToRepository() {
this.repository = repositoryForPath(this.editor.getPath()) this.repository = repositoryForPath(this.editor.getPath());
if (this.repository) { if (this.repository) {
this.subscriptions.add( this.subscriptions.add(
this.repository.onDidChangeStatuses(() => { this.repository.onDidChangeStatuses(() => {
this.scheduleUpdate() this.scheduleUpdate();
}) })
) );
this.subscriptions.add( this.subscriptions.add(
this.repository.onDidChangeStatus(changedPath => { this.repository.onDidChangeStatus(changedPath => {
if (changedPath === this.editor.getPath()) this.scheduleUpdate() if (changedPath === this.editor.getPath()) this.scheduleUpdate();
}) })
) );
} }
} }
cancelUpdate () { cancelUpdate() {
clearImmediate(this.immediateId) clearImmediate(this.immediateId);
} }
scheduleUpdate () { scheduleUpdate() {
this.cancelUpdate() this.cancelUpdate();
this.immediateId = setImmediate(this.updateDiffs) this.immediateId = setImmediate(this.updateDiffs);
} }
updateDiffs () { updateDiffs() {
if (this.editor.isDestroyed()) return if (this.editor.isDestroyed()) return;
this.removeDecorations() this.removeDecorations();
const path = this.editor && this.editor.getPath() const path = this.editor && this.editor.getPath();
if ( if (
path && path &&
this.editor.getBuffer().getLength() < MAX_BUFFER_LENGTH_TO_DIFF this.editor.getBuffer().getLength() < MAX_BUFFER_LENGTH_TO_DIFF
) { ) {
this.diffs = this.diffs =
this.repository && this.repository &&
this.repository.getLineDiffs(path, this.editor.getText()) this.repository.getLineDiffs(path, this.editor.getText());
if (this.diffs) this.addDecorations(this.diffs) if (this.diffs) this.addDecorations(this.diffs);
} }
} }
addDecorations (diffs) { addDecorations(diffs) {
for (const { newStart, oldLines, newLines } of diffs) { for (const { newStart, oldLines, newLines } of diffs) {
const startRow = newStart - 1 const startRow = newStart - 1;
const endRow = newStart + newLines - 1 const endRow = newStart + newLines - 1;
if (oldLines === 0 && newLines > 0) { if (oldLines === 0 && newLines > 0) {
this.markRange(startRow, endRow, 'git-line-added') this.markRange(startRow, endRow, 'git-line-added');
} else if (newLines === 0 && oldLines > 0) { } else if (newLines === 0 && oldLines > 0) {
if (startRow < 0) { if (startRow < 0) {
this.markRange(0, 0, 'git-previous-line-removed') this.markRange(0, 0, 'git-previous-line-removed');
} else { } else {
this.markRange(startRow, startRow, 'git-line-removed') this.markRange(startRow, startRow, 'git-line-removed');
} }
} else { } else {
this.markRange(startRow, endRow, 'git-line-modified') this.markRange(startRow, endRow, 'git-line-modified');
} }
} }
} }
removeDecorations () { removeDecorations() {
for (let marker of this.markers) marker.destroy() for (let marker of this.markers) marker.destroy();
this.markers = [] this.markers = [];
} }
markRange (startRow, endRow, klass) { markRange(startRow, endRow, klass) {
const marker = this.editor.markBufferRange([[startRow, 0], [endRow, 0]], { const marker = this.editor.markBufferRange([[startRow, 0], [endRow, 0]], {
invalidate: 'never' invalidate: 'never'
}) });
this.editor.decorateMarker(marker, { type: 'line-number', class: klass }) this.editor.decorateMarker(marker, { type: 'line-number', class: klass });
this.markers.push(marker) this.markers.push(marker);
} }
} };

View File

@ -1,11 +1,11 @@
exports.repositoryForPath = function (goalPath) { exports.repositoryForPath = function(goalPath) {
const directories = atom.project.getDirectories() const directories = atom.project.getDirectories();
const repositories = atom.project.getRepositories() const repositories = atom.project.getRepositories();
for (let i = 0; i < directories.length; i++) { for (let i = 0; i < directories.length; i++) {
const directory = directories[i] const directory = directories[i];
if (goalPath === directory.getPath() || directory.contains(goalPath)) { if (goalPath === directory.getPath() || directory.contains(goalPath)) {
return repositories[i] return repositories[i];
} }
} }
return null return null;
} };

View File

@ -1,32 +1,32 @@
const GitDiffView = require('./git-diff-view') const GitDiffView = require('./git-diff-view');
const DiffListView = require('./diff-list-view') const DiffListView = require('./diff-list-view');
let diffListView = null let diffListView = null;
module.exports = { module.exports = {
activate () { activate() {
const watchedEditors = new WeakSet() const watchedEditors = new WeakSet();
atom.workspace.observeTextEditors(editor => { atom.workspace.observeTextEditors(editor => {
if (watchedEditors.has(editor)) return if (watchedEditors.has(editor)) return;
new GitDiffView(editor).start() new GitDiffView(editor).start();
atom.commands.add( atom.commands.add(
atom.views.getView(editor), atom.views.getView(editor),
'git-diff:toggle-diff-list', 'git-diff:toggle-diff-list',
() => { () => {
if (diffListView == null) diffListView = new DiffListView() if (diffListView == null) diffListView = new DiffListView();
diffListView.toggle() diffListView.toggle();
} }
) );
watchedEditors.add(editor) watchedEditors.add(editor);
editor.onDidDestroy(() => watchedEditors.delete(editor)) editor.onDidDestroy(() => watchedEditors.delete(editor));
}) });
}, },
deactivate () { deactivate() {
if (diffListView) diffListView.destroy() if (diffListView) diffListView.destroy();
diffListView = null diffListView = null;
} }
} };

View File

@ -1,49 +1,51 @@
const path = require('path') const path = require('path');
const fs = require('fs-plus') const fs = require('fs-plus');
const temp = require('temp') const temp = require('temp');
describe('git-diff:toggle-diff-list', () => { describe('git-diff:toggle-diff-list', () => {
let diffListView, editor let diffListView, editor;
beforeEach(() => { beforeEach(() => {
const projectPath = temp.mkdirSync('git-diff-spec-') const projectPath = temp.mkdirSync('git-diff-spec-');
fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath) fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath);
fs.moveSync( fs.moveSync(
path.join(projectPath, 'git.git'), path.join(projectPath, 'git.git'),
path.join(projectPath, '.git') path.join(projectPath, '.git')
) );
atom.project.setPaths([projectPath]) atom.project.setPaths([projectPath]);
jasmine.attachToDOM(atom.workspace.getElement()) jasmine.attachToDOM(atom.workspace.getElement());
waitsForPromise(() => atom.packages.activatePackage('git-diff')) waitsForPromise(() => atom.packages.activatePackage('git-diff'));
waitsForPromise(() => atom.workspace.open('sample.js')) waitsForPromise(() => atom.workspace.open('sample.js'));
runs(() => { runs(() => {
editor = atom.workspace.getActiveTextEditor() editor = atom.workspace.getActiveTextEditor();
editor.setCursorBufferPosition([8, 30]) editor.setCursorBufferPosition([8, 30]);
editor.insertText('a') editor.insertText('a');
atom.commands.dispatch(editor.getElement(), 'git-diff:toggle-diff-list') atom.commands.dispatch(editor.getElement(), 'git-diff:toggle-diff-list');
}) });
waitsFor(() => { waitsFor(() => {
diffListView = document.querySelector('.diff-list-view') diffListView = document.querySelector('.diff-list-view');
return diffListView && diffListView.querySelectorAll('li').length > 0 return diffListView && diffListView.querySelectorAll('li').length > 0;
}) });
}) });
it('shows a list of all diff hunks', () => { it('shows a list of all diff hunks', () => {
diffListView = document.querySelector('.diff-list-view ol') diffListView = document.querySelector('.diff-list-view ol');
expect(diffListView.textContent).toBe('while (items.length > 0) {a-9,1 +9,1') expect(diffListView.textContent).toBe(
}) 'while (items.length > 0) {a-9,1 +9,1'
);
});
it('moves the cursor to the selected hunk', () => { it('moves the cursor to the selected hunk', () => {
editor.setCursorBufferPosition([0, 0]) editor.setCursorBufferPosition([0, 0]);
atom.commands.dispatch( atom.commands.dispatch(
document.querySelector('.diff-list-view'), document.querySelector('.diff-list-view'),
'core:confirm' 'core:confirm'
) );
expect(editor.getCursorBufferPosition()).toEqual([8, 4]) expect(editor.getCursorBufferPosition()).toEqual([8, 4]);
}) });
}) });

View File

@ -1,244 +1,246 @@
const path = require('path') const path = require('path');
const fs = require('fs-plus') const fs = require('fs-plus');
const temp = require('temp') const temp = require('temp');
describe('GitDiff package', () => { describe('GitDiff package', () => {
let editor, editorElement, projectPath let editor, editorElement, projectPath;
beforeEach(() => { beforeEach(() => {
spyOn(window, 'setImmediate').andCallFake(fn => fn()) spyOn(window, 'setImmediate').andCallFake(fn => fn());
projectPath = temp.mkdirSync('git-diff-spec-') projectPath = temp.mkdirSync('git-diff-spec-');
const otherPath = temp.mkdirSync('some-other-path-') const otherPath = temp.mkdirSync('some-other-path-');
fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath) fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath);
fs.moveSync( fs.moveSync(
path.join(projectPath, 'git.git'), path.join(projectPath, 'git.git'),
path.join(projectPath, '.git') path.join(projectPath, '.git')
) );
atom.project.setPaths([otherPath, projectPath]) atom.project.setPaths([otherPath, projectPath]);
jasmine.attachToDOM(atom.workspace.getElement()) jasmine.attachToDOM(atom.workspace.getElement());
waitsForPromise(() => waitsForPromise(() =>
atom.workspace.open(path.join(projectPath, 'sample.js')) atom.workspace.open(path.join(projectPath, 'sample.js'))
) );
runs(() => { runs(() => {
editor = atom.workspace.getActiveTextEditor() editor = atom.workspace.getActiveTextEditor();
editorElement = editor.getElement() editorElement = editor.getElement();
}) });
waitsForPromise(() => atom.packages.activatePackage('git-diff')) waitsForPromise(() => atom.packages.activatePackage('git-diff'));
}) });
describe('when the editor has modified lines', () => { describe('when the editor has modified lines', () => {
it('highlights the modified lines', () => { it('highlights the modified lines', () => {
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe( expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
0 0
) );
editor.insertText('a') editor.insertText('a');
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe( expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
1 1
) );
expect(editorElement.querySelector('.git-line-modified')).toHaveData( expect(editorElement.querySelector('.git-line-modified')).toHaveData(
'buffer-row', 'buffer-row',
0 0
) );
}) });
}) });
describe('when the editor has added lines', () => { describe('when the editor has added lines', () => {
it('highlights the added lines', () => { it('highlights the added lines', () => {
expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0) expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0);
editor.moveToEndOfLine() editor.moveToEndOfLine();
editor.insertNewline() editor.insertNewline();
editor.insertText('a') editor.insertText('a');
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-added').length).toBe(1) expect(editorElement.querySelectorAll('.git-line-added').length).toBe(1);
expect(editorElement.querySelector('.git-line-added')).toHaveData( expect(editorElement.querySelector('.git-line-added')).toHaveData(
'buffer-row', 'buffer-row',
1 1
) );
}) });
}) });
describe('when the editor has removed lines', () => { describe('when the editor has removed lines', () => {
it('highlights the line preceeding the deleted lines', () => { it('highlights the line preceeding the deleted lines', () => {
expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0) expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0);
editor.setCursorBufferPosition([5]) editor.setCursorBufferPosition([5]);
editor.deleteLine() editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-removed').length).toBe(1) expect(editorElement.querySelectorAll('.git-line-removed').length).toBe(
1
);
expect(editorElement.querySelector('.git-line-removed')).toHaveData( expect(editorElement.querySelector('.git-line-removed')).toHaveData(
'buffer-row', 'buffer-row',
4 4
) );
}) });
}) });
describe('when the editor has removed the first line', () => { describe('when the editor has removed the first line', () => {
it('highlights the line preceeding the deleted lines', () => { it('highlights the line preceeding the deleted lines', () => {
expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0) expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0);
editor.setCursorBufferPosition([0, 0]) editor.setCursorBufferPosition([0, 0]);
editor.deleteLine() editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
expect( expect(
editorElement.querySelectorAll('.git-previous-line-removed').length editorElement.querySelectorAll('.git-previous-line-removed').length
).toBe(1) ).toBe(1);
expect( expect(
editorElement.querySelector('.git-previous-line-removed') editorElement.querySelector('.git-previous-line-removed')
).toHaveData('buffer-row', 0) ).toHaveData('buffer-row', 0);
}) });
}) });
describe('when a modified line is restored to the HEAD version contents', () => { describe('when a modified line is restored to the HEAD version contents', () => {
it('removes the diff highlight', () => { it('removes the diff highlight', () => {
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe( expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
0 0
) );
editor.insertText('a') editor.insertText('a');
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe( expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
1 1
) );
editor.backspace() editor.backspace();
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe( expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
0 0
) );
}) });
}) });
describe('when a modified file is opened', () => { describe('when a modified file is opened', () => {
it('highlights the changed lines', () => { it('highlights the changed lines', () => {
fs.writeFileSync( fs.writeFileSync(
path.join(projectPath, 'sample.txt'), path.join(projectPath, 'sample.txt'),
'Some different text.' 'Some different text.'
) );
let nextTick = false let nextTick = false;
waitsForPromise(() => waitsForPromise(() =>
atom.workspace.open(path.join(projectPath, 'sample.txt')) atom.workspace.open(path.join(projectPath, 'sample.txt'))
) );
runs(() => { runs(() => {
editorElement = atom.workspace.getActiveTextEditor().getElement() editorElement = atom.workspace.getActiveTextEditor().getElement();
}) });
setImmediate(() => { setImmediate(() => {
nextTick = true nextTick = true;
}) });
waitsFor(() => nextTick) waitsFor(() => nextTick);
runs(() => { runs(() => {
expect( expect(
editorElement.querySelectorAll('.git-line-modified').length editorElement.querySelectorAll('.git-line-modified').length
).toBe(1) ).toBe(1);
expect(editorElement.querySelector('.git-line-modified')).toHaveData( expect(editorElement.querySelector('.git-line-modified')).toHaveData(
'buffer-row', 'buffer-row',
0 0
) );
}) });
}) });
}) });
describe('when the project paths change', () => { describe('when the project paths change', () => {
it("doesn't try to use the destroyed git repository", () => { it("doesn't try to use the destroyed git repository", () => {
editor.deleteLine() editor.deleteLine();
atom.project.setPaths([temp.mkdirSync('no-repository')]) atom.project.setPaths([temp.mkdirSync('no-repository')]);
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
}) });
}) });
describe('move-to-next-diff/move-to-previous-diff events', () => { describe('move-to-next-diff/move-to-previous-diff events', () => {
it('moves the cursor to first character of the next/previous diff line', () => { it('moves the cursor to first character of the next/previous diff line', () => {
editor.insertText('a') editor.insertText('a');
editor.setCursorBufferPosition([5]) editor.setCursorBufferPosition([5]);
editor.deleteLine() editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
editor.setCursorBufferPosition([0]) editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]) expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]) expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
}) });
it('wraps around to the first/last diff in the file', () => { it('wraps around to the first/last diff in the file', () => {
editor.insertText('a') editor.insertText('a');
editor.setCursorBufferPosition([5]) editor.setCursorBufferPosition([5]);
editor.deleteLine() editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
editor.setCursorBufferPosition([0]) editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]) expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]) expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]) expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
}) });
describe('when the wrapAroundOnMoveToDiff config option is false', () => { describe('when the wrapAroundOnMoveToDiff config option is false', () => {
beforeEach(() => beforeEach(() =>
atom.config.set('git-diff.wrapAroundOnMoveToDiff', false) atom.config.set('git-diff.wrapAroundOnMoveToDiff', false)
) );
it('does not wraps around to the first/last diff in the file', () => { it('does not wraps around to the first/last diff in the file', () => {
editor.insertText('a') editor.insertText('a');
editor.setCursorBufferPosition([5]) editor.setCursorBufferPosition([5]);
editor.deleteLine() editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay) advanceClock(editor.getBuffer().stoppedChangingDelay);
editor.setCursorBufferPosition([0]) editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]) expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]) expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]) expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff') atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]) expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
}) });
}) });
}) });
describe('when the showIconsInEditorGutter config option is true', () => { describe('when the showIconsInEditorGutter config option is true', () => {
beforeEach(() => { beforeEach(() => {
atom.config.set('git-diff.showIconsInEditorGutter', true) atom.config.set('git-diff.showIconsInEditorGutter', true);
}) });
it('the gutter has a git-diff-icon class', () => it('the gutter has a git-diff-icon class', () =>
expect(editorElement.querySelector('.gutter')).toHaveClass( expect(editorElement.querySelector('.gutter')).toHaveClass(
'git-diff-icon' 'git-diff-icon'
)) ));
it('keeps the git-diff-icon class when editor.showLineNumbers is toggled', () => { it('keeps the git-diff-icon class when editor.showLineNumbers is toggled', () => {
atom.config.set('editor.showLineNumbers', false) atom.config.set('editor.showLineNumbers', false);
expect(editorElement.querySelector('.gutter')).not.toHaveClass( expect(editorElement.querySelector('.gutter')).not.toHaveClass(
'git-diff-icon' 'git-diff-icon'
) );
atom.config.set('editor.showLineNumbers', true) atom.config.set('editor.showLineNumbers', true);
expect(editorElement.querySelector('.gutter')).toHaveClass( expect(editorElement.querySelector('.gutter')).toHaveClass(
'git-diff-icon' 'git-diff-icon'
) );
}) });
it('removes the git-diff-icon class when the showIconsInEditorGutter config option set to false', () => { it('removes the git-diff-icon class when the showIconsInEditorGutter config option set to false', () => {
atom.config.set('git-diff.showIconsInEditorGutter', false) atom.config.set('git-diff.showIconsInEditorGutter', false);
expect(editorElement.querySelector('.gutter')).not.toHaveClass( expect(editorElement.querySelector('.gutter')).not.toHaveClass(
'git-diff-icon' 'git-diff-icon'
) );
}) });
}) });
}) });

View File

@ -1,111 +1,111 @@
'use babel' 'use babel';
import { Point, TextEditor } from 'atom' import { Point, TextEditor } from 'atom';
class GoToLineView { class GoToLineView {
constructor () { constructor() {
this.miniEditor = new TextEditor({ mini: true }) this.miniEditor = new TextEditor({ mini: true });
this.miniEditor.element.addEventListener('blur', this.close.bind(this)) this.miniEditor.element.addEventListener('blur', this.close.bind(this));
this.message = document.createElement('div') this.message = document.createElement('div');
this.message.classList.add('message') this.message.classList.add('message');
this.element = document.createElement('div') this.element = document.createElement('div');
this.element.classList.add('go-to-line') this.element.classList.add('go-to-line');
this.element.appendChild(this.miniEditor.element) this.element.appendChild(this.miniEditor.element);
this.element.appendChild(this.message) this.element.appendChild(this.message);
this.panel = atom.workspace.addModalPanel({ this.panel = atom.workspace.addModalPanel({
item: this, item: this,
visible: false visible: false
}) });
atom.commands.add('atom-text-editor', 'go-to-line:toggle', () => { atom.commands.add('atom-text-editor', 'go-to-line:toggle', () => {
this.toggle() this.toggle();
return false return false;
}) });
atom.commands.add(this.miniEditor.element, 'core:confirm', () => { atom.commands.add(this.miniEditor.element, 'core:confirm', () => {
this.navigate() this.navigate();
}) });
atom.commands.add(this.miniEditor.element, 'core:cancel', () => { atom.commands.add(this.miniEditor.element, 'core:cancel', () => {
this.close() this.close();
}) });
this.miniEditor.onWillInsertText(arg => { this.miniEditor.onWillInsertText(arg => {
if (arg.text.match(/[^0-9:]/)) { if (arg.text.match(/[^0-9:]/)) {
arg.cancel() arg.cancel();
} }
}) });
this.miniEditor.onDidChange(() => { this.miniEditor.onDidChange(() => {
this.navigate({ keepOpen: true }) this.navigate({ keepOpen: true });
}) });
} }
toggle () { toggle() {
this.panel.isVisible() ? this.close() : this.open() this.panel.isVisible() ? this.close() : this.open();
} }
close () { close() {
if (!this.panel.isVisible()) return if (!this.panel.isVisible()) return;
this.miniEditor.setText('') this.miniEditor.setText('');
this.panel.hide() this.panel.hide();
if (this.miniEditor.element.hasFocus()) { if (this.miniEditor.element.hasFocus()) {
this.restoreFocus() this.restoreFocus();
} }
} }
navigate (options = {}) { navigate(options = {}) {
const lineNumber = this.miniEditor.getText() const lineNumber = this.miniEditor.getText();
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
if (!options.keepOpen) { if (!options.keepOpen) {
this.close() this.close();
} }
if (!editor || !lineNumber.length) return if (!editor || !lineNumber.length) return;
const currentRow = editor.getCursorBufferPosition().row const currentRow = editor.getCursorBufferPosition().row;
const rowLineNumber = lineNumber.split(/:+/)[0] || '' const rowLineNumber = lineNumber.split(/:+/)[0] || '';
const row = const row =
rowLineNumber.length > 0 ? parseInt(rowLineNumber) - 1 : currentRow rowLineNumber.length > 0 ? parseInt(rowLineNumber) - 1 : currentRow;
const columnLineNumber = lineNumber.split(/:+/)[1] || '' const columnLineNumber = lineNumber.split(/:+/)[1] || '';
const column = const column =
columnLineNumber.length > 0 ? parseInt(columnLineNumber) - 1 : -1 columnLineNumber.length > 0 ? parseInt(columnLineNumber) - 1 : -1;
const position = new Point(row, column) const position = new Point(row, column);
editor.setCursorBufferPosition(position) editor.setCursorBufferPosition(position);
editor.unfoldBufferRow(row) editor.unfoldBufferRow(row);
if (column < 0) { if (column < 0) {
editor.moveToFirstCharacterOfLine() editor.moveToFirstCharacterOfLine();
} }
editor.scrollToBufferPosition(position, { editor.scrollToBufferPosition(position, {
center: true center: true
}) });
} }
storeFocusedElement () { storeFocusedElement() {
this.previouslyFocusedElement = document.activeElement this.previouslyFocusedElement = document.activeElement;
return this.previouslyFocusedElement return this.previouslyFocusedElement;
} }
restoreFocus () { restoreFocus() {
if ( if (
this.previouslyFocusedElement && this.previouslyFocusedElement &&
this.previouslyFocusedElement.parentElement this.previouslyFocusedElement.parentElement
) { ) {
return this.previouslyFocusedElement.focus() return this.previouslyFocusedElement.focus();
} }
atom.views.getView(atom.workspace).focus() atom.views.getView(atom.workspace).focus();
} }
open () { open() {
if (this.panel.isVisible() || !atom.workspace.getActiveTextEditor()) return if (this.panel.isVisible() || !atom.workspace.getActiveTextEditor()) return;
this.storeFocusedElement() this.storeFocusedElement();
this.panel.show() this.panel.show();
this.message.textContent = this.message.textContent =
'Enter a <row> or <row>:<column> to go there. Examples: "3" for row 3 or "2:7" for row 2 and column 7' 'Enter a <row> or <row>:<column> to go there. Examples: "3" for row 3 or "2:7" for row 2 and column 7';
this.miniEditor.element.focus() this.miniEditor.element.focus();
} }
} }
export default { export default {
activate () { activate() {
return new GoToLineView() return new GoToLineView();
} }
} };

View File

@ -1,169 +1,169 @@
'use babel' 'use babel';
/* eslint-env jasmine */ /* eslint-env jasmine */
import GoToLineView from '../lib/go-to-line-view' import GoToLineView from '../lib/go-to-line-view';
describe('GoToLine', () => { describe('GoToLine', () => {
let editor = null let editor = null;
let editorView = null let editorView = null;
let goToLine = null let goToLine = null;
beforeEach(() => { beforeEach(() => {
waitsForPromise(() => { waitsForPromise(() => {
return atom.workspace.open('sample.js') return atom.workspace.open('sample.js');
}) });
runs(() => { runs(() => {
const workspaceElement = atom.views.getView(atom.workspace) const workspaceElement = atom.views.getView(atom.workspace);
workspaceElement.style.height = '200px' workspaceElement.style.height = '200px';
workspaceElement.style.width = '1000px' workspaceElement.style.width = '1000px';
jasmine.attachToDOM(workspaceElement) jasmine.attachToDOM(workspaceElement);
editor = atom.workspace.getActiveTextEditor() editor = atom.workspace.getActiveTextEditor();
editorView = atom.views.getView(editor) editorView = atom.views.getView(editor);
goToLine = GoToLineView.activate() goToLine = GoToLineView.activate();
editor.setCursorBufferPosition([1, 0]) editor.setCursorBufferPosition([1, 0]);
}) });
}) });
describe('when go-to-line:toggle is triggered', () => { describe('when go-to-line:toggle is triggered', () => {
it('adds a modal panel', () => { it('adds a modal panel', () => {
expect(goToLine.panel.isVisible()).toBeFalsy() expect(goToLine.panel.isVisible()).toBeFalsy();
atom.commands.dispatch(editorView, 'go-to-line:toggle') atom.commands.dispatch(editorView, 'go-to-line:toggle');
expect(goToLine.panel.isVisible()).toBeTruthy() expect(goToLine.panel.isVisible()).toBeTruthy();
}) });
}) });
describe('when entering a line number', () => { describe('when entering a line number', () => {
it('only allows 0-9 and the colon character to be entered in the mini editor', () => { it('only allows 0-9 and the colon character to be entered in the mini editor', () => {
expect(goToLine.miniEditor.getText()).toBe('') expect(goToLine.miniEditor.getText()).toBe('');
goToLine.miniEditor.insertText('a') goToLine.miniEditor.insertText('a');
expect(goToLine.miniEditor.getText()).toBe('') expect(goToLine.miniEditor.getText()).toBe('');
goToLine.miniEditor.insertText('path/file.txt:56') goToLine.miniEditor.insertText('path/file.txt:56');
expect(goToLine.miniEditor.getText()).toBe('') expect(goToLine.miniEditor.getText()).toBe('');
goToLine.miniEditor.insertText(':') goToLine.miniEditor.insertText(':');
expect(goToLine.miniEditor.getText()).toBe(':') expect(goToLine.miniEditor.getText()).toBe(':');
goToLine.miniEditor.setText('') goToLine.miniEditor.setText('');
goToLine.miniEditor.insertText('4') goToLine.miniEditor.insertText('4');
expect(goToLine.miniEditor.getText()).toBe('4') expect(goToLine.miniEditor.getText()).toBe('4');
}) });
}) });
describe('when typing line numbers (auto-navigation)', () => { describe('when typing line numbers (auto-navigation)', () => {
it('automatically scrolls to the desired line', () => { it('automatically scrolls to the desired line', () => {
goToLine.miniEditor.insertText('19') goToLine.miniEditor.insertText('19');
expect(editor.getCursorBufferPosition()).toEqual([18, 0]) expect(editor.getCursorBufferPosition()).toEqual([18, 0]);
}) });
}) });
describe('when typing line and column numbers (auto-navigation)', () => { describe('when typing line and column numbers (auto-navigation)', () => {
it('automatically scrolls to the desired line and column', () => { it('automatically scrolls to the desired line and column', () => {
goToLine.miniEditor.insertText('3:8') goToLine.miniEditor.insertText('3:8');
expect(editor.getCursorBufferPosition()).toEqual([2, 7]) expect(editor.getCursorBufferPosition()).toEqual([2, 7]);
}) });
}) });
describe('when entering a line number and column number', () => { describe('when entering a line number and column number', () => {
it('moves the cursor to the column number of the line specified', () => { it('moves the cursor to the column number of the line specified', () => {
expect(goToLine.miniEditor.getText()).toBe('') expect(goToLine.miniEditor.getText()).toBe('');
goToLine.miniEditor.insertText('3:14') goToLine.miniEditor.insertText('3:14');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(editor.getCursorBufferPosition()).toEqual([2, 13]) expect(editor.getCursorBufferPosition()).toEqual([2, 13]);
}) });
it('centers the selected line', () => { it('centers the selected line', () => {
goToLine.miniEditor.insertText('45:4') goToLine.miniEditor.insertText('45:4');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
const rowsPerPage = editor.getRowsPerPage() const rowsPerPage = editor.getRowsPerPage();
const currentRow = editor.getCursorBufferPosition().row - 1 const currentRow = editor.getCursorBufferPosition().row - 1;
expect(editor.getFirstVisibleScreenRow()).toBe( expect(editor.getFirstVisibleScreenRow()).toBe(
currentRow - Math.ceil(rowsPerPage / 2) currentRow - Math.ceil(rowsPerPage / 2)
) );
expect(editor.getLastVisibleScreenRow()).toBe( expect(editor.getLastVisibleScreenRow()).toBe(
currentRow + Math.floor(rowsPerPage / 2) currentRow + Math.floor(rowsPerPage / 2)
) );
}) });
}) });
describe('when entering a line number greater than the number of rows in the buffer', () => { describe('when entering a line number greater than the number of rows in the buffer', () => {
it('moves the cursor position to the first character of the last line', () => { it('moves the cursor position to the first character of the last line', () => {
atom.commands.dispatch(editorView, 'go-to-line:toggle') atom.commands.dispatch(editorView, 'go-to-line:toggle');
expect(goToLine.panel.isVisible()).toBeTruthy() expect(goToLine.panel.isVisible()).toBeTruthy();
expect(goToLine.miniEditor.getText()).toBe('') expect(goToLine.miniEditor.getText()).toBe('');
goToLine.miniEditor.insertText('78') goToLine.miniEditor.insertText('78');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(goToLine.panel.isVisible()).toBeFalsy() expect(goToLine.panel.isVisible()).toBeFalsy();
expect(editor.getCursorBufferPosition()).toEqual([77, 0]) expect(editor.getCursorBufferPosition()).toEqual([77, 0]);
}) });
}) });
describe('when entering a column number greater than the number in the specified line', () => { describe('when entering a column number greater than the number in the specified line', () => {
it('moves the cursor position to the last character of the specified line', () => { it('moves the cursor position to the last character of the specified line', () => {
atom.commands.dispatch(editorView, 'go-to-line:toggle') atom.commands.dispatch(editorView, 'go-to-line:toggle');
expect(goToLine.panel.isVisible()).toBeTruthy() expect(goToLine.panel.isVisible()).toBeTruthy();
expect(goToLine.miniEditor.getText()).toBe('') expect(goToLine.miniEditor.getText()).toBe('');
goToLine.miniEditor.insertText('3:43') goToLine.miniEditor.insertText('3:43');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(goToLine.panel.isVisible()).toBeFalsy() expect(goToLine.panel.isVisible()).toBeFalsy();
expect(editor.getCursorBufferPosition()).toEqual([2, 39]) expect(editor.getCursorBufferPosition()).toEqual([2, 39]);
}) });
}) });
describe('when core:confirm is triggered', () => { describe('when core:confirm is triggered', () => {
describe('when a line number has been entered', () => { describe('when a line number has been entered', () => {
it('moves the cursor to the first character of the line', () => { it('moves the cursor to the first character of the line', () => {
goToLine.miniEditor.insertText('3') goToLine.miniEditor.insertText('3');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(editor.getCursorBufferPosition()).toEqual([2, 4]) expect(editor.getCursorBufferPosition()).toEqual([2, 4]);
}) });
}) });
describe('when the line number entered is nested within foldes', () => { describe('when the line number entered is nested within foldes', () => {
it('unfolds all folds containing the given row', () => { it('unfolds all folds containing the given row', () => {
expect(editor.indentationForBufferRow(9)).toEqual(3) expect(editor.indentationForBufferRow(9)).toEqual(3);
editor.foldAll() editor.foldAll();
expect(editor.screenRowForBufferRow(9)).toEqual(0) expect(editor.screenRowForBufferRow(9)).toEqual(0);
goToLine.miniEditor.insertText('10') goToLine.miniEditor.insertText('10');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(editor.getCursorBufferPosition()).toEqual([9, 6]) expect(editor.getCursorBufferPosition()).toEqual([9, 6]);
}) });
}) });
}) });
describe('when no line number has been entered', () => { describe('when no line number has been entered', () => {
it('closes the view and does not update the cursor position', () => { it('closes the view and does not update the cursor position', () => {
atom.commands.dispatch(editorView, 'go-to-line:toggle') atom.commands.dispatch(editorView, 'go-to-line:toggle');
expect(goToLine.panel.isVisible()).toBeTruthy() expect(goToLine.panel.isVisible()).toBeTruthy();
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(goToLine.panel.isVisible()).toBeFalsy() expect(goToLine.panel.isVisible()).toBeFalsy();
expect(editor.getCursorBufferPosition()).toEqual([1, 0]) expect(editor.getCursorBufferPosition()).toEqual([1, 0]);
}) });
}) });
describe('when no line number has been entered, but a column number has been entered', () => { describe('when no line number has been entered, but a column number has been entered', () => {
it('navigates to the column of the current line', () => { it('navigates to the column of the current line', () => {
atom.commands.dispatch(editorView, 'go-to-line:toggle') atom.commands.dispatch(editorView, 'go-to-line:toggle');
expect(goToLine.panel.isVisible()).toBeTruthy() expect(goToLine.panel.isVisible()).toBeTruthy();
goToLine.miniEditor.insertText('4:1') goToLine.miniEditor.insertText('4:1');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(goToLine.panel.isVisible()).toBeFalsy() expect(goToLine.panel.isVisible()).toBeFalsy();
expect(editor.getCursorBufferPosition()).toEqual([3, 0]) expect(editor.getCursorBufferPosition()).toEqual([3, 0]);
atom.commands.dispatch(editorView, 'go-to-line:toggle') atom.commands.dispatch(editorView, 'go-to-line:toggle');
expect(goToLine.panel.isVisible()).toBeTruthy() expect(goToLine.panel.isVisible()).toBeTruthy();
goToLine.miniEditor.insertText(':19') goToLine.miniEditor.insertText(':19');
atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm') atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm');
expect(goToLine.panel.isVisible()).toBeFalsy() expect(goToLine.panel.isVisible()).toBeFalsy();
expect(editor.getCursorBufferPosition()).toEqual([3, 18]) expect(editor.getCursorBufferPosition()).toEqual([3, 18]);
}) });
}) });
describe('when core:cancel is triggered', () => { describe('when core:cancel is triggered', () => {
it('closes the view and does not update the cursor position', () => { it('closes the view and does not update the cursor position', () => {
atom.commands.dispatch(editorView, 'go-to-line:toggle') atom.commands.dispatch(editorView, 'go-to-line:toggle');
expect(goToLine.panel.isVisible()).toBeTruthy() expect(goToLine.panel.isVisible()).toBeTruthy();
atom.commands.dispatch(goToLine.miniEditor.element, 'core:cancel') atom.commands.dispatch(goToLine.miniEditor.element, 'core:cancel');
expect(goToLine.panel.isVisible()).toBeFalsy() expect(goToLine.panel.isVisible()).toBeFalsy();
expect(editor.getCursorBufferPosition()).toEqual([1, 0]) expect(editor.getCursorBufferPosition()).toEqual([1, 0]);
}) });
}) });
}) });

View File

@ -1,103 +1,103 @@
const SelectListView = require('atom-select-list') const SelectListView = require('atom-select-list');
module.exports = class GrammarListView { module.exports = class GrammarListView {
constructor () { constructor() {
this.autoDetect = { name: 'Auto Detect' } this.autoDetect = { name: 'Auto Detect' };
this.selectListView = new SelectListView({ this.selectListView = new SelectListView({
itemsClassList: ['mark-active'], itemsClassList: ['mark-active'],
items: [], items: [],
filterKeyForItem: grammar => grammar.name, filterKeyForItem: grammar => grammar.name,
elementForItem: grammar => { elementForItem: grammar => {
const grammarName = grammar.name || grammar.scopeName const grammarName = grammar.name || grammar.scopeName;
const element = document.createElement('li') const element = document.createElement('li');
if (grammar === this.currentGrammar) { if (grammar === this.currentGrammar) {
element.classList.add('active') element.classList.add('active');
} }
element.textContent = grammarName element.textContent = grammarName;
element.dataset.grammar = grammarName element.dataset.grammar = grammarName;
const div = document.createElement('div') const div = document.createElement('div');
div.classList.add('pull-right') div.classList.add('pull-right');
if (grammar.scopeName) { if (grammar.scopeName) {
const scopeName = document.createElement('scopeName') const scopeName = document.createElement('scopeName');
scopeName.classList.add('key-binding') // It will be styled the same as the keybindings in the command palette scopeName.classList.add('key-binding'); // It will be styled the same as the keybindings in the command palette
scopeName.textContent = grammar.scopeName scopeName.textContent = grammar.scopeName;
div.appendChild(scopeName) div.appendChild(scopeName);
element.appendChild(div) element.appendChild(div);
} }
return element return element;
}, },
didConfirmSelection: grammar => { didConfirmSelection: grammar => {
this.cancel() this.cancel();
if (grammar === this.autoDetect) { if (grammar === this.autoDetect) {
atom.textEditors.clearGrammarOverride(this.editor) atom.textEditors.clearGrammarOverride(this.editor);
} else { } else {
atom.textEditors.setGrammarOverride(this.editor, grammar.scopeName) atom.textEditors.setGrammarOverride(this.editor, grammar.scopeName);
} }
}, },
didCancelSelection: () => { didCancelSelection: () => {
this.cancel() this.cancel();
} }
}) });
this.selectListView.element.classList.add('grammar-selector') this.selectListView.element.classList.add('grammar-selector');
} }
destroy () { destroy() {
this.cancel() this.cancel();
return this.selectListView.destroy() return this.selectListView.destroy();
} }
cancel () { cancel() {
if (this.panel != null) { if (this.panel != null) {
this.panel.destroy() this.panel.destroy();
} }
this.panel = null this.panel = null;
this.currentGrammar = null this.currentGrammar = null;
if (this.previouslyFocusedElement) { if (this.previouslyFocusedElement) {
this.previouslyFocusedElement.focus() this.previouslyFocusedElement.focus();
this.previouslyFocusedElement = null this.previouslyFocusedElement = null;
} }
} }
attach () { attach() {
this.previouslyFocusedElement = document.activeElement this.previouslyFocusedElement = document.activeElement;
if (this.panel == null) { if (this.panel == null) {
this.panel = atom.workspace.addModalPanel({ item: this.selectListView }) this.panel = atom.workspace.addModalPanel({ item: this.selectListView });
} }
this.selectListView.focus() this.selectListView.focus();
this.selectListView.reset() this.selectListView.reset();
} }
async toggle () { async toggle() {
if (this.panel != null) { if (this.panel != null) {
this.cancel() this.cancel();
} else if (atom.workspace.getActiveTextEditor()) { } else if (atom.workspace.getActiveTextEditor()) {
this.editor = atom.workspace.getActiveTextEditor() this.editor = atom.workspace.getActiveTextEditor();
this.currentGrammar = this.editor.getGrammar() this.currentGrammar = this.editor.getGrammar();
if (this.currentGrammar === atom.grammars.nullGrammar) { if (this.currentGrammar === atom.grammars.nullGrammar) {
this.currentGrammar = this.autoDetect this.currentGrammar = this.autoDetect;
} }
const grammars = atom.grammars.getGrammars().filter(grammar => { const grammars = atom.grammars.getGrammars().filter(grammar => {
return grammar !== atom.grammars.nullGrammar && grammar.name return grammar !== atom.grammars.nullGrammar && grammar.name;
}) });
grammars.sort((a, b) => { grammars.sort((a, b) => {
if (a.scopeName === 'text.plain') { if (a.scopeName === 'text.plain') {
return -1 return -1;
} else if (b.scopeName === 'text.plain') { } else if (b.scopeName === 'text.plain') {
return 1 return 1;
} else if (a.name) { } else if (a.name) {
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name);
} else if (a.scopeName) { } else if (a.scopeName) {
return a.scopeName.localeCompare(b.scopeName) return a.scopeName.localeCompare(b.scopeName);
} else { } else {
return 1 return 1;
} }
}) });
grammars.unshift(this.autoDetect) grammars.unshift(this.autoDetect);
await this.selectListView.update({ items: grammars }) await this.selectListView.update({ items: grammars });
this.attach() this.attach();
} }
} }
} };

View File

@ -1,114 +1,114 @@
const { Disposable } = require('atom') const { Disposable } = require('atom');
module.exports = class GrammarStatusView { module.exports = class GrammarStatusView {
constructor (statusBar) { constructor(statusBar) {
this.statusBar = statusBar this.statusBar = statusBar;
this.element = document.createElement('grammar-selector-status') this.element = document.createElement('grammar-selector-status');
this.element.classList.add('grammar-status', 'inline-block') this.element.classList.add('grammar-status', 'inline-block');
this.grammarLink = document.createElement('a') this.grammarLink = document.createElement('a');
this.grammarLink.classList.add('inline-block') this.grammarLink.classList.add('inline-block');
this.element.appendChild(this.grammarLink) this.element.appendChild(this.grammarLink);
this.activeItemSubscription = atom.workspace.observeActiveTextEditor( this.activeItemSubscription = atom.workspace.observeActiveTextEditor(
this.subscribeToActiveTextEditor.bind(this) this.subscribeToActiveTextEditor.bind(this)
) );
this.configSubscription = atom.config.observe( this.configSubscription = atom.config.observe(
'grammar-selector.showOnRightSideOfStatusBar', 'grammar-selector.showOnRightSideOfStatusBar',
this.attach.bind(this) this.attach.bind(this)
) );
const clickHandler = event => { const clickHandler = event => {
event.preventDefault() event.preventDefault();
atom.commands.dispatch( atom.commands.dispatch(
atom.views.getView(atom.workspace.getActiveTextEditor()), atom.views.getView(atom.workspace.getActiveTextEditor()),
'grammar-selector:show' 'grammar-selector:show'
) );
} };
this.element.addEventListener('click', clickHandler) this.element.addEventListener('click', clickHandler);
this.clickSubscription = new Disposable(() => { this.clickSubscription = new Disposable(() => {
this.element.removeEventListener('click', clickHandler) this.element.removeEventListener('click', clickHandler);
}) });
} }
attach () { attach() {
if (this.tile) { if (this.tile) {
this.tile.destroy() this.tile.destroy();
} }
this.tile = atom.config.get('grammar-selector.showOnRightSideOfStatusBar') this.tile = atom.config.get('grammar-selector.showOnRightSideOfStatusBar')
? this.statusBar.addRightTile({ item: this.element, priority: 10 }) ? this.statusBar.addRightTile({ item: this.element, priority: 10 })
: this.statusBar.addLeftTile({ item: this.element, priority: 10 }) : this.statusBar.addLeftTile({ item: this.element, priority: 10 });
} }
destroy () { destroy() {
if (this.activeItemSubscription) { if (this.activeItemSubscription) {
this.activeItemSubscription.dispose() this.activeItemSubscription.dispose();
} }
if (this.grammarSubscription) { if (this.grammarSubscription) {
this.grammarSubscription.dispose() this.grammarSubscription.dispose();
} }
if (this.clickSubscription) { if (this.clickSubscription) {
this.clickSubscription.dispose() this.clickSubscription.dispose();
} }
if (this.configSubscription) { if (this.configSubscription) {
this.configSubscription.dispose() this.configSubscription.dispose();
} }
if (this.tile) { if (this.tile) {
this.tile.destroy() this.tile.destroy();
} }
if (this.tooltip) { if (this.tooltip) {
this.tooltip.dispose() this.tooltip.dispose();
} }
} }
subscribeToActiveTextEditor () { subscribeToActiveTextEditor() {
if (this.grammarSubscription) { if (this.grammarSubscription) {
this.grammarSubscription.dispose() this.grammarSubscription.dispose();
this.grammarSubscription = null this.grammarSubscription = null;
} }
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
if (editor) { if (editor) {
this.grammarSubscription = editor.onDidChangeGrammar( this.grammarSubscription = editor.onDidChangeGrammar(
this.updateGrammarText.bind(this) this.updateGrammarText.bind(this)
) );
} }
this.updateGrammarText() this.updateGrammarText();
} }
updateGrammarText () { updateGrammarText() {
atom.views.updateDocument(() => { atom.views.updateDocument(() => {
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
const grammar = editor ? editor.getGrammar() : null const grammar = editor ? editor.getGrammar() : null;
if (this.tooltip) { if (this.tooltip) {
this.tooltip.dispose() this.tooltip.dispose();
this.tooltip = null this.tooltip = null;
} }
if (grammar) { if (grammar) {
let grammarName = null let grammarName = null;
if (grammar === atom.grammars.nullGrammar) { if (grammar === atom.grammars.nullGrammar) {
grammarName = 'Plain Text' grammarName = 'Plain Text';
} else { } else {
grammarName = grammar.name || grammar.scopeName grammarName = grammar.name || grammar.scopeName;
} }
this.grammarLink.textContent = grammarName this.grammarLink.textContent = grammarName;
this.grammarLink.dataset.grammar = grammarName this.grammarLink.dataset.grammar = grammarName;
this.element.style.display = '' this.element.style.display = '';
this.tooltip = atom.tooltips.add(this.element, { this.tooltip = atom.tooltips.add(this.element, {
title: `File uses the ${grammarName} grammar` title: `File uses the ${grammarName} grammar`
}) });
} else { } else {
this.element.style.display = 'none' this.element.style.display = 'none';
} }
}) });
} }
} };

View File

@ -1,35 +1,35 @@
const GrammarListView = require('./grammar-list-view') const GrammarListView = require('./grammar-list-view');
const GrammarStatusView = require('./grammar-status-view') const GrammarStatusView = require('./grammar-status-view');
let commandDisposable = null let commandDisposable = null;
let grammarListView = null let grammarListView = null;
let grammarStatusView = null let grammarStatusView = null;
module.exports = { module.exports = {
activate () { activate() {
commandDisposable = atom.commands.add( commandDisposable = atom.commands.add(
'atom-text-editor', 'atom-text-editor',
'grammar-selector:show', 'grammar-selector:show',
() => { () => {
if (!grammarListView) grammarListView = new GrammarListView() if (!grammarListView) grammarListView = new GrammarListView();
grammarListView.toggle() grammarListView.toggle();
} }
) );
}, },
deactivate () { deactivate() {
if (commandDisposable) commandDisposable.dispose() if (commandDisposable) commandDisposable.dispose();
commandDisposable = null commandDisposable = null;
if (grammarStatusView) grammarStatusView.destroy() if (grammarStatusView) grammarStatusView.destroy();
grammarStatusView = null grammarStatusView = null;
if (grammarListView) grammarListView.destroy() if (grammarListView) grammarListView.destroy();
grammarListView = null grammarListView = null;
}, },
consumeStatusBar (statusBar) { consumeStatusBar(statusBar) {
grammarStatusView = new GrammarStatusView(statusBar) grammarStatusView = new GrammarStatusView(statusBar);
grammarStatusView.attach() grammarStatusView.attach();
} }
} };

View File

@ -1,226 +1,226 @@
const path = require('path') const path = require('path');
const SelectListView = require('atom-select-list') const SelectListView = require('atom-select-list');
describe('GrammarSelector', () => { describe('GrammarSelector', () => {
let [editor, textGrammar, jsGrammar] = [] let [editor, textGrammar, jsGrammar] = [];
beforeEach(async () => { beforeEach(async () => {
jasmine.attachToDOM(atom.views.getView(atom.workspace)) jasmine.attachToDOM(atom.views.getView(atom.workspace));
atom.config.set('grammar-selector.showOnRightSideOfStatusBar', false) atom.config.set('grammar-selector.showOnRightSideOfStatusBar', false);
await atom.packages.activatePackage('status-bar') await atom.packages.activatePackage('status-bar');
await atom.packages.activatePackage('grammar-selector') await atom.packages.activatePackage('grammar-selector');
await atom.packages.activatePackage('language-text') await atom.packages.activatePackage('language-text');
await atom.packages.activatePackage('language-javascript') await atom.packages.activatePackage('language-javascript');
await atom.packages.activatePackage( await atom.packages.activatePackage(
path.join(__dirname, 'fixtures', 'language-with-no-name') path.join(__dirname, 'fixtures', 'language-with-no-name')
) );
editor = await atom.workspace.open('sample.js') editor = await atom.workspace.open('sample.js');
textGrammar = atom.grammars.grammarForScopeName('text.plain') textGrammar = atom.grammars.grammarForScopeName('text.plain');
expect(textGrammar).toBeTruthy() expect(textGrammar).toBeTruthy();
jsGrammar = atom.grammars.grammarForScopeName('source.js') jsGrammar = atom.grammars.grammarForScopeName('source.js');
expect(jsGrammar).toBeTruthy() expect(jsGrammar).toBeTruthy();
expect(editor.getGrammar()).toBe(jsGrammar) expect(editor.getGrammar()).toBe(jsGrammar);
}) });
describe('when grammar-selector:show is triggered', () => describe('when grammar-selector:show is triggered', () =>
it('displays a list of all the available grammars', async () => { it('displays a list of all the available grammars', async () => {
atom.commands.dispatch(editor.getElement(), 'grammar-selector:show') atom.commands.dispatch(editor.getElement(), 'grammar-selector:show');
await SelectListView.getScheduler().getNextUpdatePromise() await SelectListView.getScheduler().getNextUpdatePromise();
const grammarView = atom.workspace.getModalPanels()[0].getItem().element const grammarView = atom.workspace.getModalPanels()[0].getItem().element;
// TODO: Remove once Atom 1.23 reaches stable // TODO: Remove once Atom 1.23 reaches stable
if (parseFloat(atom.getVersion()) >= 1.23) { if (parseFloat(atom.getVersion()) >= 1.23) {
// Do not take into account the two JS regex grammars or language-with-no-name // Do not take into account the two JS regex grammars or language-with-no-name
expect(grammarView.querySelectorAll('li').length).toBe( expect(grammarView.querySelectorAll('li').length).toBe(
atom.grammars.grammars.length - 3 atom.grammars.grammars.length - 3
) );
} else { } else {
expect(grammarView.querySelectorAll('li').length).toBe( expect(grammarView.querySelectorAll('li').length).toBe(
atom.grammars.grammars.length - 1 atom.grammars.grammars.length - 1
) );
} }
expect(grammarView.querySelectorAll('li')[0].textContent).toBe( expect(grammarView.querySelectorAll('li')[0].textContent).toBe(
'Auto Detect' 'Auto Detect'
) );
expect(grammarView.textContent.includes('source.a')).toBe(false) expect(grammarView.textContent.includes('source.a')).toBe(false);
grammarView grammarView
.querySelectorAll('li') .querySelectorAll('li')
.forEach(li => .forEach(li =>
expect(li.textContent).not.toBe(atom.grammars.nullGrammar.name) expect(li.textContent).not.toBe(atom.grammars.nullGrammar.name)
) );
})) }));
describe('when a grammar is selected', () => describe('when a grammar is selected', () =>
it('sets the new grammar on the editor', async () => { it('sets the new grammar on the editor', async () => {
atom.commands.dispatch(editor.getElement(), 'grammar-selector:show') atom.commands.dispatch(editor.getElement(), 'grammar-selector:show');
await SelectListView.getScheduler().getNextUpdatePromise() await SelectListView.getScheduler().getNextUpdatePromise();
const grammarView = atom.workspace.getModalPanels()[0].getItem() const grammarView = atom.workspace.getModalPanels()[0].getItem();
grammarView.props.didConfirmSelection(textGrammar) grammarView.props.didConfirmSelection(textGrammar);
expect(editor.getGrammar()).toBe(textGrammar) expect(editor.getGrammar()).toBe(textGrammar);
})) }));
describe('when auto-detect is selected', () => describe('when auto-detect is selected', () =>
it('restores the auto-detected grammar on the editor', async () => { it('restores the auto-detected grammar on the editor', async () => {
atom.commands.dispatch(editor.getElement(), 'grammar-selector:show') atom.commands.dispatch(editor.getElement(), 'grammar-selector:show');
await SelectListView.getScheduler().getNextUpdatePromise() await SelectListView.getScheduler().getNextUpdatePromise();
let grammarView = atom.workspace.getModalPanels()[0].getItem() let grammarView = atom.workspace.getModalPanels()[0].getItem();
grammarView.props.didConfirmSelection(textGrammar) grammarView.props.didConfirmSelection(textGrammar);
expect(editor.getGrammar()).toBe(textGrammar) expect(editor.getGrammar()).toBe(textGrammar);
atom.commands.dispatch(editor.getElement(), 'grammar-selector:show') atom.commands.dispatch(editor.getElement(), 'grammar-selector:show');
await SelectListView.getScheduler().getNextUpdatePromise() await SelectListView.getScheduler().getNextUpdatePromise();
grammarView = atom.workspace.getModalPanels()[0].getItem() grammarView = atom.workspace.getModalPanels()[0].getItem();
grammarView.props.didConfirmSelection(grammarView.items[0]) grammarView.props.didConfirmSelection(grammarView.items[0]);
expect(editor.getGrammar()).toBe(jsGrammar) expect(editor.getGrammar()).toBe(jsGrammar);
})) }));
describe("when the editor's current grammar is the null grammar", () => describe("when the editor's current grammar is the null grammar", () =>
it('displays Auto Detect as the selected grammar', async () => { it('displays Auto Detect as the selected grammar', async () => {
editor.setGrammar(atom.grammars.nullGrammar) editor.setGrammar(atom.grammars.nullGrammar);
atom.commands.dispatch(editor.getElement(), 'grammar-selector:show') atom.commands.dispatch(editor.getElement(), 'grammar-selector:show');
await SelectListView.getScheduler().getNextUpdatePromise() await SelectListView.getScheduler().getNextUpdatePromise();
const grammarView = atom.workspace.getModalPanels()[0].getItem().element const grammarView = atom.workspace.getModalPanels()[0].getItem().element;
expect(grammarView.querySelector('li.active').textContent).toBe( expect(grammarView.querySelector('li.active').textContent).toBe(
'Auto Detect' 'Auto Detect'
) );
})) }));
describe('when editor is untitled', () => describe('when editor is untitled', () =>
it('sets the new grammar on the editor', async () => { it('sets the new grammar on the editor', async () => {
editor = await atom.workspace.open() editor = await atom.workspace.open();
expect(editor.getGrammar()).not.toBe(jsGrammar) expect(editor.getGrammar()).not.toBe(jsGrammar);
atom.commands.dispatch(editor.getElement(), 'grammar-selector:show') atom.commands.dispatch(editor.getElement(), 'grammar-selector:show');
await SelectListView.getScheduler().getNextUpdatePromise() await SelectListView.getScheduler().getNextUpdatePromise();
const grammarView = atom.workspace.getModalPanels()[0].getItem() const grammarView = atom.workspace.getModalPanels()[0].getItem();
grammarView.props.didConfirmSelection(jsGrammar) grammarView.props.didConfirmSelection(jsGrammar);
expect(editor.getGrammar()).toBe(jsGrammar) expect(editor.getGrammar()).toBe(jsGrammar);
})) }));
describe('Status bar grammar label', () => { describe('Status bar grammar label', () => {
let [grammarStatus, grammarTile, statusBar] = [] let [grammarStatus, grammarTile, statusBar] = [];
beforeEach(async () => { beforeEach(async () => {
statusBar = document.querySelector('status-bar') statusBar = document.querySelector('status-bar');
;[grammarTile] = statusBar.getLeftTiles().slice(-1) [grammarTile] = statusBar.getLeftTiles().slice(-1);
grammarStatus = grammarTile.getItem() grammarStatus = grammarTile.getItem();
// Wait for status bar service hook to fire // Wait for status bar service hook to fire
while (!grammarStatus || !grammarStatus.textContent) { while (!grammarStatus || !grammarStatus.textContent) {
await atom.views.getNextUpdatePromise() await atom.views.getNextUpdatePromise();
grammarStatus = document.querySelector('.grammar-status') grammarStatus = document.querySelector('.grammar-status');
} }
}) });
it('displays the name of the current grammar', () => { it('displays the name of the current grammar', () => {
expect(grammarStatus.querySelector('a').textContent).toBe('JavaScript') expect(grammarStatus.querySelector('a').textContent).toBe('JavaScript');
expect(getTooltipText(grammarStatus)).toBe( expect(getTooltipText(grammarStatus)).toBe(
'File uses the JavaScript grammar' 'File uses the JavaScript grammar'
) );
}) });
it('displays Plain Text when the current grammar is the null grammar', async () => { it('displays Plain Text when the current grammar is the null grammar', async () => {
editor.setGrammar(atom.grammars.nullGrammar) editor.setGrammar(atom.grammars.nullGrammar);
await atom.views.getNextUpdatePromise() await atom.views.getNextUpdatePromise();
expect(grammarStatus.querySelector('a').textContent).toBe('Plain Text') expect(grammarStatus.querySelector('a').textContent).toBe('Plain Text');
expect(grammarStatus).toBeVisible() expect(grammarStatus).toBeVisible();
expect(getTooltipText(grammarStatus)).toBe( expect(getTooltipText(grammarStatus)).toBe(
'File uses the Plain Text grammar' 'File uses the Plain Text grammar'
) );
editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) editor.setGrammar(atom.grammars.grammarForScopeName('source.js'));
await atom.views.getNextUpdatePromise() await atom.views.getNextUpdatePromise();
expect(grammarStatus.querySelector('a').textContent).toBe('JavaScript') expect(grammarStatus.querySelector('a').textContent).toBe('JavaScript');
expect(grammarStatus).toBeVisible() expect(grammarStatus).toBeVisible();
}) });
it('hides the label when the current grammar is null', async () => { it('hides the label when the current grammar is null', async () => {
jasmine.attachToDOM(editor.getElement()) jasmine.attachToDOM(editor.getElement());
spyOn(editor, 'getGrammar').andReturn(null) spyOn(editor, 'getGrammar').andReturn(null);
editor.setGrammar(atom.grammars.nullGrammar) editor.setGrammar(atom.grammars.nullGrammar);
await atom.views.getNextUpdatePromise() await atom.views.getNextUpdatePromise();
expect(grammarStatus.offsetHeight).toBe(0) expect(grammarStatus.offsetHeight).toBe(0);
}) });
describe('when the grammar-selector.showOnRightSideOfStatusBar setting changes', () => describe('when the grammar-selector.showOnRightSideOfStatusBar setting changes', () =>
it('moves the item to the preferred side of the status bar', () => { it('moves the item to the preferred side of the status bar', () => {
expect(statusBar.getLeftTiles().map(tile => tile.getItem())).toContain( expect(statusBar.getLeftTiles().map(tile => tile.getItem())).toContain(
grammarStatus grammarStatus
) );
expect( expect(
statusBar.getRightTiles().map(tile => tile.getItem()) statusBar.getRightTiles().map(tile => tile.getItem())
).not.toContain(grammarStatus) ).not.toContain(grammarStatus);
atom.config.set('grammar-selector.showOnRightSideOfStatusBar', true) atom.config.set('grammar-selector.showOnRightSideOfStatusBar', true);
expect( expect(
statusBar.getLeftTiles().map(tile => tile.getItem()) statusBar.getLeftTiles().map(tile => tile.getItem())
).not.toContain(grammarStatus) ).not.toContain(grammarStatus);
expect(statusBar.getRightTiles().map(tile => tile.getItem())).toContain( expect(statusBar.getRightTiles().map(tile => tile.getItem())).toContain(
grammarStatus grammarStatus
) );
atom.config.set('grammar-selector.showOnRightSideOfStatusBar', false) atom.config.set('grammar-selector.showOnRightSideOfStatusBar', false);
expect(statusBar.getLeftTiles().map(tile => tile.getItem())).toContain( expect(statusBar.getLeftTiles().map(tile => tile.getItem())).toContain(
grammarStatus grammarStatus
) );
expect( expect(
statusBar.getRightTiles().map(tile => tile.getItem()) statusBar.getRightTiles().map(tile => tile.getItem())
).not.toContain(grammarStatus) ).not.toContain(grammarStatus);
})) }));
describe("when the editor's grammar changes", () => describe("when the editor's grammar changes", () =>
it('displays the new grammar of the editor', async () => { it('displays the new grammar of the editor', async () => {
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain')) editor.setGrammar(atom.grammars.grammarForScopeName('text.plain'));
await atom.views.getNextUpdatePromise() await atom.views.getNextUpdatePromise();
expect(grammarStatus.querySelector('a').textContent).toBe('Plain Text') expect(grammarStatus.querySelector('a').textContent).toBe('Plain Text');
expect(getTooltipText(grammarStatus)).toBe( expect(getTooltipText(grammarStatus)).toBe(
'File uses the Plain Text grammar' 'File uses the Plain Text grammar'
) );
editor.setGrammar(atom.grammars.grammarForScopeName('source.a')) editor.setGrammar(atom.grammars.grammarForScopeName('source.a'));
await atom.views.getNextUpdatePromise() await atom.views.getNextUpdatePromise();
expect(grammarStatus.querySelector('a').textContent).toBe('source.a') expect(grammarStatus.querySelector('a').textContent).toBe('source.a');
expect(getTooltipText(grammarStatus)).toBe( expect(getTooltipText(grammarStatus)).toBe(
'File uses the source.a grammar' 'File uses the source.a grammar'
) );
})) }));
describe('when clicked', () => describe('when clicked', () =>
it('shows the grammar selector modal', () => { it('shows the grammar selector modal', () => {
const eventHandler = jasmine.createSpy('eventHandler') const eventHandler = jasmine.createSpy('eventHandler');
atom.commands.add( atom.commands.add(
editor.getElement(), editor.getElement(),
'grammar-selector:show', 'grammar-selector:show',
eventHandler eventHandler
) );
grammarStatus.click() grammarStatus.click();
expect(eventHandler).toHaveBeenCalled() expect(eventHandler).toHaveBeenCalled();
})) }));
describe('when the package is deactivated', () => describe('when the package is deactivated', () =>
it('removes the view', () => { it('removes the view', () => {
spyOn(grammarTile, 'destroy') spyOn(grammarTile, 'destroy');
atom.packages.deactivatePackage('grammar-selector') atom.packages.deactivatePackage('grammar-selector');
expect(grammarTile.destroy).toHaveBeenCalled() expect(grammarTile.destroy).toHaveBeenCalled();
})) }));
}) });
}) });
function getTooltipText (element) { function getTooltipText(element) {
const [tooltip] = atom.tooltips.findTooltips(element) const [tooltip] = atom.tooltips.findTooltips(element);
return tooltip.getTitle() return tooltip.getTitle();
} }

View File

@ -1,239 +1,239 @@
/** @babel */ /** @babel */
/** @jsx etch.dom */ /** @jsx etch.dom */
import etch from 'etch' import etch from 'etch';
import VIEW_URI from './view-uri' import VIEW_URI from './view-uri';
const REBUILDING = 'rebuilding' const REBUILDING = 'rebuilding';
const REBUILD_FAILED = 'rebuild-failed' const REBUILD_FAILED = 'rebuild-failed';
const REBUILD_SUCCEEDED = 'rebuild-succeeded' const REBUILD_SUCCEEDED = 'rebuild-succeeded';
export default class IncompatiblePackagesComponent { export default class IncompatiblePackagesComponent {
constructor (packageManager) { constructor(packageManager) {
this.rebuildStatuses = new Map() this.rebuildStatuses = new Map();
this.rebuildFailureOutputs = new Map() this.rebuildFailureOutputs = new Map();
this.rebuildInProgress = false this.rebuildInProgress = false;
this.rebuiltPackageCount = 0 this.rebuiltPackageCount = 0;
this.packageManager = packageManager this.packageManager = packageManager;
this.loaded = false this.loaded = false;
etch.initialize(this) etch.initialize(this);
if (this.packageManager.getActivePackages().length > 0) { if (this.packageManager.getActivePackages().length > 0) {
this.populateIncompatiblePackages() this.populateIncompatiblePackages();
} else { } else {
global.setImmediate(this.populateIncompatiblePackages.bind(this)) global.setImmediate(this.populateIncompatiblePackages.bind(this));
} }
this.element.addEventListener('click', event => { this.element.addEventListener('click', event => {
if (event.target === this.refs.rebuildButton) { if (event.target === this.refs.rebuildButton) {
this.rebuildIncompatiblePackages() this.rebuildIncompatiblePackages();
} else if (event.target === this.refs.reloadButton) { } else if (event.target === this.refs.reloadButton) {
atom.reload() atom.reload();
} else if (event.target.classList.contains('view-settings')) { } else if (event.target.classList.contains('view-settings')) {
atom.workspace.open( atom.workspace.open(
`atom://config/packages/${event.target.package.name}` `atom://config/packages/${event.target.package.name}`
) );
} }
}) });
} }
update () {} update() {}
render () { render() {
if (!this.loaded) { if (!this.loaded) {
return <div className='incompatible-packages padded'>Loading...</div> return <div className="incompatible-packages padded">Loading...</div>;
} }
return ( return (
<div <div
className='incompatible-packages padded native-key-bindings' className="incompatible-packages padded native-key-bindings"
tabIndex='-1' tabIndex="-1"
> >
{this.renderHeading()} {this.renderHeading()}
{this.renderIncompatiblePackageList()} {this.renderIncompatiblePackageList()}
</div> </div>
) );
} }
renderHeading () { renderHeading() {
if (this.incompatiblePackages.length > 0) { if (this.incompatiblePackages.length > 0) {
if (this.rebuiltPackageCount > 0) { if (this.rebuiltPackageCount > 0) {
let alertClass = let alertClass =
this.rebuiltPackageCount === this.incompatiblePackages.length this.rebuiltPackageCount === this.incompatiblePackages.length
? 'alert-success icon-check' ? 'alert-success icon-check'
: 'alert-warning icon-bug' : 'alert-warning icon-bug';
return ( return (
<div className={'alert icon ' + alertClass}> <div className={'alert icon ' + alertClass}>
{this.rebuiltPackageCount} of {this.incompatiblePackages.length}{' '} {this.rebuiltPackageCount} of {this.incompatiblePackages.length}{' '}
packages were rebuilt successfully. Reload Atom to activate them. packages were rebuilt successfully. Reload Atom to activate them.
<button ref='reloadButton' className='btn pull-right'> <button ref="reloadButton" className="btn pull-right">
Reload Atom Reload Atom
</button> </button>
</div> </div>
) );
} else { } else {
return ( return (
<div className='alert alert-danger icon icon-bug'> <div className="alert alert-danger icon icon-bug">
Some installed packages could not be loaded because they contain Some installed packages could not be loaded because they contain
native modules that were compiled for an earlier version of Atom. native modules that were compiled for an earlier version of Atom.
<button <button
ref='rebuildButton' ref="rebuildButton"
className='btn pull-right' className="btn pull-right"
disabled={this.rebuildInProgress} disabled={this.rebuildInProgress}
> >
Rebuild Packages Rebuild Packages
</button> </button>
</div> </div>
) );
} }
} else { } else {
return ( return (
<div className='alert alert-success icon icon-check'> <div className="alert alert-success icon icon-check">
None of your packages contain incompatible native modules. None of your packages contain incompatible native modules.
</div> </div>
) );
} }
} }
renderIncompatiblePackageList () { renderIncompatiblePackageList() {
return ( return (
<div> <div>
{this.incompatiblePackages.map( {this.incompatiblePackages.map(
this.renderIncompatiblePackage.bind(this) this.renderIncompatiblePackage.bind(this)
)} )}
</div> </div>
) );
} }
renderIncompatiblePackage (pack) { renderIncompatiblePackage(pack) {
let rebuildStatus = this.rebuildStatuses.get(pack) let rebuildStatus = this.rebuildStatuses.get(pack);
return ( return (
<div className={'incompatible-package'}> <div className={'incompatible-package'}>
{this.renderRebuildStatusIndicator(rebuildStatus)} {this.renderRebuildStatusIndicator(rebuildStatus)}
<button <button
className='btn view-settings icon icon-gear pull-right' className="btn view-settings icon icon-gear pull-right"
package={pack} package={pack}
> >
Package Settings Package Settings
</button> </button>
<h4 className='heading'> <h4 className="heading">
{pack.name} {pack.metadata.version} {pack.name} {pack.metadata.version}
</h4> </h4>
{rebuildStatus {rebuildStatus
? this.renderRebuildOutput(pack) ? this.renderRebuildOutput(pack)
: this.renderIncompatibleModules(pack)} : this.renderIncompatibleModules(pack)}
</div> </div>
) );
} }
renderRebuildStatusIndicator (rebuildStatus) { renderRebuildStatusIndicator(rebuildStatus) {
if (rebuildStatus === REBUILDING) { if (rebuildStatus === REBUILDING) {
return ( return (
<div className='badge badge-info pull-right icon icon-gear'> <div className="badge badge-info pull-right icon icon-gear">
Rebuilding Rebuilding
</div> </div>
) );
} else if (rebuildStatus === REBUILD_SUCCEEDED) { } else if (rebuildStatus === REBUILD_SUCCEEDED) {
return ( return (
<div className='badge badge-success pull-right icon icon-check'> <div className="badge badge-success pull-right icon icon-check">
Rebuild Succeeded Rebuild Succeeded
</div> </div>
) );
} else if (rebuildStatus === REBUILD_FAILED) { } else if (rebuildStatus === REBUILD_FAILED) {
return ( return (
<div className='badge badge-error pull-right icon icon-x'> <div className="badge badge-error pull-right icon icon-x">
Rebuild Failed Rebuild Failed
</div> </div>
) );
} else { } else {
return '' return '';
} }
} }
renderRebuildOutput (pack) { renderRebuildOutput(pack) {
if (this.rebuildStatuses.get(pack) === REBUILD_FAILED) { if (this.rebuildStatuses.get(pack) === REBUILD_FAILED) {
return <pre>{this.rebuildFailureOutputs.get(pack)}</pre> return <pre>{this.rebuildFailureOutputs.get(pack)}</pre>;
} else { } else {
return '' return '';
} }
} }
renderIncompatibleModules (pack) { renderIncompatibleModules(pack) {
return ( return (
<ul> <ul>
{pack.incompatibleModules.map(nativeModule => ( {pack.incompatibleModules.map(nativeModule => (
<li> <li>
<div className='icon icon-file-binary'> <div className="icon icon-file-binary">
{nativeModule.name}@{nativeModule.version || 'unknown'} {' '} {nativeModule.name}@{nativeModule.version || 'unknown'} {' '}
<span className='text-warning'>{nativeModule.error}</span> <span className="text-warning">{nativeModule.error}</span>
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
) );
} }
populateIncompatiblePackages () { populateIncompatiblePackages() {
this.incompatiblePackages = this.packageManager this.incompatiblePackages = this.packageManager
.getLoadedPackages() .getLoadedPackages()
.filter(pack => !pack.isCompatible()) .filter(pack => !pack.isCompatible());
for (let pack of this.incompatiblePackages) { for (let pack of this.incompatiblePackages) {
let buildFailureOutput = pack.getBuildFailureOutput() let buildFailureOutput = pack.getBuildFailureOutput();
if (buildFailureOutput) { if (buildFailureOutput) {
this.setPackageStatus(pack, REBUILD_FAILED) this.setPackageStatus(pack, REBUILD_FAILED);
this.setRebuildFailureOutput(pack, buildFailureOutput) this.setRebuildFailureOutput(pack, buildFailureOutput);
} }
} }
this.loaded = true this.loaded = true;
etch.update(this) etch.update(this);
} }
async rebuildIncompatiblePackages () { async rebuildIncompatiblePackages() {
this.rebuildInProgress = true this.rebuildInProgress = true;
let rebuiltPackageCount = 0 let rebuiltPackageCount = 0;
for (let pack of this.incompatiblePackages) { for (let pack of this.incompatiblePackages) {
this.setPackageStatus(pack, REBUILDING) this.setPackageStatus(pack, REBUILDING);
let { code, stderr } = await pack.rebuild() let { code, stderr } = await pack.rebuild();
if (code === 0) { if (code === 0) {
this.setPackageStatus(pack, REBUILD_SUCCEEDED) this.setPackageStatus(pack, REBUILD_SUCCEEDED);
rebuiltPackageCount++ rebuiltPackageCount++;
} else { } else {
this.setRebuildFailureOutput(pack, stderr) this.setRebuildFailureOutput(pack, stderr);
this.setPackageStatus(pack, REBUILD_FAILED) this.setPackageStatus(pack, REBUILD_FAILED);
} }
} }
this.rebuildInProgress = false this.rebuildInProgress = false;
this.rebuiltPackageCount = rebuiltPackageCount this.rebuiltPackageCount = rebuiltPackageCount;
etch.update(this) etch.update(this);
} }
setPackageStatus (pack, status) { setPackageStatus(pack, status) {
this.rebuildStatuses.set(pack, status) this.rebuildStatuses.set(pack, status);
etch.update(this) etch.update(this);
} }
setRebuildFailureOutput (pack, output) { setRebuildFailureOutput(pack, output) {
this.rebuildFailureOutputs.set(pack, output) this.rebuildFailureOutputs.set(pack, output);
etch.update(this) etch.update(this);
} }
getTitle () { getTitle() {
return 'Incompatible Packages' return 'Incompatible Packages';
} }
getURI () { getURI() {
return VIEW_URI return VIEW_URI;
} }
getIconName () { getIconName() {
return 'package' return 'package';
} }
serialize () { serialize() {
return { deserializer: 'IncompatiblePackagesComponent' } return { deserializer: 'IncompatiblePackagesComponent' };
} }
} }

View File

@ -1,56 +1,56 @@
/** @babel */ /** @babel */
import { Disposable, CompositeDisposable } from 'atom' import { Disposable, CompositeDisposable } from 'atom';
import VIEW_URI from './view-uri' import VIEW_URI from './view-uri';
let disposables = null let disposables = null;
export function activate () { export function activate() {
disposables = new CompositeDisposable() disposables = new CompositeDisposable();
disposables.add( disposables.add(
atom.workspace.addOpener(uri => { atom.workspace.addOpener(uri => {
if (uri === VIEW_URI) { if (uri === VIEW_URI) {
return deserializeIncompatiblePackagesComponent() return deserializeIncompatiblePackagesComponent();
} }
}) })
) );
disposables.add( disposables.add(
atom.commands.add('atom-workspace', { atom.commands.add('atom-workspace', {
'incompatible-packages:view': () => { 'incompatible-packages:view': () => {
atom.workspace.open(VIEW_URI) atom.workspace.open(VIEW_URI);
} }
}) })
) );
} }
export function deactivate () { export function deactivate() {
disposables.dispose() disposables.dispose();
} }
export function consumeStatusBar (statusBar) { export function consumeStatusBar(statusBar) {
let incompatibleCount = 0 let incompatibleCount = 0;
for (let pack of atom.packages.getLoadedPackages()) { for (let pack of atom.packages.getLoadedPackages()) {
if (!pack.isCompatible()) incompatibleCount++ if (!pack.isCompatible()) incompatibleCount++;
} }
if (incompatibleCount > 0) { if (incompatibleCount > 0) {
let icon = createIcon(incompatibleCount) let icon = createIcon(incompatibleCount);
let tile = statusBar.addRightTile({ item: icon, priority: 200 }) let tile = statusBar.addRightTile({ item: icon, priority: 200 });
icon.element.addEventListener('click', () => { icon.element.addEventListener('click', () => {
atom.commands.dispatch(icon.element, 'incompatible-packages:view') atom.commands.dispatch(icon.element, 'incompatible-packages:view');
}) });
disposables.add(new Disposable(() => tile.destroy())) disposables.add(new Disposable(() => tile.destroy()));
} }
} }
export function deserializeIncompatiblePackagesComponent () { export function deserializeIncompatiblePackagesComponent() {
const IncompatiblePackagesComponent = require('./incompatible-packages-component') const IncompatiblePackagesComponent = require('./incompatible-packages-component');
return new IncompatiblePackagesComponent(atom.packages) return new IncompatiblePackagesComponent(atom.packages);
} }
function createIcon (count) { function createIcon(count) {
const StatusIconComponent = require('./status-icon-component') const StatusIconComponent = require('./status-icon-component');
return new StatusIconComponent({ count }) return new StatusIconComponent({ count });
} }

View File

@ -1,22 +1,22 @@
/** @babel */ /** @babel */
/** @jsx etch.dom */ /** @jsx etch.dom */
import etch from 'etch' import etch from 'etch';
export default class StatusIconComponent { export default class StatusIconComponent {
constructor ({ count }) { constructor({ count }) {
this.count = count this.count = count;
etch.initialize(this) etch.initialize(this);
} }
update () {} update() {}
render () { render() {
return ( return (
<div className='incompatible-packages-status inline-block text text-error'> <div className="incompatible-packages-status inline-block text text-error">
<span className='icon icon-bug' /> <span className="icon icon-bug" />
<span className='incompatible-packages-count'>{this.count}</span> <span className="incompatible-packages-count">{this.count}</span>
</div> </div>
) );
} }
} }

View File

@ -1,3 +1,3 @@
/** @babel */ /** @babel */
export default 'atom://incompatible-packages' export default 'atom://incompatible-packages';

View File

@ -1,25 +1,25 @@
/** @babel */ /** @babel */
import etch from 'etch' import etch from 'etch';
import IncompatiblePackagesComponent from '../lib/incompatible-packages-component' import IncompatiblePackagesComponent from '../lib/incompatible-packages-component';
describe('IncompatiblePackagesComponent', () => { describe('IncompatiblePackagesComponent', () => {
let packages, etchScheduler let packages, etchScheduler;
beforeEach(() => { beforeEach(() => {
etchScheduler = etch.getScheduler() etchScheduler = etch.getScheduler();
packages = [ packages = [
{ {
name: 'incompatible-1', name: 'incompatible-1',
isCompatible () { isCompatible() {
return false return false;
}, },
rebuild: function () { rebuild: function() {
return new Promise(resolve => (this.resolveRebuild = resolve)) return new Promise(resolve => (this.resolveRebuild = resolve));
}, },
getBuildFailureOutput () { getBuildFailureOutput() {
return null return null;
}, },
path: '/Users/joe/.atom/packages/incompatible-1', path: '/Users/joe/.atom/packages/incompatible-1',
metadata: { metadata: {
@ -33,14 +33,14 @@ describe('IncompatiblePackagesComponent', () => {
}, },
{ {
name: 'incompatible-2', name: 'incompatible-2',
isCompatible () { isCompatible() {
return false return false;
}, },
rebuild () { rebuild() {
return new Promise(resolve => (this.resolveRebuild = resolve)) return new Promise(resolve => (this.resolveRebuild = resolve));
}, },
getBuildFailureOutput () { getBuildFailureOutput() {
return null return null;
}, },
path: '/Users/joe/.atom/packages/incompatible-2', path: '/Users/joe/.atom/packages/incompatible-2',
metadata: { metadata: {
@ -53,14 +53,14 @@ describe('IncompatiblePackagesComponent', () => {
}, },
{ {
name: 'compatible', name: 'compatible',
isCompatible () { isCompatible() {
return true return true;
}, },
rebuild () { rebuild() {
throw new Error('Should not rebuild a compatible package') throw new Error('Should not rebuild a compatible package');
}, },
getBuildFailureOutput () { getBuildFailureOutput() {
return null return null;
}, },
path: '/Users/joe/.atom/packages/b', path: '/Users/joe/.atom/packages/b',
metadata: { metadata: {
@ -69,8 +69,8 @@ describe('IncompatiblePackagesComponent', () => {
}, },
incompatibleModules: [] incompatibleModules: []
} }
] ];
}) });
describe('when packages have not finished loading', () => { describe('when packages have not finished loading', () => {
it('delays rendering incompatible packages until the end of the tick', () => { it('delays rendering incompatible packages until the end of the tick', () => {
@ -78,69 +78,71 @@ describe('IncompatiblePackagesComponent', () => {
let component = new IncompatiblePackagesComponent({ let component = new IncompatiblePackagesComponent({
getActivePackages: () => [], getActivePackages: () => [],
getLoadedPackages: () => packages getLoadedPackages: () => packages
}) });
let { element } = component let { element } = component;
expect( expect(
element.querySelectorAll('.incompatible-package').length element.querySelectorAll('.incompatible-package').length
).toEqual(0) ).toEqual(0);
await etchScheduler.getNextUpdatePromise() await etchScheduler.getNextUpdatePromise();
expect( expect(
element.querySelectorAll('.incompatible-package').length element.querySelectorAll('.incompatible-package').length
).toBeGreaterThan(0) ).toBeGreaterThan(0);
}) });
}) });
}) });
describe('when there are no incompatible packages', () => { describe('when there are no incompatible packages', () => {
it('does not render incompatible packages or the rebuild button', () => { it('does not render incompatible packages or the rebuild button', () => {
waitsForPromise(async () => { waitsForPromise(async () => {
expect(packages[2].isCompatible()).toBe(true) expect(packages[2].isCompatible()).toBe(true);
let compatiblePackages = [packages[2]] let compatiblePackages = [packages[2]];
let component = new IncompatiblePackagesComponent({ let component = new IncompatiblePackagesComponent({
getActivePackages: () => compatiblePackages, getActivePackages: () => compatiblePackages,
getLoadedPackages: () => compatiblePackages getLoadedPackages: () => compatiblePackages
}) });
let { element } = component let { element } = component;
await etchScheduler.getNextUpdatePromise() await etchScheduler.getNextUpdatePromise();
expect(element.querySelectorAll('.incompatible-package').length).toBe(0) expect(element.querySelectorAll('.incompatible-package').length).toBe(
expect(element.querySelector('button')).toBeNull() 0
}) );
}) expect(element.querySelector('button')).toBeNull();
}) });
});
});
describe('when some packages previously failed to rebuild', () => { describe('when some packages previously failed to rebuild', () => {
it('renders them with failed build status and error output', () => { it('renders them with failed build status and error output', () => {
waitsForPromise(async () => { waitsForPromise(async () => {
packages[1].getBuildFailureOutput = function () { packages[1].getBuildFailureOutput = function() {
return 'The build failed' return 'The build failed';
} };
let component = new IncompatiblePackagesComponent({ let component = new IncompatiblePackagesComponent({
getActivePackages: () => packages, getActivePackages: () => packages,
getLoadedPackages: () => packages getLoadedPackages: () => packages
}) });
let { element } = component let { element } = component;
await etchScheduler.getNextUpdatePromise() await etchScheduler.getNextUpdatePromise();
let packageElement = element.querySelector( let packageElement = element.querySelector(
'.incompatible-package:nth-child(2)' '.incompatible-package:nth-child(2)'
) );
expect(packageElement.querySelector('.badge').textContent).toBe( expect(packageElement.querySelector('.badge').textContent).toBe(
'Rebuild Failed' 'Rebuild Failed'
) );
expect(packageElement.querySelector('pre').textContent).toBe( expect(packageElement.querySelector('pre').textContent).toBe(
'The build failed' 'The build failed'
) );
}) });
}) });
}) });
describe('when there are incompatible packages', () => { describe('when there are incompatible packages', () => {
it('renders incompatible packages and the rebuild button', () => { it('renders incompatible packages and the rebuild button', () => {
@ -148,17 +150,17 @@ describe('IncompatiblePackagesComponent', () => {
let component = new IncompatiblePackagesComponent({ let component = new IncompatiblePackagesComponent({
getActivePackages: () => packages, getActivePackages: () => packages,
getLoadedPackages: () => packages getLoadedPackages: () => packages
}) });
let { element } = component let { element } = component;
await etchScheduler.getNextUpdatePromise() await etchScheduler.getNextUpdatePromise();
expect( expect(
element.querySelectorAll('.incompatible-package').length element.querySelectorAll('.incompatible-package').length
).toEqual(2) ).toEqual(2);
expect(element.querySelector('button')).not.toBeNull() expect(element.querySelector('button')).not.toBeNull();
}) });
}) });
describe('when the "Rebuild All" button is clicked', () => { describe('when the "Rebuild All" button is clicked', () => {
it("rebuilds every incompatible package, updating each package's view with status", () => { it("rebuilds every incompatible package, updating each package's view with status", () => {
@ -166,94 +168,94 @@ describe('IncompatiblePackagesComponent', () => {
let component = new IncompatiblePackagesComponent({ let component = new IncompatiblePackagesComponent({
getActivePackages: () => packages, getActivePackages: () => packages,
getLoadedPackages: () => packages getLoadedPackages: () => packages
}) });
let { element } = component let { element } = component;
jasmine.attachToDOM(element) jasmine.attachToDOM(element);
await etchScheduler.getNextUpdatePromise() await etchScheduler.getNextUpdatePromise();
component.refs.rebuildButton.dispatchEvent( component.refs.rebuildButton.dispatchEvent(
new CustomEvent('click', { bubbles: true }) new CustomEvent('click', { bubbles: true })
) );
await etchScheduler.getNextUpdatePromise() // view update await etchScheduler.getNextUpdatePromise(); // view update
expect(component.refs.rebuildButton.disabled).toBe(true) expect(component.refs.rebuildButton.disabled).toBe(true);
expect(packages[0].resolveRebuild).toBeDefined() expect(packages[0].resolveRebuild).toBeDefined();
expect( expect(
element.querySelector('.incompatible-package:nth-child(1) .badge') element.querySelector('.incompatible-package:nth-child(1) .badge')
.textContent .textContent
).toBe('Rebuilding') ).toBe('Rebuilding');
expect( expect(
element.querySelector('.incompatible-package:nth-child(2) .badge') element.querySelector('.incompatible-package:nth-child(2) .badge')
).toBeNull() ).toBeNull();
packages[0].resolveRebuild({ code: 0 }) // simulate rebuild success packages[0].resolveRebuild({ code: 0 }); // simulate rebuild success
await etchScheduler.getNextUpdatePromise() // view update await etchScheduler.getNextUpdatePromise(); // view update
expect(packages[1].resolveRebuild).toBeDefined() expect(packages[1].resolveRebuild).toBeDefined();
expect( expect(
element.querySelector('.incompatible-package:nth-child(1) .badge') element.querySelector('.incompatible-package:nth-child(1) .badge')
.textContent .textContent
).toBe('Rebuild Succeeded') ).toBe('Rebuild Succeeded');
expect( expect(
element.querySelector('.incompatible-package:nth-child(2) .badge') element.querySelector('.incompatible-package:nth-child(2) .badge')
.textContent .textContent
).toBe('Rebuilding') ).toBe('Rebuilding');
packages[1].resolveRebuild({ packages[1].resolveRebuild({
code: 12, code: 12,
stderr: 'This is an error from the test!' stderr: 'This is an error from the test!'
}) // simulate rebuild failure }); // simulate rebuild failure
await etchScheduler.getNextUpdatePromise() // view update await etchScheduler.getNextUpdatePromise(); // view update
expect( expect(
element.querySelector('.incompatible-package:nth-child(1) .badge') element.querySelector('.incompatible-package:nth-child(1) .badge')
.textContent .textContent
).toBe('Rebuild Succeeded') ).toBe('Rebuild Succeeded');
expect( expect(
element.querySelector('.incompatible-package:nth-child(2) .badge') element.querySelector('.incompatible-package:nth-child(2) .badge')
.textContent .textContent
).toBe('Rebuild Failed') ).toBe('Rebuild Failed');
expect( expect(
element.querySelector('.incompatible-package:nth-child(2) pre') element.querySelector('.incompatible-package:nth-child(2) pre')
.textContent .textContent
).toBe('This is an error from the test!') ).toBe('This is an error from the test!');
}) });
}) });
it('displays a prompt to reload Atom when the packages finish rebuilding', () => { it('displays a prompt to reload Atom when the packages finish rebuilding', () => {
waitsForPromise(async () => { waitsForPromise(async () => {
let component = new IncompatiblePackagesComponent({ let component = new IncompatiblePackagesComponent({
getActivePackages: () => packages, getActivePackages: () => packages,
getLoadedPackages: () => packages getLoadedPackages: () => packages
}) });
let { element } = component let { element } = component;
jasmine.attachToDOM(element) jasmine.attachToDOM(element);
await etchScheduler.getNextUpdatePromise() // view update await etchScheduler.getNextUpdatePromise(); // view update
component.refs.rebuildButton.dispatchEvent( component.refs.rebuildButton.dispatchEvent(
new CustomEvent('click', { bubbles: true }) new CustomEvent('click', { bubbles: true })
) );
expect(packages[0].resolveRebuild({ code: 0 })) expect(packages[0].resolveRebuild({ code: 0 }));
await new Promise(global.setImmediate) await new Promise(global.setImmediate);
expect(packages[1].resolveRebuild({ code: 0 })) expect(packages[1].resolveRebuild({ code: 0 }));
await etchScheduler.getNextUpdatePromise() // view update await etchScheduler.getNextUpdatePromise(); // view update
expect(component.refs.reloadButton).toBeDefined() expect(component.refs.reloadButton).toBeDefined();
expect(element.querySelector('.alert').textContent).toMatch(/2 of 2/) expect(element.querySelector('.alert').textContent).toMatch(/2 of 2/);
spyOn(atom, 'reload') spyOn(atom, 'reload');
component.refs.reloadButton.dispatchEvent( component.refs.reloadButton.dispatchEvent(
new CustomEvent('click', { bubbles: true }) new CustomEvent('click', { bubbles: true })
) );
expect(atom.reload).toHaveBeenCalled() expect(atom.reload).toHaveBeenCalled();
}) });
}) });
}) });
describe('when the "Package Settings" button is clicked', () => { describe('when the "Package Settings" button is clicked', () => {
it('opens the settings panel for the package', () => { it('opens the settings panel for the package', () => {
@ -261,21 +263,21 @@ describe('IncompatiblePackagesComponent', () => {
let component = new IncompatiblePackagesComponent({ let component = new IncompatiblePackagesComponent({
getActivePackages: () => packages, getActivePackages: () => packages,
getLoadedPackages: () => packages getLoadedPackages: () => packages
}) });
let { element } = component let { element } = component;
jasmine.attachToDOM(element) jasmine.attachToDOM(element);
await etchScheduler.getNextUpdatePromise() await etchScheduler.getNextUpdatePromise();
spyOn(atom.workspace, 'open') spyOn(atom.workspace, 'open');
element element
.querySelector('.incompatible-package:nth-child(2) button') .querySelector('.incompatible-package:nth-child(2) button')
.dispatchEvent(new CustomEvent('click', { bubbles: true })) .dispatchEvent(new CustomEvent('click', { bubbles: true }));
expect(atom.workspace.open).toHaveBeenCalledWith( expect(atom.workspace.open).toHaveBeenCalledWith(
'atom://config/packages/incompatible-2' 'atom://config/packages/incompatible-2'
) );
}) });
}) });
}) });
}) });
}) });

View File

@ -1,81 +1,83 @@
/** @babel */ /** @babel */
import path from 'path' import path from 'path';
import IncompatiblePackagesComponent from '../lib/incompatible-packages-component' import IncompatiblePackagesComponent from '../lib/incompatible-packages-component';
import StatusIconComponent from '../lib/status-icon-component' import StatusIconComponent from '../lib/status-icon-component';
// This exists only so that CI passes on both Atom 1.6 and Atom 1.8+. // This exists only so that CI passes on both Atom 1.6 and Atom 1.8+.
function findStatusBar () { function findStatusBar() {
if (typeof atom.workspace.getFooterPanels === 'function') { if (typeof atom.workspace.getFooterPanels === 'function') {
const footerPanels = atom.workspace.getFooterPanels() const footerPanels = atom.workspace.getFooterPanels();
if (footerPanels.length > 0) { if (footerPanels.length > 0) {
return footerPanels[0].getItem() return footerPanels[0].getItem();
} }
} }
return atom.workspace.getBottomPanels()[0].getItem() return atom.workspace.getBottomPanels()[0].getItem();
} }
describe('Incompatible packages', () => { describe('Incompatible packages', () => {
let statusBar let statusBar;
beforeEach(() => { beforeEach(() => {
atom.views.getView(atom.workspace) atom.views.getView(atom.workspace);
waitsForPromise(() => atom.packages.activatePackage('status-bar')) waitsForPromise(() => atom.packages.activatePackage('status-bar'));
runs(() => { runs(() => {
statusBar = findStatusBar() statusBar = findStatusBar();
}) });
}) });
describe('when there are packages with incompatible native modules', () => { describe('when there are packages with incompatible native modules', () => {
beforeEach(() => { beforeEach(() => {
let incompatiblePackage = atom.packages.loadPackage( let incompatiblePackage = atom.packages.loadPackage(
path.join(__dirname, 'fixtures', 'incompatible-package') path.join(__dirname, 'fixtures', 'incompatible-package')
) );
spyOn(incompatiblePackage, 'isCompatible').andReturn(false) spyOn(incompatiblePackage, 'isCompatible').andReturn(false);
incompatiblePackage.incompatibleModules = [] incompatiblePackage.incompatibleModules = [];
waitsForPromise(() => waitsForPromise(() =>
atom.packages.activatePackage('incompatible-packages') atom.packages.activatePackage('incompatible-packages')
) );
waits(1) waits(1);
}) });
it('adds an icon to the status bar', () => { it('adds an icon to the status bar', () => {
let statusBarIcon = statusBar.getRightTiles()[0].getItem() let statusBarIcon = statusBar.getRightTiles()[0].getItem();
expect(statusBarIcon.constructor).toBe(StatusIconComponent) expect(statusBarIcon.constructor).toBe(StatusIconComponent);
}) });
describe('clicking the icon', () => { describe('clicking the icon', () => {
it('displays the incompatible packages view in a pane', () => { it('displays the incompatible packages view in a pane', () => {
let statusBarIcon = statusBar.getRightTiles()[0].getItem() let statusBarIcon = statusBar.getRightTiles()[0].getItem();
statusBarIcon.element.dispatchEvent(new MouseEvent('click')) statusBarIcon.element.dispatchEvent(new MouseEvent('click'));
let activePaneItem let activePaneItem;
waitsFor(() => (activePaneItem = atom.workspace.getActivePaneItem())) waitsFor(() => (activePaneItem = atom.workspace.getActivePaneItem()));
runs(() => { runs(() => {
expect(activePaneItem.constructor).toBe(IncompatiblePackagesComponent) expect(activePaneItem.constructor).toBe(
}) IncompatiblePackagesComponent
}) );
}) });
}) });
});
});
describe('when there are no packages with incompatible native modules', () => { describe('when there are no packages with incompatible native modules', () => {
beforeEach(() => { beforeEach(() => {
waitsForPromise(() => waitsForPromise(() =>
atom.packages.activatePackage('incompatible-packages') atom.packages.activatePackage('incompatible-packages')
) );
}) });
it('does not add an icon to the status bar', () => { it('does not add an icon to the status bar', () => {
let statusBarItemClasses = statusBar let statusBarItemClasses = statusBar
.getRightTiles() .getRightTiles()
.map(tile => tile.getItem().className) .map(tile => tile.getItem().className);
expect(statusBarItemClasses).not.toContain('incompatible-packages') expect(statusBarItemClasses).not.toContain('incompatible-packages');
}) });
}) });
}) });

View File

@ -1,7 +1,7 @@
'use babel' 'use babel';
export default { export default {
getProcessPlatform () { getProcessPlatform() {
return process.platform return process.platform;
} }
} };

View File

@ -1,26 +1,26 @@
'use babel' 'use babel';
import _ from 'underscore-plus' import _ from 'underscore-plus';
import { CompositeDisposable, Disposable } from 'atom' import { CompositeDisposable, Disposable } from 'atom';
import SelectListView from 'atom-select-list' import SelectListView from 'atom-select-list';
import StatusBarItem from './status-bar-item' import StatusBarItem from './status-bar-item';
import helpers from './helpers' import helpers from './helpers';
const LineEndingRegExp = /\r\n|\n/g const LineEndingRegExp = /\r\n|\n/g;
// the following regular expression is executed natively via the `substring` package, // the following regular expression is executed natively via the `substring` package,
// where `\A` corresponds to the beginning of the string. // where `\A` corresponds to the beginning of the string.
// More info: https://github.com/atom/line-ending-selector/pull/56 // More info: https://github.com/atom/line-ending-selector/pull/56
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const LFRegExp = /(\A|[^\r])\n/g const LFRegExp = /(\A|[^\r])\n/g;
const CRLFRegExp = /\r\n/g const CRLFRegExp = /\r\n/g;
let disposables = null let disposables = null;
let modalPanel = null let modalPanel = null;
let lineEndingListView = null let lineEndingListView = null;
export function activate () { export function activate() {
disposables = new CompositeDisposable() disposables = new CompositeDisposable();
disposables.add( disposables.add(
atom.commands.add('atom-text-editor', { atom.commands.add('atom-text-editor', {
@ -36,161 +36,161 @@ export function activate () {
setLineEnding( setLineEnding(
atom.workspace.getActiveTextEditor(), atom.workspace.getActiveTextEditor(),
lineEnding.value lineEnding.value
) );
modalPanel.hide() modalPanel.hide();
}, },
didCancelSelection: () => { didCancelSelection: () => {
modalPanel.hide() modalPanel.hide();
}, },
elementForItem: lineEnding => { elementForItem: lineEnding => {
const element = document.createElement('li') const element = document.createElement('li');
element.textContent = lineEnding.name element.textContent = lineEnding.name;
return element return element;
} }
}) });
modalPanel = atom.workspace.addModalPanel({ modalPanel = atom.workspace.addModalPanel({
item: lineEndingListView item: lineEndingListView
}) });
disposables.add( disposables.add(
new Disposable(() => { new Disposable(() => {
lineEndingListView.destroy() lineEndingListView.destroy();
modalPanel.destroy() modalPanel.destroy();
modalPanel = null modalPanel = null;
}) })
) );
} }
lineEndingListView.reset() lineEndingListView.reset();
modalPanel.show() modalPanel.show();
lineEndingListView.focus() lineEndingListView.focus();
}, },
'line-ending-selector:convert-to-LF': event => { 'line-ending-selector:convert-to-LF': event => {
const editorElement = event.target.closest('atom-text-editor') const editorElement = event.target.closest('atom-text-editor');
setLineEnding(editorElement.getModel(), '\n') setLineEnding(editorElement.getModel(), '\n');
}, },
'line-ending-selector:convert-to-CRLF': event => { 'line-ending-selector:convert-to-CRLF': event => {
const editorElement = event.target.closest('atom-text-editor') const editorElement = event.target.closest('atom-text-editor');
setLineEnding(editorElement.getModel(), '\r\n') setLineEnding(editorElement.getModel(), '\r\n');
} }
}) })
) );
} }
export function deactivate () { export function deactivate() {
disposables.dispose() disposables.dispose();
} }
export function consumeStatusBar (statusBar) { export function consumeStatusBar(statusBar) {
let statusBarItem = new StatusBarItem() let statusBarItem = new StatusBarItem();
let currentBufferDisposable = null let currentBufferDisposable = null;
let tooltipDisposable = null let tooltipDisposable = null;
const updateTile = _.debounce(buffer => { const updateTile = _.debounce(buffer => {
getLineEndings(buffer).then(lineEndings => { getLineEndings(buffer).then(lineEndings => {
if (lineEndings.size === 0) { if (lineEndings.size === 0) {
let defaultLineEnding = getDefaultLineEnding() let defaultLineEnding = getDefaultLineEnding();
buffer.setPreferredLineEnding(defaultLineEnding) buffer.setPreferredLineEnding(defaultLineEnding);
lineEndings = new Set().add(defaultLineEnding) lineEndings = new Set().add(defaultLineEnding);
} }
statusBarItem.setLineEndings(lineEndings) statusBarItem.setLineEndings(lineEndings);
}) });
}, 0) }, 0);
disposables.add( disposables.add(
atom.workspace.observeActiveTextEditor(editor => { atom.workspace.observeActiveTextEditor(editor => {
if (currentBufferDisposable) currentBufferDisposable.dispose() if (currentBufferDisposable) currentBufferDisposable.dispose();
if (editor && editor.getBuffer) { if (editor && editor.getBuffer) {
let buffer = editor.getBuffer() let buffer = editor.getBuffer();
updateTile(buffer) updateTile(buffer);
currentBufferDisposable = buffer.onDidChange(({ oldText, newText }) => { currentBufferDisposable = buffer.onDidChange(({ oldText, newText }) => {
if (!statusBarItem.hasLineEnding('\n')) { if (!statusBarItem.hasLineEnding('\n')) {
if (newText.indexOf('\n') >= 0) { if (newText.indexOf('\n') >= 0) {
updateTile(buffer) updateTile(buffer);
} }
} else if (!statusBarItem.hasLineEnding('\r\n')) { } else if (!statusBarItem.hasLineEnding('\r\n')) {
if (newText.indexOf('\r\n') >= 0) { if (newText.indexOf('\r\n') >= 0) {
updateTile(buffer) updateTile(buffer);
} }
} else if (oldText.indexOf('\n')) { } else if (oldText.indexOf('\n')) {
updateTile(buffer) updateTile(buffer);
} }
}) });
} else { } else {
statusBarItem.setLineEndings(new Set()) statusBarItem.setLineEndings(new Set());
currentBufferDisposable = null currentBufferDisposable = null;
} }
if (tooltipDisposable) { if (tooltipDisposable) {
disposables.remove(tooltipDisposable) disposables.remove(tooltipDisposable);
tooltipDisposable.dispose() tooltipDisposable.dispose();
} }
tooltipDisposable = atom.tooltips.add(statusBarItem.element, { tooltipDisposable = atom.tooltips.add(statusBarItem.element, {
title () { title() {
return `File uses ${statusBarItem.description()} line endings` return `File uses ${statusBarItem.description()} line endings`;
} }
}) });
disposables.add(tooltipDisposable) disposables.add(tooltipDisposable);
}) })
) );
disposables.add( disposables.add(
new Disposable(() => { new Disposable(() => {
if (currentBufferDisposable) currentBufferDisposable.dispose() if (currentBufferDisposable) currentBufferDisposable.dispose();
}) })
) );
statusBarItem.onClick(() => { statusBarItem.onClick(() => {
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
atom.commands.dispatch( atom.commands.dispatch(
atom.views.getView(editor), atom.views.getView(editor),
'line-ending-selector:show' 'line-ending-selector:show'
) );
}) });
let tile = statusBar.addRightTile({ item: statusBarItem, priority: 200 }) let tile = statusBar.addRightTile({ item: statusBarItem, priority: 200 });
disposables.add(new Disposable(() => tile.destroy())) disposables.add(new Disposable(() => tile.destroy()));
} }
function getDefaultLineEnding () { function getDefaultLineEnding() {
switch (atom.config.get('line-ending-selector.defaultLineEnding')) { switch (atom.config.get('line-ending-selector.defaultLineEnding')) {
case 'LF': case 'LF':
return '\n' return '\n';
case 'CRLF': case 'CRLF':
return '\r\n' return '\r\n';
case 'OS Default': case 'OS Default':
default: default:
return helpers.getProcessPlatform() === 'win32' ? '\r\n' : '\n' return helpers.getProcessPlatform() === 'win32' ? '\r\n' : '\n';
} }
} }
function getLineEndings (buffer) { function getLineEndings(buffer) {
if (typeof buffer.find === 'function') { if (typeof buffer.find === 'function') {
return Promise.all([buffer.find(LFRegExp), buffer.find(CRLFRegExp)]).then( return Promise.all([buffer.find(LFRegExp), buffer.find(CRLFRegExp)]).then(
([hasLF, hasCRLF]) => { ([hasLF, hasCRLF]) => {
const result = new Set() const result = new Set();
if (hasLF) result.add('\n') if (hasLF) result.add('\n');
if (hasCRLF) result.add('\r\n') if (hasCRLF) result.add('\r\n');
return result return result;
} }
) );
} else { } else {
return new Promise(resolve => { return new Promise(resolve => {
const result = new Set() const result = new Set();
for (let i = 0; i < buffer.getLineCount() - 1; i++) { for (let i = 0; i < buffer.getLineCount() - 1; i++) {
result.add(buffer.lineEndingForRow(i)) result.add(buffer.lineEndingForRow(i));
} }
resolve(result) resolve(result);
}) });
} }
} }
function setLineEnding (item, lineEnding) { function setLineEnding(item, lineEnding) {
if (item && item.getBuffer) { if (item && item.getBuffer) {
let buffer = item.getBuffer() let buffer = item.getBuffer();
buffer.setPreferredLineEnding(lineEnding) buffer.setPreferredLineEnding(lineEnding);
buffer.setText(buffer.getText().replace(LineEndingRegExp, lineEnding)) buffer.setText(buffer.getText().replace(LineEndingRegExp, lineEnding));
} }
} }

View File

@ -1,57 +1,57 @@
const { Emitter } = require('atom') const { Emitter } = require('atom');
module.exports = class StatusBarItem { module.exports = class StatusBarItem {
constructor () { constructor() {
this.element = document.createElement('a') this.element = document.createElement('a');
this.element.className = 'line-ending-tile inline-block' this.element.className = 'line-ending-tile inline-block';
this.emitter = new Emitter() this.emitter = new Emitter();
this.setLineEndings(new Set()) this.setLineEndings(new Set());
} }
setLineEndings (lineEndings) { setLineEndings(lineEndings) {
this.lineEndings = lineEndings this.lineEndings = lineEndings;
this.element.textContent = lineEndingName(lineEndings) this.element.textContent = lineEndingName(lineEndings);
this.emitter.emit('did-change') this.emitter.emit('did-change');
} }
onDidChange (callback) { onDidChange(callback) {
return this.emitter.on('did-change', callback) return this.emitter.on('did-change', callback);
} }
hasLineEnding (lineEnding) { hasLineEnding(lineEnding) {
return this.lineEndings.has(lineEnding) return this.lineEndings.has(lineEnding);
} }
description () { description() {
return lineEndingDescription(this.lineEndings) return lineEndingDescription(this.lineEndings);
} }
onClick (callback) { onClick(callback) {
this.element.addEventListener('click', callback) this.element.addEventListener('click', callback);
} }
} };
function lineEndingName (lineEndings) { function lineEndingName(lineEndings) {
if (lineEndings.size > 1) { if (lineEndings.size > 1) {
return 'Mixed' return 'Mixed';
} else if (lineEndings.has('\n')) { } else if (lineEndings.has('\n')) {
return 'LF' return 'LF';
} else if (lineEndings.has('\r\n')) { } else if (lineEndings.has('\r\n')) {
return 'CRLF' return 'CRLF';
} else { } else {
return '' return '';
} }
} }
function lineEndingDescription (lineEndings) { function lineEndingDescription(lineEndings) {
switch (lineEndingName(lineEndings)) { switch (lineEndingName(lineEndings)) {
case 'Mixed': case 'Mixed':
return 'mixed' return 'mixed';
case 'LF': case 'LF':
return 'LF (Unix)' return 'LF (Unix)';
case 'CRLF': case 'CRLF':
return 'CRLF (Windows)' return 'CRLF (Windows)';
default: default:
return 'unknown' return 'unknown';
} }
} }

View File

@ -1,374 +1,378 @@
const helpers = require('../lib/helpers') const helpers = require('../lib/helpers');
const { TextEditor } = require('atom') const { TextEditor } = require('atom');
describe('line ending selector', () => { describe('line ending selector', () => {
let lineEndingTile let lineEndingTile;
beforeEach(() => { beforeEach(() => {
jasmine.useRealClock() jasmine.useRealClock();
waitsForPromise(() => { waitsForPromise(() => {
return atom.packages.activatePackage('status-bar') return atom.packages.activatePackage('status-bar');
}) });
waitsForPromise(() => { waitsForPromise(() => {
return atom.packages.activatePackage('line-ending-selector') return atom.packages.activatePackage('line-ending-selector');
}) });
waits(1) waits(1);
runs(() => { runs(() => {
const statusBar = atom.workspace.getFooterPanels()[0].getItem() const statusBar = atom.workspace.getFooterPanels()[0].getItem();
lineEndingTile = statusBar.getRightTiles()[0].getItem() lineEndingTile = statusBar.getRightTiles()[0].getItem();
expect(lineEndingTile.element.className).toMatch(/line-ending-tile/) expect(lineEndingTile.element.className).toMatch(/line-ending-tile/);
expect(lineEndingTile.element.textContent).toBe('') expect(lineEndingTile.element.textContent).toBe('');
}) });
}) });
describe('Commands', () => { describe('Commands', () => {
let editor, editorElement let editor, editorElement;
beforeEach(() => { beforeEach(() => {
waitsForPromise(() => { waitsForPromise(() => {
return atom.workspace.open('mixed-endings.md').then(e => { return atom.workspace.open('mixed-endings.md').then(e => {
editor = e editor = e;
editorElement = atom.views.getView(editor) editorElement = atom.views.getView(editor);
jasmine.attachToDOM(editorElement) jasmine.attachToDOM(editorElement);
}) });
}) });
}) });
describe('When "line-ending-selector:convert-to-LF" is run', () => { describe('When "line-ending-selector:convert-to-LF" is run', () => {
it('converts the file to LF line endings', () => { it('converts the file to LF line endings', () => {
editorElement.focus() editorElement.focus();
atom.commands.dispatch( atom.commands.dispatch(
document.activeElement, document.activeElement,
'line-ending-selector:convert-to-LF' 'line-ending-selector:convert-to-LF'
) );
expect(editor.getText()).toBe('Hello\nGoodbye\nMixed\n') expect(editor.getText()).toBe('Hello\nGoodbye\nMixed\n');
}) });
}) });
describe('When "line-ending-selector:convert-to-LF" is run', () => { describe('When "line-ending-selector:convert-to-LF" is run', () => {
it('converts the file to CRLF line endings', () => { it('converts the file to CRLF line endings', () => {
editorElement.focus() editorElement.focus();
atom.commands.dispatch( atom.commands.dispatch(
document.activeElement, document.activeElement,
'line-ending-selector:convert-to-CRLF' 'line-ending-selector:convert-to-CRLF'
) );
expect(editor.getText()).toBe('Hello\r\nGoodbye\r\nMixed\r\n') expect(editor.getText()).toBe('Hello\r\nGoodbye\r\nMixed\r\n');
}) });
}) });
}) });
describe('Status bar tile', () => { describe('Status bar tile', () => {
describe('when an empty file is opened', () => { describe('when an empty file is opened', () => {
it('uses the default line endings for the platform', () => { it('uses the default line endings for the platform', () => {
waitsFor(done => { waitsFor(done => {
spyOn(helpers, 'getProcessPlatform').andReturn('win32') spyOn(helpers, 'getProcessPlatform').andReturn('win32');
atom.workspace.open('').then(editor => { atom.workspace.open('').then(editor => {
const subscription = lineEndingTile.onDidChange(() => { const subscription = lineEndingTile.onDidChange(() => {
subscription.dispose() subscription.dispose();
expect(lineEndingTile.element.textContent).toBe('CRLF') expect(lineEndingTile.element.textContent).toBe('CRLF');
expect(editor.getBuffer().getPreferredLineEnding()).toBe('\r\n') expect(editor.getBuffer().getPreferredLineEnding()).toBe('\r\n');
expect(getTooltipText(lineEndingTile.element)).toBe( expect(getTooltipText(lineEndingTile.element)).toBe(
'File uses CRLF (Windows) line endings' 'File uses CRLF (Windows) line endings'
) );
done() done();
}) });
}) });
}) });
waitsFor(done => { waitsFor(done => {
helpers.getProcessPlatform.andReturn('darwin') helpers.getProcessPlatform.andReturn('darwin');
atom.workspace.open('').then(editor => { atom.workspace.open('').then(editor => {
const subscription = lineEndingTile.onDidChange(() => { const subscription = lineEndingTile.onDidChange(() => {
subscription.dispose() subscription.dispose();
expect(lineEndingTile.element.textContent).toBe('LF') expect(lineEndingTile.element.textContent).toBe('LF');
expect(editor.getBuffer().getPreferredLineEnding()).toBe('\n') expect(editor.getBuffer().getPreferredLineEnding()).toBe('\n');
expect(getTooltipText(lineEndingTile.element)).toBe( expect(getTooltipText(lineEndingTile.element)).toBe(
'File uses LF (Unix) line endings' 'File uses LF (Unix) line endings'
) );
done() done();
}) });
}) });
}) });
}) });
describe('when the "defaultLineEnding" setting is set to "LF"', () => { describe('when the "defaultLineEnding" setting is set to "LF"', () => {
beforeEach(() => { beforeEach(() => {
atom.config.set('line-ending-selector.defaultLineEnding', 'LF') atom.config.set('line-ending-selector.defaultLineEnding', 'LF');
}) });
it('uses LF line endings, regardless of the platform', () => { it('uses LF line endings, regardless of the platform', () => {
waitsFor(done => { waitsFor(done => {
spyOn(helpers, 'getProcessPlatform').andReturn('win32') spyOn(helpers, 'getProcessPlatform').andReturn('win32');
atom.workspace.open('').then(editor => { atom.workspace.open('').then(editor => {
lineEndingTile.onDidChange(() => { lineEndingTile.onDidChange(() => {
expect(lineEndingTile.element.textContent).toBe('LF') expect(lineEndingTile.element.textContent).toBe('LF');
expect(editor.getBuffer().getPreferredLineEnding()).toBe('\n') expect(editor.getBuffer().getPreferredLineEnding()).toBe('\n');
done() done();
}) });
}) });
}) });
}) });
}) });
describe('when the "defaultLineEnding" setting is set to "CRLF"', () => { describe('when the "defaultLineEnding" setting is set to "CRLF"', () => {
beforeEach(() => { beforeEach(() => {
atom.config.set('line-ending-selector.defaultLineEnding', 'CRLF') atom.config.set('line-ending-selector.defaultLineEnding', 'CRLF');
}) });
it('uses CRLF line endings, regardless of the platform', () => { it('uses CRLF line endings, regardless of the platform', () => {
waitsFor(done => { waitsFor(done => {
atom.workspace.open('').then(editor => { atom.workspace.open('').then(editor => {
lineEndingTile.onDidChange(() => { lineEndingTile.onDidChange(() => {
expect(lineEndingTile.element.textContent).toBe('CRLF') expect(lineEndingTile.element.textContent).toBe('CRLF');
expect(editor.getBuffer().getPreferredLineEnding()).toBe('\r\n') expect(editor.getBuffer().getPreferredLineEnding()).toBe(
done() '\r\n'
}) );
}) done();
}) });
}) });
}) });
}) });
});
});
describe('when a file is opened that contains only CRLF line endings', () => { describe('when a file is opened that contains only CRLF line endings', () => {
it('displays "CRLF" as the line ending', () => { it('displays "CRLF" as the line ending', () => {
waitsFor(done => { waitsFor(done => {
atom.workspace.open('windows-endings.md').then(() => { atom.workspace.open('windows-endings.md').then(() => {
lineEndingTile.onDidChange(() => { lineEndingTile.onDidChange(() => {
expect(lineEndingTile.element.textContent).toBe('CRLF') expect(lineEndingTile.element.textContent).toBe('CRLF');
done() done();
}) });
}) });
}) });
}) });
}) });
describe('when a file is opened that contains only LF line endings', () => { describe('when a file is opened that contains only LF line endings', () => {
it('displays "LF" as the line ending', () => { it('displays "LF" as the line ending', () => {
waitsFor(done => { waitsFor(done => {
atom.workspace.open('unix-endings.md').then(editor => { atom.workspace.open('unix-endings.md').then(editor => {
lineEndingTile.onDidChange(() => { lineEndingTile.onDidChange(() => {
expect(lineEndingTile.element.textContent).toBe('LF') expect(lineEndingTile.element.textContent).toBe('LF');
expect(editor.getBuffer().getPreferredLineEnding()).toBe(null) expect(editor.getBuffer().getPreferredLineEnding()).toBe(null);
done() done();
}) });
}) });
}) });
}) });
}) });
describe('when a file is opened that contains mixed line endings', () => { describe('when a file is opened that contains mixed line endings', () => {
it('displays "Mixed" as the line ending', () => { it('displays "Mixed" as the line ending', () => {
waitsFor(done => { waitsFor(done => {
atom.workspace.open('mixed-endings.md').then(() => { atom.workspace.open('mixed-endings.md').then(() => {
lineEndingTile.onDidChange(() => { lineEndingTile.onDidChange(() => {
expect(lineEndingTile.element.textContent).toBe('Mixed') expect(lineEndingTile.element.textContent).toBe('Mixed');
done() done();
}) });
}) });
}) });
}) });
}) });
describe('clicking the tile', () => { describe('clicking the tile', () => {
let lineEndingModal, lineEndingSelector let lineEndingModal, lineEndingSelector;
beforeEach(() => { beforeEach(() => {
jasmine.attachToDOM(atom.views.getView(atom.workspace)) jasmine.attachToDOM(atom.views.getView(atom.workspace));
waitsFor(done => waitsFor(done =>
atom.workspace atom.workspace
.open('unix-endings.md') .open('unix-endings.md')
.then(() => lineEndingTile.onDidChange(done)) .then(() => lineEndingTile.onDidChange(done))
) );
}) });
describe('when the text editor has focus', () => { describe('when the text editor has focus', () => {
it('opens the line ending selector modal for the text editor', () => { it('opens the line ending selector modal for the text editor', () => {
atom.workspace.getCenter().activate() atom.workspace.getCenter().activate();
const item = atom.workspace.getActivePaneItem() const item = atom.workspace.getActivePaneItem();
expect(item.getFileName && item.getFileName()).toBe('unix-endings.md') expect(item.getFileName && item.getFileName()).toBe(
'unix-endings.md'
);
lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})) lineEndingTile.element.dispatchEvent(new MouseEvent('click', {}));
lineEndingModal = atom.workspace.getModalPanels()[0] lineEndingModal = atom.workspace.getModalPanels()[0];
lineEndingSelector = lineEndingModal.getItem() lineEndingSelector = lineEndingModal.getItem();
expect(lineEndingModal.isVisible()).toBe(true) expect(lineEndingModal.isVisible()).toBe(true);
expect( expect(
lineEndingSelector.element.contains(document.activeElement) lineEndingSelector.element.contains(document.activeElement)
).toBe(true) ).toBe(true);
let listItems = lineEndingSelector.element.querySelectorAll('li') let listItems = lineEndingSelector.element.querySelectorAll('li');
expect(listItems[0].textContent).toBe('LF') expect(listItems[0].textContent).toBe('LF');
expect(listItems[1].textContent).toBe('CRLF') expect(listItems[1].textContent).toBe('CRLF');
}) });
}) });
describe('when the text editor does not have focus', () => { describe('when the text editor does not have focus', () => {
it('opens the line ending selector modal for the active text editor', () => { it('opens the line ending selector modal for the active text editor', () => {
atom.workspace.getLeftDock().activate() atom.workspace.getLeftDock().activate();
const item = atom.workspace.getActivePaneItem() const item = atom.workspace.getActivePaneItem();
expect(item instanceof TextEditor).toBe(false) expect(item instanceof TextEditor).toBe(false);
lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})) lineEndingTile.element.dispatchEvent(new MouseEvent('click', {}));
lineEndingModal = atom.workspace.getModalPanels()[0] lineEndingModal = atom.workspace.getModalPanels()[0];
lineEndingSelector = lineEndingModal.getItem() lineEndingSelector = lineEndingModal.getItem();
expect(lineEndingModal.isVisible()).toBe(true) expect(lineEndingModal.isVisible()).toBe(true);
expect( expect(
lineEndingSelector.element.contains(document.activeElement) lineEndingSelector.element.contains(document.activeElement)
).toBe(true) ).toBe(true);
let listItems = lineEndingSelector.element.querySelectorAll('li') let listItems = lineEndingSelector.element.querySelectorAll('li');
expect(listItems[0].textContent).toBe('LF') expect(listItems[0].textContent).toBe('LF');
expect(listItems[1].textContent).toBe('CRLF') expect(listItems[1].textContent).toBe('CRLF');
}) });
}) });
describe('when selecting a different line ending for the file', () => { describe('when selecting a different line ending for the file', () => {
it('changes the line endings in the buffer', () => { it('changes the line endings in the buffer', () => {
lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})) lineEndingTile.element.dispatchEvent(new MouseEvent('click', {}));
lineEndingModal = atom.workspace.getModalPanels()[0] lineEndingModal = atom.workspace.getModalPanels()[0];
lineEndingSelector = lineEndingModal.getItem() lineEndingSelector = lineEndingModal.getItem();
const lineEndingChangedPromise = new Promise(resolve => { const lineEndingChangedPromise = new Promise(resolve => {
lineEndingTile.onDidChange(() => { lineEndingTile.onDidChange(() => {
expect(lineEndingTile.element.textContent).toBe('CRLF') expect(lineEndingTile.element.textContent).toBe('CRLF');
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
expect(editor.getText()).toBe('Hello\r\nGoodbye\r\nUnix\r\n') expect(editor.getText()).toBe('Hello\r\nGoodbye\r\nUnix\r\n');
expect(editor.getBuffer().getPreferredLineEnding()).toBe('\r\n') expect(editor.getBuffer().getPreferredLineEnding()).toBe('\r\n');
resolve() resolve();
}) });
}) });
lineEndingSelector.refs.queryEditor.setText('CR') lineEndingSelector.refs.queryEditor.setText('CR');
lineEndingSelector.confirmSelection() lineEndingSelector.confirmSelection();
expect(lineEndingModal.isVisible()).toBe(false) expect(lineEndingModal.isVisible()).toBe(false);
waitsForPromise(() => lineEndingChangedPromise) waitsForPromise(() => lineEndingChangedPromise);
}) });
}) });
describe('when modal is exited', () => { describe('when modal is exited', () => {
it('leaves the tile selection as-is', () => { it('leaves the tile selection as-is', () => {
lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})) lineEndingTile.element.dispatchEvent(new MouseEvent('click', {}));
lineEndingModal = atom.workspace.getModalPanels()[0] lineEndingModal = atom.workspace.getModalPanels()[0];
lineEndingSelector = lineEndingModal.getItem() lineEndingSelector = lineEndingModal.getItem();
lineEndingSelector.cancelSelection() lineEndingSelector.cancelSelection();
expect(lineEndingTile.element.textContent).toBe('LF') expect(lineEndingTile.element.textContent).toBe('LF');
}) });
}) });
}) });
describe('closing the last text editor', () => { describe('closing the last text editor', () => {
it('displays no line ending in the status bar', () => { it('displays no line ending in the status bar', () => {
waitsForPromise(() => { waitsForPromise(() => {
return atom.workspace.open('unix-endings.md').then(() => { return atom.workspace.open('unix-endings.md').then(() => {
atom.workspace.getActivePane().destroy() atom.workspace.getActivePane().destroy();
expect(lineEndingTile.element.textContent).toBe('') expect(lineEndingTile.element.textContent).toBe('');
}) });
}) });
}) });
}) });
describe("when the buffer's line endings change", () => { describe("when the buffer's line endings change", () => {
let editor let editor;
beforeEach(() => { beforeEach(() => {
waitsFor(done => { waitsFor(done => {
atom.workspace.open('unix-endings.md').then(e => { atom.workspace.open('unix-endings.md').then(e => {
editor = e editor = e;
lineEndingTile.onDidChange(done) lineEndingTile.onDidChange(done);
}) });
}) });
}) });
it('updates the line ending text in the tile', () => { it('updates the line ending text in the tile', () => {
let tileText = lineEndingTile.element.textContent let tileText = lineEndingTile.element.textContent;
let tileUpdateCount = 0 let tileUpdateCount = 0;
Object.defineProperty(lineEndingTile.element, 'textContent', { Object.defineProperty(lineEndingTile.element, 'textContent', {
get () { get() {
return tileText return tileText;
}, },
set (text) { set(text) {
tileUpdateCount++ tileUpdateCount++;
tileText = text tileText = text;
} }
}) });
expect(lineEndingTile.element.textContent).toBe('LF') expect(lineEndingTile.element.textContent).toBe('LF');
expect(getTooltipText(lineEndingTile.element)).toBe( expect(getTooltipText(lineEndingTile.element)).toBe(
'File uses LF (Unix) line endings' 'File uses LF (Unix) line endings'
) );
waitsFor(done => { waitsFor(done => {
editor.setTextInBufferRange([[0, 0], [0, 0]], '... ') editor.setTextInBufferRange([[0, 0], [0, 0]], '... ');
editor.setTextInBufferRange([[0, Infinity], [1, 0]], '\r\n', { editor.setTextInBufferRange([[0, Infinity], [1, 0]], '\r\n', {
normalizeLineEndings: false normalizeLineEndings: false
}) });
lineEndingTile.onDidChange(done) lineEndingTile.onDidChange(done);
}) });
runs(() => { runs(() => {
expect(tileUpdateCount).toBe(1) expect(tileUpdateCount).toBe(1);
expect(lineEndingTile.element.textContent).toBe('Mixed') expect(lineEndingTile.element.textContent).toBe('Mixed');
expect(getTooltipText(lineEndingTile.element)).toBe( expect(getTooltipText(lineEndingTile.element)).toBe(
'File uses mixed line endings' 'File uses mixed line endings'
) );
}) });
waitsFor(done => { waitsFor(done => {
atom.commands.dispatch( atom.commands.dispatch(
editor.getElement(), editor.getElement(),
'line-ending-selector:convert-to-CRLF' 'line-ending-selector:convert-to-CRLF'
) );
lineEndingTile.onDidChange(done) lineEndingTile.onDidChange(done);
}) });
runs(() => { runs(() => {
expect(tileUpdateCount).toBe(2) expect(tileUpdateCount).toBe(2);
expect(lineEndingTile.element.textContent).toBe('CRLF') expect(lineEndingTile.element.textContent).toBe('CRLF');
expect(getTooltipText(lineEndingTile.element)).toBe( expect(getTooltipText(lineEndingTile.element)).toBe(
'File uses CRLF (Windows) line endings' 'File uses CRLF (Windows) line endings'
) );
}) });
waitsFor(done => { waitsFor(done => {
atom.commands.dispatch( atom.commands.dispatch(
editor.getElement(), editor.getElement(),
'line-ending-selector:convert-to-LF' 'line-ending-selector:convert-to-LF'
) );
lineEndingTile.onDidChange(done) lineEndingTile.onDidChange(done);
}) });
runs(() => { runs(() => {
expect(tileUpdateCount).toBe(3) expect(tileUpdateCount).toBe(3);
expect(lineEndingTile.element.textContent).toBe('LF') expect(lineEndingTile.element.textContent).toBe('LF');
}) });
runs(() => { runs(() => {
editor.setTextInBufferRange([[0, 0], [0, 0]], '\n') editor.setTextInBufferRange([[0, 0], [0, 0]], '\n');
}) });
waits(100) waits(100);
runs(() => { runs(() => {
expect(tileUpdateCount).toBe(3) expect(tileUpdateCount).toBe(3);
}) });
}) });
}) });
}) });
}) });
function getTooltipText (element) { function getTooltipText(element) {
const [tooltip] = atom.tooltips.findTooltips(element) const [tooltip] = atom.tooltips.findTooltips(element);
return tooltip.getTitle() return tooltip.getTitle();
} }

View File

@ -1,64 +1,64 @@
const url = require('url') const url = require('url');
const { shell } = require('electron') const { shell } = require('electron');
const _ = require('underscore-plus') const _ = require('underscore-plus');
const LINK_SCOPE_REGEX = /markup\.underline\.link/ const LINK_SCOPE_REGEX = /markup\.underline\.link/;
module.exports = { module.exports = {
activate () { activate() {
this.commandDisposable = atom.commands.add( this.commandDisposable = atom.commands.add(
'atom-text-editor', 'atom-text-editor',
'link:open', 'link:open',
() => this.openLink() () => this.openLink()
) );
}, },
deactivate () { deactivate() {
this.commandDisposable.dispose() this.commandDisposable.dispose();
}, },
openLink () { openLink() {
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
if (editor == null) return if (editor == null) return;
let link = this.linkUnderCursor(editor) let link = this.linkUnderCursor(editor);
if (link == null) return if (link == null) return;
if (editor.getGrammar().scopeName === 'source.gfm') { if (editor.getGrammar().scopeName === 'source.gfm') {
link = this.linkForName(editor, link) link = this.linkForName(editor, link);
} }
const { protocol } = url.parse(link) const { protocol } = url.parse(link);
if (protocol === 'http:' || protocol === 'https:' || protocol === 'atom:') { if (protocol === 'http:' || protocol === 'https:' || protocol === 'atom:') {
shell.openExternal(link) shell.openExternal(link);
} }
}, },
// Get the link under the cursor in the editor // Get the link under the cursor in the editor
// //
// Returns a {String} link or undefined if no link found. // Returns a {String} link or undefined if no link found.
linkUnderCursor (editor) { linkUnderCursor(editor) {
const cursorPosition = editor.getCursorBufferPosition() const cursorPosition = editor.getCursorBufferPosition();
const link = this.linkAtPosition(editor, cursorPosition) const link = this.linkAtPosition(editor, cursorPosition);
if (link != null) return link if (link != null) return link;
// Look for a link to the left of the cursor // Look for a link to the left of the cursor
if (cursorPosition.column > 0) { if (cursorPosition.column > 0) {
return this.linkAtPosition(editor, cursorPosition.translate([0, -1])) return this.linkAtPosition(editor, cursorPosition.translate([0, -1]));
} }
}, },
// Get the link at the buffer position in the editor. // Get the link at the buffer position in the editor.
// //
// Returns a {String} link or undefined if no link found. // Returns a {String} link or undefined if no link found.
linkAtPosition (editor, bufferPosition) { linkAtPosition(editor, bufferPosition) {
const token = editor.tokenForBufferPosition(bufferPosition) const token = editor.tokenForBufferPosition(bufferPosition);
if ( if (
token && token &&
token.value && token.value &&
token.scopes.some(scope => LINK_SCOPE_REGEX.test(scope)) token.scopes.some(scope => LINK_SCOPE_REGEX.test(scope))
) { ) {
return token.value return token.value;
} }
}, },
@ -73,20 +73,20 @@ module.exports = {
// ``` // ```
// //
// Returns a {String} link // Returns a {String} link
linkForName (editor, linkName) { linkForName(editor, linkName) {
let link = linkName let link = linkName;
const regex = new RegExp( const regex = new RegExp(
`^\\s*\\[${_.escapeRegExp(linkName)}\\]\\s*:\\s*(.+)$`, `^\\s*\\[${_.escapeRegExp(linkName)}\\]\\s*:\\s*(.+)$`,
'g' 'g'
) );
editor.backwardsScanInBufferRange( editor.backwardsScanInBufferRange(
regex, regex,
[[0, 0], [Infinity, Infinity]], [[0, 0], [Infinity, Infinity]],
({ match, stop }) => { ({ match, stop }) => {
link = match[1] link = match[1];
stop() stop();
} }
) );
return link return link;
} }
} };

View File

@ -1,136 +1,136 @@
const { shell } = require('electron') const { shell } = require('electron');
describe('link package', () => { describe('link package', () => {
beforeEach(async () => { beforeEach(async () => {
await atom.packages.activatePackage('language-gfm') await atom.packages.activatePackage('language-gfm');
await atom.packages.activatePackage('language-hyperlink') await atom.packages.activatePackage('language-hyperlink');
const activationPromise = atom.packages.activatePackage('link') const activationPromise = atom.packages.activatePackage('link');
atom.commands.dispatch(atom.views.getView(atom.workspace), 'link:open') atom.commands.dispatch(atom.views.getView(atom.workspace), 'link:open');
await activationPromise await activationPromise;
}) });
describe('when the cursor is on a link', () => { describe('when the cursor is on a link', () => {
it("opens the link using the 'open' command", async () => { it("opens the link using the 'open' command", async () => {
await atom.workspace.open('sample.md') await atom.workspace.open('sample.md');
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
editor.setText('// "http://github.com"') editor.setText('// "http://github.com"');
spyOn(shell, 'openExternal') spyOn(shell, 'openExternal');
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).not.toHaveBeenCalled() expect(shell.openExternal).not.toHaveBeenCalled();
editor.setCursorBufferPosition([0, 4]) editor.setCursorBufferPosition([0, 4]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com');
shell.openExternal.reset() shell.openExternal.reset();
editor.setCursorBufferPosition([0, 8]) editor.setCursorBufferPosition([0, 8]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com');
shell.openExternal.reset() shell.openExternal.reset();
editor.setCursorBufferPosition([0, 21]) editor.setCursorBufferPosition([0, 21]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com');
}) });
// only works in Atom >= 1.33.0 // only works in Atom >= 1.33.0
// https://github.com/atom/link/pull/33#issuecomment-419643655 // https://github.com/atom/link/pull/33#issuecomment-419643655
const atomVersion = atom.getVersion().split('.') const atomVersion = atom.getVersion().split('.');
console.error('atomVersion', atomVersion) console.error('atomVersion', atomVersion);
if (+atomVersion[0] > 1 || +atomVersion[1] >= 33) { if (+atomVersion[0] > 1 || +atomVersion[1] >= 33) {
it("opens an 'atom:' link", async () => { it("opens an 'atom:' link", async () => {
await atom.workspace.open('sample.md') await atom.workspace.open('sample.md');
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
editor.setText( editor.setText(
'// "atom://core/open/file?filename=sample.js&line=1&column=2"' '// "atom://core/open/file?filename=sample.js&line=1&column=2"'
) );
spyOn(shell, 'openExternal') spyOn(shell, 'openExternal');
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).not.toHaveBeenCalled() expect(shell.openExternal).not.toHaveBeenCalled();
editor.setCursorBufferPosition([0, 4]) editor.setCursorBufferPosition([0, 4]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(shell.openExternal.argsForCall[0][0]).toBe( expect(shell.openExternal.argsForCall[0][0]).toBe(
'atom://core/open/file?filename=sample.js&line=1&column=2' 'atom://core/open/file?filename=sample.js&line=1&column=2'
) );
shell.openExternal.reset() shell.openExternal.reset();
editor.setCursorBufferPosition([0, 8]) editor.setCursorBufferPosition([0, 8]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(shell.openExternal.argsForCall[0][0]).toBe( expect(shell.openExternal.argsForCall[0][0]).toBe(
'atom://core/open/file?filename=sample.js&line=1&column=2' 'atom://core/open/file?filename=sample.js&line=1&column=2'
) );
shell.openExternal.reset() shell.openExternal.reset();
editor.setCursorBufferPosition([0, 60]) editor.setCursorBufferPosition([0, 60]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(shell.openExternal.argsForCall[0][0]).toBe( expect(shell.openExternal.argsForCall[0][0]).toBe(
'atom://core/open/file?filename=sample.js&line=1&column=2' 'atom://core/open/file?filename=sample.js&line=1&column=2'
) );
}) });
} }
describe('when the cursor is on a [name][url-name] style markdown link', () => describe('when the cursor is on a [name][url-name] style markdown link', () =>
it('opens the named url', async () => { it('opens the named url', async () => {
await atom.workspace.open('README.md') await atom.workspace.open('README.md');
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
editor.setText(`\ editor.setText(`\
you should [click][here] you should [click][here]
you should not [click][her] you should not [click][her]
[here]: http://github.com\ [here]: http://github.com\
`) `);
spyOn(shell, 'openExternal') spyOn(shell, 'openExternal');
editor.setCursorBufferPosition([0, 0]) editor.setCursorBufferPosition([0, 0]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).not.toHaveBeenCalled() expect(shell.openExternal).not.toHaveBeenCalled();
editor.setCursorBufferPosition([0, 20]) editor.setCursorBufferPosition([0, 20]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).toHaveBeenCalled() expect(shell.openExternal).toHaveBeenCalled();
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com');
shell.openExternal.reset() shell.openExternal.reset();
editor.setCursorBufferPosition([1, 24]) editor.setCursorBufferPosition([1, 24]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).not.toHaveBeenCalled() expect(shell.openExternal).not.toHaveBeenCalled();
})) }));
it('does not open non http/https/atom links', async () => { it('does not open non http/https/atom links', async () => {
await atom.workspace.open('sample.md') await atom.workspace.open('sample.md');
const editor = atom.workspace.getActiveTextEditor() const editor = atom.workspace.getActiveTextEditor();
editor.setText('// ftp://github.com\n') editor.setText('// ftp://github.com\n');
spyOn(shell, 'openExternal') spyOn(shell, 'openExternal');
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).not.toHaveBeenCalled() expect(shell.openExternal).not.toHaveBeenCalled();
editor.setCursorBufferPosition([0, 5]) editor.setCursorBufferPosition([0, 5]);
atom.commands.dispatch(atom.views.getView(editor), 'link:open') atom.commands.dispatch(atom.views.getView(editor), 'link:open');
expect(shell.openExternal).not.toHaveBeenCalled() expect(shell.openExternal).not.toHaveBeenCalled();
}) });
}) });
}) });

View File

@ -1,88 +1,88 @@
const root = document.documentElement const root = document.documentElement;
const themeName = 'one-dark-ui' const themeName = 'one-dark-ui';
module.exports = { module.exports = {
activate (state) { activate(state) {
atom.config.observe(`${themeName}.fontSize`, setFontSize) atom.config.observe(`${themeName}.fontSize`, setFontSize);
atom.config.observe(`${themeName}.tabSizing`, setTabSizing) atom.config.observe(`${themeName}.tabSizing`, setTabSizing);
atom.config.observe(`${themeName}.tabCloseButton`, setTabCloseButton) atom.config.observe(`${themeName}.tabCloseButton`, setTabCloseButton);
atom.config.observe(`${themeName}.hideDockButtons`, setHideDockButtons) atom.config.observe(`${themeName}.hideDockButtons`, setHideDockButtons);
atom.config.observe(`${themeName}.stickyHeaders`, setStickyHeaders) atom.config.observe(`${themeName}.stickyHeaders`, setStickyHeaders);
// DEPRECATED: This can be removed at some point (added in Atom 1.17/1.18ish) // DEPRECATED: This can be removed at some point (added in Atom 1.17/1.18ish)
// It removes `layoutMode` // It removes `layoutMode`
if (atom.config.get(`${themeName}.layoutMode`)) { if (atom.config.get(`${themeName}.layoutMode`)) {
atom.config.unset(`${themeName}.layoutMode`) atom.config.unset(`${themeName}.layoutMode`);
} }
}, },
deactivate () { deactivate() {
unsetFontSize() unsetFontSize();
unsetTabSizing() unsetTabSizing();
unsetTabCloseButton() unsetTabCloseButton();
unsetHideDockButtons() unsetHideDockButtons();
unsetStickyHeaders() unsetStickyHeaders();
} }
} };
// Font Size ----------------------- // Font Size -----------------------
function setFontSize (currentFontSize) { function setFontSize(currentFontSize) {
root.style.fontSize = `${currentFontSize}px` root.style.fontSize = `${currentFontSize}px`;
} }
function unsetFontSize () { function unsetFontSize() {
root.style.fontSize = '' root.style.fontSize = '';
} }
// Tab Sizing ----------------------- // Tab Sizing -----------------------
function setTabSizing (tabSizing) { function setTabSizing(tabSizing) {
root.setAttribute(`theme-${themeName}-tabsizing`, tabSizing.toLowerCase()) root.setAttribute(`theme-${themeName}-tabsizing`, tabSizing.toLowerCase());
} }
function unsetTabSizing () { function unsetTabSizing() {
root.removeAttribute(`theme-${themeName}-tabsizing`) root.removeAttribute(`theme-${themeName}-tabsizing`);
} }
// Tab Close Button ----------------------- // Tab Close Button -----------------------
function setTabCloseButton (tabCloseButton) { function setTabCloseButton(tabCloseButton) {
if (tabCloseButton === 'Left') { if (tabCloseButton === 'Left') {
root.setAttribute(`theme-${themeName}-tab-close-button`, 'left') root.setAttribute(`theme-${themeName}-tab-close-button`, 'left');
} else { } else {
unsetTabCloseButton() unsetTabCloseButton();
} }
} }
function unsetTabCloseButton () { function unsetTabCloseButton() {
root.removeAttribute(`theme-${themeName}-tab-close-button`) root.removeAttribute(`theme-${themeName}-tab-close-button`);
} }
// Dock Buttons ----------------------- // Dock Buttons -----------------------
function setHideDockButtons (hideDockButtons) { function setHideDockButtons(hideDockButtons) {
if (hideDockButtons) { if (hideDockButtons) {
root.setAttribute(`theme-${themeName}-dock-buttons`, 'hidden') root.setAttribute(`theme-${themeName}-dock-buttons`, 'hidden');
} else { } else {
unsetHideDockButtons() unsetHideDockButtons();
} }
} }
function unsetHideDockButtons () { function unsetHideDockButtons() {
root.removeAttribute(`theme-${themeName}-dock-buttons`) root.removeAttribute(`theme-${themeName}-dock-buttons`);
} }
// Sticky Headers ----------------------- // Sticky Headers -----------------------
function setStickyHeaders (stickyHeaders) { function setStickyHeaders(stickyHeaders) {
if (stickyHeaders) { if (stickyHeaders) {
root.setAttribute(`theme-${themeName}-sticky-headers`, 'sticky') root.setAttribute(`theme-${themeName}-sticky-headers`, 'sticky');
} else { } else {
unsetStickyHeaders() unsetStickyHeaders();
} }
} }
function unsetStickyHeaders () { function unsetStickyHeaders() {
root.removeAttribute(`theme-${themeName}-sticky-headers`) root.removeAttribute(`theme-${themeName}-sticky-headers`);
} }

View File

@ -1,58 +1,58 @@
const themeName = 'one-dark-ui' const themeName = 'one-dark-ui';
describe(`${themeName} theme`, () => { describe(`${themeName} theme`, () => {
beforeEach(() => { beforeEach(() => {
waitsForPromise(() => atom.packages.activatePackage(themeName)) waitsForPromise(() => atom.packages.activatePackage(themeName));
}) });
it('allows the font size to be set via config', () => { it('allows the font size to be set via config', () => {
expect(document.documentElement.style.fontSize).toBe('12px') expect(document.documentElement.style.fontSize).toBe('12px');
atom.config.set(`${themeName}.fontSize`, '10') atom.config.set(`${themeName}.fontSize`, '10');
expect(document.documentElement.style.fontSize).toBe('10px') expect(document.documentElement.style.fontSize).toBe('10px');
}) });
it('allows the tab sizing to be set via config', () => { it('allows the tab sizing to be set via config', () => {
atom.config.set(`${themeName}.tabSizing`, 'Maximum') atom.config.set(`${themeName}.tabSizing`, 'Maximum');
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) document.documentElement.getAttribute(`theme-${themeName}-tabsizing`)
).toBe('maximum') ).toBe('maximum');
}) });
it('allows the tab sizing to be set via config', () => { it('allows the tab sizing to be set via config', () => {
atom.config.set(`${themeName}.tabSizing`, 'Minimum') atom.config.set(`${themeName}.tabSizing`, 'Minimum');
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) document.documentElement.getAttribute(`theme-${themeName}-tabsizing`)
).toBe('minimum') ).toBe('minimum');
}) });
it('allows the tab close button to be shown on the left via config', () => { it('allows the tab close button to be shown on the left via config', () => {
atom.config.set(`${themeName}.tabCloseButton`, 'Left') atom.config.set(`${themeName}.tabCloseButton`, 'Left');
expect( expect(
document.documentElement.getAttribute( document.documentElement.getAttribute(
`theme-${themeName}-tab-close-button` `theme-${themeName}-tab-close-button`
) )
).toBe('left') ).toBe('left');
}) });
it('allows the dock toggle buttons to be hidden via config', () => { it('allows the dock toggle buttons to be hidden via config', () => {
atom.config.set(`${themeName}.hideDockButtons`, true) atom.config.set(`${themeName}.hideDockButtons`, true);
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-dock-buttons`) document.documentElement.getAttribute(`theme-${themeName}-dock-buttons`)
).toBe('hidden') ).toBe('hidden');
}) });
it('allows the tree-view headers to be sticky via config', () => { it('allows the tree-view headers to be sticky via config', () => {
atom.config.set(`${themeName}.stickyHeaders`, true) atom.config.set(`${themeName}.stickyHeaders`, true);
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`)
).toBe('sticky') ).toBe('sticky');
}) });
it('allows the tree-view headers to not be sticky via config', () => { it('allows the tree-view headers to not be sticky via config', () => {
atom.config.set(`${themeName}.stickyHeaders`, false) atom.config.set(`${themeName}.stickyHeaders`, false);
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`)
).toBe(null) ).toBe(null);
}) });
}) });

View File

@ -1,88 +1,88 @@
const root = document.documentElement const root = document.documentElement;
const themeName = 'one-light-ui' const themeName = 'one-light-ui';
module.exports = { module.exports = {
activate (state) { activate(state) {
atom.config.observe(`${themeName}.fontSize`, setFontSize) atom.config.observe(`${themeName}.fontSize`, setFontSize);
atom.config.observe(`${themeName}.tabSizing`, setTabSizing) atom.config.observe(`${themeName}.tabSizing`, setTabSizing);
atom.config.observe(`${themeName}.tabCloseButton`, setTabCloseButton) atom.config.observe(`${themeName}.tabCloseButton`, setTabCloseButton);
atom.config.observe(`${themeName}.hideDockButtons`, setHideDockButtons) atom.config.observe(`${themeName}.hideDockButtons`, setHideDockButtons);
atom.config.observe(`${themeName}.stickyHeaders`, setStickyHeaders) atom.config.observe(`${themeName}.stickyHeaders`, setStickyHeaders);
// DEPRECATED: This can be removed at some point (added in Atom 1.17/1.18ish) // DEPRECATED: This can be removed at some point (added in Atom 1.17/1.18ish)
// It removes `layoutMode` // It removes `layoutMode`
if (atom.config.get(`${themeName}.layoutMode`)) { if (atom.config.get(`${themeName}.layoutMode`)) {
atom.config.unset(`${themeName}.layoutMode`) atom.config.unset(`${themeName}.layoutMode`);
} }
}, },
deactivate () { deactivate() {
unsetFontSize() unsetFontSize();
unsetTabSizing() unsetTabSizing();
unsetTabCloseButton() unsetTabCloseButton();
unsetHideDockButtons() unsetHideDockButtons();
unsetStickyHeaders() unsetStickyHeaders();
} }
} };
// Font Size ----------------------- // Font Size -----------------------
function setFontSize (currentFontSize) { function setFontSize(currentFontSize) {
root.style.fontSize = `${currentFontSize}px` root.style.fontSize = `${currentFontSize}px`;
} }
function unsetFontSize () { function unsetFontSize() {
root.style.fontSize = '' root.style.fontSize = '';
} }
// Tab Sizing ----------------------- // Tab Sizing -----------------------
function setTabSizing (tabSizing) { function setTabSizing(tabSizing) {
root.setAttribute(`theme-${themeName}-tabsizing`, tabSizing.toLowerCase()) root.setAttribute(`theme-${themeName}-tabsizing`, tabSizing.toLowerCase());
} }
function unsetTabSizing () { function unsetTabSizing() {
root.removeAttribute(`theme-${themeName}-tabsizing`) root.removeAttribute(`theme-${themeName}-tabsizing`);
} }
// Tab Close Button ----------------------- // Tab Close Button -----------------------
function setTabCloseButton (tabCloseButton) { function setTabCloseButton(tabCloseButton) {
if (tabCloseButton === 'Left') { if (tabCloseButton === 'Left') {
root.setAttribute(`theme-${themeName}-tab-close-button`, 'left') root.setAttribute(`theme-${themeName}-tab-close-button`, 'left');
} else { } else {
unsetTabCloseButton() unsetTabCloseButton();
} }
} }
function unsetTabCloseButton () { function unsetTabCloseButton() {
root.removeAttribute(`theme-${themeName}-tab-close-button`) root.removeAttribute(`theme-${themeName}-tab-close-button`);
} }
// Dock Buttons ----------------------- // Dock Buttons -----------------------
function setHideDockButtons (hideDockButtons) { function setHideDockButtons(hideDockButtons) {
if (hideDockButtons) { if (hideDockButtons) {
root.setAttribute(`theme-${themeName}-dock-buttons`, 'hidden') root.setAttribute(`theme-${themeName}-dock-buttons`, 'hidden');
} else { } else {
unsetHideDockButtons() unsetHideDockButtons();
} }
} }
function unsetHideDockButtons () { function unsetHideDockButtons() {
root.removeAttribute(`theme-${themeName}-dock-buttons`) root.removeAttribute(`theme-${themeName}-dock-buttons`);
} }
// Sticky Headers ----------------------- // Sticky Headers -----------------------
function setStickyHeaders (stickyHeaders) { function setStickyHeaders(stickyHeaders) {
if (stickyHeaders) { if (stickyHeaders) {
root.setAttribute(`theme-${themeName}-sticky-headers`, 'sticky') root.setAttribute(`theme-${themeName}-sticky-headers`, 'sticky');
} else { } else {
unsetStickyHeaders() unsetStickyHeaders();
} }
} }
function unsetStickyHeaders () { function unsetStickyHeaders() {
root.removeAttribute(`theme-${themeName}-sticky-headers`) root.removeAttribute(`theme-${themeName}-sticky-headers`);
} }

View File

@ -1,58 +1,58 @@
const themeName = 'one-light-ui' const themeName = 'one-light-ui';
describe(`${themeName} theme`, () => { describe(`${themeName} theme`, () => {
beforeEach(() => { beforeEach(() => {
waitsForPromise(() => atom.packages.activatePackage(themeName)) waitsForPromise(() => atom.packages.activatePackage(themeName));
}) });
it('allows the font size to be set via config', () => { it('allows the font size to be set via config', () => {
expect(document.documentElement.style.fontSize).toBe('12px') expect(document.documentElement.style.fontSize).toBe('12px');
atom.config.set(`${themeName}.fontSize`, '10') atom.config.set(`${themeName}.fontSize`, '10');
expect(document.documentElement.style.fontSize).toBe('10px') expect(document.documentElement.style.fontSize).toBe('10px');
}) });
it('allows the tab sizing to be set via config', () => { it('allows the tab sizing to be set via config', () => {
atom.config.set(`${themeName}.tabSizing`, 'Maximum') atom.config.set(`${themeName}.tabSizing`, 'Maximum');
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) document.documentElement.getAttribute(`theme-${themeName}-tabsizing`)
).toBe('maximum') ).toBe('maximum');
}) });
it('allows the tab sizing to be set via config', () => { it('allows the tab sizing to be set via config', () => {
atom.config.set(`${themeName}.tabSizing`, 'Minimum') atom.config.set(`${themeName}.tabSizing`, 'Minimum');
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) document.documentElement.getAttribute(`theme-${themeName}-tabsizing`)
).toBe('minimum') ).toBe('minimum');
}) });
it('allows the tab close button to be shown on the left via config', () => { it('allows the tab close button to be shown on the left via config', () => {
atom.config.set(`${themeName}.tabCloseButton`, 'Left') atom.config.set(`${themeName}.tabCloseButton`, 'Left');
expect( expect(
document.documentElement.getAttribute( document.documentElement.getAttribute(
`theme-${themeName}-tab-close-button` `theme-${themeName}-tab-close-button`
) )
).toBe('left') ).toBe('left');
}) });
it('allows the dock toggle buttons to be hidden via config', () => { it('allows the dock toggle buttons to be hidden via config', () => {
atom.config.set(`${themeName}.hideDockButtons`, true) atom.config.set(`${themeName}.hideDockButtons`, true);
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-dock-buttons`) document.documentElement.getAttribute(`theme-${themeName}-dock-buttons`)
).toBe('hidden') ).toBe('hidden');
}) });
it('allows the tree-view headers to be sticky via config', () => { it('allows the tree-view headers to be sticky via config', () => {
atom.config.set(`${themeName}.stickyHeaders`, true) atom.config.set(`${themeName}.stickyHeaders`, true);
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`)
).toBe('sticky') ).toBe('sticky');
}) });
it('allows the tree-view headers to not be sticky via config', () => { it('allows the tree-view headers to not be sticky via config', () => {
atom.config.set(`${themeName}.stickyHeaders`, false) atom.config.set(`${themeName}.stickyHeaders`, false);
expect( expect(
document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`)
).toBe(null) ).toBe(null);
}) });
}) });

View File

@ -1,9 +1,9 @@
var path = require('path'); var path = require('path');
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var atomCommandPath = path.resolve(__dirname, '..', '..', process.argv[2]); var atomCommandPath = path.resolve(__dirname, '..', '..', process.argv[2]);
var args = process.argv.slice(3); var args = process.argv.slice(3);
args.unshift('--executed-from', process.cwd()); args.unshift('--executed-from', process.cwd());
var options = {detached: true, stdio: 'ignore'}; var options = { detached: true, stdio: 'ignore' };
spawn(atomCommandPath, args, options); spawn(atomCommandPath, args, options);
process.exit(0); process.exit(0);

View File

@ -1,22 +1,23 @@
// This module exports paths, names, and other metadata that is referenced // This module exports paths, names, and other metadata that is referenced
// throughout the build. // throughout the build.
'use strict' 'use strict';
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
const spawnSync = require('./lib/spawn-sync') const spawnSync = require('./lib/spawn-sync');
const repositoryRootPath = path.resolve(__dirname, '..') const repositoryRootPath = path.resolve(__dirname, '..');
const apmRootPath = path.join(repositoryRootPath, 'apm') const apmRootPath = path.join(repositoryRootPath, 'apm');
const scriptRootPath = path.join(repositoryRootPath, 'script') const scriptRootPath = path.join(repositoryRootPath, 'script');
const buildOutputPath = path.join(repositoryRootPath, 'out') const buildOutputPath = path.join(repositoryRootPath, 'out');
const docsOutputPath = path.join(repositoryRootPath, 'docs', 'output') const docsOutputPath = path.join(repositoryRootPath, 'docs', 'output');
const intermediateAppPath = path.join(buildOutputPath, 'app') const intermediateAppPath = path.join(buildOutputPath, 'app');
const symbolsPath = path.join(buildOutputPath, 'symbols') const symbolsPath = path.join(buildOutputPath, 'symbols');
const electronDownloadPath = path.join(repositoryRootPath, 'electron') const electronDownloadPath = path.join(repositoryRootPath, 'electron');
const homeDirPath = process.env.HOME || process.env.USERPROFILE const homeDirPath = process.env.HOME || process.env.USERPROFILE;
const atomHomeDirPath = process.env.ATOM_HOME || path.join(homeDirPath, '.atom') const atomHomeDirPath =
process.env.ATOM_HOME || path.join(homeDirPath, '.atom');
const appMetadata = require(path.join(repositoryRootPath, 'package.json')) const appMetadata = require(path.join(repositoryRootPath, 'package.json'))
const apmMetadata = require(path.join(apmRootPath, 'package.json')) const apmMetadata = require(path.join(apmRootPath, 'package.json'))
@ -45,23 +46,24 @@ module.exports = {
getApmBinPath, getApmBinPath,
getNpmBinPath, getNpmBinPath,
snapshotAuxiliaryData: {} snapshotAuxiliaryData: {}
} };
function getChannel (version) { function getChannel(version) {
const match = version.match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/) const match = version.match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/);
if (!match) { if (!match) {
throw new Error(`Found incorrectly formatted Atom version ${version}`) throw new Error(`Found incorrectly formatted Atom version ${version}`);
} else if (match[2]) { } else if (match[2]) {
return match[2] return match[2];
} }
return 'stable' return 'stable';
} }
function getAppName (channel) { function getAppName(channel) {
return channel === 'stable' return channel === 'stable'
? 'Atom' ? 'Atom'
: `Atom ${process.env.ATOM_CHANNEL_DISPLAY_NAME || channel.charAt(0).toUpperCase() + channel.slice(1)}` : `Atom ${process.env.ATOM_CHANNEL_DISPLAY_NAME ||
channel.charAt(0).toUpperCase() + channel.slice(1)}`;
} }
function getExecutableName (channel, appName) { function getExecutableName (channel, appName) {
@ -76,22 +78,38 @@ function getExecutableName (channel, appName) {
function computeAppVersion (version) { function computeAppVersion (version) {
if (version.match(/-dev$/)) { if (version.match(/-dev$/)) {
const result = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {cwd: repositoryRootPath}) const result = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {
const commitHash = result.stdout.toString().trim() cwd: repositoryRootPath
version += '-' + commitHash });
const commitHash = result.stdout.toString().trim();
version += '-' + commitHash;
} }
return version return version;
} }
function getApmBinPath () { function getApmBinPath() {
const apmBinName = process.platform === 'win32' ? 'apm.cmd' : 'apm' const apmBinName = process.platform === 'win32' ? 'apm.cmd' : 'apm';
return path.join(apmRootPath, 'node_modules', 'atom-package-manager', 'bin', apmBinName) return path.join(
apmRootPath,
'node_modules',
'atom-package-manager',
'bin',
apmBinName
);
} }
function getNpmBinPath (external = false) { function getNpmBinPath(external = false) {
if (process.env.NPM_BIN_PATH) return process.env.NPM_BIN_PATH if (process.env.NPM_BIN_PATH) return process.env.NPM_BIN_PATH;
const npmBinName = process.platform === 'win32' ? 'npm.cmd' : 'npm' const npmBinName = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const localNpmBinPath = path.resolve(repositoryRootPath, 'script', 'node_modules', '.bin', npmBinName) const localNpmBinPath = path.resolve(
return !external && fs.existsSync(localNpmBinPath) ? localNpmBinPath : npmBinName repositoryRootPath,
'script',
'node_modules',
'.bin',
npmBinName
);
return !external && fs.existsSync(localNpmBinPath)
? localNpmBinPath
: npmBinName;
} }

View File

@ -1,41 +1,54 @@
const fs = require('fs-extra') const fs = require('fs-extra');
const path = require('path') const path = require('path');
module.exports = function (packagePath) { module.exports = function(packagePath) {
const nodeModulesPath = path.join(packagePath, 'node_modules') const nodeModulesPath = path.join(packagePath, 'node_modules');
const nodeModulesBackupPath = path.join(packagePath, 'node_modules.bak') const nodeModulesBackupPath = path.join(packagePath, 'node_modules.bak');
if (fs.existsSync(nodeModulesBackupPath)) { if (fs.existsSync(nodeModulesBackupPath)) {
throw new Error('Cannot back up ' + nodeModulesPath + '; ' + nodeModulesBackupPath + ' already exists') throw new Error(
'Cannot back up ' +
nodeModulesPath +
'; ' +
nodeModulesBackupPath +
' already exists'
);
} }
// some packages may have no node_modules after deduping, but we still want // some packages may have no node_modules after deduping, but we still want
// to "back-up" and later restore that fact // to "back-up" and later restore that fact
if (!fs.existsSync(nodeModulesPath)) { if (!fs.existsSync(nodeModulesPath)) {
const msg = 'Skipping backing up ' + nodeModulesPath + ' as it does not exist' const msg =
console.log(msg.gray) 'Skipping backing up ' + nodeModulesPath + ' as it does not exist';
console.log(msg.gray);
const restore = function stubRestoreNodeModules () { const restore = function stubRestoreNodeModules() {
if (fs.existsSync(nodeModulesPath)) { if (fs.existsSync(nodeModulesPath)) {
fs.removeSync(nodeModulesPath) fs.removeSync(nodeModulesPath);
} }
} };
return {restore, nodeModulesPath, nodeModulesBackupPath} return { restore, nodeModulesPath, nodeModulesBackupPath };
} }
fs.copySync(nodeModulesPath, nodeModulesBackupPath) fs.copySync(nodeModulesPath, nodeModulesBackupPath);
const restore = function restoreNodeModules () { const restore = function restoreNodeModules() {
if (!fs.existsSync(nodeModulesBackupPath)) { if (!fs.existsSync(nodeModulesBackupPath)) {
throw new Error('Cannot restore ' + nodeModulesPath + '; ' + nodeModulesBackupPath + ' does not exist') throw new Error(
'Cannot restore ' +
nodeModulesPath +
'; ' +
nodeModulesBackupPath +
' does not exist'
);
} }
if (fs.existsSync(nodeModulesPath)) { if (fs.existsSync(nodeModulesPath)) {
fs.removeSync(nodeModulesPath) fs.removeSync(nodeModulesPath);
} }
fs.renameSync(nodeModulesBackupPath, nodeModulesPath) fs.renameSync(nodeModulesBackupPath, nodeModulesPath);
} };
return {restore, nodeModulesPath, nodeModulesBackupPath} return { restore, nodeModulesPath, nodeModulesBackupPath };
} };

View File

@ -1,33 +1,45 @@
'use strict' 'use strict';
const buildMetadata = require('../package.json') const buildMetadata = require('../package.json');
const CONFIG = require('../config') const CONFIG = require('../config');
const semver = require('semver') const semver = require('semver');
module.exports = function () { module.exports = function() {
// Chromedriver should be specified as ^n.x where n matches the Electron major version // Chromedriver should be specified as ^n.x where n matches the Electron major version
const chromedriverVer = buildMetadata.dependencies['electron-chromedriver'] const chromedriverVer = buildMetadata.dependencies['electron-chromedriver'];
const mksnapshotVer = buildMetadata.dependencies['electron-mksnapshot'] const mksnapshotVer = buildMetadata.dependencies['electron-mksnapshot'];
// Always use caret on electron-chromedriver so that it can pick up the best minor/patch versions // Always use caret on electron-chromedriver so that it can pick up the best minor/patch versions
if (!chromedriverVer.startsWith('^')) { if (!chromedriverVer.startsWith('^')) {
throw new Error(`electron-chromedriver version in script/package.json should start with a caret to match latest patch version.`) throw new Error(
`electron-chromedriver version in script/package.json should start with a caret to match latest patch version.`
);
} }
if (!mksnapshotVer.startsWith('^')) { if (!mksnapshotVer.startsWith('^')) {
throw new Error(`electron-mksnapshot version in script/package.json should start with a caret to match latest patch version.`) throw new Error(
`electron-mksnapshot version in script/package.json should start with a caret to match latest patch version.`
);
} }
const electronVer = CONFIG.appMetadata.electronVersion const electronVer = CONFIG.appMetadata.electronVersion;
if (!semver.satisfies(electronVer, chromedriverVer)) { if (!semver.satisfies(electronVer, chromedriverVer)) {
throw new Error(`electron-chromedriver ${chromedriverVer} incompatible with electron ${electronVer}.\n` + throw new Error(
'Did you upgrade electron in package.json and forget to upgrade electron-chromedriver in ' + `electron-chromedriver ${chromedriverVer} incompatible with electron ${electronVer}.\n` +
`script/package.json to '~${semver.major(electronVer)}.${semver.minor(electronVer)}' ?`) 'Did you upgrade electron in package.json and forget to upgrade electron-chromedriver in ' +
`script/package.json to '~${semver.major(electronVer)}.${semver.minor(
electronVer
)}' ?`
);
} }
if (!semver.satisfies(electronVer, mksnapshotVer)) { if (!semver.satisfies(electronVer, mksnapshotVer)) {
throw new Error(`electron-mksnapshot ${mksnapshotVer} incompatible with electron ${electronVer}.\n` + throw new Error(
'Did you upgrade electron in package.json and forget to upgrade electron-mksnapshot in ' + `electron-mksnapshot ${mksnapshotVer} incompatible with electron ${electronVer}.\n` +
`script/package.json to '~${semver.major(electronVer)}.${semver.minor(electronVer)}' ?`) 'Did you upgrade electron in package.json and forget to upgrade electron-mksnapshot in ' +
`script/package.json to '~${semver.major(electronVer)}.${semver.minor(
electronVer
)}' ?`
);
} }
} };

View File

@ -1,12 +1,12 @@
'use strict' 'use strict';
const fs = require('fs-extra') const fs = require('fs-extra');
const os = require('os') const os = require('os');
const path = require('path') const path = require('path');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function () { module.exports = function() {
const cachePaths = [ const cachePaths = [
path.join(CONFIG.repositoryRootPath, 'electron'), path.join(CONFIG.repositoryRootPath, 'electron'),
path.join(CONFIG.atomHomeDirPath, '.node-gyp'), path.join(CONFIG.atomHomeDirPath, '.node-gyp'),
@ -19,10 +19,10 @@ module.exports = function () {
path.join(CONFIG.atomHomeDirPath, 'electron'), path.join(CONFIG.atomHomeDirPath, 'electron'),
path.join(os.tmpdir(), 'atom-build'), path.join(os.tmpdir(), 'atom-build'),
path.join(os.tmpdir(), 'atom-cached-atom-shells') path.join(os.tmpdir(), 'atom-cached-atom-shells')
] ];
for (let path of cachePaths) { for (let path of cachePaths) {
console.log(`Cleaning ${path}`) console.log(`Cleaning ${path}`);
fs.removeSync(path) fs.removeSync(path);
} }
} };

View File

@ -1,29 +1,42 @@
const path = require('path') const path = require('path');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function () { module.exports = function() {
// We can't require fs-extra or glob if `script/bootstrap` has never been run, // We can't require fs-extra or glob if `script/bootstrap` has never been run,
// because they are third party modules. This is okay because cleaning // because they are third party modules. This is okay because cleaning
// dependencies only makes sense if dependencies have been installed at least // dependencies only makes sense if dependencies have been installed at least
// once. // once.
const fs = require('fs-extra') const fs = require('fs-extra');
const glob = require('glob') const glob = require('glob');
const apmDependenciesPath = path.join(CONFIG.apmRootPath, 'node_modules') const apmDependenciesPath = path.join(CONFIG.apmRootPath, 'node_modules');
console.log(`Cleaning ${apmDependenciesPath}`) console.log(`Cleaning ${apmDependenciesPath}`);
fs.removeSync(apmDependenciesPath) fs.removeSync(apmDependenciesPath);
const atomDependenciesPath = path.join(CONFIG.repositoryRootPath, 'node_modules') const atomDependenciesPath = path.join(
console.log(`Cleaning ${atomDependenciesPath}`) CONFIG.repositoryRootPath,
fs.removeSync(atomDependenciesPath) 'node_modules'
);
console.log(`Cleaning ${atomDependenciesPath}`);
fs.removeSync(atomDependenciesPath);
const scriptDependenciesPath = path.join(CONFIG.scriptRootPath, 'node_modules') const scriptDependenciesPath = path.join(
console.log(`Cleaning ${scriptDependenciesPath}`) CONFIG.scriptRootPath,
fs.removeSync(scriptDependenciesPath) 'node_modules'
);
console.log(`Cleaning ${scriptDependenciesPath}`);
fs.removeSync(scriptDependenciesPath);
const bundledPackageDependenciesPaths = path.join(CONFIG.repositoryRootPath, 'packages', '**', 'node_modules') const bundledPackageDependenciesPaths = path.join(
for (const bundledPackageDependencyPath of glob.sync(bundledPackageDependenciesPaths)) { CONFIG.repositoryRootPath,
fs.removeSync(bundledPackageDependencyPath) 'packages',
'**',
'node_modules'
);
for (const bundledPackageDependencyPath of glob.sync(
bundledPackageDependenciesPaths
)) {
fs.removeSync(bundledPackageDependencyPath);
} }
} };

View File

@ -1,9 +1,9 @@
const fs = require('fs-extra') const fs = require('fs-extra');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function () { module.exports = function() {
if (fs.existsSync(CONFIG.buildOutputPath)) { if (fs.existsSync(CONFIG.buildOutputPath)) {
console.log(`Cleaning ${CONFIG.buildOutputPath}`) console.log(`Cleaning ${CONFIG.buildOutputPath}`);
fs.removeSync(CONFIG.buildOutputPath) fs.removeSync(CONFIG.buildOutputPath);
} }
} };

View File

@ -1,89 +1,143 @@
const downloadFileFromGithub = require('./download-file-from-github') const downloadFileFromGithub = require('./download-file-from-github');
const fs = require('fs-extra') const fs = require('fs-extra');
const os = require('os') const os = require('os');
const path = require('path') const path = require('path');
const spawnSync = require('./spawn-sync') const spawnSync = require('./spawn-sync');
module.exports = function (packagedAppPath) { module.exports = function(packagedAppPath) {
if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) { if (
console.log('Skipping code signing because the ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray) !process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL &&
return !process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH
) {
console.log(
'Skipping code signing because the ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'
.gray
);
return;
} }
let certPath = process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH let certPath = process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH;
if (!certPath) { if (!certPath) {
certPath = path.join(os.tmpdir(), 'mac.p12') certPath = path.join(os.tmpdir(), 'mac.p12');
downloadFileFromGithub(process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) downloadFileFromGithub(
process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL,
certPath
);
} }
try { try {
console.log(`Ensuring keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN} exists`) console.log(
`Ensuring keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN} exists`
);
try { try {
spawnSync('security', [ spawnSync(
'show-keychain-info', 'security',
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN ['show-keychain-info', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN],
], {stdio: 'inherit'}) { stdio: 'inherit' }
);
} catch (err) { } catch (err) {
console.log(`Creating keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`) console.log(
`Creating keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`
);
// The keychain doesn't exist, try to create it // The keychain doesn't exist, try to create it
spawnSync('security', [ spawnSync(
'create-keychain', 'security',
'-p', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD, [
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN 'create-keychain',
], {stdio: 'inherit'}) '-p',
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD,
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN
],
{ stdio: 'inherit' }
);
// List the keychain to "activate" it. Somehow this seems // List the keychain to "activate" it. Somehow this seems
// to be needed otherwise the signing operation fails // to be needed otherwise the signing operation fails
spawnSync('security', [ spawnSync(
'list-keychains', 'security',
'-s', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN ['list-keychains', '-s', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN],
], {stdio: 'inherit'}) { stdio: 'inherit' }
);
// Make sure it doesn't time out before we use it // Make sure it doesn't time out before we use it
spawnSync('security', [ spawnSync(
'set-keychain-settings', 'security',
'-t', '3600', [
'-u', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN 'set-keychain-settings',
], {stdio: 'inherit'}) '-t',
'3600',
'-u',
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN
],
{ stdio: 'inherit' }
);
} }
console.log(`Unlocking keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`) console.log(
const unlockArgs = ['unlock-keychain'] `Unlocking keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`
);
const unlockArgs = ['unlock-keychain'];
// For signing on local workstations, password could be entered interactively // For signing on local workstations, password could be entered interactively
if (process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD) { if (process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD) {
unlockArgs.push('-p', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD) unlockArgs.push(
'-p',
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD
);
} }
unlockArgs.push(process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN) unlockArgs.push(process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN);
spawnSync('security', unlockArgs, {stdio: 'inherit'}) spawnSync('security', unlockArgs, { stdio: 'inherit' });
console.log(`Importing certificate at ${certPath} into ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN} keychain`) console.log(
`Importing certificate at ${certPath} into ${
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN
} keychain`
);
spawnSync('security', [ spawnSync('security', [
'import', certPath, 'import',
'-P', process.env.ATOM_MAC_CODE_SIGNING_CERT_PASSWORD, certPath,
'-k', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN, '-P',
'-T', '/usr/bin/codesign' process.env.ATOM_MAC_CODE_SIGNING_CERT_PASSWORD,
]) '-k',
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN,
'-T',
'/usr/bin/codesign'
]);
console.log('Running incantation to suppress dialog when signing on macOS Sierra') console.log(
'Running incantation to suppress dialog when signing on macOS Sierra'
);
try { try {
spawnSync('security', [ spawnSync('security', [
'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', 'set-key-partition-list',
'-k', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD, '-S',
'apple-tool:,apple:',
'-s',
'-k',
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD,
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN
]) ]);
} catch (e) { } catch (e) {
console.log('Incantation failed... maybe this isn\'t Sierra?') console.log("Incantation failed... maybe this isn't Sierra?");
} }
console.log(`Code-signing application at ${packagedAppPath}`) console.log(`Code-signing application at ${packagedAppPath}`);
spawnSync('codesign', [ spawnSync(
'--deep', '--force', '--verbose', 'codesign',
'--keychain', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN, [
'--sign', 'Developer ID Application: GitHub', packagedAppPath '--deep',
], {stdio: 'inherit'}) '--force',
'--verbose',
'--keychain',
process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN,
'--sign',
'Developer ID Application: GitHub',
packagedAppPath
],
{ stdio: 'inherit' }
);
} finally { } finally {
if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) { if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) {
console.log(`Deleting certificate at ${certPath}`) console.log(`Deleting certificate at ${certPath}`);
fs.removeSync(certPath) fs.removeSync(certPath);
} }
} }
} };

View File

@ -1,33 +1,49 @@
const downloadFileFromGithub = require('./download-file-from-github') const downloadFileFromGithub = require('./download-file-from-github');
const fs = require('fs-extra') const fs = require('fs-extra');
const os = require('os') const os = require('os');
const path = require('path') const path = require('path');
const {spawnSync} = require('child_process') const { spawnSync } = require('child_process');
module.exports = function (filesToSign) { module.exports = function(filesToSign) {
if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { if (
console.log('Skipping code signing because the ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray) !process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL &&
return !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH
) {
console.log(
'Skipping code signing because the ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'
.gray
);
return;
} }
let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH;
if (!certPath) { if (!certPath) {
certPath = path.join(os.tmpdir(), 'win.p12') certPath = path.join(os.tmpdir(), 'win.p12');
downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) downloadFileFromGithub(
process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL,
certPath
);
} }
try { try {
for (const fileToSign of filesToSign) { for (const fileToSign of filesToSign) {
console.log(`Code-signing executable at ${fileToSign}`) console.log(`Code-signing executable at ${fileToSign}`);
signFile(fileToSign) signFile(fileToSign);
} }
} finally { } finally {
if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) {
fs.removeSync(certPath) fs.removeSync(certPath);
} }
} }
function signFile (fileToSign) { function signFile(fileToSign) {
const signCommand = path.resolve(__dirname, '..', 'node_modules', 'electron-winstaller', 'vendor', 'signtool.exe') const signCommand = path.resolve(
__dirname,
'..',
'node_modules',
'electron-winstaller',
'vendor',
'signtool.exe'
);
const args = [ const args = [
'sign', 'sign',
`/f ${certPath}`, // Signing cert file `/f ${certPath}`, // Signing cert file
@ -36,11 +52,20 @@ module.exports = function (filesToSign) {
'/tr http://timestamp.digicert.com', // Time stamp server '/tr http://timestamp.digicert.com', // Time stamp server
'/td sha256', // Times stamp algorithm '/td sha256', // Times stamp algorithm
`"${fileToSign}"` `"${fileToSign}"`
] ];
const result = spawnSync(signCommand, args, {stdio: 'inherit', shell: true}) const result = spawnSync(signCommand, args, {
stdio: 'inherit',
shell: true
});
if (result.status !== 0) { if (result.status !== 0) {
// Ensure we do not dump the signing password into the logs if something goes wrong // Ensure we do not dump the signing password into the logs if something goes wrong
throw new Error(`Command ${signCommand} ${args.map(a => a.replace(process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD, '******')).join(' ')} exited with code ${result.status}`) throw new Error(
`Command ${signCommand} ${args
.map(a =>
a.replace(process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD, '******')
)
.join(' ')} exited with code ${result.status}`
);
} }
} }
} };

View File

@ -1,56 +1,67 @@
'use strict' 'use strict';
const fs = require('fs-extra') const fs = require('fs-extra');
const path = require('path') const path = require('path');
const spawnSync = require('./spawn-sync') const spawnSync = require('./spawn-sync');
const { path7za } = require('7zip-bin') const { path7za } = require('7zip-bin');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function (packagedAppPath) { module.exports = function(packagedAppPath) {
const appArchivePath = path.join(CONFIG.buildOutputPath, getArchiveName()) const appArchivePath = path.join(CONFIG.buildOutputPath, getArchiveName());
compress(packagedAppPath, appArchivePath) compress(packagedAppPath, appArchivePath);
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const symbolsArchivePath = path.join(CONFIG.buildOutputPath, 'atom-mac-symbols.zip') const symbolsArchivePath = path.join(
compress(CONFIG.symbolsPath, symbolsArchivePath) CONFIG.buildOutputPath,
'atom-mac-symbols.zip'
);
compress(CONFIG.symbolsPath, symbolsArchivePath);
} }
} };
function getArchiveName () { function getArchiveName() {
switch (process.platform) { switch (process.platform) {
case 'darwin': return 'atom-mac.zip' case 'darwin':
case 'win32': return `atom-${process.arch === 'x64' ? 'x64-' : ''}windows.zip` return 'atom-mac.zip';
default: return `atom-${getLinuxArchiveArch()}.tar.gz` case 'win32':
return `atom-${process.arch === 'x64' ? 'x64-' : ''}windows.zip`;
default:
return `atom-${getLinuxArchiveArch()}.tar.gz`;
} }
} }
function getLinuxArchiveArch () { function getLinuxArchiveArch() {
switch (process.arch) { switch (process.arch) {
case 'ia32': return 'i386' case 'ia32':
case 'x64' : return 'amd64' return 'i386';
default: return process.arch case 'x64':
return 'amd64';
default:
return process.arch;
} }
} }
function compress (inputDirPath, outputArchivePath) { function compress(inputDirPath, outputArchivePath) {
if (fs.existsSync(outputArchivePath)) { if (fs.existsSync(outputArchivePath)) {
console.log(`Deleting "${outputArchivePath}"`) console.log(`Deleting "${outputArchivePath}"`);
fs.removeSync(outputArchivePath) fs.removeSync(outputArchivePath);
} }
console.log(`Compressing "${inputDirPath}" to "${outputArchivePath}"`) console.log(`Compressing "${inputDirPath}" to "${outputArchivePath}"`);
let compressCommand, compressArguments let compressCommand, compressArguments;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
compressCommand = 'zip' compressCommand = 'zip';
compressArguments = ['-r', '--symlinks'] compressArguments = ['-r', '--symlinks'];
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
compressCommand = path7za compressCommand = path7za;
compressArguments = ['a', '-r'] compressArguments = ['a', '-r'];
} else { } else {
compressCommand = 'tar' compressCommand = 'tar';
compressArguments = ['caf'] compressArguments = ['caf'];
} }
compressArguments.push(outputArchivePath, path.basename(inputDirPath)) compressArguments.push(outputArchivePath, path.basename(inputDirPath));
spawnSync(compressCommand, compressArguments, {cwd: path.dirname(inputDirPath)}) spawnSync(compressCommand, compressArguments, {
cwd: path.dirname(inputDirPath)
});
} }

View File

@ -1,16 +1,16 @@
// This module exports a function that copies all the static assets into the // This module exports a function that copies all the static assets into the
// appropriate location in the build output directory. // appropriate location in the build output directory.
'use strict' 'use strict';
const path = require('path') const path = require('path');
const fs = require('fs-extra') const fs = require('fs-extra');
const CONFIG = require('../config') const CONFIG = require('../config');
const glob = require('glob') const glob = require('glob');
const includePathInPackagedApp = require('./include-path-in-packaged-app') const includePathInPackagedApp = require('./include-path-in-packaged-app');
module.exports = function () { module.exports = function() {
console.log(`Copying assets to ${CONFIG.intermediateAppPath}`) console.log(`Copying assets to ${CONFIG.intermediateAppPath}`);
let srcPaths = [ let srcPaths = [
path.join(CONFIG.repositoryRootPath, 'benchmarks', 'benchmark-runner.js'), path.join(CONFIG.repositoryRootPath, 'benchmarks', 'benchmark-runner.js'),
path.join(CONFIG.repositoryRootPath, 'dot-atom'), path.join(CONFIG.repositoryRootPath, 'dot-atom'),
@ -19,10 +19,16 @@ module.exports = function () {
path.join(CONFIG.repositoryRootPath, 'static'), path.join(CONFIG.repositoryRootPath, 'static'),
path.join(CONFIG.repositoryRootPath, 'src'), path.join(CONFIG.repositoryRootPath, 'src'),
path.join(CONFIG.repositoryRootPath, 'vendor') path.join(CONFIG.repositoryRootPath, 'vendor')
] ];
srcPaths = srcPaths.concat(glob.sync(path.join(CONFIG.repositoryRootPath, 'spec', '*.*'), {ignore: path.join('**', '*-spec.*')})) srcPaths = srcPaths.concat(
glob.sync(path.join(CONFIG.repositoryRootPath, 'spec', '*.*'), {
ignore: path.join('**', '*-spec.*')
})
);
for (let srcPath of srcPaths) { for (let srcPath of srcPaths) {
fs.copySync(srcPath, computeDestinationPath(srcPath), {filter: includePathInPackagedApp}) fs.copySync(srcPath, computeDestinationPath(srcPath), {
filter: includePathInPackagedApp
});
} }
// Run a copy pass to dereference symlinked directories under node_modules. // Run a copy pass to dereference symlinked directories under node_modules.
@ -30,21 +36,37 @@ module.exports = function () {
// copied to the output folder correctly. We dereference only the top-level // copied to the output folder correctly. We dereference only the top-level
// symlinks and not nested symlinks to avoid issues where symlinked binaries // symlinks and not nested symlinks to avoid issues where symlinked binaries
// are duplicated in Atom's installation packages (see atom/atom#18490). // are duplicated in Atom's installation packages (see atom/atom#18490).
const nodeModulesPath = path.join(CONFIG.repositoryRootPath, 'node_modules') const nodeModulesPath = path.join(CONFIG.repositoryRootPath, 'node_modules');
glob.sync(path.join(nodeModulesPath, '*')) glob
.map(p => fs.lstatSync(p).isSymbolicLink() ? path.resolve(nodeModulesPath, fs.readlinkSync(p)) : p) .sync(path.join(nodeModulesPath, '*'))
.forEach(modulePath => { .map(p =>
const destPath = path.join(CONFIG.intermediateAppPath, 'node_modules', path.basename(modulePath)) fs.lstatSync(p).isSymbolicLink()
fs.copySync(modulePath, destPath, { filter: includePathInPackagedApp }) ? path.resolve(nodeModulesPath, fs.readlinkSync(p))
}) : p
)
.forEach(modulePath => {
const destPath = path.join(
CONFIG.intermediateAppPath,
'node_modules',
path.basename(modulePath)
);
fs.copySync(modulePath, destPath, { filter: includePathInPackagedApp });
});
fs.copySync( fs.copySync(
path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png', '1024.png'), path.join(
CONFIG.repositoryRootPath,
'resources',
'app-icons',
CONFIG.channel,
'png',
'1024.png'
),
path.join(CONFIG.intermediateAppPath, 'resources', 'atom.png') path.join(CONFIG.intermediateAppPath, 'resources', 'atom.png')
) );
} };
function computeDestinationPath (srcPath) { function computeDestinationPath(srcPath) {
const relativePath = path.relative(CONFIG.repositoryRootPath, srcPath) const relativePath = path.relative(CONFIG.repositoryRootPath, srcPath);
return path.join(CONFIG.intermediateAppPath, relativePath) return path.join(CONFIG.intermediateAppPath, relativePath);
} }

View File

@ -1,127 +1,224 @@
'use strict' 'use strict';
const fs = require('fs-extra') const fs = require('fs-extra');
const os = require('os') const os = require('os');
const path = require('path') const path = require('path');
const spawnSync = require('./spawn-sync') const spawnSync = require('./spawn-sync');
const template = require('lodash.template') const template = require('lodash.template');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function (packagedAppPath) { module.exports = function(packagedAppPath) {
console.log(`Creating Debian package for "${packagedAppPath}"`) console.log(`Creating Debian package for "${packagedAppPath}"`);
const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}` const atomExecutableName =
const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}` CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`;
const appDescription = CONFIG.appMetadata.description const apmExecutableName =
const appVersion = CONFIG.appMetadata.version CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}`;
let arch const appDescription = CONFIG.appMetadata.description;
const appVersion = CONFIG.appMetadata.version;
let arch;
if (process.arch === 'ia32') { if (process.arch === 'ia32') {
arch = 'i386' arch = 'i386';
} else if (process.arch === 'x64') { } else if (process.arch === 'x64') {
arch = 'amd64' arch = 'amd64';
} else if (process.arch === 'ppc') { } else if (process.arch === 'ppc') {
arch = 'powerpc' arch = 'powerpc';
} else { } else {
arch = process.arch arch = process.arch;
} }
const outputDebianPackageFilePath = path.join(CONFIG.buildOutputPath, `atom-${arch}.deb`) const outputDebianPackageFilePath = path.join(
const debianPackageDirPath = path.join(os.tmpdir(), path.basename(packagedAppPath)) CONFIG.buildOutputPath,
const debianPackageConfigPath = path.join(debianPackageDirPath, 'DEBIAN') `atom-${arch}.deb`
const debianPackageInstallDirPath = path.join(debianPackageDirPath, 'usr') );
const debianPackageBinDirPath = path.join(debianPackageInstallDirPath, 'bin') const debianPackageDirPath = path.join(
const debianPackageShareDirPath = path.join(debianPackageInstallDirPath, 'share') os.tmpdir(),
const debianPackageAtomDirPath = path.join(debianPackageShareDirPath, atomExecutableName) path.basename(packagedAppPath)
const debianPackageApplicationsDirPath = path.join(debianPackageShareDirPath, 'applications') );
const debianPackageIconsDirPath = path.join(debianPackageShareDirPath, 'pixmaps') const debianPackageConfigPath = path.join(debianPackageDirPath, 'DEBIAN');
const debianPackageLintianOverridesDirPath = path.join(debianPackageShareDirPath, 'lintian', 'overrides') const debianPackageInstallDirPath = path.join(debianPackageDirPath, 'usr');
const debianPackageDocsDirPath = path.join(debianPackageShareDirPath, 'doc', atomExecutableName) const debianPackageBinDirPath = path.join(debianPackageInstallDirPath, 'bin');
const debianPackageShareDirPath = path.join(
debianPackageInstallDirPath,
'share'
);
const debianPackageAtomDirPath = path.join(
debianPackageShareDirPath,
atomExecutableName
);
const debianPackageApplicationsDirPath = path.join(
debianPackageShareDirPath,
'applications'
);
const debianPackageIconsDirPath = path.join(
debianPackageShareDirPath,
'pixmaps'
);
const debianPackageLintianOverridesDirPath = path.join(
debianPackageShareDirPath,
'lintian',
'overrides'
);
const debianPackageDocsDirPath = path.join(
debianPackageShareDirPath,
'doc',
atomExecutableName
);
if (fs.existsSync(debianPackageDirPath)) { if (fs.existsSync(debianPackageDirPath)) {
console.log(`Deleting existing build dir for Debian package at "${debianPackageDirPath}"`) console.log(
fs.removeSync(debianPackageDirPath) `Deleting existing build dir for Debian package at "${debianPackageDirPath}"`
);
fs.removeSync(debianPackageDirPath);
} }
if (fs.existsSync(`${debianPackageDirPath}.deb`)) { if (fs.existsSync(`${debianPackageDirPath}.deb`)) {
console.log(`Deleting existing Debian package at "${debianPackageDirPath}.deb"`) console.log(
fs.removeSync(`${debianPackageDirPath}.deb`) `Deleting existing Debian package at "${debianPackageDirPath}.deb"`
);
fs.removeSync(`${debianPackageDirPath}.deb`);
} }
if (fs.existsSync(debianPackageDirPath)) { if (fs.existsSync(debianPackageDirPath)) {
console.log(`Deleting existing Debian package at "${outputDebianPackageFilePath}"`) console.log(
fs.removeSync(debianPackageDirPath) `Deleting existing Debian package at "${outputDebianPackageFilePath}"`
);
fs.removeSync(debianPackageDirPath);
} }
console.log(`Creating Debian package directory structure at "${debianPackageDirPath}"`) console.log(
fs.mkdirpSync(debianPackageDirPath) `Creating Debian package directory structure at "${debianPackageDirPath}"`
fs.mkdirpSync(debianPackageConfigPath) );
fs.mkdirpSync(debianPackageInstallDirPath) fs.mkdirpSync(debianPackageDirPath);
fs.mkdirpSync(debianPackageShareDirPath) fs.mkdirpSync(debianPackageConfigPath);
fs.mkdirpSync(debianPackageApplicationsDirPath) fs.mkdirpSync(debianPackageInstallDirPath);
fs.mkdirpSync(debianPackageIconsDirPath) fs.mkdirpSync(debianPackageShareDirPath);
fs.mkdirpSync(debianPackageLintianOverridesDirPath) fs.mkdirpSync(debianPackageApplicationsDirPath);
fs.mkdirpSync(debianPackageDocsDirPath) fs.mkdirpSync(debianPackageIconsDirPath);
fs.mkdirpSync(debianPackageBinDirPath) fs.mkdirpSync(debianPackageLintianOverridesDirPath);
fs.mkdirpSync(debianPackageDocsDirPath);
fs.mkdirpSync(debianPackageBinDirPath);
console.log(`Copying "${packagedAppPath}" to "${debianPackageAtomDirPath}"`) console.log(`Copying "${packagedAppPath}" to "${debianPackageAtomDirPath}"`);
fs.copySync(packagedAppPath, debianPackageAtomDirPath) fs.copySync(packagedAppPath, debianPackageAtomDirPath);
fs.chmodSync(debianPackageAtomDirPath, '755') fs.chmodSync(debianPackageAtomDirPath, '755');
console.log(`Copying binaries into "${debianPackageBinDirPath}"`) console.log(`Copying binaries into "${debianPackageBinDirPath}"`);
fs.copySync(path.join(CONFIG.repositoryRootPath, 'atom.sh'), path.join(debianPackageBinDirPath, atomExecutableName)) fs.copySync(
path.join(CONFIG.repositoryRootPath, 'atom.sh'),
path.join(debianPackageBinDirPath, atomExecutableName)
);
fs.symlinkSync( fs.symlinkSync(
path.join('..', 'share', atomExecutableName, 'resources', 'app', 'apm', 'node_modules', '.bin', 'apm'), path.join(
'..',
'share',
atomExecutableName,
'resources',
'app',
'apm',
'node_modules',
'.bin',
'apm'
),
path.join(debianPackageBinDirPath, apmExecutableName) path.join(debianPackageBinDirPath, apmExecutableName)
) );
console.log(`Writing control file into "${debianPackageConfigPath}"`) console.log(`Writing control file into "${debianPackageConfigPath}"`);
const packageSizeInKilobytes = spawnSync('du', ['-sk', packagedAppPath]).stdout.toString().split(/\s+/)[0] const packageSizeInKilobytes = spawnSync('du', ['-sk', packagedAppPath])
const controlFileTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'debian', 'control.in')) .stdout.toString()
.split(/\s+/)[0];
const controlFileTemplate = fs.readFileSync(
path.join(
CONFIG.repositoryRootPath,
'resources',
'linux',
'debian',
'control.in'
)
);
const controlFileContents = template(controlFileTemplate)({ const controlFileContents = template(controlFileTemplate)({
appFileName: atomExecutableName, appFileName: atomExecutableName,
version: appVersion, version: appVersion,
arch: arch, arch: arch,
installedSize: packageSizeInKilobytes, installedSize: packageSizeInKilobytes,
description: appDescription description: appDescription
}) });
fs.writeFileSync(path.join(debianPackageConfigPath, 'control'), controlFileContents) fs.writeFileSync(
path.join(debianPackageConfigPath, 'control'),
controlFileContents
);
console.log(`Writing desktop entry file into "${debianPackageApplicationsDirPath}"`) console.log(
const desktopEntryTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in')) `Writing desktop entry file into "${debianPackageApplicationsDirPath}"`
);
const desktopEntryTemplate = fs.readFileSync(
path.join(
CONFIG.repositoryRootPath,
'resources',
'linux',
'atom.desktop.in'
)
);
const desktopEntryContents = template(desktopEntryTemplate)({ const desktopEntryContents = template(desktopEntryTemplate)({
appName: CONFIG.appName, appName: CONFIG.appName,
appFileName: atomExecutableName, appFileName: atomExecutableName,
description: appDescription, description: appDescription,
installDir: '/usr', installDir: '/usr',
iconPath: atomExecutableName iconPath: atomExecutableName
}) });
fs.writeFileSync(path.join(debianPackageApplicationsDirPath, `${atomExecutableName}.desktop`), desktopEntryContents) fs.writeFileSync(
path.join(
debianPackageApplicationsDirPath,
`${atomExecutableName}.desktop`
),
desktopEntryContents
);
console.log(`Copying icon into "${debianPackageIconsDirPath}"`) console.log(`Copying icon into "${debianPackageIconsDirPath}"`);
fs.copySync( fs.copySync(
path.join(packagedAppPath, 'resources', 'app.asar.unpacked', 'resources', 'atom.png'), path.join(
packagedAppPath,
'resources',
'app.asar.unpacked',
'resources',
'atom.png'
),
path.join(debianPackageIconsDirPath, `${atomExecutableName}.png`) path.join(debianPackageIconsDirPath, `${atomExecutableName}.png`)
) );
console.log(`Copying license into "${debianPackageDocsDirPath}"`) console.log(`Copying license into "${debianPackageDocsDirPath}"`);
fs.copySync( fs.copySync(
path.join(packagedAppPath, 'resources', 'LICENSE.md'), path.join(packagedAppPath, 'resources', 'LICENSE.md'),
path.join(debianPackageDocsDirPath, 'copyright') path.join(debianPackageDocsDirPath, 'copyright')
) );
console.log(`Copying lintian overrides into "${debianPackageLintianOverridesDirPath}"`) console.log(
`Copying lintian overrides into "${debianPackageLintianOverridesDirPath}"`
);
fs.copySync( fs.copySync(
path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'debian', 'lintian-overrides'), path.join(
CONFIG.repositoryRootPath,
'resources',
'linux',
'debian',
'lintian-overrides'
),
path.join(debianPackageLintianOverridesDirPath, atomExecutableName) path.join(debianPackageLintianOverridesDirPath, atomExecutableName)
) );
console.log(`Copying polkit configuration into "${debianPackageShareDirPath}"`) console.log(
`Copying polkit configuration into "${debianPackageShareDirPath}"`
);
fs.copySync( fs.copySync(
path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.policy'), path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.policy'),
path.join(debianPackageShareDirPath, 'polkit-1', 'actions', 'atom.policy') path.join(debianPackageShareDirPath, 'polkit-1', 'actions', 'atom.policy')
) );
console.log(`Generating .deb file from ${debianPackageDirPath}`) console.log(`Generating .deb file from ${debianPackageDirPath}`);
spawnSync('fakeroot', ['dpkg-deb', '-b', debianPackageDirPath], {stdio: 'inherit'}) spawnSync('fakeroot', ['dpkg-deb', '-b', debianPackageDirPath], {
stdio: 'inherit'
});
console.log(`Copying generated package into "${outputDebianPackageFilePath}"`) console.log(
fs.copySync(`${debianPackageDirPath}.deb`, outputDebianPackageFilePath) `Copying generated package into "${outputDebianPackageFilePath}"`
} );
fs.copySync(`${debianPackageDirPath}.deb`, outputDebianPackageFilePath);
};

View File

@ -1,54 +1,79 @@
'use strict' 'use strict';
const assert = require('assert') const assert = require('assert');
const fs = require('fs-extra') const fs = require('fs-extra');
const path = require('path') const path = require('path');
const spawnSync = require('./spawn-sync') const spawnSync = require('./spawn-sync');
const template = require('lodash.template') const template = require('lodash.template');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function (packagedAppPath) { module.exports = function(packagedAppPath) {
console.log(`Creating rpm package for "${packagedAppPath}"`) console.log(`Creating rpm package for "${packagedAppPath}"`);
const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}` const atomExecutableName =
const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}` CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`;
const appName = CONFIG.appName const apmExecutableName =
const appDescription = CONFIG.appMetadata.description CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}`;
const appName = CONFIG.appName;
const appDescription = CONFIG.appMetadata.description;
// RPM versions can't have dashes or tildes in them. // RPM versions can't have dashes or tildes in them.
// (Ref.: https://twiki.cern.ch/twiki/bin/view/Main/RPMAndDebVersioning) // (Ref.: https://twiki.cern.ch/twiki/bin/view/Main/RPMAndDebVersioning)
const appVersion = CONFIG.appMetadata.version.replace(/-/g, '.') const appVersion = CONFIG.appMetadata.version.replace(/-/g, '.');
const rpmPackageDirPath = path.join(CONFIG.homeDirPath, 'rpmbuild') const rpmPackageDirPath = path.join(CONFIG.homeDirPath, 'rpmbuild');
const rpmPackageBuildDirPath = path.join(rpmPackageDirPath, 'BUILD') const rpmPackageBuildDirPath = path.join(rpmPackageDirPath, 'BUILD');
const rpmPackageSourcesDirPath = path.join(rpmPackageDirPath, 'SOURCES') const rpmPackageSourcesDirPath = path.join(rpmPackageDirPath, 'SOURCES');
const rpmPackageSpecsDirPath = path.join(rpmPackageDirPath, 'SPECS') const rpmPackageSpecsDirPath = path.join(rpmPackageDirPath, 'SPECS');
const rpmPackageRpmsDirPath = path.join(rpmPackageDirPath, 'RPMS') const rpmPackageRpmsDirPath = path.join(rpmPackageDirPath, 'RPMS');
const rpmPackageApplicationDirPath = path.join(rpmPackageBuildDirPath, appName) const rpmPackageApplicationDirPath = path.join(
const rpmPackageIconsDirPath = path.join(rpmPackageBuildDirPath, 'icons') rpmPackageBuildDirPath,
appName
);
const rpmPackageIconsDirPath = path.join(rpmPackageBuildDirPath, 'icons');
if (fs.existsSync(rpmPackageDirPath)) { if (fs.existsSync(rpmPackageDirPath)) {
console.log(`Deleting existing rpm build directory at "${rpmPackageDirPath}"`) console.log(
fs.removeSync(rpmPackageDirPath) `Deleting existing rpm build directory at "${rpmPackageDirPath}"`
);
fs.removeSync(rpmPackageDirPath);
} }
console.log(`Creating rpm package directory structure at "${rpmPackageDirPath}"`) console.log(
fs.mkdirpSync(rpmPackageDirPath) `Creating rpm package directory structure at "${rpmPackageDirPath}"`
fs.mkdirpSync(rpmPackageBuildDirPath) );
fs.mkdirpSync(rpmPackageSourcesDirPath) fs.mkdirpSync(rpmPackageDirPath);
fs.mkdirpSync(rpmPackageSpecsDirPath) fs.mkdirpSync(rpmPackageBuildDirPath);
fs.mkdirpSync(rpmPackageSourcesDirPath);
fs.mkdirpSync(rpmPackageSpecsDirPath);
console.log(`Copying "${packagedAppPath}" to "${rpmPackageApplicationDirPath}"`) console.log(
fs.copySync(packagedAppPath, rpmPackageApplicationDirPath) `Copying "${packagedAppPath}" to "${rpmPackageApplicationDirPath}"`
);
fs.copySync(packagedAppPath, rpmPackageApplicationDirPath);
console.log(`Copying icons into "${rpmPackageIconsDirPath}"`) console.log(`Copying icons into "${rpmPackageIconsDirPath}"`);
fs.copySync( fs.copySync(
path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png'), path.join(
CONFIG.repositoryRootPath,
'resources',
'app-icons',
CONFIG.channel,
'png'
),
rpmPackageIconsDirPath rpmPackageIconsDirPath
) );
console.log(`Writing rpm package spec file into "${rpmPackageSpecsDirPath}"`) console.log(`Writing rpm package spec file into "${rpmPackageSpecsDirPath}"`);
const rpmPackageSpecFilePath = path.join(rpmPackageSpecsDirPath, 'atom.spec') const rpmPackageSpecFilePath = path.join(rpmPackageSpecsDirPath, 'atom.spec');
const rpmPackageSpecsTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'redhat', 'atom.spec.in')) const rpmPackageSpecsTemplate = fs.readFileSync(
path.join(
CONFIG.repositoryRootPath,
'resources',
'linux',
'redhat',
'atom.spec.in'
)
);
const rpmPackageSpecsContents = template(rpmPackageSpecsTemplate)({ const rpmPackageSpecsContents = template(rpmPackageSpecsTemplate)({
appName: appName, appName: appName,
appFileName: atomExecutableName, appFileName: atomExecutableName,
@ -56,41 +81,65 @@ module.exports = function (packagedAppPath) {
description: appDescription, description: appDescription,
installDir: '/usr', installDir: '/usr',
version: appVersion version: appVersion
}) });
fs.writeFileSync(rpmPackageSpecFilePath, rpmPackageSpecsContents) fs.writeFileSync(rpmPackageSpecFilePath, rpmPackageSpecsContents);
console.log(`Writing desktop entry file into "${rpmPackageBuildDirPath}"`) console.log(`Writing desktop entry file into "${rpmPackageBuildDirPath}"`);
const desktopEntryTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in')) const desktopEntryTemplate = fs.readFileSync(
path.join(
CONFIG.repositoryRootPath,
'resources',
'linux',
'atom.desktop.in'
)
);
const desktopEntryContents = template(desktopEntryTemplate)({ const desktopEntryContents = template(desktopEntryTemplate)({
appName: appName, appName: appName,
appFileName: atomExecutableName, appFileName: atomExecutableName,
description: appDescription, description: appDescription,
installDir: '/usr', installDir: '/usr',
iconPath: atomExecutableName iconPath: atomExecutableName
}) });
fs.writeFileSync(path.join(rpmPackageBuildDirPath, `${atomExecutableName}.desktop`), desktopEntryContents) fs.writeFileSync(
path.join(rpmPackageBuildDirPath, `${atomExecutableName}.desktop`),
desktopEntryContents
);
console.log(`Copying atom.sh into "${rpmPackageBuildDirPath}"`) console.log(`Copying atom.sh into "${rpmPackageBuildDirPath}"`);
fs.copySync( fs.copySync(
path.join(CONFIG.repositoryRootPath, 'atom.sh'), path.join(CONFIG.repositoryRootPath, 'atom.sh'),
path.join(rpmPackageBuildDirPath, 'atom.sh') path.join(rpmPackageBuildDirPath, 'atom.sh')
) );
console.log(`Copying atom.policy into "${rpmPackageBuildDirPath}"`) console.log(`Copying atom.policy into "${rpmPackageBuildDirPath}"`);
fs.copySync( fs.copySync(
path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.policy'), path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.policy'),
path.join(rpmPackageBuildDirPath, 'atom.policy') path.join(rpmPackageBuildDirPath, 'atom.policy')
) );
console.log(`Generating .rpm package from "${rpmPackageDirPath}"`) console.log(`Generating .rpm package from "${rpmPackageDirPath}"`);
spawnSync('rpmbuild', ['-ba', '--clean', rpmPackageSpecFilePath]) spawnSync('rpmbuild', ['-ba', '--clean', rpmPackageSpecFilePath]);
for (let generatedArch of fs.readdirSync(rpmPackageRpmsDirPath)) { for (let generatedArch of fs.readdirSync(rpmPackageRpmsDirPath)) {
const generatedArchDirPath = path.join(rpmPackageRpmsDirPath, generatedArch) const generatedArchDirPath = path.join(
const generatedPackageFileNames = fs.readdirSync(generatedArchDirPath) rpmPackageRpmsDirPath,
assert(generatedPackageFileNames.length === 1, 'Generated more than one rpm package') generatedArch
const generatedPackageFilePath = path.join(generatedArchDirPath, generatedPackageFileNames[0]) );
const outputRpmPackageFilePath = path.join(CONFIG.buildOutputPath, `atom.${generatedArch}.rpm`) const generatedPackageFileNames = fs.readdirSync(generatedArchDirPath);
console.log(`Copying "${generatedPackageFilePath}" into "${outputRpmPackageFilePath}"`) assert(
fs.copySync(generatedPackageFilePath, outputRpmPackageFilePath) generatedPackageFileNames.length === 1,
'Generated more than one rpm package'
);
const generatedPackageFilePath = path.join(
generatedArchDirPath,
generatedPackageFileNames[0]
);
const outputRpmPackageFilePath = path.join(
CONFIG.buildOutputPath,
`atom.${generatedArch}.rpm`
);
console.log(
`Copying "${generatedPackageFilePath}" into "${outputRpmPackageFilePath}"`
);
fs.copySync(generatedPackageFilePath, outputRpmPackageFilePath);
} }
} };

View File

@ -1,40 +1,58 @@
'use strict' 'use strict';
const electronInstaller = require('electron-winstaller') const electronInstaller = require('electron-winstaller');
const fs = require('fs') const fs = require('fs');
const glob = require('glob') const glob = require('glob');
const path = require('path') const path = require('path');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = (packagedAppPath) => { module.exports = packagedAppPath => {
const archSuffix = process.arch === 'ia32' ? '' : '-' + process.arch const archSuffix = process.arch === 'ia32' ? '' : '-' + process.arch;
const updateUrlPrefix = process.env.ATOM_UPDATE_URL_PREFIX || 'https://atom.io' const updateUrlPrefix =
process.env.ATOM_UPDATE_URL_PREFIX || 'https://atom.io';
const options = { const options = {
title: CONFIG.appName, title: CONFIG.appName,
appDirectory: packagedAppPath, appDirectory: packagedAppPath,
authors: 'GitHub Inc.', authors: 'GitHub Inc.',
iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${CONFIG.channel}/atom.ico`, iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${
loadingGif: path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif'), CONFIG.channel
}/atom.ico`,
loadingGif: path.join(
CONFIG.repositoryRootPath,
'resources',
'win',
'loading.gif'
),
outputDirectory: CONFIG.buildOutputPath, outputDirectory: CONFIG.buildOutputPath,
noMsi: true, noMsi: true,
noDelta: CONFIG.channel === 'nightly', // Delta packages are broken for nightly versions past nightly9 due to Squirrel/NuGet limitations noDelta: CONFIG.channel === 'nightly', // Delta packages are broken for nightly versions past nightly9 due to Squirrel/NuGet limitations
remoteReleases: `${updateUrlPrefix}/api/updates${archSuffix}?version=${CONFIG.computedAppVersion}`, remoteReleases: `${updateUrlPrefix}/api/updates${archSuffix}?version=${
CONFIG.computedAppVersion
}`,
setupExe: `AtomSetup${process.arch === 'x64' ? '-x64' : ''}.exe`, setupExe: `AtomSetup${process.arch === 'x64' ? '-x64' : ''}.exe`,
setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico') setupIcon: path.join(
} CONFIG.repositoryRootPath,
'resources',
'app-icons',
CONFIG.channel,
'atom.ico'
)
};
const cleanUp = () => { const cleanUp = () => {
const releasesPath = `${CONFIG.buildOutputPath}/RELEASES` const releasesPath = `${CONFIG.buildOutputPath}/RELEASES`;
if (process.arch === 'x64' && fs.existsSync(releasesPath)) { if (process.arch === 'x64' && fs.existsSync(releasesPath)) {
fs.renameSync(releasesPath, `${releasesPath}-x64`) fs.renameSync(releasesPath, `${releasesPath}-x64`);
} }
let appName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}` let appName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`
for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/${appName}-*.nupkg`)) { for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/${appName}-*.nupkg`)) {
if (!nupkgPath.includes(CONFIG.computedAppVersion)) { if (!nupkgPath.includes(CONFIG.computedAppVersion)) {
console.log(`Deleting downloaded nupkg for previous version at ${nupkgPath} to prevent it from being stored as an artifact`) console.log(
fs.unlinkSync(nupkgPath) `Deleting downloaded nupkg for previous version at ${nupkgPath} to prevent it from being stored as an artifact`
);
fs.unlinkSync(nupkgPath);
} else { } else {
if (process.arch === 'x64') { if (process.arch === 'x64') {
// Use the original .nupkg filename to generate the `atom-x64` name by inserting `-x64` after `atom` // Use the original .nupkg filename to generate the `atom-x64` name by inserting `-x64` after `atom`
@ -44,13 +62,14 @@ module.exports = (packagedAppPath) => {
} }
} }
return `${CONFIG.buildOutputPath}/${options.setupExe}` return `${CONFIG.buildOutputPath}/${options.setupExe}`;
} };
console.log(`Creating Windows Installer for ${packagedAppPath}`) console.log(`Creating Windows Installer for ${packagedAppPath}`);
return electronInstaller.createWindowsInstaller(options) return electronInstaller
.createWindowsInstaller(options)
.then(cleanUp, error => { .then(cleanUp, error => {
cleanUp() cleanUp();
return Promise.reject(error) return Promise.reject(error);
}) });
} };

View File

@ -1,19 +1,22 @@
'use strict' 'use strict';
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
module.exports = function () { module.exports = function() {
process.env['PATH'] = process.env['PATH'] = process.env['PATH']
process.env['PATH'] .split(';')
.split(';') .filter(function(p) {
.filter(function (p) { if (fs.existsSync(path.join(p, 'msbuild.exe'))) {
if (fs.existsSync(path.join(p, 'msbuild.exe'))) { console.log(
console.log('Excluding "' + p + '" from PATH to avoid msbuild.exe mismatch that causes errors during module installation') 'Excluding "' +
return false p +
} else { '" from PATH to avoid msbuild.exe mismatch that causes errors during module installation'
return true );
} return false;
}) } else {
.join(';') return true;
} }
})
.join(';');
};

View File

@ -1,28 +1,49 @@
const crypto = require('crypto') const crypto = require('crypto');
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
const CONFIG = require('../config') const CONFIG = require('../config');
const FINGERPRINT_PATH = path.join(CONFIG.repositoryRootPath, 'node_modules', '.dependencies-fingerprint') const FINGERPRINT_PATH = path.join(
CONFIG.repositoryRootPath,
'node_modules',
'.dependencies-fingerprint'
);
module.exports = { module.exports = {
write: function () { write: function() {
const fingerprint = this.compute() const fingerprint = this.compute();
fs.writeFileSync(FINGERPRINT_PATH, fingerprint) fs.writeFileSync(FINGERPRINT_PATH, fingerprint);
console.log('Wrote Dependencies Fingerprint:', FINGERPRINT_PATH, fingerprint) console.log(
'Wrote Dependencies Fingerprint:',
FINGERPRINT_PATH,
fingerprint
);
}, },
read: function () { read: function() {
return fs.existsSync(FINGERPRINT_PATH) ? fs.readFileSync(FINGERPRINT_PATH, 'utf8') : null return fs.existsSync(FINGERPRINT_PATH)
? fs.readFileSync(FINGERPRINT_PATH, 'utf8')
: null;
}, },
isOutdated: function () { isOutdated: function() {
const fingerprint = this.read() const fingerprint = this.read();
return fingerprint ? fingerprint !== this.compute() : false return fingerprint ? fingerprint !== this.compute() : false;
}, },
compute: function () { compute: function() {
// Include the electron minor version in the fingerprint since that changing requires a re-install // Include the electron minor version in the fingerprint since that changing requires a re-install
const electronVersion = CONFIG.appMetadata.electronVersion.replace(/\.\d+$/, '') const electronVersion = CONFIG.appMetadata.electronVersion.replace(
const apmVersion = CONFIG.apmMetadata.dependencies['atom-package-manager'] /\.\d+$/,
const body = electronVersion + apmVersion + process.platform + process.version + process.arch ''
return crypto.createHash('sha1').update(body).digest('hex') );
const apmVersion = CONFIG.apmMetadata.dependencies['atom-package-manager'];
const body =
electronVersion +
apmVersion +
process.platform +
process.version +
process.arch;
return crypto
.createHash('sha1')
.update(body)
.digest('hex');
} }
} };

View File

@ -1,19 +1,24 @@
'use strict' 'use strict';
const fs = require('fs-extra') const fs = require('fs-extra');
const path = require('path') const path = require('path');
const syncRequest = require('sync-request') const syncRequest = require('sync-request');
module.exports = function (downloadURL, destinationPath) { module.exports = function(downloadURL, destinationPath) {
console.log(`Downloading file from GitHub Repository to ${destinationPath}`) console.log(`Downloading file from GitHub Repository to ${destinationPath}`);
const response = syncRequest('GET', downloadURL, { const response = syncRequest('GET', downloadURL, {
'headers': {'Accept': 'application/vnd.github.v3.raw', 'User-Agent': 'Atom Build'} headers: {
}) Accept: 'application/vnd.github.v3.raw',
'User-Agent': 'Atom Build'
}
});
if (response.statusCode === 200) { if (response.statusCode === 200) {
fs.mkdirpSync(path.dirname(destinationPath)) fs.mkdirpSync(path.dirname(destinationPath));
fs.writeFileSync(destinationPath, response.body) fs.writeFileSync(destinationPath, response.body);
} else { } else {
throw new Error('Error downloading file. HTTP Status ' + response.statusCode + '.') throw new Error(
'Error downloading file. HTTP Status ' + response.statusCode + '.'
);
} }
} };

View File

@ -1,44 +1,55 @@
'use strict' 'use strict';
const fs = require('fs-extra') const fs = require('fs-extra');
const glob = require('glob') const glob = require('glob');
const path = require('path') const path = require('path');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function () { module.exports = function() {
if (process.platform === 'win32') { if (process.platform === 'win32') {
console.log('Skipping symbol dumping because minidump is not supported on Windows'.gray) console.log(
return Promise.resolve() 'Skipping symbol dumping because minidump is not supported on Windows'
.gray
);
return Promise.resolve();
} else { } else {
console.log(`Dumping symbols in ${CONFIG.symbolsPath}`) console.log(`Dumping symbols in ${CONFIG.symbolsPath}`);
const binaryPaths = glob.sync(path.join(CONFIG.intermediateAppPath, 'node_modules', '**', '*.node')) const binaryPaths = glob.sync(
return Promise.all(binaryPaths.map(dumpSymbol)) path.join(CONFIG.intermediateAppPath, 'node_modules', '**', '*.node')
);
return Promise.all(binaryPaths.map(dumpSymbol));
} }
} };
function dumpSymbol (binaryPath) { function dumpSymbol(binaryPath) {
const minidump = require('minidump') const minidump = require('minidump');
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
minidump.dumpSymbol(binaryPath, function (error, content) { minidump.dumpSymbol(binaryPath, function(error, content) {
if (error) { if (error) {
console.error(error) console.error(error);
throw new Error(error) throw new Error(error);
} else { } else {
const moduleLine = /MODULE [^ ]+ [^ ]+ ([0-9A-F]+) (.*)\n/.exec(content) const moduleLine = /MODULE [^ ]+ [^ ]+ ([0-9A-F]+) (.*)\n/.exec(
content
);
if (moduleLine.length !== 3) { if (moduleLine.length !== 3) {
const errorMessage = `Invalid output when dumping symbol for ${binaryPath}` const errorMessage = `Invalid output when dumping symbol for ${binaryPath}`;
console.error(errorMessage) console.error(errorMessage);
throw new Error(errorMessage) throw new Error(errorMessage);
} else { } else {
const filename = moduleLine[2] const filename = moduleLine[2];
const symbolDirPath = path.join(CONFIG.symbolsPath, filename, moduleLine[1]) const symbolDirPath = path.join(
const symbolFilePath = path.join(symbolDirPath, `${filename}.sym`) CONFIG.symbolsPath,
fs.mkdirpSync(symbolDirPath) filename,
fs.writeFileSync(symbolFilePath, content) moduleLine[1]
resolve() );
const symbolFilePath = path.join(symbolDirPath, `${filename}.sym`);
fs.mkdirpSync(symbolDirPath);
fs.writeFileSync(symbolFilePath, content);
resolve();
} }
} }
}) });
}) });
} }

View File

@ -1,19 +1,21 @@
'use strict' 'use strict';
const glob = require('glob') const glob = require('glob');
module.exports = function (globPaths) { module.exports = function(globPaths) {
return Promise.all(globPaths.map(g => expandGlobPath(g))).then(paths => paths.reduce((a, b) => a.concat(b), [])) return Promise.all(globPaths.map(g => expandGlobPath(g))).then(paths =>
} paths.reduce((a, b) => a.concat(b), [])
);
};
function expandGlobPath (globPath) { function expandGlobPath(globPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
glob(globPath, (error, paths) => { glob(globPath, (error, paths) => {
if (error) { if (error) {
reject(error) reject(error);
} else { } else {
resolve(paths) resolve(paths);
} }
}) });
}) });
} }

View File

@ -1,53 +1,55 @@
'use strict' 'use strict';
const donna = require('donna') const donna = require('donna');
const tello = require('tello') const tello = require('tello');
const joanna = require('joanna') const joanna = require('joanna');
const glob = require('glob') const glob = require('glob');
const fs = require('fs-extra') const fs = require('fs-extra');
const path = require('path') const path = require('path');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function () { module.exports = function() {
const generatedJSONPath = path.join(CONFIG.docsOutputPath, 'atom-api.json') const generatedJSONPath = path.join(CONFIG.docsOutputPath, 'atom-api.json');
console.log(`Generating API docs at ${generatedJSONPath}`) console.log(`Generating API docs at ${generatedJSONPath}`);
// Unfortunately, correct relative paths depend on a specific working // Unfortunately, correct relative paths depend on a specific working
// directory, but this script should be able to run from anywhere, so we // directory, but this script should be able to run from anywhere, so we
// muck with the cwd temporarily. // muck with the cwd temporarily.
const oldWorkingDirectoryPath = process.cwd() const oldWorkingDirectoryPath = process.cwd();
process.chdir(CONFIG.repositoryRootPath) process.chdir(CONFIG.repositoryRootPath);
const coffeeMetadata = donna.generateMetadata(['.'])[0] const coffeeMetadata = donna.generateMetadata(['.'])[0];
const jsMetadata = joanna(glob.sync(`src/**/*.js`)) const jsMetadata = joanna(glob.sync(`src/**/*.js`));
process.chdir(oldWorkingDirectoryPath) process.chdir(oldWorkingDirectoryPath);
const metadata = { const metadata = {
repository: coffeeMetadata.repository, repository: coffeeMetadata.repository,
version: coffeeMetadata.version, version: coffeeMetadata.version,
files: Object.assign(coffeeMetadata.files, jsMetadata.files) files: Object.assign(coffeeMetadata.files, jsMetadata.files)
};
const api = tello.digest([metadata]);
Object.assign(api.classes, getAPIDocsForDependencies());
api.classes = sortObjectByKey(api.classes);
fs.mkdirpSync(CONFIG.docsOutputPath);
fs.writeFileSync(generatedJSONPath, JSON.stringify(api, null, 2));
};
function getAPIDocsForDependencies() {
const classes = {};
for (let apiJSONPath of glob.sync(
`${CONFIG.repositoryRootPath}/node_modules/*/api.json`
)) {
Object.assign(classes, require(apiJSONPath).classes);
} }
return classes;
const api = tello.digest([metadata])
Object.assign(api.classes, getAPIDocsForDependencies())
api.classes = sortObjectByKey(api.classes)
fs.mkdirpSync(CONFIG.docsOutputPath)
fs.writeFileSync(generatedJSONPath, JSON.stringify(api, null, 2))
} }
function getAPIDocsForDependencies () { function sortObjectByKey(object) {
const classes = {} const sortedObject = {};
for (let apiJSONPath of glob.sync(`${CONFIG.repositoryRootPath}/node_modules/*/api.json`)) {
Object.assign(classes, require(apiJSONPath).classes)
}
return classes
}
function sortObjectByKey (object) {
const sortedObject = {}
for (let keyName of Object.keys(object).sort()) { for (let keyName of Object.keys(object).sort()) {
sortedObject[keyName] = object[keyName] sortedObject[keyName] = object[keyName];
} }
return sortedObject return sortedObject;
} }

View File

@ -29,146 +29,255 @@ module.exports = function () {
fs.writeFileSync(path.join(CONFIG.intermediateAppPath, 'package.json'), JSON.stringify(CONFIG.appMetadata)) fs.writeFileSync(path.join(CONFIG.intermediateAppPath, 'package.json'), JSON.stringify(CONFIG.appMetadata))
} }
function buildBundledPackagesMetadata () { module.exports = function() {
const packages = {} console.log(
`Generating metadata for ${path.join(
CONFIG.intermediateAppPath,
'package.json'
)}`
);
CONFIG.appMetadata._atomPackages = buildBundledPackagesMetadata();
CONFIG.appMetadata._atomMenu = buildPlatformMenuMetadata();
CONFIG.appMetadata._atomKeymaps = buildPlatformKeymapsMetadata();
CONFIG.appMetadata._deprecatedPackages = deprecatedPackagesMetadata;
CONFIG.appMetadata.version = CONFIG.computedAppVersion;
checkDeprecatedPackagesMetadata();
fs.writeFileSync(
path.join(CONFIG.intermediateAppPath, 'package.json'),
JSON.stringify(CONFIG.appMetadata)
);
};
function buildBundledPackagesMetadata() {
const packages = {};
for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) {
const packagePath = path.join(CONFIG.intermediateAppPath, 'node_modules', packageName) const packagePath = path.join(
const packageMetadataPath = path.join(packagePath, 'package.json') CONFIG.intermediateAppPath,
const packageMetadata = JSON.parse(fs.readFileSync(packageMetadataPath, 'utf8')) 'node_modules',
normalizePackageData(packageMetadata, (msg) => { packageName
if (!msg.match(/No README data$/)) { );
console.warn(`Invalid package metadata. ${packageMetadata.name}: ${msg}`) const packageMetadataPath = path.join(packagePath, 'package.json');
} const packageMetadata = JSON.parse(
}, true) fs.readFileSync(packageMetadataPath, 'utf8')
if (packageMetadata.repository && packageMetadata.repository.url && packageMetadata.repository.type === 'git') { );
packageMetadata.repository.url = packageMetadata.repository.url.replace(/^git\+/, '') normalizePackageData(
packageMetadata,
msg => {
if (!msg.match(/No README data$/)) {
console.warn(
`Invalid package metadata. ${packageMetadata.name}: ${msg}`
);
}
},
true
);
if (
packageMetadata.repository &&
packageMetadata.repository.url &&
packageMetadata.repository.type === 'git'
) {
packageMetadata.repository.url = packageMetadata.repository.url.replace(
/^git\+/,
''
);
} }
delete packageMetadata['_from'] delete packageMetadata['_from'];
delete packageMetadata['_id'] delete packageMetadata['_id'];
delete packageMetadata['dist'] delete packageMetadata['dist'];
delete packageMetadata['readme'] delete packageMetadata['readme'];
delete packageMetadata['readmeFilename'] delete packageMetadata['readmeFilename'];
const packageModuleCache = packageMetadata._atomModuleCache || {} const packageModuleCache = packageMetadata._atomModuleCache || {};
if (packageModuleCache.extensions && packageModuleCache.extensions['.json']) { if (
const index = packageModuleCache.extensions['.json'].indexOf('package.json') packageModuleCache.extensions &&
packageModuleCache.extensions['.json']
) {
const index = packageModuleCache.extensions['.json'].indexOf(
'package.json'
);
if (index !== -1) { if (index !== -1) {
packageModuleCache.extensions['.json'].splice(index, 1) packageModuleCache.extensions['.json'].splice(index, 1);
} }
} }
const packageNewMetadata = {metadata: packageMetadata, keymaps: {}, menus: {}, grammarPaths: [], settings: {}} const packageNewMetadata = {
metadata: packageMetadata,
keymaps: {},
menus: {},
grammarPaths: [],
settings: {}
};
packageNewMetadata.rootDirPath = path.relative(CONFIG.intermediateAppPath, packagePath) packageNewMetadata.rootDirPath = path.relative(
CONFIG.intermediateAppPath,
packagePath
);
if (packageMetadata.main) { if (packageMetadata.main) {
const mainPath = require.resolve(path.resolve(packagePath, packageMetadata.main)) const mainPath = require.resolve(
packageNewMetadata.main = path.relative(path.join(CONFIG.intermediateAppPath, 'static'), mainPath) path.resolve(packagePath, packageMetadata.main)
);
packageNewMetadata.main = path.relative(
path.join(CONFIG.intermediateAppPath, 'static'),
mainPath
);
// Convert backward slashes to forward slashes in order to allow package // Convert backward slashes to forward slashes in order to allow package
// main modules to be required from the snapshot. This is because we use // main modules to be required from the snapshot. This is because we use
// forward slashes to cache the sources in the snapshot, so we need to use // forward slashes to cache the sources in the snapshot, so we need to use
// them here as well. // them here as well.
packageNewMetadata.main = packageNewMetadata.main.replace(/\\/g, '/') packageNewMetadata.main = packageNewMetadata.main.replace(/\\/g, '/');
} }
const packageKeymapsPath = path.join(packagePath, 'keymaps') const packageKeymapsPath = path.join(packagePath, 'keymaps');
if (fs.existsSync(packageKeymapsPath)) { if (fs.existsSync(packageKeymapsPath)) {
for (let packageKeymapName of fs.readdirSync(packageKeymapsPath)) { for (let packageKeymapName of fs.readdirSync(packageKeymapsPath)) {
const packageKeymapPath = path.join(packageKeymapsPath, packageKeymapName) const packageKeymapPath = path.join(
if (packageKeymapPath.endsWith('.cson') || packageKeymapPath.endsWith('.json')) { packageKeymapsPath,
const relativePath = path.relative(CONFIG.intermediateAppPath, packageKeymapPath) packageKeymapName
packageNewMetadata.keymaps[relativePath] = CSON.readFileSync(packageKeymapPath) );
if (
packageKeymapPath.endsWith('.cson') ||
packageKeymapPath.endsWith('.json')
) {
const relativePath = path.relative(
CONFIG.intermediateAppPath,
packageKeymapPath
);
packageNewMetadata.keymaps[relativePath] = CSON.readFileSync(
packageKeymapPath
);
} }
} }
} }
const packageMenusPath = path.join(packagePath, 'menus') const packageMenusPath = path.join(packagePath, 'menus');
if (fs.existsSync(packageMenusPath)) { if (fs.existsSync(packageMenusPath)) {
for (let packageMenuName of fs.readdirSync(packageMenusPath)) { for (let packageMenuName of fs.readdirSync(packageMenusPath)) {
const packageMenuPath = path.join(packageMenusPath, packageMenuName) const packageMenuPath = path.join(packageMenusPath, packageMenuName);
if (packageMenuPath.endsWith('.cson') || packageMenuPath.endsWith('.json')) { if (
const relativePath = path.relative(CONFIG.intermediateAppPath, packageMenuPath) packageMenuPath.endsWith('.cson') ||
packageNewMetadata.menus[relativePath] = CSON.readFileSync(packageMenuPath) packageMenuPath.endsWith('.json')
) {
const relativePath = path.relative(
CONFIG.intermediateAppPath,
packageMenuPath
);
packageNewMetadata.menus[relativePath] = CSON.readFileSync(
packageMenuPath
);
} }
} }
} }
const packageGrammarsPath = path.join(packagePath, 'grammars') const packageGrammarsPath = path.join(packagePath, 'grammars');
for (let packageGrammarPath of fs.listSync(packageGrammarsPath, ['json', 'cson'])) { for (let packageGrammarPath of fs.listSync(packageGrammarsPath, [
const relativePath = path.relative(CONFIG.intermediateAppPath, packageGrammarPath) 'json',
packageNewMetadata.grammarPaths.push(relativePath) 'cson'
])) {
const relativePath = path.relative(
CONFIG.intermediateAppPath,
packageGrammarPath
);
packageNewMetadata.grammarPaths.push(relativePath);
} }
const packageSettingsPath = path.join(packagePath, 'settings') const packageSettingsPath = path.join(packagePath, 'settings');
for (let packageSettingPath of fs.listSync(packageSettingsPath, ['json', 'cson'])) { for (let packageSettingPath of fs.listSync(packageSettingsPath, [
const relativePath = path.relative(CONFIG.intermediateAppPath, packageSettingPath) 'json',
packageNewMetadata.settings[relativePath] = CSON.readFileSync(packageSettingPath) 'cson'
])) {
const relativePath = path.relative(
CONFIG.intermediateAppPath,
packageSettingPath
);
packageNewMetadata.settings[relativePath] = CSON.readFileSync(
packageSettingPath
);
} }
const packageStyleSheetsPath = path.join(packagePath, 'styles') const packageStyleSheetsPath = path.join(packagePath, 'styles');
let styleSheets = null let styleSheets = null;
if (packageMetadata.mainStyleSheet) { if (packageMetadata.mainStyleSheet) {
styleSheets = [fs.resolve(packagePath, packageMetadata.mainStyleSheet)] styleSheets = [fs.resolve(packagePath, packageMetadata.mainStyleSheet)];
} else if (packageMetadata.styleSheets) { } else if (packageMetadata.styleSheets) {
styleSheets = packageMetadata.styleSheets.map((name) => ( styleSheets = packageMetadata.styleSheets.map(name =>
fs.resolve(packageStyleSheetsPath, name, ['css', 'less', '']) fs.resolve(packageStyleSheetsPath, name, ['css', 'less', ''])
)) );
} else { } else {
const indexStylesheet = fs.resolve(packagePath, 'index', ['css', 'less']) const indexStylesheet = fs.resolve(packagePath, 'index', ['css', 'less']);
if (indexStylesheet) { if (indexStylesheet) {
styleSheets = [indexStylesheet] styleSheets = [indexStylesheet];
} else { } else {
styleSheets = fs.listSync(packageStyleSheetsPath, ['css', 'less']) styleSheets = fs.listSync(packageStyleSheetsPath, ['css', 'less']);
} }
} }
packageNewMetadata.styleSheetPaths = packageNewMetadata.styleSheetPaths = styleSheets.map(styleSheetPath =>
styleSheets.map(styleSheetPath => path.relative(packagePath, styleSheetPath)) path.relative(packagePath, styleSheetPath)
);
packages[packageMetadata.name] = packageNewMetadata packages[packageMetadata.name] = packageNewMetadata;
if (packageModuleCache.extensions) { if (packageModuleCache.extensions) {
for (let extension of Object.keys(packageModuleCache.extensions)) { for (let extension of Object.keys(packageModuleCache.extensions)) {
const paths = packageModuleCache.extensions[extension] const paths = packageModuleCache.extensions[extension];
if (paths.length === 0) { if (paths.length === 0) {
delete packageModuleCache.extensions[extension] delete packageModuleCache.extensions[extension];
} }
} }
} }
} }
return packages return packages;
} }
function buildPlatformMenuMetadata () { function buildPlatformMenuMetadata() {
const menuPath = path.join(CONFIG.repositoryRootPath, 'menus', `${process.platform}.cson`) const menuPath = path.join(
CONFIG.repositoryRootPath,
'menus',
`${process.platform}.cson`
);
if (fs.existsSync(menuPath)) { if (fs.existsSync(menuPath)) {
return CSON.readFileSync(menuPath) return CSON.readFileSync(menuPath);
} else { } else {
return null return null;
} }
} }
function buildPlatformKeymapsMetadata () { function buildPlatformKeymapsMetadata() {
const invalidPlatforms = ['darwin', 'freebsd', 'linux', 'sunos', 'win32'].filter(p => p !== process.platform) const invalidPlatforms = [
const keymapsPath = path.join(CONFIG.repositoryRootPath, 'keymaps') 'darwin',
const keymaps = {} 'freebsd',
'linux',
'sunos',
'win32'
].filter(p => p !== process.platform);
const keymapsPath = path.join(CONFIG.repositoryRootPath, 'keymaps');
const keymaps = {};
for (let keymapName of fs.readdirSync(keymapsPath)) { for (let keymapName of fs.readdirSync(keymapsPath)) {
const keymapPath = path.join(keymapsPath, keymapName) const keymapPath = path.join(keymapsPath, keymapName);
if (keymapPath.endsWith('.cson') || keymapPath.endsWith('.json')) { if (keymapPath.endsWith('.cson') || keymapPath.endsWith('.json')) {
const keymapPlatform = path.basename(keymapPath, path.extname(keymapPath)) const keymapPlatform = path.basename(
keymapPath,
path.extname(keymapPath)
);
if (invalidPlatforms.indexOf(keymapPlatform) === -1) { if (invalidPlatforms.indexOf(keymapPlatform) === -1) {
keymaps[path.basename(keymapPath)] = CSON.readFileSync(keymapPath) keymaps[path.basename(keymapPath)] = CSON.readFileSync(keymapPath);
} }
} }
} }
return keymaps return keymaps;
} }
function checkDeprecatedPackagesMetadata () { function checkDeprecatedPackagesMetadata() {
for (let packageName of Object.keys(deprecatedPackagesMetadata)) { for (let packageName of Object.keys(deprecatedPackagesMetadata)) {
const packageMetadata = deprecatedPackagesMetadata[packageName] const packageMetadata = deprecatedPackagesMetadata[packageName];
if (packageMetadata.version && !semver.validRange(packageMetadata.version)) { if (
throw new Error(`Invalid range: ${packageMetadata.version} (${packageName}).`) packageMetadata.version &&
!semver.validRange(packageMetadata.version)
) {
throw new Error(
`Invalid range: ${packageMetadata.version} (${packageName}).`
);
} }
} }
} }

View File

@ -1,18 +1,22 @@
'use strict' 'use strict';
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
const ModuleCache = require('../../src/module-cache') const ModuleCache = require('../../src/module-cache');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function () { module.exports = function() {
console.log(`Generating module cache for ${CONFIG.intermediateAppPath}`) console.log(`Generating module cache for ${CONFIG.intermediateAppPath}`);
for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) {
ModuleCache.create(path.join(CONFIG.intermediateAppPath, 'node_modules', packageName)) ModuleCache.create(
path.join(CONFIG.intermediateAppPath, 'node_modules', packageName)
);
} }
ModuleCache.create(CONFIG.intermediateAppPath) ModuleCache.create(CONFIG.intermediateAppPath);
const newMetadata = JSON.parse(fs.readFileSync(path.join(CONFIG.intermediateAppPath, 'package.json'))) const newMetadata = JSON.parse(
fs.readFileSync(path.join(CONFIG.intermediateAppPath, 'package.json'))
);
for (let folder of newMetadata._atomModuleCache.folders) { for (let folder of newMetadata._atomModuleCache.folders) {
if (folder.paths.indexOf('') !== -1) { if (folder.paths.indexOf('') !== -1) {
folder.paths = [ folder.paths = [
@ -23,9 +27,12 @@ module.exports = function () {
'src/main-process', 'src/main-process',
'static', 'static',
'vendor' 'vendor'
] ];
} }
} }
CONFIG.appMetadata = newMetadata CONFIG.appMetadata = newMetadata;
fs.writeFileSync(path.join(CONFIG.intermediateAppPath, 'package.json'), JSON.stringify(CONFIG.appMetadata)) fs.writeFileSync(
} path.join(CONFIG.intermediateAppPath, 'package.json'),
JSON.stringify(CONFIG.appMetadata)
);
};

View File

@ -1,96 +1,262 @@
const childProcess = require('child_process') const childProcess = require('child_process');
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
const electronLink = require('electron-link') const electronLink = require('electron-link');
const terser = require('terser') const terser = require('terser');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function (packagedAppPath) { module.exports = function(packagedAppPath) {
const snapshotScriptPath = path.join(CONFIG.buildOutputPath, 'startup.js') const snapshotScriptPath = path.join(CONFIG.buildOutputPath, 'startup.js');
const coreModules = new Set(['electron', 'atom', 'shell', 'WNdb', 'lapack', 'remote']) const coreModules = new Set([
const baseDirPath = path.join(CONFIG.intermediateAppPath, 'static') 'electron',
let processedFiles = 0 'atom',
'shell',
'WNdb',
'lapack',
'remote'
]);
const baseDirPath = path.join(CONFIG.intermediateAppPath, 'static');
let processedFiles = 0;
return electronLink({ return electronLink({
baseDirPath, baseDirPath,
mainPath: path.resolve(baseDirPath, '..', 'src', 'initialize-application-window.js'), mainPath: path.resolve(
baseDirPath,
'..',
'src',
'initialize-application-window.js'
),
cachePath: path.join(CONFIG.atomHomeDirPath, 'snapshot-cache'), cachePath: path.join(CONFIG.atomHomeDirPath, 'snapshot-cache'),
auxiliaryData: CONFIG.snapshotAuxiliaryData, auxiliaryData: CONFIG.snapshotAuxiliaryData,
shouldExcludeModule: ({requiringModulePath, requiredModulePath}) => { shouldExcludeModule: ({ requiringModulePath, requiredModulePath }) => {
if (processedFiles > 0) { if (processedFiles > 0) {
process.stdout.write('\r') process.stdout.write('\r');
} }
process.stdout.write(`Generating snapshot script at "${snapshotScriptPath}" (${++processedFiles})`) process.stdout.write(
`Generating snapshot script at "${snapshotScriptPath}" (${++processedFiles})`
);
const requiringModuleRelativePath = path.relative(baseDirPath, requiringModulePath) const requiringModuleRelativePath = path.relative(
const requiredModuleRelativePath = path.relative(baseDirPath, requiredModulePath) baseDirPath,
requiringModulePath
);
const requiredModuleRelativePath = path.relative(
baseDirPath,
requiredModulePath
);
return ( return (
requiredModulePath.endsWith('.node') || requiredModulePath.endsWith('.node') ||
coreModules.has(requiredModulePath) || coreModules.has(requiredModulePath) ||
requiringModuleRelativePath.endsWith(path.join('node_modules/xregexp/xregexp-all.js')) || requiringModuleRelativePath.endsWith(
(requiredModuleRelativePath.startsWith(path.join('..', 'src')) && requiredModuleRelativePath.endsWith('-element.js')) || path.join('node_modules/xregexp/xregexp-all.js')
requiredModuleRelativePath.startsWith(path.join('..', 'node_modules', 'dugite')) || ) ||
requiredModuleRelativePath.startsWith(path.join('..', 'node_modules', 'markdown-preview', 'node_modules', 'yaml-front-matter')) || (requiredModuleRelativePath.startsWith(path.join('..', 'src')) &&
requiredModuleRelativePath.startsWith(path.join('..', 'node_modules', 'markdown-preview', 'node_modules', 'cheerio')) || requiredModuleRelativePath.endsWith('-element.js')) ||
requiredModuleRelativePath.startsWith(path.join('..', 'node_modules', 'markdown-preview', 'node_modules', 'marked')) || requiredModuleRelativePath.startsWith(
requiredModuleRelativePath.startsWith(path.join('..', 'node_modules', 'typescript-simple')) || path.join('..', 'node_modules', 'dugite')
requiredModuleRelativePath.endsWith(path.join('node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js')) || ) ||
requiredModuleRelativePath.endsWith(path.join('node_modules', 'fs-extra', 'lib', 'index.js')) || requiredModuleRelativePath.startsWith(
requiredModuleRelativePath.endsWith(path.join('node_modules', 'graceful-fs', 'graceful-fs.js')) || path.join(
requiredModuleRelativePath.endsWith(path.join('node_modules', 'htmlparser2', 'lib', 'index.js')) || '..',
requiredModuleRelativePath.endsWith(path.join('node_modules', 'minimatch', 'minimatch.js')) || 'node_modules',
requiredModuleRelativePath.endsWith(path.join('node_modules', 'request', 'index.js')) || 'markdown-preview',
requiredModuleRelativePath.endsWith(path.join('node_modules', 'request', 'request.js')) || 'node_modules',
requiredModuleRelativePath.endsWith(path.join('node_modules', 'superstring', 'index.js')) || 'yaml-front-matter'
requiredModuleRelativePath.endsWith(path.join('node_modules', 'temp', 'lib', 'temp.js')) || )
) ||
requiredModuleRelativePath.startsWith(
path.join(
'..',
'node_modules',
'markdown-preview',
'node_modules',
'cheerio'
)
) ||
requiredModuleRelativePath.startsWith(
path.join(
'..',
'node_modules',
'markdown-preview',
'node_modules',
'marked'
)
) ||
requiredModuleRelativePath.startsWith(
path.join('..', 'node_modules', 'typescript-simple')
) ||
requiredModuleRelativePath.endsWith(
path.join(
'node_modules',
'coffee-script',
'lib',
'coffee-script',
'register.js'
)
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'fs-extra', 'lib', 'index.js')
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'graceful-fs', 'graceful-fs.js')
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'htmlparser2', 'lib', 'index.js')
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'minimatch', 'minimatch.js')
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'request', 'index.js')
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'request', 'request.js')
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'superstring', 'index.js')
) ||
requiredModuleRelativePath.endsWith(
path.join('node_modules', 'temp', 'lib', 'temp.js')
) ||
requiredModuleRelativePath === path.join('..', 'exports', 'atom.js') || requiredModuleRelativePath === path.join('..', 'exports', 'atom.js') ||
requiredModuleRelativePath === path.join('..', 'src', 'electron-shims.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'atom-keymap', 'lib', 'command-event.js') || path.join('..', 'src', 'electron-shims.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'babel-core', 'index.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'debug', 'node.js') || path.join(
requiredModuleRelativePath === path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') || '..',
requiredModuleRelativePath === path.join('..', 'node_modules', 'glob', 'glob.js') || 'node_modules',
requiredModuleRelativePath === path.join('..', 'node_modules', 'iconv-lite', 'lib', 'index.js') || 'atom-keymap',
requiredModuleRelativePath === path.join('..', 'node_modules', 'less', 'index.js') || 'lib',
requiredModuleRelativePath === path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') || 'command-event.js'
requiredModuleRelativePath === path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') || ) ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'lodash.isequal', 'index.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') || path.join('..', 'node_modules', 'babel-core', 'index.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'resolve', 'index.js') || path.join('..', 'node_modules', 'debug', 'node.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js') || path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'spellchecker', 'lib', 'spellchecker.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') || path.join('..', 'node_modules', 'glob', 'glob.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'tar', 'tar.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'ls-archive', 'node_modules', 'tar', 'tar.js') || path.join('..', 'node_modules', 'iconv-lite', 'lib', 'index.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'tree-sitter', 'index.js') || path.join('..', 'node_modules', 'less', 'index.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', 'yauzl', 'index.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'winreg', 'lib', 'registry.js') || path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') ||
requiredModuleRelativePath === path.join('..', 'node_modules', '@atom', 'fuzzy-native', 'lib', 'main.js') || requiredModuleRelativePath ===
requiredModuleRelativePath === path.join('..', 'node_modules', 'vscode-ripgrep', 'lib', 'index.js') || path.join(
'..',
'node_modules',
'less',
'lib',
'less-node',
'index.js'
) ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'lodash.isequal', 'index.js') ||
requiredModuleRelativePath ===
path.join(
'..',
'node_modules',
'node-fetch',
'lib',
'fetch-error.js'
) ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'resolve', 'index.js') ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') ||
requiredModuleRelativePath ===
path.join(
'..',
'node_modules',
'settings-view',
'node_modules',
'glob',
'glob.js'
) ||
requiredModuleRelativePath ===
path.join(
'..',
'node_modules',
'spellchecker',
'lib',
'spellchecker.js'
) ||
requiredModuleRelativePath ===
path.join(
'..',
'node_modules',
'spelling-manager',
'node_modules',
'natural',
'lib',
'natural',
'index.js'
) ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'tar', 'tar.js') ||
requiredModuleRelativePath ===
path.join(
'..',
'node_modules',
'ls-archive',
'node_modules',
'tar',
'tar.js'
) ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'tree-sitter', 'index.js') ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'yauzl', 'index.js') ||
requiredModuleRelativePath ===
path.join('..', 'node_modules', 'winreg', 'lib', 'registry.js') ||
requiredModuleRelativePath ===
path.join(
'..',
'node_modules',
'@atom',
'fuzzy-native',
'lib',
'main.js'
) ||
requiredModuleRelativePath ===
path.join(
'..',
'node_modules',
'vscode-ripgrep',
'lib',
'index.js'
) ||
// The startup-time script is used by both the renderer and the main process and having it in the // The startup-time script is used by both the renderer and the main process and having it in the
// snapshot causes issues. // snapshot causes issues.
requiredModuleRelativePath === path.join('..', 'src', 'startup-time.js') requiredModuleRelativePath === path.join('..', 'src', 'startup-time.js')
) );
} }
}).then(({snapshotScript}) => { }).then(({ snapshotScript }) => {
process.stdout.write('\n') process.stdout.write('\n');
process.stdout.write('Minifying startup script') process.stdout.write('Minifying startup script');
const minification = terser.minify(snapshotScript, { const minification = terser.minify(snapshotScript, {
keep_fnames: true, keep_fnames: true,
keep_classnames: true, keep_classnames: true,
compress: {keep_fargs: true, keep_infinity: true} compress: { keep_fargs: true, keep_infinity: true }
}) });
if (minification.error) throw minification.error if (minification.error) throw minification.error;
process.stdout.write('\n') process.stdout.write('\n');
fs.writeFileSync(snapshotScriptPath, minification.code) fs.writeFileSync(snapshotScriptPath, minification.code);
console.log('Verifying if snapshot can be executed via `mksnapshot`') console.log('Verifying if snapshot can be executed via `mksnapshot`');
const verifySnapshotScriptPath = path.join(CONFIG.repositoryRootPath, 'script', 'verify-snapshot-script') const verifySnapshotScriptPath = path.join(
let nodeBundledInElectronPath CONFIG.repositoryRootPath,
'script',
'verify-snapshot-script'
);
let nodeBundledInElectronPath;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
nodeBundledInElectronPath = path.join(packagedAppPath, 'Contents', 'MacOS', CONFIG.executableName) nodeBundledInElectronPath = path.join(packagedAppPath, 'Contents', 'MacOS', CONFIG.executableName)
} else { } else {
@ -99,39 +265,49 @@ module.exports = function (packagedAppPath) {
childProcess.execFileSync( childProcess.execFileSync(
nodeBundledInElectronPath, nodeBundledInElectronPath,
[verifySnapshotScriptPath, snapshotScriptPath], [verifySnapshotScriptPath, snapshotScriptPath],
{env: Object.assign({}, process.env, {ELECTRON_RUN_AS_NODE: 1})} { env: Object.assign({}, process.env, { ELECTRON_RUN_AS_NODE: 1 }) }
) );
console.log('Generating startup blob with mksnapshot') console.log('Generating startup blob with mksnapshot');
childProcess.spawnSync( childProcess.spawnSync(process.execPath, [
process.execPath, [ path.join(
path.join(CONFIG.repositoryRootPath, 'script', 'node_modules', 'electron-mksnapshot', 'mksnapshot.js'), CONFIG.repositoryRootPath,
snapshotScriptPath, 'script',
'--output_dir', 'node_modules',
CONFIG.buildOutputPath 'electron-mksnapshot',
] 'mksnapshot.js'
) ),
snapshotScriptPath,
'--output_dir',
CONFIG.buildOutputPath
]);
let startupBlobDestinationPath let startupBlobDestinationPath;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
startupBlobDestinationPath = `${packagedAppPath}/Contents/Frameworks/Electron Framework.framework/Resources` startupBlobDestinationPath = `${packagedAppPath}/Contents/Frameworks/Electron Framework.framework/Resources`;
} else { } else {
startupBlobDestinationPath = packagedAppPath startupBlobDestinationPath = packagedAppPath;
} }
const snapshotBinaries = ['v8_context_snapshot.bin', 'snapshot_blob.bin'] const snapshotBinaries = ['v8_context_snapshot.bin', 'snapshot_blob.bin'];
for (let snapshotBinary of snapshotBinaries) { for (let snapshotBinary of snapshotBinaries) {
const destinationPath = path.join(startupBlobDestinationPath, snapshotBinary) const destinationPath = path.join(
console.log(`Moving generated startup blob into "${destinationPath}"`) startupBlobDestinationPath,
snapshotBinary
);
console.log(`Moving generated startup blob into "${destinationPath}"`);
try { try {
fs.unlinkSync(destinationPath) fs.unlinkSync(destinationPath);
} catch (err) { } catch (err) {
// Doesn't matter if the file doesn't exist already // Doesn't matter if the file doesn't exist already
if (!err.code || err.code !== 'ENOENT') { if (!err.code || err.code !== 'ENOENT') {
throw err throw err;
} }
} }
fs.renameSync(path.join(CONFIG.buildOutputPath, snapshotBinary), destinationPath) fs.renameSync(
path.join(CONFIG.buildOutputPath, snapshotBinary),
destinationPath
);
} }
}) });
} };

View File

@ -1,38 +1,46 @@
'use strict' 'use strict';
const fs = require('fs') const fs = require('fs');
const path = require('path') const path = require('path');
const legalEagle = require('legal-eagle') const legalEagle = require('legal-eagle');
const licenseOverrides = require('../license-overrides') const licenseOverrides = require('../license-overrides');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function () { module.exports = function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
legalEagle({path: CONFIG.repositoryRootPath, overrides: licenseOverrides}, (err, packagesLicenses) => { legalEagle(
if (err) { { path: CONFIG.repositoryRootPath, overrides: licenseOverrides },
reject(err) (err, packagesLicenses) => {
throw new Error(err) if (err) {
} else { reject(err);
let text = throw new Error(err);
fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'LICENSE.md'), 'utf8') + '\n\n' + } else {
'This application bundles the following third-party packages in accordance\n' + let text =
'with the following licenses:\n\n' fs.readFileSync(
for (let packageName of Object.keys(packagesLicenses).sort()) { path.join(CONFIG.repositoryRootPath, 'LICENSE.md'),
const packageLicense = packagesLicenses[packageName] 'utf8'
text += '-------------------------------------------------------------------------\n\n' ) +
text += `Package: ${packageName}\n` '\n\n' +
text += `License: ${packageLicense.license}\n` 'This application bundles the following third-party packages in accordance\n' +
if (packageLicense.source) { 'with the following licenses:\n\n';
text += `License Source: ${packageLicense.source}\n` for (let packageName of Object.keys(packagesLicenses).sort()) {
const packageLicense = packagesLicenses[packageName];
text +=
'-------------------------------------------------------------------------\n\n';
text += `Package: ${packageName}\n`;
text += `License: ${packageLicense.license}\n`;
if (packageLicense.source) {
text += `License Source: ${packageLicense.source}\n`;
}
if (packageLicense.sourceText) {
text += `Source Text:\n\n${packageLicense.sourceText}`;
}
text += '\n';
} }
if (packageLicense.sourceText) { resolve(text);
text += `Source Text:\n\n${packageLicense.sourceText}`
}
text += '\n'
} }
resolve(text)
} }
}) );
}) });
} };

View File

@ -1,24 +1,29 @@
'use strict' 'use strict';
const os = require('os') const os = require('os');
const passwdUser = require('passwd-user') const passwdUser = require('passwd-user');
const path = require('path') const path = require('path');
module.exports = function (aPath) { module.exports = function(aPath) {
if (!aPath.startsWith('~')) { if (!aPath.startsWith('~')) {
return aPath return aPath;
} }
const sepIndex = aPath.indexOf(path.sep) const sepIndex = aPath.indexOf(path.sep);
const user = (sepIndex < 0) ? aPath.substring(1) : aPath.substring(1, sepIndex) const user = sepIndex < 0 ? aPath.substring(1) : aPath.substring(1, sepIndex);
const rest = (sepIndex < 0) ? '' : aPath.substring(sepIndex) const rest = sepIndex < 0 ? '' : aPath.substring(sepIndex);
const home = (user === '') ? os.homedir() : (() => { const home =
const passwd = passwdUser.sync(user) user === ''
if (passwd === undefined) { ? os.homedir()
throw new Error(`Failed to expand the tilde in ${aPath} - user "${user}" does not exist`) : (() => {
} const passwd = passwdUser.sync(user);
return passwd.homedir if (passwd === undefined) {
})() throw new Error(
`Failed to expand the tilde in ${aPath} - user "${user}" does not exist`
);
}
return passwd.homedir;
})();
return `${home}${rest}` return `${home}${rest}`;
} };

View File

@ -1,11 +1,14 @@
'use strict' 'use strict';
const path = require('path') const path = require('path');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function (filePath) { module.exports = function(filePath) {
return !EXCLUDED_PATHS_REGEXP.test(filePath) || INCLUDED_PATHS_REGEXP.test(filePath) return (
} !EXCLUDED_PATHS_REGEXP.test(filePath) ||
INCLUDED_PATHS_REGEXP.test(filePath)
);
};
const EXCLUDE_REGEXPS_SOURCES = [ const EXCLUDE_REGEXPS_SOURCES = [
escapeRegExp('.DS_Store'), escapeRegExp('.DS_Store'),
@ -35,7 +38,9 @@ const EXCLUDE_REGEXPS_SOURCES = [
escapeRegExp(path.join('npm', 'node_modules', '.bin', 'starwars')), escapeRegExp(path.join('npm', 'node_modules', '.bin', 'starwars')),
escapeRegExp(path.join('pegjs', 'examples')), escapeRegExp(path.join('pegjs', 'examples')),
escapeRegExp(path.join('get-parameter-names', 'node_modules', 'testla')), escapeRegExp(path.join('get-parameter-names', 'node_modules', 'testla')),
escapeRegExp(path.join('get-parameter-names', 'node_modules', '.bin', 'testla')), escapeRegExp(
path.join('get-parameter-names', 'node_modules', '.bin', 'testla')
),
escapeRegExp(path.join('jasmine-reporters', 'ext')), escapeRegExp(path.join('jasmine-reporters', 'ext')),
escapeRegExp(path.join('node_modules', 'nan')), escapeRegExp(path.join('node_modules', 'nan')),
escapeRegExp(path.join('node_modules', 'native-mate')), escapeRegExp(path.join('node_modules', 'native-mate')),
@ -53,7 +58,9 @@ const EXCLUDE_REGEXPS_SOURCES = [
escapeRegExp(path.join('node_modules', 'loophole')), escapeRegExp(path.join('node_modules', 'loophole')),
escapeRegExp(path.join('node_modules', 'pegjs')), escapeRegExp(path.join('node_modules', 'pegjs')),
escapeRegExp(path.join('node_modules', '.bin', 'pegjs')), escapeRegExp(path.join('node_modules', '.bin', 'pegjs')),
escapeRegExp(path.join('node_modules', 'spellchecker', 'vendor', 'hunspell') + path.sep) + '.*', escapeRegExp(
path.join('node_modules', 'spellchecker', 'vendor', 'hunspell') + path.sep
) + '.*',
// node_modules of the fuzzy-native package are only required for building it. // node_modules of the fuzzy-native package are only required for building it.
escapeRegExp(path.join('node_modules', 'fuzzy-native', 'node_modules')), escapeRegExp(path.join('node_modules', 'fuzzy-native', 'node_modules')),
@ -66,34 +73,59 @@ const EXCLUDE_REGEXPS_SOURCES = [
escapeRegExp(path.sep) + '.+\\.target.mk$', escapeRegExp(path.sep) + '.+\\.target.mk$',
escapeRegExp(path.sep) + 'linker\\.lock$', escapeRegExp(path.sep) + 'linker\\.lock$',
escapeRegExp(path.join('build', 'Release') + path.sep) + '.+\\.node\\.dSYM', escapeRegExp(path.join('build', 'Release') + path.sep) + '.+\\.node\\.dSYM',
escapeRegExp(path.join('build', 'Release') + path.sep) + '.*\\.(pdb|lib|exp|map|ipdb|iobj)', escapeRegExp(path.join('build', 'Release') + path.sep) +
'.*\\.(pdb|lib|exp|map|ipdb|iobj)',
// Ignore node_module files we won't need at runtime // 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' +
'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + 'examples?' + escapeRegExp(path.sep), 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) + '.*' + '\\.d\\.ts$', 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.d\\.ts$',
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$', 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$',
'.*' + escapeRegExp(path.sep) + 'test.*\\.html$' '.*' + escapeRegExp(path.sep) + 'test.*\\.html$'
] ];
// Ignore spec directories in all bundled packages // Ignore spec directories in all bundled packages
for (let packageName in CONFIG.appMetadata.packageDependencies) { for (let packageName in CONFIG.appMetadata.packageDependencies) {
EXCLUDE_REGEXPS_SOURCES.push('^' + escapeRegExp(path.join(CONFIG.repositoryRootPath, 'node_modules', packageName, 'spec'))) EXCLUDE_REGEXPS_SOURCES.push(
'^' +
escapeRegExp(
path.join(
CONFIG.repositoryRootPath,
'node_modules',
packageName,
'spec'
)
)
);
} }
// Ignore Hunspell dictionaries only on macOS. // Ignore Hunspell dictionaries only on macOS.
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
EXCLUDE_REGEXPS_SOURCES.push(escapeRegExp(path.join('spellchecker', 'vendor', 'hunspell_dictionaries'))) EXCLUDE_REGEXPS_SOURCES.push(
escapeRegExp(path.join('spellchecker', 'vendor', 'hunspell_dictionaries'))
);
} }
const EXCLUDED_PATHS_REGEXP = new RegExp( const EXCLUDED_PATHS_REGEXP = new RegExp(
EXCLUDE_REGEXPS_SOURCES.map(path => `(${path})`).join('|') EXCLUDE_REGEXPS_SOURCES.map(path => `(${path})`).join('|')
) );
const INCLUDED_PATHS_REGEXP = new RegExp( const INCLUDED_PATHS_REGEXP = new RegExp(
escapeRegExp(path.join('node_modules', 'node-gyp', 'src', 'win_delay_load_hook.cc')) escapeRegExp(
) path.join('node_modules', 'node-gyp', 'src', 'win_delay_load_hook.cc')
)
);
function escapeRegExp (string) { function escapeRegExp(string) {
return string.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&') return string.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&');
} }

View File

@ -1,15 +1,15 @@
'use strict' 'use strict';
const childProcess = require('child_process') const childProcess = require('child_process');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function (ci) { module.exports = function(ci) {
console.log('Installing apm') console.log('Installing apm');
// npm ci leaves apm with a bunch of unmet dependencies // npm ci leaves apm with a bunch of unmet dependencies
childProcess.execFileSync( childProcess.execFileSync(
CONFIG.getNpmBinPath(), CONFIG.getNpmBinPath(),
['--global-style', '--loglevel=error', 'install'], ['--global-style', '--loglevel=error', 'install'],
{env: process.env, cwd: CONFIG.apmRootPath} { env: process.env, cwd: CONFIG.apmRootPath }
) );
} };

View File

@ -1,22 +1,26 @@
'use strict' 'use strict';
const fs = require('fs-extra') const fs = require('fs-extra');
const handleTilde = require('./handle-tilde') const handleTilde = require('./handle-tilde');
const path = require('path') const path = require('path');
const template = require('lodash.template') const template = require('lodash.template');
const startCase = require('lodash.startcase') const startCase = require('lodash.startcase');
const execSync = require('child_process').execSync const execSync = require('child_process').execSync;
const CONFIG = require('../config') const CONFIG = require('../config');
function install (installationDirPath, packagedAppFileName, packagedAppPath) { function install(installationDirPath, packagedAppFileName, packagedAppPath) {
if (fs.existsSync(installationDirPath)) { if (fs.existsSync(installationDirPath)) {
console.log(`Removing previously installed "${packagedAppFileName}" at "${installationDirPath}"`) console.log(
fs.removeSync(installationDirPath) `Removing previously installed "${packagedAppFileName}" at "${installationDirPath}"`
);
fs.removeSync(installationDirPath);
} }
console.log(`Installing "${packagedAppFileName}" at "${installationDirPath}"`) console.log(
fs.copySync(packagedAppPath, installationDirPath) `Installing "${packagedAppFileName}" at "${installationDirPath}"`
);
fs.copySync(packagedAppPath, installationDirPath);
} }
/** /**
@ -26,132 +30,205 @@ function install (installationDirPath, packagedAppFileName, packagedAppPath) {
* and the XDG Base Directory Specification: * and the XDG Base Directory Specification:
* https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables
*/ */
function findBaseIconThemeDirPath () { function findBaseIconThemeDirPath() {
const defaultBaseIconThemeDir = '/usr/share/icons/hicolor' const defaultBaseIconThemeDir = '/usr/share/icons/hicolor';
const dataDirsString = process.env.XDG_DATA_DIRS const dataDirsString = process.env.XDG_DATA_DIRS;
if (dataDirsString) { if (dataDirsString) {
const dataDirs = dataDirsString.split(path.delimiter) const dataDirs = dataDirsString.split(path.delimiter);
if (dataDirs.includes('/usr/share/') || dataDirs.includes('/usr/share')) { if (dataDirs.includes('/usr/share/') || dataDirs.includes('/usr/share')) {
return defaultBaseIconThemeDir return defaultBaseIconThemeDir;
} else { } else {
return path.join(dataDirs[0], 'icons', 'hicolor') return path.join(dataDirs[0], 'icons', 'hicolor');
} }
} else { } else {
return defaultBaseIconThemeDir return defaultBaseIconThemeDir;
} }
} }
module.exports = function (packagedAppPath, installDir) { module.exports = function(packagedAppPath, installDir) {
const packagedAppFileName = path.basename(packagedAppPath) const packagedAppFileName = path.basename(packagedAppPath);
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const installPrefix = installDir !== '' ? handleTilde(installDir) : path.join(path.sep, 'Applications') const installPrefix =
const installationDirPath = path.join(installPrefix, packagedAppFileName) installDir !== ''
install(installationDirPath, packagedAppFileName, packagedAppPath) ? handleTilde(installDir)
: path.join(path.sep, 'Applications');
const installationDirPath = path.join(installPrefix, packagedAppFileName);
install(installationDirPath, packagedAppFileName, packagedAppPath);
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
const installPrefix = installDir !== '' ? installDir : process.env.LOCALAPPDATA const installPrefix =
const installationDirPath = path.join(installPrefix, packagedAppFileName, 'app-dev') installDir !== '' ? installDir : process.env.LOCALAPPDATA;
const installationDirPath = path.join(
installPrefix,
packagedAppFileName,
'app-dev'
);
try { try {
install(installationDirPath, packagedAppFileName, packagedAppPath) install(installationDirPath, packagedAppFileName, packagedAppPath);
} catch (e) { } catch (e) {
console.log(`Administrator elevation required to install into "${installationDirPath}"`) console.log(
const fsAdmin = require('fs-admin') `Administrator elevation required to install into "${installationDirPath}"`
);
const fsAdmin = require('fs-admin');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fsAdmin.recursiveCopy(packagedAppPath, installationDirPath, (error) => { fsAdmin.recursiveCopy(packagedAppPath, installationDirPath, error => {
error ? reject(error) : resolve() error ? reject(error) : resolve();
}) });
}) });
} }
} else { } else {
const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : 'atom-' + CONFIG.channel const atomExecutableName =
const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : 'apm-' + CONFIG.channel CONFIG.channel === 'stable' ? 'atom' : 'atom-' + CONFIG.channel;
const appName = CONFIG.channel === 'stable' ? 'Atom' : startCase('Atom ' + CONFIG.channel) const apmExecutableName =
const appDescription = CONFIG.appMetadata.description CONFIG.channel === 'stable' ? 'apm' : 'apm-' + CONFIG.channel;
const prefixDirPath = installDir !== '' ? handleTilde(installDir) : path.join('/usr', 'local') const appName =
const shareDirPath = path.join(prefixDirPath, 'share') CONFIG.channel === 'stable'
const installationDirPath = path.join(shareDirPath, atomExecutableName) ? 'Atom'
const applicationsDirPath = path.join(shareDirPath, 'applications') : startCase('Atom ' + CONFIG.channel);
const appDescription = CONFIG.appMetadata.description;
const prefixDirPath =
installDir !== '' ? handleTilde(installDir) : path.join('/usr', 'local');
const shareDirPath = path.join(prefixDirPath, 'share');
const installationDirPath = path.join(shareDirPath, atomExecutableName);
const applicationsDirPath = path.join(shareDirPath, 'applications');
const binDirPath = path.join(prefixDirPath, 'bin') const binDirPath = path.join(prefixDirPath, 'bin');
fs.mkdirpSync(applicationsDirPath) fs.mkdirpSync(applicationsDirPath);
fs.mkdirpSync(binDirPath) fs.mkdirpSync(binDirPath);
install(installationDirPath, packagedAppFileName, packagedAppPath) install(installationDirPath, packagedAppFileName, packagedAppPath);
{ // Install icons {
const baseIconThemeDirPath = findBaseIconThemeDirPath() // Install icons
const fullIconName = atomExecutableName + '.png' const baseIconThemeDirPath = findBaseIconThemeDirPath();
const fullIconName = atomExecutableName + '.png';
let existingIconsFound = false let existingIconsFound = false;
fs.readdirSync(baseIconThemeDirPath).forEach(size => { fs.readdirSync(baseIconThemeDirPath).forEach(size => {
const iconPath = path.join(baseIconThemeDirPath, size, 'apps', fullIconName) const iconPath = path.join(
baseIconThemeDirPath,
size,
'apps',
fullIconName
);
if (fs.existsSync(iconPath)) { if (fs.existsSync(iconPath)) {
if (!existingIconsFound) { if (!existingIconsFound) {
console.log(`Removing existing icons from "${baseIconThemeDirPath}"`) console.log(
`Removing existing icons from "${baseIconThemeDirPath}"`
);
} }
existingIconsFound = true existingIconsFound = true;
fs.removeSync(iconPath) fs.removeSync(iconPath);
} }
}) });
console.log(`Installing icons at "${baseIconThemeDirPath}"`) console.log(`Installing icons at "${baseIconThemeDirPath}"`);
const appIconsPath = path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png') const appIconsPath = path.join(
CONFIG.repositoryRootPath,
'resources',
'app-icons',
CONFIG.channel,
'png'
);
fs.readdirSync(appIconsPath).forEach(imageName => { fs.readdirSync(appIconsPath).forEach(imageName => {
if (/\.png$/.test(imageName)) { if (/\.png$/.test(imageName)) {
const size = path.basename(imageName, '.png') const size = path.basename(imageName, '.png');
const iconPath = path.join(appIconsPath, imageName) const iconPath = path.join(appIconsPath, imageName);
fs.copySync(iconPath, path.join(baseIconThemeDirPath, `${size}x${size}`, 'apps', fullIconName)) fs.copySync(
iconPath,
path.join(
baseIconThemeDirPath,
`${size}x${size}`,
'apps',
fullIconName
)
);
} }
}) });
console.log(`Updating icon cache for "${baseIconThemeDirPath}"`) console.log(`Updating icon cache for "${baseIconThemeDirPath}"`);
try { try {
execSync(`gtk-update-icon-cache ${baseIconThemeDirPath} --force`) execSync(`gtk-update-icon-cache ${baseIconThemeDirPath} --force`);
} catch (e) {} } catch (e) {}
} }
{ // Install xdg desktop file {
const desktopEntryPath = path.join(applicationsDirPath, `${atomExecutableName}.desktop`) // Install xdg desktop file
const desktopEntryPath = path.join(
applicationsDirPath,
`${atomExecutableName}.desktop`
);
if (fs.existsSync(desktopEntryPath)) { if (fs.existsSync(desktopEntryPath)) {
console.log(`Removing existing desktop entry file at "${desktopEntryPath}"`) console.log(
fs.removeSync(desktopEntryPath) `Removing existing desktop entry file at "${desktopEntryPath}"`
);
fs.removeSync(desktopEntryPath);
} }
console.log(`Writing desktop entry file at "${desktopEntryPath}"`) console.log(`Writing desktop entry file at "${desktopEntryPath}"`);
const desktopEntryTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in')) const desktopEntryTemplate = fs.readFileSync(
path.join(
CONFIG.repositoryRootPath,
'resources',
'linux',
'atom.desktop.in'
)
);
const desktopEntryContents = template(desktopEntryTemplate)({ const desktopEntryContents = template(desktopEntryTemplate)({
appName, appName,
appFileName: atomExecutableName, appFileName: atomExecutableName,
description: appDescription, description: appDescription,
installDir: prefixDirPath, installDir: prefixDirPath,
iconPath: atomExecutableName iconPath: atomExecutableName
}) });
fs.writeFileSync(desktopEntryPath, desktopEntryContents) fs.writeFileSync(desktopEntryPath, desktopEntryContents);
} }
{ // Add atom executable to the PATH {
const atomBinDestinationPath = path.join(binDirPath, atomExecutableName) // Add atom executable to the PATH
const atomBinDestinationPath = path.join(binDirPath, atomExecutableName);
if (fs.existsSync(atomBinDestinationPath)) { if (fs.existsSync(atomBinDestinationPath)) {
console.log(`Removing existing executable at "${atomBinDestinationPath}"`) console.log(
fs.removeSync(atomBinDestinationPath) `Removing existing executable at "${atomBinDestinationPath}"`
);
fs.removeSync(atomBinDestinationPath);
} }
console.log(`Copying atom.sh to "${atomBinDestinationPath}"`) console.log(`Copying atom.sh to "${atomBinDestinationPath}"`);
fs.copySync(path.join(CONFIG.repositoryRootPath, 'atom.sh'), atomBinDestinationPath) fs.copySync(
path.join(CONFIG.repositoryRootPath, 'atom.sh'),
atomBinDestinationPath
);
} }
{ // Link apm executable to the PATH {
const apmBinDestinationPath = path.join(binDirPath, apmExecutableName) // Link apm executable to the PATH
const apmBinDestinationPath = path.join(binDirPath, apmExecutableName);
try { try {
fs.lstatSync(apmBinDestinationPath) fs.lstatSync(apmBinDestinationPath);
console.log(`Removing existing executable at "${apmBinDestinationPath}"`) console.log(
fs.removeSync(apmBinDestinationPath) `Removing existing executable at "${apmBinDestinationPath}"`
} catch (e) { } );
console.log(`Symlinking apm to "${apmBinDestinationPath}"`) fs.removeSync(apmBinDestinationPath);
fs.symlinkSync(path.join('..', 'share', atomExecutableName, 'resources', 'app', 'apm', 'node_modules', '.bin', 'apm'), apmBinDestinationPath) } catch (e) {}
console.log(`Symlinking apm to "${apmBinDestinationPath}"`);
fs.symlinkSync(
path.join(
'..',
'share',
atomExecutableName,
'resources',
'app',
'apm',
'node_modules',
'.bin',
'apm'
),
apmBinDestinationPath
);
} }
console.log(`Changing permissions to 755 for "${installationDirPath}"`) console.log(`Changing permissions to 755 for "${installationDirPath}"`);
fs.chmodSync(installationDirPath, '755') fs.chmodSync(installationDirPath, '755');
} }
return Promise.resolve() return Promise.resolve();
} };

View File

@ -1,14 +1,14 @@
'use strict' 'use strict';
const childProcess = require('child_process') const childProcess = require('child_process');
const CONFIG = require('../config') const CONFIG = require('../config');
module.exports = function (ci) { module.exports = function(ci) {
console.log('Installing script dependencies') console.log('Installing script dependencies');
childProcess.execFileSync( childProcess.execFileSync(
CONFIG.getNpmBinPath(ci), CONFIG.getNpmBinPath(ci),
['--loglevel=error', ci ? 'ci' : 'install'], ['--loglevel=error', ci ? 'ci' : 'install'],
{env: process.env, cwd: CONFIG.scriptRootPath} { env: process.env, cwd: CONFIG.scriptRootPath }
) );
} };

Some files were not shown because too many files have changed in this diff Show More