Merge remote-tracking branch 'origin/master' into migrate-to-context-aware-modules

This commit is contained in:
Maurício Szabo 2023-05-15 11:06:45 -03:00
commit 4594d6929a
37 changed files with 3811 additions and 69 deletions

View File

@ -1,5 +1,7 @@
env:
PYTHON_VERSION: 3.10
GITHUB_TOKEN: ENCRYPTED[13da504dc34d1608564d891fb7f456b546019d07d1abb059f9ab4296c56ccc0e6e32c7b313629776eda40ab74a54e95c]
# The above token, is a GitHub API Token, that allows us to download RipGrep without concern of API limits
linux_task:
alias: linux

View File

@ -4,7 +4,7 @@ on:
push:
branches: [ "master" ]
workflow_dispatch:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,6 +1,8 @@
name: Editor tests
on:
- pull_request
pull_request:
push:
branches: ['master']
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ATOM_JASMINE_REPORTER: list

View File

@ -1,6 +1,8 @@
name: Package tests for Pulsar on Linux
name: Package tests
on:
- pull_request
pull_request:
push:
branches: ['master']
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ATOM_JASMINE_REPORTER: list
@ -107,7 +109,7 @@ jobs:
- package: "spell-check"
- package: "status-bar"
- package: "styleguide"
- package: "symbols-view"
# - package: "symbols-view"
- package: "tabs"
- package: "timecop"
- package: "tree-view"

View File

@ -8,6 +8,10 @@
- Using some modules that are context-aware (remove some warnings, makes upgrading
to newer Electron easier).
- Rebranded notifications, using our backend to find new versions of package,
and our github repository to find issues on Pulsar. Also fixed the "view issue"
and "create issue" buttons that were not working
- Bumped to latest version of `second-mate`, fixing a memory usage issue in `vscode-oniguruma`
- Removed a cache for native modules - fix bugs where an user rebuilds a native
module outside of Pulsar, but Pulsar refuses to load anyway
- Removed `nslog` dependency

View File

@ -126,7 +126,7 @@
"mocha-multi-reporters": "^1.1.4",
"mock-spawn": "^0.2.6",
"normalize-package-data": "3.0.2",
"notifications": "https://codeload.github.com/atom/notifications/legacy.tar.gz/refs/tags/v0.72.1",
"notifications": "file:./packages/notifications",
"nsfw": "2.2.2",
"one-dark-syntax": "file:packages/one-dark-syntax",
"one-dark-ui": "file:packages/one-dark-ui",
@ -143,7 +143,7 @@
"scoped-property-store": "^0.17.0",
"scrollbar-style": "^4.0.1",
"season": "^6.0.2",
"second-mate": "https://github.com/pulsar-edit/second-mate.git#14aa7bd",
"second-mate": "https://github.com/pulsar-edit/second-mate.git#9686771",
"semver": "7.3.8",
"service-hub": "^0.7.4",
"settings-view": "file:packages/settings-view",
@ -215,7 +215,7 @@
"line-ending-selector": "file:./packages/line-ending-selector",
"link": "file:./packages/link",
"markdown-preview": "file:./packages/markdown-preview",
"notifications": "0.72.1",
"notifications": "file:./packages/notifications",
"open-on-github": "file:./packages/open-on-github",
"package-generator": "file:./packages/package-generator",
"settings-view": "file:./packages/settings-view",

View File

@ -0,0 +1,31 @@
class BookmarksProvider {
constructor(main) {
this.main = main
}
// Returns all bookmarks present in the given editor.
//
// Each bookmark tracks a buffer range and is represented by an instance of
// {DisplayMarker}.
//
// Will return an empty array if there are no bookmarks in the given editor.
//
// Keep in mind that a single bookmark can span multiple buffer rows and/or
// screen rows. Thus there isn't necessarily a 1:1 correlation between the
// number of bookmarks in the editor and the number of bookmark icons that
// the user will see in the gutter.
getBookmarksForEditor(editor) {
let instance = this.getInstanceForEditor(editor)
if (!instance) return null
return instance.getAllBookmarks()
}
// Returns the instance of the `Bookmarks` class that is responsible for
// managing bookmarks in the given editor.
getInstanceForEditor(editor) {
return this.main.editorsBookmarks.find(b => b.editor.id === editor.id)
}
}
module.exports = BookmarksProvider

View File

@ -1,12 +1,13 @@
const {CompositeDisposable} = require('atom')
const {CompositeDisposable, Emitter} = require('atom')
module.exports =
class Bookmarks {
static deserialize (editor, state) {
static deserialize(editor, state) {
return new Bookmarks(editor, editor.getMarkerLayer(state.markerLayerId))
}
constructor (editor, markerLayer) {
constructor(editor, markerLayer) {
this.emitter = new Emitter()
this.editor = editor
this.markerLayer = markerLayer || this.editor.addMarkerLayer({persistent: true})
this.decorationLayer = this.editor.decorateMarkerLayer(this.markerLayer, {type: 'line-number', class: 'bookmarked'})
@ -24,23 +25,23 @@ class Bookmarks {
this.disposables.add(this.editor.onDidDestroy(this.destroy.bind(this)))
}
destroy () {
destroy() {
this.deactivate()
this.markerLayer.destroy()
}
deactivate () {
deactivate() {
this.decorationLayer.destroy()
this.decorationLayerLine.destroy()
this.decorationLayerHighlight.destroy()
this.disposables.dispose()
}
serialize () {
serialize() {
return {markerLayerId: this.markerLayer.id}
}
toggleBookmark () {
toggleBookmark() {
for (const range of this.editor.getSelectedBufferRanges()) {
const bookmarks = this.markerLayer.findMarkers({intersectsRowRange: [range.start.row, range.end.row]})
if (bookmarks && bookmarks.length > 0) {
@ -52,19 +53,34 @@ class Bookmarks {
this.disposables.add(bookmark.onDidChange(({isValid}) => {
if (!isValid) {
bookmark.destroy()
// TODO: If N bookmarks are affected by a buffer change,
// `did-change-bookmarks` will be emitted N times. We could
// debounce this if we were willing to go async.
this.emitter.emit('did-change-bookmarks', this.getAllBookmarks())
}
}))
}
this.emitter.emit('did-change-bookmarks', this.getAllBookmarks())
}
}
clearBookmarks () {
getAllBookmarks() {
let markers = this.markerLayer.getMarkers()
return markers
}
onDidChangeBookmarks(callback) {
return this.emitter.on('did-change-bookmarks', callback)
}
clearBookmarks() {
for (const bookmark of this.markerLayer.getMarkers()) {
bookmark.destroy()
}
this.emitter.emit('did-change-bookmarks', [])
}
jumpToNextBookmark () {
jumpToNextBookmark() {
if (this.markerLayer.getMarkerCount() > 0) {
const bufferRow = this.editor.getLastCursor().getMarker().getStartBufferPosition().row
const markers = this.markerLayer.getMarkers().sort((a, b) => a.compare(b))
@ -76,7 +92,7 @@ class Bookmarks {
}
}
jumpToPreviousBookmark () {
jumpToPreviousBookmark() {
if (this.markerLayer.getMarkerCount() > 0) {
const bufferRow = this.editor.getLastCursor().getMarker().getStartBufferPosition().row
const markers = this.markerLayer.getMarkers().sort((a, b) => b.compare(a))
@ -88,7 +104,7 @@ class Bookmarks {
}
}
selectToNextBookmark () {
selectToNextBookmark() {
if (this.markerLayer.getMarkerCount() > 0) {
const bufferRow = this.editor.getLastCursor().getMarker().getStartBufferPosition().row
const markers = this.markerLayer.getMarkers().sort((a, b) => a.compare(b))
@ -103,7 +119,7 @@ class Bookmarks {
}
}
selectToPreviousBookmark () {
selectToPreviousBookmark() {
if (this.markerLayer.getMarkerCount() > 0) {
const bufferRow = this.editor.getLastCursor().getMarker().getStartBufferPosition().row
const markers = this.markerLayer.getMarkers().sort((a, b) => b.compare(a))

View File

@ -2,9 +2,10 @@ const {CompositeDisposable} = require('atom')
const Bookmarks = require('./bookmarks')
const BookmarksView = require('./bookmarks-view')
const BookmarksProvider = require('./bookmarks-provider')
module.exports = {
activate (bookmarksByEditorId) {
activate(bookmarksByEditorId) {
this.bookmarksView = null
this.editorsBookmarks = []
this.disposables = new CompositeDisposable()
@ -44,7 +45,7 @@ module.exports = {
})
},
deactivate () {
deactivate() {
if (this.bookmarksView != null) {
this.bookmarksView.destroy()
this.bookmarksView = null
@ -56,11 +57,16 @@ module.exports = {
this.disposables.dispose()
},
serialize () {
serialize() {
const bookmarksByEditorId = {}
for (let bookmarks of this.editorsBookmarks) {
bookmarksByEditorId[bookmarks.editor.id] = bookmarks.serialize()
}
return bookmarksByEditorId
},
provideBookmarks() {
this.bookmarksProvider ??= new BookmarksProvider(this)
return this.bookmarksProvider
}
}

View File

@ -10,5 +10,14 @@
},
"dependencies": {
"atom-select-list": "^0.7.0"
},
"providedServices": {
"bookmarks": {
"description": "Provides a list of bookmarks to any package that wants to know about them.",
"versions": {
"1.0.0": "provideBookmarks"
}
}
}
}

View File

@ -1,5 +1,5 @@
describe('Bookmarks package', () => {
let [workspaceElement, editorElement, editor, bookmarks] = []
let workspaceElement, editorElement, editor, bookmarks, provider
const bookmarkedRangesForEditor = editor => {
const decorationsById = editor.decorationsStateForScreenRowRange(0, editor.getLastScreenRow())
@ -18,6 +18,7 @@ describe('Bookmarks package', () => {
await atom.workspace.open('sample.js')
bookmarks = (await atom.packages.activatePackage('bookmarks')).mainModule
provider = bookmarks.bookmarksProvider
jasmine.attachToDOM(workspaceElement)
editor = atom.workspace.getActiveTextEditor()
@ -32,17 +33,28 @@ describe('Bookmarks package', () => {
expect(bookmarkedRangesForEditor(editor)).toEqual([])
atom.commands.dispatch(editorElement, 'bookmarks:toggle-bookmark')
expect(bookmarkedRangesForEditor(editor)).toEqual([[[3, 10], [3, 10]]])
let marks = provider.getBookmarksForEditor(editor)
expect(marks.length).toBe(1)
expect(marks.map(m => m.getScreenRange())).toEqual(bookmarkedRangesForEditor(editor))
})
it('removes marker when toggled', () => {
let callback = jasmine.createSpy()
let instance = provider.getInstanceForEditor(editor)
instance.onDidChangeBookmarks(callback)
editor.setCursorBufferPosition([3, 10])
expect(bookmarkedRangesForEditor(editor).length).toBe(0)
atom.commands.dispatch(editorElement, 'bookmarks:toggle-bookmark')
expect(bookmarkedRangesForEditor(editor).length).toBe(1)
expect(callback.callCount).toBe(1)
atom.commands.dispatch(editorElement, 'bookmarks:toggle-bookmark')
expect(bookmarkedRangesForEditor(editor).length).toBe(0)
expect(callback.callCount).toBe(2)
})
})
@ -53,6 +65,8 @@ describe('Bookmarks package', () => {
expect(bookmarkedRangesForEditor(editor)).toEqual([])
atom.commands.dispatch(editorElement, 'bookmarks:toggle-bookmark')
expect(bookmarkedRangesForEditor(editor)).toEqual([[[3, 10], [3, 10]], [[6, 11], [6, 11]]])
let instance = provider.getInstanceForEditor(editor)
expect(instance.getAllBookmarks().length).toBe(2)
})
it('removes multiple markers when toggled', () => {
@ -215,27 +229,39 @@ describe('Bookmarks package', () => {
})
it('clears all bookmarks', () => {
let callback = jasmine.createSpy()
let instance = provider.getInstanceForEditor(editor)
instance.onDidChangeBookmarks(callback)
editor.setCursorBufferPosition([3, 10])
atom.commands.dispatch(editorElement, 'bookmarks:toggle-bookmark')
expect(callback.callCount).toBe(1)
editor.setCursorBufferPosition([5, 0])
atom.commands.dispatch(editorElement, 'bookmarks:toggle-bookmark')
expect(callback.callCount).toBe(2)
atom.commands.dispatch(editorElement, 'bookmarks:clear-bookmarks')
expect(getBookmarkedLineNodes(editorElement).length).toBe(0)
expect(callback.callCount).toBe(3)
})
})
describe('when a bookmark is invalidated', () => {
it('creates a marker when toggled', () => {
let callback = jasmine.createSpy()
let instance = provider.getInstanceForEditor(editor)
instance.onDidChangeBookmarks(callback)
editor.setCursorBufferPosition([3, 10])
expect(bookmarkedRangesForEditor(editor).length).toBe(0)
atom.commands.dispatch(editorElement, 'bookmarks:toggle-bookmark')
expect(bookmarkedRangesForEditor(editor).length).toBe(1)
expect(callback.callCount).toBe(1)
editor.setText('')
expect(bookmarkedRangesForEditor(editor).length).toBe(0)
expect(callback.callCount).toBe(2)
})
})

View File

@ -559,7 +559,7 @@ describe("FindView", () => {
it("displays the error", () => {
expect(findView.refs.descriptionLabel).toHaveClass("text-error");
expect(findView.refs.descriptionLabel.textContent).toBe("regular expression is too large");
expect(findView.refs.descriptionLabel.textContent).toContain("too large");
});
it("will be reset when there is no longer an error", () => {

View File

@ -909,12 +909,17 @@ describe('ResultsView', () => {
atom.commands.dispatch(projectFindView.element, 'core:confirm');
await resultsPromise();
let resultsPane = getResultsPane();
const resultsPane = getResultsPane();
await genPromiseToCheck( () =>
resultsPane?.refs?.previewCount?.textContent.match(/3 files/)
);
expect(resultsPane.refs.previewCount.textContent).toContain('3 files');
projectFindView.findEditor.setText('');
atom.commands.dispatch(projectFindView.element, 'core:confirm');
await etch.update(resultsPane);
await genPromiseToCheck( () =>
resultsPane.refs.previewCount.textContent.match(/Project/)
);
expect(resultsPane.refs.previewCount.textContent).toContain('Project search results');
})
});

View File

@ -0,0 +1 @@
spec/fixtures

3
packages/notifications/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
npm-debug.log
node_modules

View File

@ -0,0 +1,20 @@
Copyright (c) 2014 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,28 @@
### Requirements
* Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.
* All new code requires tests to ensure against regressions
### Description of the Change
<!--
We must be able to understand the design of your change from this description. If we can't get a good idea of what the code will be doing from the description here, the pull request may be closed at the maintainers' discretion. Keep in mind that the maintainer reviewing this PR may not be familiar with or have worked with the code here recently, so please walk us through the concepts.
-->
### Alternate Designs
<!-- Explain what other alternates were considered and why the proposed version was selected -->
### Benefits
<!-- What benefits will be realized by the code change? -->
### Possible Drawbacks
<!-- What are the possible side-effects or negative impacts of the code change? -->
### Applicable Issues
<!-- Enter any applicable Issues here -->

View File

@ -0,0 +1,8 @@
# Notifications package
![notifications](https://cloud.githubusercontent.com/assets/69169/5176406/350d0e80-73fd-11e4-8101-1776b9d6d8bf.gif)
### Docs
Notifications are available for use in your Pulsar packages via the `atom.notifications` `NotificationManager` object. See
https://atom.io/docs/api/latest/NotificationManager and https://atom.io/docs/api/latest/Notification for documentation.

View File

@ -0,0 +1,11 @@
# Keybindings require three things to be fully defined: A selector that is
# matched against the focused element, the keystroke and the command to
# execute.
#
# Below is a basic keybinding which registers on all platforms by applying to
# the root workspace element.
# For more detailed documentation see
# https://atom.io/docs/latest/advanced/keymaps
'atom-workspace':
'cmd-alt-t': 'notifications:trigger-error'

View File

@ -0,0 +1,214 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
// Originally from lee-dohm/bug-report
// https://github.com/lee-dohm/bug-report/blob/master/lib/command-logger.coffee
// Command names that are ignored and not included in the log. This uses an Object to provide fast
// string matching.
let CommandLogger;
const ignoredCommands = {
'show.bs.tooltip': true,
'shown.bs.tooltip': true,
'hide.bs.tooltip': true,
'hidden.bs.tooltip': true,
'editor:display-updated': true,
'mousewheel': true
};
// Ten minutes in milliseconds.
const tenMinutes = 10 * 60 * 1000;
// Public: Handles logging all of the Pulsar commands for the automatic repro steps feature.
//
// It uses an array as a circular data structure to log only the most recent commands.
module.exports =
(CommandLogger = (function() {
CommandLogger = class CommandLogger {
static initClass() {
// Public: Maximum size of the log.
this.prototype.logSize = 16;
}
static instance() {
return this._instance != null ? this._instance : (this._instance = new CommandLogger);
}
static start() {
return this.instance().start();
}
// Public: Creates a new logger.
constructor() {
this.initLog();
}
start() {
return atom.commands.onWillDispatch(event => {
return this.logCommand(event);
});
}
// Public: Formats the command log for the bug report.
//
// * `externalData` An {Object} containing other information to include in the log.
//
// Returns a {String} of the Markdown for the report.
getText(externalData) {
const lines = [];
const lastTime = Date.now();
this.eachEvent(event => {
if (event.time > lastTime) { return; }
if (!event.name || ((lastTime - event.time) >= tenMinutes)) { return; }
return lines.push(this.formatEvent(event, lastTime));
});
if (externalData) {
lines.push(` ${this.formatTime(0)} ${externalData.title}`);
}
lines.unshift('```');
lines.push('```');
return lines.join("\n");
}
// Public: Gets the latest event from the log.
//
// Returns the event {Object}.
latestEvent() {
return this.eventLog[this.logIndex];
}
// Public: Logs the command.
//
// * `command` Command {Object} to be logged
// * `type` Name {String} of the command
// * `target` {String} describing where the command was triggered
logCommand(command) {
const {type: name, target, time} = command;
if (command.detail != null ? command.detail.jQueryTrigger : undefined) { return; }
if (name in ignoredCommands) { return; }
let event = this.latestEvent();
if (event.name === name) {
return event.count++;
} else {
this.logIndex = (this.logIndex + 1) % this.logSize;
event = this.latestEvent();
event.name = name;
event.targetNodeName = target.nodeName;
event.targetClassName = target.className;
event.targetId = target.id;
event.count = 1;
return event.time = time != null ? time : Date.now();
}
}
// Private: Calculates the time of the last event to be reported.
//
// * `data` Data from an external bug passed in from another package.
//
// Returns the {Date} of the last event that should be reported.
calculateLastEventTime(data) {
if (data) { return data.time; }
let lastTime = null;
this.eachEvent(event => lastTime = event.time);
return lastTime;
}
// Private: Executes a function on each event in chronological order.
//
// This function is used instead of similar underscore functions because the log is held in a
// circular buffer.
//
// * `fn` {Function} to execute for each event in the log.
// * `event` An {Object} describing the event passed to your function.
//
// ## Examples
//
// This code would output the name of each event to the console.
//
// ```coffee
// logger.eachEvent (event) ->
// console.log event.name
// ```
eachEvent(fn) {
for (let offset = 1, end = this.logSize, asc = 1 <= end; asc ? offset <= end : offset >= end; asc ? offset++ : offset--) {
fn(this.eventLog[(this.logIndex + offset) % this.logSize]);
}
}
// Private: Format the command count for reporting.
//
// Returns the {String} format of the command count.
formatCount(count) {
switch (false) {
case !(count < 2): return ' ';
case !(count < 10): return ` ${count}x`;
case !(count < 100): return ` ${count}x`;
}
}
// Private: Formats a command event for reporting.
//
// * `event` Event {Object} to be formatted.
// * `lastTime` {Date} of the last event to report.
//
// Returns the {String} format of the command event.
formatEvent(event, lastTime) {
const {count, time, name, targetNodeName, targetClassName, targetId} = event;
const nodeText = targetNodeName.toLowerCase();
const idText = targetId ? `#${targetId}` : '';
let classText = '';
if (targetClassName != null) { for (var klass of Array.from(targetClassName.split(" "))) { classText += `.${klass}`; } }
return `${this.formatCount(count)} ${this.formatTime(lastTime - time)} ${name} (${nodeText}${idText}${classText})`;
}
// Private: Format the command time for reporting.
//
// * `time` {Date} to format
//
// Returns the {String} format of the command time.
formatTime(time) {
const minutes = Math.floor(time / 60000);
let seconds = Math.floor(((time % 60000) / 1000) * 10) / 10;
if (seconds < 10) { seconds = `0${seconds}`; }
if (Math.floor(seconds) !== seconds) { seconds = `${seconds}.0`; }
return `-${minutes}:${seconds}`;
}
// Private: Initializes the log structure for speed.
initLog() {
this.logIndex = 0;
return this.eventLog = __range__(0, this.logSize, false).map((i) => ({
name: null,
count: 0,
targetNodeName: null,
targetClassName: null,
targetId: null,
time: null
}));
}
};
CommandLogger.initClass();
return CommandLogger;
})());
function __range__(left, right, inclusive) {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}

View File

@ -0,0 +1,192 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const {Notification, CompositeDisposable} = require('atom');
const fs = require('fs-plus');
let StackTraceParser = null;
const NotificationElement = require('./notification-element');
const NotificationsLog = require('./notifications-log');
const Notifications = {
isInitialized: false,
subscriptions: null,
duplicateTimeDelay: 500,
lastNotification: null,
activate(state) {
let notification;
const CommandLogger = require('./command-logger');
CommandLogger.start();
this.subscriptions = new CompositeDisposable;
for (notification of Array.from(atom.notifications.getNotifications())) { this.addNotificationView(notification); }
this.subscriptions.add(atom.notifications.onDidAddNotification(notification => this.addNotificationView(notification)));
this.subscriptions.add(atom.onWillThrowError(function({message, url, line, originalError, preventDefault}) {
let match;
if (originalError.name === 'BufferedProcessError') {
message = message.replace('Uncaught BufferedProcessError: ', '');
return atom.notifications.addError(message, {dismissable: true});
} else if ((originalError.code === 'ENOENT') && !/\/atom/i.test(message) && (match = /spawn (.+) ENOENT/.exec(message))) {
message = `\
'${match[1]}' could not be spawned.
Is it installed and on your path?
If so please open an issue on the package spawning the process.\
`;
return atom.notifications.addError(message, {dismissable: true});
} else if (!atom.inDevMode() || atom.config.get('notifications.showErrorsInDevMode')) {
preventDefault();
// Ignore errors with no paths in them since they are impossible to trace
if (originalError.stack && !isCoreOrPackageStackTrace(originalError.stack)) {
return;
}
const options = {
detail: `${url}:${line}`,
stack: originalError.stack,
dismissable: true
};
return atom.notifications.addFatalError(message, options);
}
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'core:cancel', () => (() => {
const result = [];
for (notification of Array.from(atom.notifications.getNotifications())) { result.push(notification.dismiss());
}
return result;
})())
);
this.subscriptions.add(atom.config.observe('notifications.defaultTimeout', value => { return this.visibilityDuration = value; }));
if (atom.inDevMode()) {
this.subscriptions.add(atom.commands.add('atom-workspace', 'notifications:trigger-error', function() {
try {
return abc + 2; // nope
} catch (error) {
const options = {
detail: error.stack.split('\n')[1],
stack: error.stack,
dismissable: true
};
return atom.notifications.addFatalError(`Uncaught ${error.stack.split('\n')[0]}`, options);
}
})
);
}
if (this.notificationsLog != null) { this.addNotificationsLogSubscriptions(); }
this.subscriptions.add(atom.workspace.addOpener(uri => { if (uri === NotificationsLog.prototype.getURI()) { return this.createLog(); } }));
this.subscriptions.add(atom.commands.add('atom-workspace', 'notifications:toggle-log', () => atom.workspace.toggle(NotificationsLog.prototype.getURI())));
return this.subscriptions.add(atom.commands.add('atom-workspace', 'notifications:clear-log', function() {
for (notification of Array.from(atom.notifications.getNotifications())) {
notification.options.dismissable = true;
notification.dismissed = false;
notification.dismiss();
}
return atom.notifications.clear();
})
);
},
deactivate() {
this.subscriptions.dispose();
if (this.notificationsElement != null) {
this.notificationsElement.remove();
}
if (this.notificationsPanel != null) {
this.notificationsPanel.destroy();
}
if (this.notificationsLog != null) {
this.notificationsLog.destroy();
}
this.subscriptions = null;
this.notificationsElement = null;
this.notificationsPanel = null;
return this.isInitialized = false;
},
initializeIfNotInitialized() {
if (this.isInitialized) { return; }
this.subscriptions.add(atom.views.addViewProvider(Notification, model => {
return new NotificationElement(model, this.visibilityDuration);
})
);
this.notificationsElement = document.createElement('atom-notifications');
atom.views.getView(atom.workspace).appendChild(this.notificationsElement);
return this.isInitialized = true;
},
createLog(state) {
this.notificationsLog = new NotificationsLog(this.duplicateTimeDelay, state != null ? state.typesHidden : undefined);
if (this.subscriptions != null) { this.addNotificationsLogSubscriptions(); }
return this.notificationsLog;
},
addNotificationsLogSubscriptions() {
this.subscriptions.add(this.notificationsLog.onDidDestroy(() => { return this.notificationsLog = null; }));
return this.subscriptions.add(this.notificationsLog.onItemClick(notification => {
const view = atom.views.getView(notification);
view.makeDismissable();
if (!view.element.classList.contains('remove')) { return; }
view.element.classList.remove('remove');
this.notificationsElement.appendChild(view.element);
notification.dismissed = false;
return notification.setDisplayed(true);
})
);
},
addNotificationView(notification) {
if (notification == null) { return; }
this.initializeIfNotInitialized();
if (notification.wasDisplayed()) { return; }
if (this.lastNotification != null) {
// do not show duplicates unless some amount of time has passed
const timeSpan = notification.getTimestamp() - this.lastNotification.getTimestamp();
if (!(timeSpan < this.duplicateTimeDelay) || !notification.isEqual(this.lastNotification)) {
this.notificationsElement.appendChild(atom.views.getView(notification).element);
if (this.notificationsLog != null) {
this.notificationsLog.addNotification(notification);
}
}
} else {
this.notificationsElement.appendChild(atom.views.getView(notification).element);
if (this.notificationsLog != null) {
this.notificationsLog.addNotification(notification);
}
}
notification.setDisplayed(true);
return this.lastNotification = notification;
}
};
var isCoreOrPackageStackTrace = function(stack) {
if (StackTraceParser == null) { StackTraceParser = require('stacktrace-parser'); }
for (var {file} of Array.from(StackTraceParser.parse(stack))) {
if ((file === '<embedded>') || fs.isAbsolute(file)) {
return true;
}
}
return false;
};
module.exports = Notifications;

View File

@ -0,0 +1,367 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
let NotificationElement;
const createDOMPurify = require('dompurify');
const fs = require('fs-plus');
const path = require('path');
const marked = require('marked');
const {shell} = require('electron');
const NotificationIssue = require('./notification-issue');
const TemplateHelper = require('./template-helper');
const UserUtilities = require('./user-utilities');
let DOMPurify = null;
const NotificationTemplate = `\
<div class="content">
<div class="message item"></div>
<div class="detail item">
<div class="detail-content"></div>
<a href="#" class="stack-toggle"></a>
<div class="stack-container"></div>
</div>
<div class="meta item"></div>
</div>
<div class="close icon icon-x"></div>
<div class="close-all btn btn-error">Close All</div>\
`;
const FatalMetaNotificationTemplate = `\
<div class="description fatal-notification"></div>
<div class="btn-toolbar">
<a href="#" class="btn-issue btn btn-error"></a>
<a href="#" class="btn-copy-report icon icon-clippy" title="Copy error report to clipboard"></a>
</div>\
`;
const MetaNotificationTemplate = `\
<div class="description"></div>\
`;
const ButtonListTemplate = `\
<div class="btn-toolbar"></div>\
`;
const ButtonTemplate = `\
<a href="#" class="btn"></a>\
`;
module.exports =
(NotificationElement = (function() {
NotificationElement = class NotificationElement {
static initClass() {
this.prototype.animationDuration = 360;
this.prototype.visibilityDuration = 5000;
this.prototype.autohideTimeout = null;
}
constructor(model, visibilityDuration) {
this.model = model;
this.visibilityDuration = visibilityDuration;
this.fatalTemplate = TemplateHelper.create(FatalMetaNotificationTemplate);
this.metaTemplate = TemplateHelper.create(MetaNotificationTemplate);
this.buttonListTemplate = TemplateHelper.create(ButtonListTemplate);
this.buttonTemplate = TemplateHelper.create(ButtonTemplate);
this.element = document.createElement('atom-notification');
if (this.model.getType() === 'fatal') { this.issue = new NotificationIssue(this.model); }
this.renderPromise = this.render().catch(function(e) {
console.error(e.message);
return console.error(e.stack);
});
this.model.onDidDismiss(() => this.removeNotification());
if (!this.model.isDismissable()) {
this.autohide();
this.element.addEventListener('click', this.makeDismissable.bind(this), {once: true});
}
this.element.issue = this.issue;
this.element.getRenderPromise = this.getRenderPromise.bind(this);
}
getModel() { return this.model; }
getRenderPromise() { return this.renderPromise; }
render() {
let detail, metaContainer, metaContent;
this.element.classList.add(`${this.model.getType()}`);
this.element.classList.add("icon", `icon-${this.model.getIcon()}`, "native-key-bindings");
if (detail = this.model.getDetail()) { this.element.classList.add('has-detail'); }
if (this.model.isDismissable()) { this.element.classList.add('has-close'); }
if (detail && (this.model.getOptions().stack != null)) { this.element.classList.add('has-stack'); }
this.element.setAttribute('tabindex', '-1');
this.element.innerHTML = NotificationTemplate;
const options = this.model.getOptions();
const notificationContainer = this.element.querySelector('.message');
if (DOMPurify === null) {
DOMPurify = createDOMPurify();
}
notificationContainer.innerHTML = DOMPurify.sanitize(marked(this.model.getMessage()));
if (detail = this.model.getDetail()) {
let stack;
addSplitLinesToContainer(this.element.querySelector('.detail-content'), detail);
if (stack = options.stack) {
const stackToggle = this.element.querySelector('.stack-toggle');
const stackContainer = this.element.querySelector('.stack-container');
addSplitLinesToContainer(stackContainer, stack);
stackToggle.addEventListener('click', e => this.handleStackTraceToggleClick(e, stackContainer));
this.handleStackTraceToggleClick({currentTarget: stackToggle}, stackContainer);
}
}
if (metaContent = options.description) {
this.element.classList.add('has-description');
metaContainer = this.element.querySelector('.meta');
metaContainer.appendChild(TemplateHelper.render(this.metaTemplate));
const description = this.element.querySelector('.description');
description.innerHTML = marked(metaContent);
}
if (options.buttons && (options.buttons.length > 0)) {
this.element.classList.add('has-buttons');
metaContainer = this.element.querySelector('.meta');
metaContainer.appendChild(TemplateHelper.render(this.buttonListTemplate));
const toolbar = this.element.querySelector('.btn-toolbar');
let buttonClass = this.model.getType();
if (buttonClass === 'fatal') { buttonClass = 'error'; }
buttonClass = `btn-${buttonClass}`;
options.buttons.forEach(button => {
toolbar.appendChild(TemplateHelper.render(this.buttonTemplate));
const buttonEl = toolbar.childNodes[toolbar.childNodes.length - 1];
buttonEl.textContent = button.text;
buttonEl.classList.add(buttonClass);
if (button.className != null) {
buttonEl.classList.add.apply(buttonEl.classList, button.className.split(' '));
}
if (button.onDidClick != null) {
return buttonEl.addEventListener('click', e => {
return button.onDidClick.call(this, e);
});
}
});
}
const closeButton = this.element.querySelector('.close');
closeButton.addEventListener('click', () => this.handleRemoveNotificationClick());
const closeAllButton = this.element.querySelector('.close-all');
closeAllButton.classList.add(this.getButtonClass());
closeAllButton.addEventListener('click', () => this.handleRemoveAllNotificationsClick());
if (this.model.getType() === 'fatal') {
return this.renderFatalError();
} else {
return Promise.resolve();
}
}
renderFatalError() {
const repoUrl = this.issue.getRepoUrl();
const packageName = this.issue.getPackageName();
const fatalContainer = this.element.querySelector('.meta');
fatalContainer.appendChild(TemplateHelper.render(this.fatalTemplate));
const fatalNotification = this.element.querySelector('.fatal-notification');
const issueButton = fatalContainer.querySelector('.btn-issue');
const copyReportButton = fatalContainer.querySelector('.btn-copy-report');
atom.tooltips.add(copyReportButton, {title: copyReportButton.getAttribute('title')});
copyReportButton.addEventListener('click', e => {
e.preventDefault();
return this.issue.getIssueBody().then(issueBody => atom.clipboard.write(issueBody));
});
if ((packageName != null) && (repoUrl != null)) {
fatalNotification.innerHTML = `The error was thrown from the <a href=\"${repoUrl}\">${packageName} package</a>. `;
} else if (packageName != null) {
issueButton.remove();
fatalNotification.textContent = `The error was thrown from the ${packageName} package. `;
} else {
fatalNotification.textContent = "This is likely a bug in Pulsar. ";
}
// We only show the create issue button if it's clearly in atom core or in a package with a repo url
if (issueButton.parentNode != null) {
if ((packageName != null) && (repoUrl != null)) {
issueButton.textContent = `Create issue on the ${packageName} package`;
} else {
issueButton.textContent = "Create issue on pulsar-edit/pulsar";
}
const promises = [];
promises.push(this.issue.findSimilarIssues());
promises.push(UserUtilities.checkPulsarUpToDate());
if (packageName != null) {
promises.push(UserUtilities.checkPackageUpToDate(packageName));
}
return Promise.all(promises).then(allData => {
let issue;
const [issues, atomCheck, packageCheck] = Array.from(allData);
if ((issues != null ? issues.open : undefined) || (issues != null ? issues.closed : undefined)) {
issue = issues.open || issues.closed;
issueButton.setAttribute('href', issue.html_url);
issueButton.textContent = "View Issue";
fatalNotification.innerHTML += " This issue has already been reported.";
} else if ((packageCheck != null) && !packageCheck.upToDate && !packageCheck.isCore) {
issueButton.setAttribute('href', '#');
issueButton.textContent = "Check for package updates";
issueButton.addEventListener('click', function(e) {
e.preventDefault();
const command = 'settings-view:check-for-package-updates';
return atom.commands.dispatch(atom.views.getView(atom.workspace), command);
});
fatalNotification.innerHTML += `\
<code>${packageName}</code> is out of date: ${packageCheck.installedVersion} installed;
${packageCheck.latestVersion} latest.
Upgrading to the latest version may fix this issue.\
`;
} else if ((packageCheck != null) && !packageCheck.upToDate && packageCheck.isCore) {
issueButton.remove();
fatalNotification.innerHTML += `\
<br><br>
Locally installed core Pulsar package <code>${packageName}</code> is out of date: ${packageCheck.installedVersion} installed locally;
${packageCheck.versionShippedWithPulsar} included with the version of Pulsar you're running.
Removing the locally installed version may fix this issue.\
`;
const packagePath = __guard__(atom.packages.getLoadedPackage(packageName), x => x.path);
if (fs.isSymbolicLinkSync(packagePath)) {
fatalNotification.innerHTML += `\
<br><br>
Use: <code>apm unlink ${packagePath}</code>\
`;
}
} else if ((atomCheck != null) && !atomCheck.upToDate) {
issueButton.remove();
fatalNotification.innerHTML += `\
Pulsar is out of date: ${atomCheck.installedVersion} installed;
${atomCheck.latestVersion} latest.
Upgrading to the <a href='https://github.com/pulsar-edit/pulsar/releases/tag/v${atomCheck.latestVersion}'>latest version</a> may fix this issue.\
`;
} else {
fatalNotification.innerHTML += " You can help by creating an issue. Please explain what actions triggered this error.";
issueButton.addEventListener('click', e => {
e.preventDefault();
issueButton.classList.add('opening');
return this.issue.getIssueUrlForSystem().then(function(issueUrl) {
shell.openExternal(issueUrl);
return issueButton.classList.remove('opening');
});
});
}
});
} else {
return Promise.resolve();
}
}
makeDismissable() {
if (!this.model.isDismissable()) {
clearTimeout(this.autohideTimeout);
this.model.options.dismissable = true;
this.model.dismissed = false;
return this.element.classList.add('has-close');
}
}
removeNotification() {
if (!this.element.classList.contains('remove')) {
this.element.classList.add('remove');
return this.removeNotificationAfterTimeout();
}
}
handleRemoveNotificationClick() {
this.removeNotification();
return this.model.dismiss();
}
handleRemoveAllNotificationsClick() {
const notifications = atom.notifications.getNotifications();
for (var notification of Array.from(notifications)) {
atom.views.getView(notification).removeNotification();
if (notification.isDismissable() && !notification.isDismissed()) {
notification.dismiss();
}
}
}
handleStackTraceToggleClick(e, container) {
if (typeof e.preventDefault === 'function') {
e.preventDefault();
}
if (container.style.display === 'none') {
e.currentTarget.innerHTML = '<span class="icon icon-dash"></span>Hide Stack Trace';
return container.style.display = 'block';
} else {
e.currentTarget.innerHTML = '<span class="icon icon-plus"></span>Show Stack Trace';
return container.style.display = 'none';
}
}
autohide() {
return this.autohideTimeout = setTimeout(() => {
return this.removeNotification();
}
, this.visibilityDuration);
}
removeNotificationAfterTimeout() {
if (this.element === document.activeElement) { atom.workspace.getActivePane().activate(); }
return setTimeout(() => {
return this.element.remove();
}
, this.animationDuration); // keep in sync with CSS animation
}
getButtonClass() {
const type = `btn-${this.model.getType()}`;
if (type === 'btn-fatal') { return 'btn-error'; } else { return type; }
}
};
NotificationElement.initClass();
return NotificationElement;
})());
var addSplitLinesToContainer = function(container, content) {
if (typeof content !== 'string') { content = content.toString(); }
for (var line of Array.from(content.split('\n'))) {
var div = document.createElement('div');
div.classList.add('line');
div.textContent = line;
container.appendChild(div);
}
};
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

View File

@ -0,0 +1,317 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
let NotificationIssue;
const fs = require('fs-plus');
const path = require('path');
const StackTraceParser = require('stacktrace-parser');
const CommandLogger = require('./command-logger');
const UserUtilities = require('./user-utilities');
const TITLE_CHAR_LIMIT = 100; // Truncate issue title to 100 characters (including ellipsis)
const FileURLRegExp = new RegExp('file://\w*/(.*)');
module.exports = class NotificationIssue {
constructor(notification) {
this.normalizedStackPaths = this.normalizedStackPaths.bind(this);
this.notification = notification;
}
findSimilarIssues() {
let repoUrl = this.getRepoUrl();
if (repoUrl == null) { repoUrl = 'pulsar-edit/pulsar'; }
const repo = repoUrl.replace(/http(s)?:\/\/(\d+\.)?github.com\//gi, '');
const issueTitle = this.getIssueTitle();
const query = `${issueTitle} repo:${repo}`;
const githubHeaders = new Headers({
accept: 'application/vnd.github.v3+json',
contentType: "application/json"
});
return fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(query)}&sort=created`, {headers: githubHeaders})
.then(r => r != null ? r.json() : undefined)
.then(function(data) {
if ((data != null ? data.items : undefined) != null) {
const issues = {};
for (var issue of Array.from(data.items)) {
if ((issue.title.indexOf(issueTitle) > -1) && (issues[issue.state] == null)) {
issues[issue.state] = issue;
if ((issues.open != null) && (issues.closed != null)) { return issues; }
}
}
if ((issues.open != null) || (issues.closed != null)) { return issues; }
}
return null;
}).catch(_ => null);
}
getIssueUrlForSystem() {
// Windows will not launch URLs greater than ~2000 bytes so we need to shrink it
// Also is.gd has a limit of 5000 bytes...
return this.getIssueUrl().then(issueUrl => fetch("https://is.gd/create.php?format=simple", {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `url=${encodeURIComponent(issueUrl)}`
})
.then(r => r.text())
.catch(e => null));
}
getIssueUrl() {
return this.getIssueBody().then(issueBody => {
let repoUrl = this.getRepoUrl();
if (repoUrl == null) {
repoUrl = 'https://github.com/pulsar-edit/pulsar';
}
return `${repoUrl}/issues/new?title=${this.encodeURI(this.getIssueTitle())}&body=${this.encodeURI(issueBody)}`;
});
}
encodeURI(str) {
return encodeURI(str).replace(/#/g, '%23').replace(/;/g, '%3B').replace(/%20/g, '+');
}
getIssueTitle() {
let title = this.notification.getMessage();
title = title.replace(process.env.ATOM_HOME, '$ATOM_HOME');
if (process.platform === 'win32') {
title = title.replace(process.env.USERPROFILE, '~');
title = title.replace(path.sep, path.posix.sep); // Standardize issue titles
} else {
title = title.replace(process.env.HOME, '~');
}
if (title.length > TITLE_CHAR_LIMIT) {
title = title.substring(0, TITLE_CHAR_LIMIT - 3) + '...';
}
return title.replace(/\r?\n|\r/g, "");
}
getIssueBody() {
return new Promise((resolve, reject) => {
if (this.issueBody) { return resolve(this.issueBody); }
const systemPromise = UserUtilities.getOSVersion();
const nonCorePackagesPromise = UserUtilities.getNonCorePackages();
return Promise.all([systemPromise, nonCorePackagesPromise]).then(all => {
let packageMessage, packageVersion;
const [systemName, nonCorePackages] = Array.from(all);
const message = this.notification.getMessage();
const options = this.notification.getOptions();
const repoUrl = this.getRepoUrl();
const packageName = this.getPackageName();
if (packageName != null) {
packageVersion = atom.packages.getLoadedPackage(packageName)?.metadata?.version;
}
const copyText = '';
const systemUser = process.env.USER;
let rootUserStatus = '';
if (systemUser === 'root') {
rootUserStatus = '**User**: root';
}
if ((packageName != null) && (repoUrl != null)) {
packageMessage = `[${packageName}](${repoUrl}) package ${packageVersion}`;
} else if (packageName != null) {
packageMessage = `'${packageName}' package v${packageVersion}`;
} else {
packageMessage = 'Pulsar Core';
}
this.issueBody = `\
<!--
Have you read Pulsar's Code of Conduct? By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/atom/.github/blob/master/CODE_OF_CONDUCT.md
Do you want to ask a question? Are you looking for support? The Pulsar message board is the best place for getting support: https://discuss.atom.io
-->
### Prerequisites
* [ ] Put an X between the brackets on this line if you have done all of the following:
* Reproduced the problem in Safe Mode: <https://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode>
* Followed all applicable steps in the debugging guide: <https://flight-manual.atom.io/hacking-atom/sections/debugging/>
* Checked the FAQs on the message board for common solutions: <https://discuss.atom.io/c/faq>
* Checked that your issue isn't already filed: <https://github.com/issues?q=is%3Aissue+user%3Aatom>
* Checked that there is not already an Pulsar package that provides the described functionality: <https://atom.io/packages>
### Description
<!-- Description of the issue -->
### Steps to Reproduce
1. <!-- First Step -->
2. <!-- Second Step -->
3. <!-- and so on -->
**Expected behavior:**
<!-- What you expect to happen -->
**Actual behavior:**
<!-- What actually happens -->
### Versions
**Pulsar**: ${atom.getVersion()} ${process.arch}
**Electron**: ${process.versions.electron}
**OS**: ${systemName}
**Thrown From**: ${packageMessage}
${rootUserStatus}
### Stack Trace
${message}
\`\`\`
At ${options.detail}
${this.normalizedStackPaths(options.stack)}
\`\`\`
### Commands
${CommandLogger.instance().getText()}
### Non-Core Packages
\`\`\`
${nonCorePackages.join('\n')}
\`\`\`
### Additional Information
<!-- Any additional information, configuration or data that might be necessary to reproduce the issue. -->
${copyText}\
`;
return resolve(this.issueBody);
});
});
}
normalizedStackPaths(stack) {
return stack != null ? stack.replace(/(^\W+at )([\w.]{2,} [(])?(.*)(:\d+:\d+[)]?)/gm, (m, p1, p2, p3, p4) => p1 + (p2 || '') +
this.normalizePath(p3) + p4
) : undefined;
}
normalizePath(path) {
return path.replace('file:///', '') // Randomly inserted file url protocols
.replace(/[/]/g, '\\') // Temp switch for Windows home matching
.replace(fs.getHomeDirectory(), '~') // Remove users home dir for apm-dev'ed packages
.replace(/\\/g, '/') // Switch \ back to / for everyone
.replace(/.*(\/(app\.asar|packages\/).*)/, '$1'); // Remove everything before app.asar or pacakges
}
getRepoUrl() {
const packageName = this.getPackageName();
if (packageName == null) { return; }
let repo = atom.packages.getLoadedPackage(packageName)?.metadata?.repository;
let repoUrl = (repo != null ? repo.url : undefined) != null ? (repo != null ? repo.url : undefined) : repo;
if (!repoUrl) {
let packagePath;
if (packagePath = atom.packages.resolvePackagePath(packageName)) {
try {
repo = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json')))?.repository;
repoUrl = (repo != null ? repo.url : undefined) != null ? (repo != null ? repo.url : undefined) : repo;
} catch (error) {}
}
}
return repoUrl != null ? repoUrl.replace(/\.git$/, '').replace(/^git\+/, '') : undefined;
}
getPackageNameFromFilePath(filePath) {
if (!filePath) { return; }
let packageName = __guard__(/\/\.atom\/dev\/packages\/([^\/]+)\//.exec(filePath), x => x[1]);
if (packageName) { return packageName; }
packageName = __guard__(/\\\.atom\\dev\\packages\\([^\\]+)\\/.exec(filePath), x1 => x1[1]);
if (packageName) { return packageName; }
packageName = __guard__(/\/\.atom\/packages\/([^\/]+)\//.exec(filePath), x2 => x2[1]);
if (packageName) { return packageName; }
packageName = __guard__(/\\\.atom\\packages\\([^\\]+)\\/.exec(filePath), x3 => x3[1]);
if (packageName) { return packageName; }
}
getPackageName() {
let packageName, packagePath;
const options = this.notification.getOptions();
if (options.packageName != null) { return options.packageName; }
if ((options.stack == null) && (options.detail == null)) { return; }
const packagePaths = this.getPackagePathsByPackageName();
for (packageName in packagePaths) {
packagePath = packagePaths[packageName];
if ((packagePath.indexOf(path.join('.atom', 'dev', 'packages')) > -1) || (packagePath.indexOf(path.join('.atom', 'packages')) > -1)) {
packagePaths[packageName] = fs.realpathSync(packagePath);
}
}
const getPackageName = filePath => {
let match;
filePath = /\((.+?):\d+|\((.+)\)|(.+)/.exec(filePath)[0];
// Stack traces may be a file URI
if (match = FileURLRegExp.exec(filePath)) {
filePath = match[1];
}
filePath = path.normalize(filePath);
if (path.isAbsolute(filePath)) {
for (var packName in packagePaths) {
packagePath = packagePaths[packName];
if (filePath === 'node.js') { continue; }
var isSubfolder = filePath.indexOf(path.normalize(packagePath + path.sep)) === 0;
if (isSubfolder) { return packName; }
}
}
return this.getPackageNameFromFilePath(filePath);
};
if ((options.detail != null) && (packageName = getPackageName(options.detail))) {
return packageName;
}
if (options.stack != null) {
const stack = StackTraceParser.parse(options.stack);
for (let i = 0, end = stack.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
var {file} = stack[i];
// Empty when it was run from the dev console
if (!file) { return; }
packageName = getPackageName(file);
if (packageName != null) { return packageName; }
}
}
}
getPackagePathsByPackageName() {
const packagePathsByPackageName = {};
for (var pack of Array.from(atom.packages.getLoadedPackages())) {
packagePathsByPackageName[pack.name] = pack.path;
}
return packagePathsByPackageName;
}
}
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

View File

@ -0,0 +1,110 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
let NotificationsLogItem;
const {Emitter, CompositeDisposable, Disposable} = require('atom');
const moment = require('moment');
module.exports = (NotificationsLogItem = (function() {
NotificationsLogItem = class NotificationsLogItem {
static initClass() {
this.prototype.subscriptions = null;
this.prototype.timestampInterval = null;
}
constructor(notification) {
this.notification = notification;
this.emitter = new Emitter;
this.subscriptions = new CompositeDisposable;
this.render();
}
render() {
const notificationView = atom.views.getView(this.notification);
const notificationElement = this.renderNotification(notificationView);
this.timestamp = document.createElement('div');
this.timestamp.classList.add('timestamp');
this.notification.moment = moment(this.notification.getTimestamp());
this.subscriptions.add(atom.tooltips.add(this.timestamp, {title: this.notification.moment.format("ll LTS")}));
this.updateTimestamp();
this.timestampInterval = setInterval(this.updateTimestamp.bind(this), 60 * 1000);
this.subscriptions.add(new Disposable(() => clearInterval(this.timestampInterval)));
this.element = document.createElement('li');
this.element.classList.add('notifications-log-item', this.notification.getType());
this.element.appendChild(notificationElement);
this.element.appendChild(this.timestamp);
this.element.addEventListener('click', e => {
if (e.target.closest('.btn-toolbar a, .btn-toolbar button') == null) {
return this.emitter.emit('click');
}
});
this.element.getRenderPromise = () => notificationView.getRenderPromise();
if (this.notification.getType() === 'fatal') {
notificationView.getRenderPromise().then(() => {
return this.element.replaceChild(this.renderNotification(notificationView), notificationElement);
});
}
return this.subscriptions.add(new Disposable(() => this.element.remove()));
}
renderNotification(view) {
const message = document.createElement('div');
message.classList.add('message');
message.innerHTML = view.element.querySelector(".content > .message").innerHTML;
const buttons = document.createElement('div');
buttons.classList.add('btn-toolbar');
const nButtons = view.element.querySelector(".content > .meta > .btn-toolbar");
if (nButtons != null) {
for (var button of Array.from(nButtons.children)) {
var logButton = button.cloneNode(true);
logButton.originalButton = button;
logButton.addEventListener('click', function(e) {
const newEvent = new MouseEvent('click', e);
return e.target.originalButton.dispatchEvent(newEvent);
});
for (var tooltip of Array.from(atom.tooltips.findTooltips(button))) {
this.subscriptions.add(atom.tooltips.add(logButton, tooltip.options));
}
buttons.appendChild(logButton);
}
}
const nElement = document.createElement('div');
nElement.classList.add('notifications-log-notification', 'icon', `icon-${this.notification.getIcon()}`, this.notification.getType());
nElement.appendChild(message);
nElement.appendChild(buttons);
return nElement;
}
getElement() { return this.element; }
destroy() {
this.subscriptions.dispose();
return this.emitter.emit('did-destroy');
}
onClick(callback) {
return this.emitter.on('click', callback);
}
onDidDestroy(callback) {
return this.emitter.on('did-destroy', callback);
}
updateTimestamp() {
return this.timestamp.textContent = this.notification.moment.fromNow();
}
};
NotificationsLogItem.initClass();
return NotificationsLogItem;
})());

View File

@ -0,0 +1,148 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
let NotificationsLog;
const {Emitter, CompositeDisposable, Disposable} = require('atom');
const NotificationsLogItem = require('./notifications-log-item');
const typeIcons = {
fatal: 'bug',
error: 'flame',
warning: 'alert',
info: 'info',
success: 'check'
};
module.exports = (NotificationsLog = (function() {
NotificationsLog = class NotificationsLog {
static initClass() {
this.prototype.subscriptions = null;
this.prototype.logItems = [];
this.prototype.typesHidden = {
fatal: false,
error: false,
warning: false,
info: false,
success: false
};
}
constructor(duplicateTimeDelay, typesHidden = null) {
this.duplicateTimeDelay = duplicateTimeDelay;
if (typesHidden != null) { this.typesHidden = typesHidden; }
this.emitter = new Emitter;
this.subscriptions = new CompositeDisposable;
this.subscriptions.add(atom.notifications.onDidClearNotifications(() => this.clearLogItems()));
this.render();
this.subscriptions.add(new Disposable(() => this.clearLogItems()));
}
render() {
let button;
this.element = document.createElement('div');
this.element.classList.add('notifications-log');
const header = document.createElement('header');
this.element.appendChild(header);
this.list = document.createElement('ul');
this.list.classList.add('notifications-log-items');
this.element.appendChild(this.list);
for (var type in typeIcons) {
var icon = typeIcons[type];
button = document.createElement('button');
button.classList.add('notification-type', 'btn', 'icon', `icon-${icon}`, type);
button.classList.toggle('show-type', !this.typesHidden[type]);
this.list.classList.toggle(`hide-${type}`, this.typesHidden[type]);
button.dataset.type = type;
button.addEventListener('click', e => this.toggleType(e.target.dataset.type));
this.subscriptions.add(atom.tooltips.add(button, {title: `Toggle ${type} notifications`}));
header.appendChild(button);
}
button = document.createElement('button');
button.classList.add('notifications-clear-log', 'btn', 'icon', 'icon-trashcan');
button.addEventListener('click', e => atom.commands.dispatch(atom.views.getView(atom.workspace), "notifications:clear-log"));
this.subscriptions.add(atom.tooltips.add(button, {title: "Clear notifications"}));
header.appendChild(button);
let lastNotification = null;
for (var notification of Array.from(atom.notifications.getNotifications())) {
if (lastNotification != null) {
// do not show duplicates unless some amount of time has passed
var timeSpan = notification.getTimestamp() - lastNotification.getTimestamp();
if (!(timeSpan < this.duplicateTimeDelay) || !notification.isEqual(lastNotification)) {
this.addNotification(notification);
}
} else {
this.addNotification(notification);
}
lastNotification = notification;
}
return this.subscriptions.add(new Disposable(() => this.element.remove()));
}
destroy() {
this.subscriptions.dispose();
return this.emitter.emit('did-destroy');
}
getElement() { return this.element; }
getURI() { return 'atom://notifications/log'; }
getTitle() { return 'Log'; }
getLongTitle() { return 'Notifications Log'; }
getIconName() { return 'alert'; }
getDefaultLocation() { return 'bottom'; }
getAllowedLocations() { return ['left', 'right', 'bottom']; }
serialize() {
return {
typesHidden: this.typesHidden,
deserializer: 'notifications/NotificationsLog'
};
}
toggleType(type, force) {
const button = this.element.querySelector(`.notification-type.${type}`);
const hide = !button.classList.toggle('show-type', force);
this.list.classList.toggle(`hide-${type}`, hide);
return this.typesHidden[type] = hide;
}
addNotification(notification) {
const logItem = new NotificationsLogItem(notification);
logItem.onClick(() => this.emitter.emit('item-clicked', notification));
this.logItems.push(logItem);
return this.list.insertBefore(logItem.getElement(), this.list.firstChild);
}
onItemClick(callback) {
return this.emitter.on('item-clicked', callback);
}
onDidDestroy(callback) {
return this.emitter.on('did-destroy', callback);
}
clearLogItems() {
for (var logItem of Array.from(this.logItems)) { logItem.destroy(); }
return this.logItems = [];
}
};
NotificationsLog.initClass();
return NotificationsLog;
})());

View File

@ -0,0 +1,17 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
module.exports = {
create(htmlString) {
const template = document.createElement('template');
template.innerHTML = htmlString;
document.body.appendChild(template);
return template;
},
render(template) {
return document.importNode(template.content, true);
}
};

View File

@ -0,0 +1,193 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
const os = require('os');
const fs = require('fs');
const path = require('path');
const semver = require('semver');
const {BufferedProcess} = require('atom');
/*
A collection of methods for retrieving information about the user's system for
bug report purposes.
*/
const DEV_PACKAGE_PATH = path.join('dev', 'packages');
module.exports = {
/*
Section: System Information
*/
getPlatform() {
return os.platform();
},
// OS version strings lifted from https://github.com/lee-dohm/bug-report
getOSVersion() {
return new Promise((resolve, reject) => {
switch (this.getPlatform()) {
case 'darwin': return resolve(this.macVersionText());
case 'win32': return resolve(this.winVersionText());
case 'linux': return resolve(this.linuxVersionText());
default: return resolve(`${os.platform()} ${os.release()}`);
}
});
},
macVersionText() {
return this.macVersionInfo().then(function(info) {
if (!info.ProductName || !info.ProductVersion) { return 'Unknown macOS version'; }
return `${info.ProductName} ${info.ProductVersion}`;
});
},
macVersionInfo() {
return new Promise(function(resolve, reject) {
let stdout = '';
const plistBuddy = new BufferedProcess({
command: '/usr/libexec/PlistBuddy',
args: [
'-c',
'Print ProductVersion',
'-c',
'Print ProductName',
'/System/Library/CoreServices/SystemVersion.plist'
],
stdout(output) { return stdout += output; },
exit() {
const [ProductVersion, ProductName] = Array.from(stdout.trim().split('\n'));
return resolve({ProductVersion, ProductName});
}
});
return plistBuddy.onWillThrowError(function({handle}) {
handle();
return resolve({});
});
});
},
linuxVersionText() {
return this.linuxVersionInfo().then(function(info) {
if (info.DistroName && info.DistroVersion) {
return `${info.DistroName} ${info.DistroVersion}`;
} else {
return `${os.platform()} ${os.release()}`;
}
});
},
linuxVersionInfo() {
return new Promise(function(resolve, reject) {
let stdout = '';
const lsbRelease = new BufferedProcess({
command: 'lsb_release',
args: ['-ds'],
stdout(output) { return stdout += output; },
exit(exitCode) {
const [DistroName, DistroVersion] = Array.from(stdout.trim().split(' '));
return resolve({DistroName, DistroVersion});
}
});
return lsbRelease.onWillThrowError(function({handle}) {
handle();
return resolve({});
});
});
},
winVersionText() {
return new Promise(function(resolve, reject) {
const data = [];
const systemInfo = new BufferedProcess({
command: 'systeminfo',
stdout(oneLine) { return data.push(oneLine); },
exit() {
let res;
let info = data.join('\n');
info = (res = /OS.Name.\s+(.*)$/im.exec(info)) ? res[1] : 'Unknown Windows version';
return resolve(info);
}
});
return systemInfo.onWillThrowError(function({handle}) {
handle();
return resolve('Unknown Windows version');
});
});
},
/*
Section: Installed Packages
*/
getNonCorePackages() {
return new Promise(function(resolve, reject) {
const nonCorePackages = atom.packages.getAvailablePackageMetadata().filter(p => !atom.packages.isBundledPackage(p.name));
const devPackageNames = atom.packages.getAvailablePackagePaths().filter(p => p.includes(DEV_PACKAGE_PATH)).map(p => path.basename(p));
return resolve(Array.from(nonCorePackages).map((pack) => `${pack.name} ${pack.version} ${Array.from(devPackageNames).includes(pack.name) ? '(dev)' : ''}`));
});
},
checkPulsarUpToDate() {
const installedVersion = atom.getVersion().replace(/-.*$/, '');
return {
upToDate: true,
latestVersion: installedVersion,
installedVersion
}
},
getPackageVersion(packageName) {
const pack = atom.packages.getLoadedPackage(packageName);
return (pack != null ? pack.metadata.version : undefined);
},
getPackageVersionShippedWithPulsar(packageName) {
return require(path.join(atom.getLoadSettings().resourcePath, 'package.json')).packageDependencies[packageName];
},
getLatestPackageData(packageName) {
const githubHeaders = new Headers({
accept: 'application/json',
contentType: "application/json"
});
const apiURL = process.env.ATOM_API_URL || 'https://api.pulsar-edit.dev/api';
return fetch(`${apiURL}/${packageName}`, {headers: githubHeaders})
.then(r => {
if (r.ok) {
return r.json();
} else {
return Promise.reject(new Error(`Fetching updates resulted in status ${r.status}`));
}
});
},
checkPackageUpToDate(packageName) {
return this.getLatestPackageData(packageName).then(latestPackageData => {
let isCore;
const installedVersion = this.getPackageVersion(packageName);
let upToDate = (installedVersion != null) && semver.gte(installedVersion, latestPackageData?.releases?.latest);
const latestVersion = latestPackageData?.releases?.latest;
const versionShippedWithPulsar = this.getPackageVersionShippedWithPulsar(packageName);
if (isCore = (versionShippedWithPulsar != null)) {
// A core package is out of date if the version which is being used
// is lower than the version which normally ships with the version
// of Pulsar which is running. This will happen when there's a locally
// installed version of the package with a lower version than Pulsar's.
upToDate = (installedVersion != null) && semver.gte(installedVersion, versionShippedWithPulsar);
}
return {isCore, upToDate, latestVersion, installedVersion, versionShippedWithPulsar};
});
}
};

View File

@ -0,0 +1,39 @@
{
"name": "notifications",
"main": "./lib/main",
"version": "0.73.0",
"repository": "https://github.com/pulsar-edit/pulsar",
"description": "A tidy way to display Pulsar notifications.",
"license": "MIT",
"engines": {
"atom": ">0.50.0"
},
"dependencies": {
"dompurify": "^1.0.3",
"fs-plus": "^3.0.0",
"marked": "^0.3.6",
"moment": "^2.19.3",
"semver": "^4.3.2",
"stacktrace-parser": "^0.1.3",
"temp": "^0.8.1"
},
"devDependencies": {
"coffeelint": "^1.9.7"
},
"configSchema": {
"showErrorsInDevMode": {
"type": "boolean",
"default": false,
"description": "Show notifications for uncaught exceptions even if Pulsar is running in dev mode. If this config setting is disabled, uncaught exceptions will trigger the dev tools to open and be logged in the console tab."
},
"defaultTimeout": {
"type": "integer",
"default": 5000,
"minimum": 1000,
"description": "The default notification timeout for a non-dismissable notification."
}
},
"deserializers": {
"notifications/NotificationsLog": "createLog"
}
}

View File

@ -0,0 +1,141 @@
# Originally from lee-dohm/bug-report
CommandLogger = require '../lib/command-logger'
describe 'CommandLogger', ->
[element, logger] = []
dispatch = (command) ->
atom.commands.dispatch(element, command)
beforeEach ->
element = document.createElement("section")
element.id = "some-id"
element.className = "some-class another-class"
logger = new CommandLogger
logger.start()
describe 'logging of commands', ->
it 'catches the name of the command', ->
dispatch('foo:bar')
expect(logger.latestEvent().name).toBe 'foo:bar'
it 'catches the target of the command', ->
dispatch('foo:bar')
expect(logger.latestEvent().targetNodeName).toBe "SECTION"
expect(logger.latestEvent().targetClassName).toBe "some-class another-class"
expect(logger.latestEvent().targetId).toBe "some-id"
it 'logs repeat commands as one command', ->
dispatch('foo:bar')
dispatch('foo:bar')
expect(logger.latestEvent().name).toBe 'foo:bar'
expect(logger.latestEvent().count).toBe 2
it 'ignores show.bs.tooltip commands', ->
dispatch('show.bs.tooltip')
expect(logger.latestEvent().name).not.toBe 'show.bs.tooltip'
it 'ignores editor:display-updated commands', ->
dispatch('editor:display-updated')
expect(logger.latestEvent().name).not.toBe 'editor:display-updated'
it 'ignores mousewheel commands', ->
dispatch('mousewheel')
expect(logger.latestEvent().name).not.toBe 'mousewheel'
it 'only logs up to `logSize` commands', ->
dispatch(char) for char in ['a'..'z']
expect(logger.eventLog.length).toBe(logger.logSize)
describe 'formatting of text log', ->
it 'does not output empty log items', ->
expect(logger.getText()).toBe """
```
```
"""
it 'formats commands with the time, name and target', ->
dispatch('foo:bar')
expect(logger.getText()).toBe """
```
-0:00.0 foo:bar (section#some-id.some-class.another-class)
```
"""
it 'omits the target ID if it has none', ->
element.id = ""
dispatch('foo:bar')
expect(logger.getText()).toBe """
```
-0:00.0 foo:bar (section.some-class.another-class)
```
"""
it 'formats commands in chronological order', ->
dispatch('foo:first')
dispatch('foo:second')
dispatch('foo:third')
expect(logger.getText()).toBe """
```
-0:00.0 foo:first (section#some-id.some-class.another-class)
-0:00.0 foo:second (section#some-id.some-class.another-class)
-0:00.0 foo:third (section#some-id.some-class.another-class)
```
"""
it 'displays a multiplier for repeated commands', ->
dispatch('foo:bar')
dispatch('foo:bar')
expect(logger.getText()).toBe """
```
2x -0:00.0 foo:bar (section#some-id.some-class.another-class)
```
"""
it 'logs the external data event as the last event', ->
dispatch('foo:bar')
event =
time: Date.now()
title: 'bummer'
expect(logger.getText(event)).toBe """
```
-0:00.0 foo:bar (section#some-id.some-class.another-class)
-0:00.0 bummer
```
"""
it 'does not report anything older than ten minutes', ->
logger.logCommand
type: 'foo:bar'
time: Date.now() - 11 * 60 * 1000
target: nodeName: 'DIV'
logger.logCommand
type: 'wow:bummer'
target: nodeName: 'DIV'
expect(logger.getText()).toBe """
```
-0:00.0 wow:bummer (div)
```
"""
it 'does not report commands that have no name', ->
dispatch('')
expect(logger.getText()).toBe """
```
```
"""

View File

@ -0,0 +1,35 @@
###
A collection of methods for retrieving information about the user's system for
bug report purposes.
###
module.exports =
generateException: ->
try
a + 1
catch e
errMsg = "#{e.toString()} in #{process.env.ATOM_HOME}/somewhere"
window.onerror.call(window, errMsg, '/dev/null', 2, 3, e)
# shortenerResponse
# packageResponse
# issuesResponse
generateFakeFetchResponses: (options) ->
spyOn(window, 'fetch') unless window.fetch.isSpy
fetch.andCallFake (url) ->
if url.indexOf('api.pulsar-edit.dev/api') > -1
return jsonPromise(options?.packageResponse ? {
repository: url: 'https://github.com/pulsar-edit/notifications'
releases: latest: '0.0.0'
})
if options?.issuesErrorResponse?
return Promise.reject(options?.issuesErrorResponse)
jsonPromise(options?.issuesResponse ? {items: []})
jsonPromise = (object) -> Promise.resolve {ok: true, json: -> Promise.resolve object}
textPromise = (text) -> Promise.resolve {ok: true, text: -> Promise.resolve text}

View File

@ -0,0 +1,325 @@
{Notification} = require 'atom'
NotificationElement = require '../lib/notification-element'
NotificationIssue = require '../lib/notification-issue'
NotificationsLog = require '../lib/notifications-log'
{generateFakeFetchResponses, generateException} = require './helper'
describe "Notifications Log", ->
workspaceElement = null
beforeEach ->
workspaceElement = atom.views.getView(atom.workspace)
atom.notifications.clear()
jasmine.attachToDOM(workspaceElement)
waitsForPromise ->
atom.packages.activatePackage('notifications')
waitsForPromise ->
atom.workspace.open(NotificationsLog::getURI())
describe "when the package is activated", ->
it "attaches an atom-notifications element to the dom", ->
expect(workspaceElement.querySelector('.notifications-log-items')).toBeDefined()
describe "when there are notifications before activation", ->
beforeEach ->
waitsForPromise ->
atom.packages.deactivatePackage('notifications')
it "displays all non displayed notifications", ->
warning = new Notification('warning', 'Un-displayed warning')
error = new Notification('error', 'Displayed error')
error.setDisplayed(true)
atom.notifications.addNotification(error)
atom.notifications.addNotification(warning)
waitsForPromise ->
atom.packages.activatePackage('notifications')
waitsForPromise ->
atom.workspace.open(NotificationsLog::getURI())
runs ->
notificationsLogContainer = workspaceElement.querySelector('.notifications-log-items')
notification = notificationsLogContainer.querySelector('.notifications-log-notification.warning')
expect(notification).toExist()
notification = notificationsLogContainer.querySelector('.notifications-log-notification.error')
expect(notification).toExist()
describe "when notifications are added to atom.notifications", ->
notificationsLogContainer = null
beforeEach ->
enableInitNotification = atom.notifications.addSuccess('A message to trigger initialization', dismissable: true)
enableInitNotification.dismiss()
advanceClock(NotificationElement::visibilityDuration)
advanceClock(NotificationElement::animationDuration)
notificationsLogContainer = workspaceElement.querySelector('.notifications-log-items')
jasmine.attachToDOM(workspaceElement)
generateFakeFetchResponses()
it "adds an .notifications-log-item element to the container with a class corresponding to the type", ->
atom.notifications.addSuccess('A message')
notification = notificationsLogContainer.querySelector('.notifications-log-item.success')
expect(notificationsLogContainer.childNodes).toHaveLength 2
expect(notification.querySelector('.message').textContent.trim()).toBe 'A message'
expect(notification.querySelector('.btn-toolbar')).toBeEmpty()
atom.notifications.addInfo('A message')
expect(notificationsLogContainer.childNodes).toHaveLength 3
expect(notificationsLogContainer.querySelector('.notifications-log-item.info')).toBeDefined()
atom.notifications.addWarning('A message')
expect(notificationsLogContainer.childNodes).toHaveLength 4
expect(notificationsLogContainer.querySelector('.notifications-log-item.warning')).toBeDefined()
atom.notifications.addError('A message')
expect(notificationsLogContainer.childNodes).toHaveLength 5
expect(notificationsLogContainer.querySelector('.notifications-log-item.error')).toBeDefined()
atom.notifications.addFatalError('A message')
notification = notificationsLogContainer.querySelector('.notifications-log-item.fatal')
expect(notificationsLogContainer.childNodes).toHaveLength 6
expect(notification).toBeDefined()
expect(notification.querySelector('.btn-toolbar')).not.toBeEmpty()
describe "when the `buttons` options is used", ->
it "displays the buttons in the .btn-toolbar element", ->
clicked = []
atom.notifications.addSuccess 'A message',
buttons: [{
text: 'Button One'
className: 'btn-one'
onDidClick: -> clicked.push 'one'
}, {
text: 'Button Two'
className: 'btn-two'
onDidClick: -> clicked.push 'two'
}]
notification = notificationsLogContainer.querySelector('.notifications-log-item.success')
expect(notification.querySelector('.btn-toolbar')).not.toBeEmpty()
btnOne = notification.querySelector('.btn-one')
btnTwo = notification.querySelector('.btn-two')
expect(btnOne).toHaveClass 'btn-success'
expect(btnOne.textContent).toBe 'Button One'
expect(btnTwo).toHaveClass 'btn-success'
expect(btnTwo.textContent).toBe 'Button Two'
btnTwo.click()
btnOne.click()
expect(clicked).toEqual ['two', 'one']
describe "when an exception is thrown", ->
fatalError = null
describe "when the there is an error searching for the issue", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses(issuesErrorResponse: '403')
generateException()
fatalError = notificationsLogContainer.querySelector('.notifications-log-item.fatal')
waitsForPromise ->
fatalError.getRenderPromise()
it "asks the user to create an issue", ->
button = fatalError.querySelector('.btn')
copyReport = fatalError.querySelector('.btn-copy-report')
expect(button).toBeDefined()
expect(button.textContent).toContain 'Create issue'
expect(copyReport).toBeDefined()
describe "when the package is out of date", ->
beforeEach ->
installedVersion = '0.9.0'
UserUtilities = require '../lib/user-utilities'
spyOn(UserUtilities, 'getPackageVersion').andCallFake -> installedVersion
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses
packageResponse:
repository: url: 'https://github.com/someguy/somepackage'
releases: latest: '0.10.0'
spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
generateException()
fatalError = notificationsLogContainer.querySelector('.notifications-log-item.fatal')
waitsForPromise ->
fatalError.getRenderPromise()
it "asks the user to update their packages", ->
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'Check for package updates'
expect(button.getAttribute('href')).toBe '#'
describe "when the error has been reported", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses
issuesResponse:
items: [
{
title: 'ReferenceError: a is not defined in $ATOM_HOME/somewhere'
html_url: 'http://url.com/ok'
state: 'open'
}
]
spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
generateException()
fatalError = notificationsLogContainer.querySelector('.notifications-log-item.fatal')
waitsForPromise ->
fatalError.getRenderPromise()
it "shows the user a view issue button", ->
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'View Issue'
expect(button.getAttribute('href')).toBe 'http://url.com/ok'
describe "when a log item is clicked", ->
[notification, notificationView, logItem] = []
describe "when the notification is not dismissed", ->
describe "when the notification is not dismissable", ->
beforeEach ->
notification = atom.notifications.addInfo('A message')
notificationView = atom.views.getView(notification)
logItem = notificationsLogContainer.querySelector('.notifications-log-item.info')
it "makes the notification dismissable", ->
logItem.click()
expect(notificationView.element.classList.contains('has-close')).toBe true
expect(notification.isDismissable()).toBe true
advanceClock(NotificationElement::visibilityDuration)
advanceClock(NotificationElement::animationDuration)
expect(notificationView.element).toBeVisible()
describe "when the notification is dismissed", ->
beforeEach ->
notification = atom.notifications.addInfo('A message', dismissable: true)
notificationView = atom.views.getView(notification)
logItem = notificationsLogContainer.querySelector('.notifications-log-item.info')
notification.dismiss()
advanceClock(NotificationElement::animationDuration)
it "displays the notification", ->
didDisplay = false
notification.onDidDisplay -> didDisplay = true
logItem.click()
expect(didDisplay).toBe true
expect(notification.dismissed).toBe false
expect(notificationView.element).toBeVisible()
describe "when the notification is dismissed again", ->
it "emits did-dismiss", ->
didDismiss = false
notification.onDidDismiss -> didDismiss = true
logItem.click()
notification.dismiss()
advanceClock(NotificationElement::animationDuration)
expect(didDismiss).toBe true
expect(notification.dismissed).toBe true
expect(notificationView.element).not.toBeVisible()
describe "when notifications are cleared", ->
beforeEach ->
clearButton = workspaceElement.querySelector('.notifications-log .notifications-clear-log')
atom.notifications.addInfo('A message', dismissable: true)
atom.notifications.addInfo('non-dismissable')
clearButton.click()
it "clears the notifications", ->
expect(atom.notifications.getNotifications()).toHaveLength 0
notifications = workspaceElement.querySelector('atom-notifications')
advanceClock(NotificationElement::animationDuration)
expect(notifications.children).toHaveLength 0
logItems = workspaceElement.querySelector('.notifications-log-items')
expect(logItems.children).toHaveLength 0
describe "the dock pane", ->
notificationsLogPane = null
beforeEach ->
notificationsLogPane = atom.workspace.paneForURI(NotificationsLog::getURI())
describe "when notifications:toggle-log is dispatched", ->
it "toggles the pane URI", ->
spyOn(atom.workspace, "toggle")
atom.commands.dispatch(workspaceElement, "notifications:toggle-log")
expect(atom.workspace.toggle).toHaveBeenCalledWith(NotificationsLog::getURI())
describe "when the pane is destroyed", ->
beforeEach ->
notificationsLogPane.destroyItems()
it "opens the pane", ->
[notificationsLog] = []
waitsForPromise ->
atom.workspace.toggle(NotificationsLog::getURI()).then (paneItem) ->
notificationsLog = paneItem
runs ->
expect(notificationsLog).toBeDefined()
describe "when notifications are displayed", ->
beforeEach ->
atom.notifications.addSuccess("success")
it "lists all notifications", ->
waitsForPromise ->
atom.workspace.toggle(NotificationsLog::getURI())
runs ->
notificationsLogContainer = workspaceElement.querySelector('.notifications-log-items')
expect(notificationsLogContainer.childNodes).toHaveLength 1
describe "when the pane is hidden", ->
beforeEach ->
atom.workspace.hide(NotificationsLog::getURI())
it "opens the pane", ->
[notificationsLog] = []
waitsForPromise ->
atom.workspace.toggle(NotificationsLog::getURI()).then (paneItem) ->
notificationsLog = paneItem
runs ->
expect(notificationsLog).toBeDefined()
describe "when the pane is open", ->
beforeEach ->
waitsForPromise ->
atom.workspace.open(NotificationsLog::getURI())
it "closes the pane", ->
notificationsLog = null
waitsForPromise ->
atom.workspace.toggle(NotificationsLog::getURI()).then (paneItem) ->
notificationsLog = paneItem
runs ->
expect(notificationsLog).not.toBeDefined()

View File

@ -0,0 +1,974 @@
fs = require 'fs-plus'
path = require 'path'
temp = require('temp').track()
{Notification} = require 'atom'
NotificationElement = require '../lib/notification-element'
NotificationIssue = require '../lib/notification-issue'
UserUtils = require '../lib/user-utilities'
{generateFakeFetchResponses, generateException} = require './helper'
describe "Notifications", ->
[workspaceElement, activationPromise] = []
beforeEach ->
workspaceElement = atom.views.getView(atom.workspace)
atom.notifications.clear()
activationPromise = atom.packages.activatePackage('notifications')
waitsForPromise ->
activationPromise
describe "when the package is activated", ->
it "attaches an atom-notifications element to the dom", ->
expect(workspaceElement.querySelector('atom-notifications')).toBeDefined()
describe "when there are notifications before activation", ->
beforeEach ->
waitsForPromise ->
# Wrapped in Promise.resolve so this test continues to work on earlier versions of Pulsar
Promise.resolve(atom.packages.deactivatePackage('notifications'))
it "displays all non displayed notifications", ->
warning = new Notification('warning', 'Un-displayed warning')
error = new Notification('error', 'Displayed error')
error.setDisplayed(true)
atom.notifications.addNotification(error)
atom.notifications.addNotification(warning)
activationPromise = atom.packages.activatePackage('notifications')
waitsForPromise ->
activationPromise
runs ->
notificationContainer = workspaceElement.querySelector('atom-notifications')
notification = notificationContainer.querySelector('atom-notification.warning')
expect(notification).toExist()
notification = notificationContainer.querySelector('atom-notification.error')
expect(notification).not.toExist()
describe "when notifications are added to atom.notifications", ->
notificationContainer = null
beforeEach ->
enableInitNotification = atom.notifications.addSuccess('A message to trigger initialization', dismissable: true)
enableInitNotification.dismiss()
advanceClock(NotificationElement::visibilityDuration)
advanceClock(NotificationElement::animationDuration)
notificationContainer = workspaceElement.querySelector('atom-notifications')
jasmine.attachToDOM(workspaceElement)
generateFakeFetchResponses()
it "adds an atom-notification element to the container with a class corresponding to the type", ->
expect(notificationContainer.childNodes.length).toBe 0
atom.notifications.addSuccess('A message')
notification = notificationContainer.querySelector('atom-notification.success')
expect(notificationContainer.childNodes.length).toBe 1
expect(notification).toHaveClass 'success'
expect(notification.querySelector('.message').textContent.trim()).toBe 'A message'
expect(notification.querySelector('.meta')).not.toBeVisible()
atom.notifications.addInfo('A message')
expect(notificationContainer.childNodes.length).toBe 2
expect(notificationContainer.querySelector('atom-notification.info')).toBeDefined()
atom.notifications.addWarning('A message')
expect(notificationContainer.childNodes.length).toBe 3
expect(notificationContainer.querySelector('atom-notification.warning')).toBeDefined()
atom.notifications.addError('A message')
expect(notificationContainer.childNodes.length).toBe 4
expect(notificationContainer.querySelector('atom-notification.error')).toBeDefined()
atom.notifications.addFatalError('A message')
expect(notificationContainer.childNodes.length).toBe 5
expect(notificationContainer.querySelector('atom-notification.fatal')).toBeDefined()
it "displays notification with a detail when a detail is specified", ->
atom.notifications.addInfo('A message', detail: 'Some detail')
notification = notificationContainer.childNodes[0]
expect(notification.querySelector('.detail').textContent).toContain 'Some detail'
atom.notifications.addInfo('A message', detail: null)
notification = notificationContainer.childNodes[1]
expect(notification.querySelector('.detail')).not.toBeVisible()
atom.notifications.addInfo('A message', detail: 1)
notification = notificationContainer.childNodes[2]
expect(notification.querySelector('.detail').textContent).toContain '1'
atom.notifications.addInfo('A message', detail: {something: 'ok'})
notification = notificationContainer.childNodes[3]
expect(notification.querySelector('.detail').textContent).toContain 'Object'
atom.notifications.addInfo('A message', detail: ['cats', 'ok'])
notification = notificationContainer.childNodes[4]
expect(notification.querySelector('.detail').textContent).toContain 'cats,ok'
it "does not add the has-stack class if a stack is provided without any detail", ->
atom.notifications.addInfo('A message', stack: 'Some stack')
notification = notificationContainer.childNodes[0]
notificationElement = notificationContainer.querySelector('atom-notification.info')
expect(notificationElement).not.toHaveClass 'has-stack'
it "renders the message as sanitized markdown", ->
atom.notifications.addInfo('test <b>html</b> <iframe>but sanitized</iframe>')
notification = notificationContainer.childNodes[0]
expect(notification.querySelector('.message').innerHTML).toContain(
'test <b>html</b> but sanitized'
)
describe "when a dismissable notification is added", ->
it "is removed when Notification::dismiss() is called", ->
notification = atom.notifications.addSuccess('A message', dismissable: true)
notificationElement = notificationContainer.querySelector('atom-notification.success')
expect(notificationContainer.childNodes.length).toBe 1
notification.dismiss()
advanceClock(NotificationElement::visibilityDuration)
expect(notificationElement).toHaveClass 'remove'
advanceClock(NotificationElement::animationDuration)
expect(notificationContainer.childNodes.length).toBe 0
it "is removed when the close icon is clicked", ->
jasmine.attachToDOM(workspaceElement)
waitsForPromise ->
atom.workspace.open()
runs ->
notification = atom.notifications.addSuccess('A message', dismissable: true)
notificationElement = notificationContainer.querySelector('atom-notification.success')
expect(notificationContainer.childNodes.length).toBe 1
notificationElement.focus()
notificationElement.querySelector('.close.icon').click()
advanceClock(NotificationElement::visibilityDuration)
expect(notificationElement).toHaveClass 'remove'
advanceClock(NotificationElement::animationDuration)
expect(notificationContainer.childNodes.length).toBe 0
it "is removed when core:cancel is triggered", ->
notification = atom.notifications.addSuccess('A message', dismissable: true)
notificationElement = notificationContainer.querySelector('atom-notification.success')
expect(notificationContainer.childNodes.length).toBe 1
atom.commands.dispatch(workspaceElement, 'core:cancel')
advanceClock(NotificationElement::visibilityDuration * 3)
expect(notificationElement).toHaveClass 'remove'
advanceClock(NotificationElement::animationDuration * 3)
expect(notificationContainer.childNodes.length).toBe 0
it "focuses the active pane only if the dismissed notification has focus", ->
jasmine.attachToDOM(workspaceElement)
waitsForPromise ->
atom.workspace.open()
runs ->
notification1 = atom.notifications.addSuccess('First message', dismissable: true)
notification2 = atom.notifications.addError('Second message', dismissable: true)
notificationElement1 = notificationContainer.querySelector('atom-notification.success')
notificationElement2 = notificationContainer.querySelector('atom-notification.error')
expect(notificationContainer.childNodes.length).toBe 2
notificationElement2.focus()
notification1.dismiss()
advanceClock(NotificationElement::visibilityDuration)
advanceClock(NotificationElement::animationDuration)
expect(notificationContainer.childNodes.length).toBe 1
expect(notificationElement2).toHaveFocus()
notificationElement2.querySelector('.close.icon').click()
advanceClock(NotificationElement::visibilityDuration)
advanceClock(NotificationElement::animationDuration)
expect(notificationContainer.childNodes.length).toBe 0
expect(atom.views.getView(atom.workspace.getActiveTextEditor())).toHaveFocus()
describe "when an autoclose notification is added", ->
[notification, model] = []
beforeEach ->
model = atom.notifications.addSuccess('A message')
notification = notificationContainer.querySelector('atom-notification.success')
it "closes and removes the message after a given amount of time", ->
expect(notification).not.toHaveClass 'remove'
advanceClock(NotificationElement::visibilityDuration)
expect(notification).toHaveClass 'remove'
expect(notificationContainer.childNodes.length).toBe 1
advanceClock(NotificationElement::animationDuration)
expect(notificationContainer.childNodes.length).toBe 0
describe "when the notification is clicked", ->
beforeEach ->
notification.click()
it "makes the notification dismissable", ->
expect(notification).toHaveClass 'has-close'
advanceClock(NotificationElement::visibilityDuration)
expect(notification).not.toHaveClass 'remove'
it "removes the notification when dismissed", ->
model.dismiss()
expect(notification).toHaveClass 'remove'
describe "when the default timeout setting is changed", ->
[notification] = []
beforeEach ->
atom.config.set("notifications.defaultTimeout", 1000)
atom.notifications.addSuccess('A message')
notification = notificationContainer.querySelector('atom-notification.success')
it "uses the setting value for the autoclose timeout", ->
expect(notification).not.toHaveClass 'remove'
advanceClock(1000)
expect(notification).toHaveClass 'remove'
describe "when the `description` option is used", ->
it "displays the description text in the .description element", ->
atom.notifications.addSuccess('A message', description: 'This is [a link](http://atom.io)')
notification = notificationContainer.querySelector('atom-notification.success')
expect(notification).toHaveClass('has-description')
expect(notification.querySelector('.meta')).toBeVisible()
expect(notification.querySelector('.description').textContent.trim()).toBe 'This is a link'
expect(notification.querySelector('.description a').href).toBe 'http://atom.io/'
describe "when the `buttons` options is used", ->
it "displays the buttons in the .description element", ->
clicked = []
atom.notifications.addSuccess 'A message',
buttons: [{
text: 'Button One'
className: 'btn-one'
onDidClick: -> clicked.push 'one'
}, {
text: 'Button Two'
className: 'btn-two'
onDidClick: -> clicked.push 'two'
}]
notification = notificationContainer.querySelector('atom-notification.success')
expect(notification).toHaveClass('has-buttons')
expect(notification.querySelector('.meta')).toBeVisible()
btnOne = notification.querySelector('.btn-one')
btnTwo = notification.querySelector('.btn-two')
expect(btnOne).toHaveClass 'btn-success'
expect(btnOne.textContent).toBe 'Button One'
expect(btnTwo).toHaveClass 'btn-success'
expect(btnTwo.textContent).toBe 'Button Two'
btnTwo.click()
btnOne.click()
expect(clicked).toEqual ['two', 'one']
describe "when an exception is thrown", ->
[notificationContainer, fatalError, issueTitle, issueBody] = []
describe "when the editor is in dev mode", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn true
generateException()
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "does not display a notification", ->
expect(notificationContainer.childNodes.length).toBe 0
expect(fatalError).toBe null
describe "when the exception has no core or package paths in the stack trace", ->
it "does not display a notification", ->
atom.notifications.clear()
spyOn(atom, 'inDevMode').andReturn false
handler = jasmine.createSpy('onWillThrowErrorHandler')
atom.onWillThrowError(handler)
# Fake an unhandled error with a call stack located outside of the source
# of Pulsar or an Pulsar package
fs.readFile(__dirname, ->
err = new Error()
err.stack = 'FakeError: foo is not bar\n at blah.fakeFunc (directory/fakefile.js:1:25)'
throw err
)
waitsFor ->
handler.callCount is 1
runs ->
expect(atom.notifications.getNotifications().length).toBe 0
describe "when the message contains a newline", ->
it "removes the newline when generating the issue title", ->
message = "Uncaught Error: Cannot read property 'object' of undefined\nTypeError: Cannot read property 'object' of undefined"
atom.notifications.addFatalError(message)
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then ->
issueTitle = fatalError.issue.getIssueTitle()
runs ->
expect(issueTitle).not.toContain "\n"
expect(issueTitle).toBe "Uncaught Error: Cannot read property 'object' of undefinedTypeError: Cannot read property 'objec..."
describe "when the message contains continguous newlines", ->
it "removes the newlines when generating the issue title", ->
message = "Uncaught Error: Cannot do the thing\n\nSuper sorry about this"
atom.notifications.addFatalError(message)
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then ->
issueTitle = fatalError.issue.getIssueTitle()
runs ->
expect(issueTitle).toBe "Uncaught Error: Cannot do the thingSuper sorry about this"
describe "when there are multiple packages in the stack trace", ->
beforeEach ->
stack = """
TypeError: undefined is not a function
at Object.module.exports.Pane.promptToSaveItem [as defaultSavePrompt] (/Applications/Pulsar.app/Contents/Resources/app/src/pane.js:490:23)
at Pane.promptToSaveItem (/Users/someguy/.atom/packages/save-session/lib/save-prompt.coffee:21:15)
at Pane.module.exports.Pane.destroyItem (/Applications/Pulsar.app/Contents/Resources/app/src/pane.js:442:18)
at HTMLDivElement.<anonymous> (/Applications/Pulsar.app/Contents/Resources/app/node_modules/tabs/lib/tab-bar-view.js:174:22)
at space-pen-ul.jQuery.event.dispatch (/Applications/Pulsar.app/Contents/Resources/app/node_modules/archive-view/node_modules/atom-space-pen-views/node_modules/space-pen/vendor/jquery.js:4676:9)
at space-pen-ul.elemData.handle (/Applications/Pulsar.app/Contents/Resources/app/node_modules/archive-view/node_modules/atom-space-pen-views/node_modules/space-pen/vendor/jquery.js:4360:46)
"""
detail = 'ok'
atom.notifications.addFatalError('TypeError: undefined', {detail, stack})
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
spyOn(fs, 'realpathSync').andCallFake (p) -> p
spyOn(fatalError.issue, 'getPackagePathsByPackageName').andCallFake ->
'save-session': '/Users/someguy/.atom/packages/save-session'
'tabs': '/Applications/Pulsar.app/Contents/Resources/app/node_modules/tabs'
it "chooses the first package in the trace", ->
expect(fatalError.issue.getPackageName()).toBe 'save-session'
describe "when an exception is thrown from a package", ->
beforeEach ->
issueTitle = null
issueBody = null
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
spyOn(UserUtils, 'getPackageVersionShippedWithPulsar').andCallFake -> '0.0.0'
generateException()
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "displays a fatal error with the package name in the error", ->
waitsForPromise ->
fatalError.getRenderPromise().then ->
issueTitle = fatalError.issue.getIssueTitle()
fatalError.issue.getIssueBody().then (result) ->
issueBody = result
runs ->
expect(notificationContainer.childNodes.length).toBe 1
expect(fatalError).toHaveClass 'has-close'
expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined'
expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/pulsar\">notifications package</a>"
expect(fatalError.issue.getPackageName()).toBe 'notifications'
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'Create issue on the notifications package'
expect(issueTitle).toContain '$ATOM_HOME'
expect(issueTitle).not.toContain process.env.ATOM_HOME
expect(issueBody).toMatch /Pulsar\*\*: [0-9].[0-9]+.[0-9]+/ig
expect(issueBody).not.toMatch /Unknown/ig
expect(issueBody).toContain 'ReferenceError: a is not defined'
expect(issueBody).toContain 'Thrown From**: [notifications](https://github.com/pulsar-edit/pulsar) package '
expect(issueBody).toContain '### Non-Core Packages'
# FIXME: this doesnt work on the test server. `apm ls` is not working for some reason.
# expect(issueBody).toContain 'notifications '
it "standardizes platform separators on #win32", ->
waitsForPromise ->
fatalError.getRenderPromise().then ->
issueTitle = fatalError.issue.getIssueTitle()
runs ->
expect(issueTitle).toContain path.posix.sep
expect(issueTitle).not.toContain path.win32.sep
describe "when an exception contains the user's home directory", ->
beforeEach ->
issueTitle = null
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
# Create a custom error message that contains the user profile but not ATOM_HOME
try
a + 1
catch e
home = if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME
errMsg = "#{e.toString()} in #{home}#{path.sep}somewhere"
window.onerror.call(window, errMsg, '/dev/null', 2, 3, e)
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "replaces the directory with a ~", ->
waitsForPromise ->
fatalError.getRenderPromise().then ->
issueTitle = fatalError.issue.getIssueTitle()
runs ->
expect(issueTitle).toContain '~'
if process.platform is 'win32'
expect(issueTitle).not.toContain process.env.USERPROFILE
else
expect(issueTitle).not.toContain process.env.HOME
describe "when an exception is thrown from a linked package", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
packagesDir = path.join(temp.mkdirSync('atom-packages-'), '.atom', 'packages')
atom.packages.packageDirPaths.push(packagesDir)
packageDir = path.join(packagesDir, '..', '..', 'github', 'linked-package')
fs.makeTreeSync path.dirname(path.join(packagesDir, 'linked-package'))
fs.symlinkSync(packageDir, path.join(packagesDir, 'linked-package'), 'junction')
fs.writeFileSync path.join(packageDir, 'package.json'), """
{
"name": "linked-package",
"version": "1.0.0",
"repository": "https://github.com/pulsar-edit/notifications"
}
"""
atom.packages.enablePackage('linked-package')
stack = """
ReferenceError: path is not defined
at Object.module.exports.LinkedPackage.wow (#{path.join(fs.realpathSync(packageDir), 'linked-package.coffee')}:29:15)
at atom-workspace.subscriptions.add.atom.commands.add.linked-package:wow (#{path.join(packageDir, 'linked-package.coffee')}:18:102)
at CommandRegistry.module.exports.CommandRegistry.handleCommandEvent (/Applications/Pulsar.app/Contents/Resources/app/src/command-registry.js:238:29)
at /Applications/Pulsar.app/Contents/Resources/app/src/command-registry.js:3:61
at CommandPaletteView.module.exports.CommandPaletteView.confirmed (/Applications/Pulsar.app/Contents/Resources/app/node_modules/command-palette/lib/command-palette-view.js:159:32)
"""
detail = "At #{path.join(packageDir, 'linked-package.coffee')}:41"
message = "Uncaught ReferenceError: path is not defined"
atom.notifications.addFatalError(message, {stack, detail, dismissable: true})
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "displays a fatal error with the package name in the error", ->
waitsForPromise ->
fatalError.getRenderPromise()
runs ->
expect(notificationContainer.childNodes.length).toBe 1
expect(fatalError).toHaveClass 'has-close'
expect(fatalError.innerHTML).toContain "Uncaught ReferenceError: path is not defined"
expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">linked-package package</a>"
expect(fatalError.issue.getPackageName()).toBe 'linked-package'
describe "when an exception is thrown from an unloaded package", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
packagesDir = temp.mkdirSync('atom-packages-')
atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
packageDir = path.join(packagesDir, '.atom', 'packages', 'unloaded')
fs.writeFileSync path.join(packageDir, 'package.json'), """
{
"name": "unloaded",
"version": "1.0.0",
"repository": "https://github.com/pulsar-edit/notifications"
}
"""
stack = "Error\n at #{path.join(packageDir, 'index.js')}:1:1"
detail = 'ReferenceError: unloaded error'
message = "Error"
atom.notifications.addFatalError(message, {stack, detail, dismissable: true})
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "displays a fatal error with the package name in the error", ->
waitsForPromise ->
fatalError.getRenderPromise()
runs ->
expect(notificationContainer.childNodes.length).toBe 1
expect(fatalError).toHaveClass 'has-close'
expect(fatalError.innerHTML).toContain 'ReferenceError: unloaded error'
expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">unloaded package</a>"
expect(fatalError.issue.getPackageName()).toBe 'unloaded'
describe "when an exception is thrown from a package trying to load", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
packagesDir = temp.mkdirSync('atom-packages-')
atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
packageDir = path.join(packagesDir, '.atom', 'packages', 'broken-load')
fs.writeFileSync path.join(packageDir, 'package.json'), """
{
"name": "broken-load",
"version": "1.0.0",
"repository": "https://github.com/pulsar-edit/notifications"
}
"""
stack = "TypeError: Cannot read property 'prototype' of undefined\n at __extends (<anonymous>:1:1)\n at Object.defineProperty.value [as .coffee] (/Applications/Pulsar.app/Contents/Resources/app.asar/src/compile-cache.js:169:21)"
detail = "TypeError: Cannot read property 'prototype' of undefined"
message = "Failed to load the broken-load package"
atom.notifications.addFatalError(message, {stack, detail, packageName: 'broken-load', dismissable: true})
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "displays a fatal error with the package name in the error", ->
waitsForPromise ->
fatalError.getRenderPromise()
runs ->
expect(notificationContainer.childNodes.length).toBe 1
expect(fatalError).toHaveClass 'has-close'
expect(fatalError.innerHTML).toContain "TypeError: Cannot read property 'prototype' of undefined"
expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">broken-load package</a>"
expect(fatalError.issue.getPackageName()).toBe 'broken-load'
describe "when an exception is thrown from a package trying to load a grammar", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
packagesDir = temp.mkdirSync('atom-packages-')
atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
packageDir = path.join(packagesDir, '.atom', 'packages', 'language-broken-grammar')
fs.writeFileSync path.join(packageDir, 'package.json'), """
{
"name": "language-broken-grammar",
"version": "1.0.0",
"repository": "https://github.com/pulsar-edit/notifications"
}
"""
stack = """
Unexpected string
at nodeTransforms.Literal (/usr/share/atom/resources/app/node_modules/season/node_modules/cson-parser/lib/parse.js:100:15)
at #{path.join('packageDir', 'grammars', 'broken-grammar.cson')}:1:1
"""
detail = """
At Syntax error on line 241, column 18: evalmachine.<anonymous>:1
"#\\{" "end": "\\}"
^^^^^
Unexpected string in #{path.join('packageDir', 'grammars', 'broken-grammar.cson')}
SyntaxError: Syntax error on line 241, column 18: evalmachine.<anonymous>:1
"#\\{" "end": "\\}"
^^^^^
"""
message = "Failed to load a language-broken-grammar package grammar"
atom.notifications.addFatalError(message, {stack, detail, packageName: 'language-broken-grammar', dismissable: true})
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "displays a fatal error with the package name in the error", ->
waitsForPromise ->
fatalError.getRenderPromise()
runs ->
expect(notificationContainer.childNodes.length).toBe 1
expect(fatalError).toHaveClass 'has-close'
expect(fatalError.innerHTML).toContain "Failed to load a language-broken-grammar package grammar"
expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">language-broken-grammar package</a>"
expect(fatalError.issue.getPackageName()).toBe 'language-broken-grammar'
describe "when an exception is thrown from a package trying to activate", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
packagesDir = temp.mkdirSync('atom-packages-')
atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
packageDir = path.join(packagesDir, '.atom', 'packages', 'broken-activation')
fs.writeFileSync path.join(packageDir, 'package.json'), """
{
"name": "broken-activation",
"version": "1.0.0",
"repository": "https://github.com/pulsar-edit/notifications"
}
"""
stack = "TypeError: Cannot read property 'command' of undefined\n at Object.module.exports.activate (<anonymous>:7:23)\n at Package.module.exports.Package.activateNow (/Applications/Pulsar.app/Contents/Resources/app.asar/src/package.js:232:19)"
detail = "TypeError: Cannot read property 'command' of undefined"
message = "Failed to activate the broken-activation package"
atom.notifications.addFatalError(message, {stack, detail, packageName: 'broken-activation', dismissable: true})
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
it "displays a fatal error with the package name in the error", ->
waitsForPromise ->
fatalError.getRenderPromise()
runs ->
expect(notificationContainer.childNodes.length).toBe 1
expect(fatalError).toHaveClass 'has-close'
expect(fatalError.innerHTML).toContain "TypeError: Cannot read property 'command' of undefined"
expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">broken-activation package</a>"
expect(fatalError.issue.getPackageName()).toBe 'broken-activation'
describe "when an exception is thrown from a package without a trace, but with a URL", ->
beforeEach ->
issueBody = null
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
try
a + 1
catch e
# Pull the file path from the stack
filePath = e.stack.split('\n')[1].match(/\((.+?):\d+/)[1]
window.onerror.call(window, e.toString(), filePath, 2, 3, message: e.toString(), stack: undefined)
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
# TODO: Have to be honest, NO IDEA where this detection happens...
xit "detects the package name from the URL", ->
waitsForPromise -> fatalError.getRenderPromise()
runs ->
expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined'
expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">notifications package</a>"
expect(fatalError.issue.getPackageName()).toBe 'notifications'
describe "when an exception is thrown from core", ->
beforeEach ->
atom.commands.dispatch(workspaceElement, 'some-package:a-command')
atom.commands.dispatch(workspaceElement, 'some-package:a-command')
atom.commands.dispatch(workspaceElement, 'some-package:a-command')
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses()
try
a + 1
catch e
# Mung the stack so it looks like its from core
e.stack = e.stack.replace(new RegExp(__filename, 'g'), '<embedded>').replace(/notifications/g, 'core')
window.onerror.call(window, e.toString(), '/dev/null', 2, 3, e)
notificationContainer = workspaceElement.querySelector('atom-notifications')
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then ->
fatalError.issue.getIssueBody().then (result) ->
issueBody = result
it "displays a fatal error with the package name in the error", ->
expect(notificationContainer.childNodes.length).toBe 1
expect(fatalError).toBeDefined()
expect(fatalError).toHaveClass 'has-close'
expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined'
expect(fatalError.innerHTML).toContain 'bug in Pulsar'
expect(fatalError.issue.getPackageName()).toBeUndefined()
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'Create issue on pulsar-edit/pulsar'
expect(issueBody).toContain 'ReferenceError: a is not defined'
expect(issueBody).toContain '**Thrown From**: Pulsar Core'
it "contains the commands that the user run in the issue body", ->
expect(issueBody).toContain 'some-package:a-command'
it "allows the user to toggle the stack trace", ->
stackToggle = fatalError.querySelector('.stack-toggle')
stackContainer = fatalError.querySelector('.stack-container')
expect(stackToggle).toExist()
expect(stackContainer.style.display).toBe 'none'
stackToggle.click()
expect(stackContainer.style.display).toBe 'block'
stackToggle.click()
expect(stackContainer.style.display).toBe 'none'
describe "when the there is an error searching for the issue", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
generateFakeFetchResponses(issuesErrorResponse: '403')
generateException()
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
it "asks the user to create an issue", ->
button = fatalError.querySelector('.btn')
fatalNotification = fatalError.querySelector('.fatal-notification')
expect(button.textContent).toContain 'Create issue'
expect(fatalNotification.textContent).toContain 'The error was thrown from the notifications package.'
describe "when the error has not been reported", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
describe "when the message is longer than 100 characters", ->
message = "Uncaught Error: Cannot find module 'dialog'Error: Cannot find module 'dialog' at Function.Module._resolveFilename (module.js:351:15) at Function.Module._load (module.js:293:25) at Module.require (module.js:380:17) at EventEmitter.<anonymous> (/Applications/Pulsar.app/Contents/Resources/atom/browser/lib/rpc-server.js:128:79) at EventEmitter.emit (events.js:119:17) at EventEmitter.<anonymous> (/Applications/Pulsar.app/Contents/Resources/atom/browser/api/lib/web-contents.js:99:23) at EventEmitter.emit (events.js:119:17)"
expectedIssueTitle = "Uncaught Error: Cannot find module 'dialog'Error: Cannot find module 'dialog' at Function.Module...."
beforeEach ->
generateFakeFetchResponses()
try
a + 1
catch e
e.code = 'Error'
e.message = message
window.onerror.call(window, e.message, 'abc', 2, 3, e)
it "truncates the issue title to 100 characters", ->
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise()
runs ->
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'Create issue'
expect(fatalError.issue.getIssueTitle()).toBe(expectedIssueTitle)
describe "when the package is out of date", ->
beforeEach ->
installedVersion = '0.9.0'
UserUtilities = require '../lib/user-utilities'
spyOn(UserUtilities, 'getPackageVersion').andCallFake -> installedVersion
spyOn(atom, 'inDevMode').andReturn false
describe "when the package is a non-core package", ->
beforeEach ->
generateFakeFetchResponses
packageResponse:
repository: url: 'https://github.com/someguy/somepackage'
releases: latest: '0.10.0'
spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
generateException()
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
it "asks the user to update their packages", ->
fatalNotification = fatalError.querySelector('.fatal-notification')
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'Check for package updates'
expect(fatalNotification.textContent).toContain 'Upgrading to the latest'
expect(button.getAttribute('href')).toBe '#'
describe "when the package is an atom-owned non-core package", ->
beforeEach ->
generateFakeFetchResponses
packageResponse:
repository: url: 'https://github.com/pulsar-edit/sort-lines'
releases: latest: '0.10.0'
spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "sort-lines"
spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/pulsar-edit/sort-lines"
generateException()
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
it "asks the user to update their packages", ->
fatalNotification = fatalError.querySelector('.fatal-notification')
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'Check for package updates'
expect(fatalNotification.textContent).toContain 'Upgrading to the latest'
expect(button.getAttribute('href')).toBe '#'
describe "when the package is a core package", ->
beforeEach ->
generateFakeFetchResponses
packageResponse:
repository: url: 'https://github.com/pulsar-edit/notifications'
releases: latest: '0.11.0'
describe "when the locally installed version is lower than Pulsar's version", ->
beforeEach ->
versionShippedWithPulsar = '0.10.0'
UserUtilities = require '../lib/user-utilities'
spyOn(UserUtilities, 'getPackageVersionShippedWithPulsar').andCallFake -> versionShippedWithPulsar
generateException()
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
it "doesn't show the Create Issue button", ->
button = fatalError.querySelector('.btn-issue')
expect(button).not.toExist()
it "tells the user that the package is a locally installed core package and out of date", ->
fatalNotification = fatalError.querySelector('.fatal-notification')
expect(fatalNotification.textContent).toContain 'Locally installed core Pulsar package'
expect(fatalNotification.textContent).toContain 'is out of date'
describe "when the locally installed version matches Pulsar's version", ->
beforeEach ->
versionShippedWithPulsar = '0.9.0'
UserUtilities = require '../lib/user-utilities'
spyOn(UserUtilities, 'getPackageVersionShippedWithPulsar').andCallFake -> versionShippedWithPulsar
generateException()
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
it "ignores the out of date package because they cant upgrade it without upgrading atom", ->
fatalError = notificationContainer.querySelector('atom-notification.fatal')
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'Create issue'
# TODO: Re-enable when Pulsar have a way to check this
# describe "when Pulsar is out of date", ->
# beforeEach ->
# installedVersion = '0.179.0'
# spyOn(atom, 'getVersion').andCallFake -> installedVersion
# spyOn(atom, 'inDevMode').andReturn false
#
# generateFakeFetchResponses
# atomResponse:
# name: '0.180.0'
#
# generateException()
#
# fatalError = notificationContainer.querySelector('atom-notification.fatal')
# waitsForPromise ->
# fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
#
# it "doesn't show the Create Issue button", ->
# button = fatalError.querySelector('.btn-issue')
# expect(button).not.toExist()
#
# it "tells the user that Pulsar is out of date", ->
# fatalNotification = fatalError.querySelector('.fatal-notification')
# expect(fatalNotification.textContent).toContain 'Pulsar is out of date'
#
# it "provides a link to the latest released version", ->
# fatalNotification = fatalError.querySelector('.fatal-notification')
# expect(fatalNotification.innerHTML).toContain '<a href="https://github.com/pulsar-edit/pulsar/releases/tag/v0.180.0">latest version</a>'
describe "when the error has been reported", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
describe "when the issue is open", ->
beforeEach ->
generateFakeFetchResponses
issuesResponse:
items: [
{
title: 'ReferenceError: a is not defined in $ATOM_HOME/somewhere'
html_url: 'http://url.com/ok'
state: 'open'
}
]
spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
generateException()
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
it "shows the user a view issue button", ->
fatalNotification = fatalError.querySelector('.fatal-notification')
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'View Issue'
expect(button.getAttribute('href')).toBe 'http://url.com/ok'
expect(fatalNotification.textContent).toContain 'already been reported'
expect(fetch.calls[0].args[0]).toContain encodeURIComponent('someguy/somepackage')
describe "when the issue is closed", ->
beforeEach ->
generateFakeFetchResponses
issuesResponse:
items: [
{
title: 'ReferenceError: a is not defined in $ATOM_HOME/somewhere'
html_url: 'http://url.com/closed'
state: 'closed'
}
]
spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
generateException()
fatalError = notificationContainer.querySelector('atom-notification.fatal')
waitsForPromise ->
fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
it "shows the user a view issue button", ->
button = fatalError.querySelector('.btn')
expect(button.textContent).toContain 'View Issue'
expect(button.getAttribute('href')).toBe 'http://url.com/closed'
describe "when a BufferedProcessError is thrown", ->
it "adds an error to the notifications", ->
expect(notificationContainer.querySelector('atom-notification.error')).not.toExist()
window.onerror('Uncaught BufferedProcessError: Failed to spawn command `bad-command`', 'abc', 2, 3, {name: 'BufferedProcessError'})
error = notificationContainer.querySelector('atom-notification.error')
expect(error).toExist()
expect(error.innerHTML).toContain 'Failed to spawn command'
expect(error.innerHTML).not.toContain 'BufferedProcessError'
describe "when a spawn ENOENT error is thrown", ->
beforeEach ->
spyOn(atom, 'inDevMode').andReturn false
describe "when the binary has no path", ->
beforeEach ->
error = new Error('Error: spawn some_binary ENOENT')
error.code = 'ENOENT'
window.onerror.call(window, error.message, 'abc', 2, 3, error)
it "displays a dismissable error without the stack trace", ->
notificationContainer = workspaceElement.querySelector('atom-notifications')
error = notificationContainer.querySelector('atom-notification.error')
expect(error.textContent).toContain "'some_binary' could not be spawned"
describe "when the binary has /atom in the path", ->
beforeEach ->
try
a + 1
catch e
e.code = 'ENOENT'
message = 'Error: spawn /opt/atom/Pulsar Helper (deleted) ENOENT'
window.onerror.call(window, message, 'abc', 2, 3, e)
it "displays a fatal error", ->
notificationContainer = workspaceElement.querySelector('atom-notifications')
error = notificationContainer.querySelector('atom-notification.fatal')
expect(error).toExist()

View File

@ -0,0 +1,193 @@
@import "ui-variables";
@import "octicon-mixins";
@icon-size: 30px;
@font-family-monospace: Consolas, "Liberation Mono", Menlo, Courier, monospace;
.notifications-log {
display: flex;
flex-direction: column;
min-width: 200px;
header {
flex: none;
background-color: @base-background-color;
border-bottom: 1px solid @base-border-color;
button {
border: none;
width: @icon-size;
height: @icon-size;
margin: 1px;
padding: 0;
color: @text-color-subtle;
opacity: .5;
background: @base-background-color;
}
.notifications-clear-log {
float: right;
}
}
.notifications-log-items {
flex: auto;
list-style: none;
padding: 0;
margin: 0;
overflow: auto;
.notifications-log-item {
display: flex;
box-sizing: content-box; // Keep spacing even
border-bottom: 1px solid @base-border-color;
max-height: @icon-size;
overflow: hidden;
.notifications-log-notification {
flex: auto;
display: flex;
position: relative;
padding-left: @icon-size;
overflow: hidden;
&.icon:before {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: @icon-size;
height: 100%;
padding-top: @component-padding/2;
text-align: center;
}
.message {
flex: 0 1 auto;
word-wrap: break-word;
padding: @component-padding/2 @component-padding;
border-left: 1px solid @base-border-color;
}
.btn-toolbar {
flex: 1 0 auto;
display: flex;
align-items: center;
white-space: nowrap;
&:empty {
display: none;
}
.btn-copy-report {
vertical-align: middle;
margin-left: @component-padding/2;
&::before {
margin: 0;
}
}
}
}
.timestamp {
flex: 0 0 auto;
white-space: nowrap;
text-align: center;
line-height: @icon-size;
padding: 0 @component-padding;
}
}
}
}
// Types -------------------------------
.notifications-log {
// fatal
.notification-type.fatal {
.type(@text-color-error; @background-color-error);
}
.hide-fatal li.fatal {
display: none;
}
.notifications-log-notification.fatal {
.log(@text-color-error; @background-color-error);
}
// error
.notification-type.error {
.type(@text-color-error; @background-color-error);
}
.hide-error li.error {
display: none;
}
.notifications-log-notification.error {
.log(@text-color-error; @background-color-error);
}
// warning
.notification-type.warning {
.type(@text-color-warning; @background-color-warning);
}
.hide-warning li.warning {
display: none;
}
.notifications-log-notification.warning {
.log(@text-color-warning; @background-color-warning);
}
// info
.notification-type.info {
.type(@text-color-info; @background-color-info);
}
.hide-info li.info {
display: none;
}
.notifications-log-notification.info {
.log(@text-color-info; @background-color-info);
}
// success
.notification-type.success {
.type(@text-color-success; @background-color-success);
}
.hide-success li.success {
display: none;
}
.notifications-log-notification.success {
.log(@text-color-success; @background-color-success);
}
}
// Type Mixin
.type(@txt; @bg) {
&.show-type {
color: @txt;
opacity: 1;
}
}
.log(@txt; @bg) {
.message {
color: lighten(@txt, 0%);
}
&.icon:before {
color: @txt;
}
}

View File

@ -0,0 +1,336 @@
@import "ui-variables";
@import "octicon-mixins";
@icon-size: 30px;
@width: 450px;
@width-detail: 450px;
@max-height-message: 200px;
@max-height-detail: 500px;
@max-height: @max-height-message + @max-height-detail + 100px; // 100px for footer. This is only used for the closing animation
@notification-gap: 2px;
@font-family-monospace: Consolas, "Liberation Mono", Menlo, Courier, monospace;
atom-notifications {
display: block;
z-index: 1000; // TODO: Have some convention about z-index stacking
position: absolute;
top: 35px;
right: 0;
bottom: 0;
padding: @component-padding;
font-size: 1.2em;
overflow-x: hidden;
overflow-y: auto;
pointer-events: none;
&::-webkit-scrollbar {
display: none;
}
atom-notification {
.close-all {
display: none;
}
}
atom-notification:first-child {
.close-all {
display: block;
}
.message {
padding-right: @component-padding * 2 + 95px; // space for icon and button
}
}
atom-notification:only-child {
.close-all {
display: none;
}
.message {
padding-right: inherit;
}
&.has-close .message {
padding-right: @component-padding + 24px; // space for icon
}
}
atom-notification {
float: right;
clear: right;
position: relative;
width: @width;
padding-left: @icon-size;
margin-bottom: @notification-gap;
max-height: @max-height;
word-wrap: break-word;
pointer-events: auto;
&.icon:before {
position: absolute;
top: 0;
left: 0;
width: @icon-size;
height: 100%;
padding-top: @component-padding;
text-align: center;
border-radius: @component-border-radius 0 0 @component-border-radius;
}
// fill space between notifiactions to prevent click throughs
&:after {
content: "";
display: block;
position: absolute;
left: 0;
right: 0;
bottom: -@notification-gap;
height: @notification-gap;
}
.meta,
.close,
.detail,
.stack-toggle,
.stack-container {
display: none;
}
&.fatal .meta,
&.has-description .meta,
&.has-buttons .meta,
&.has-close .close,
&.has-detail .detail,
&.has-stack .stack-toggle,
&.has-stack .stack-container {
display: block;
}
// .item's are used as general containers
.item {
padding: @component-padding;
border-top: 1px solid hsla(0,0%,0%,.1);
&.message {
border-top: none;
p:last-child {
margin-bottom: 0;
}
}
}
&.has-close .message {
padding-right: @component-padding + 24px; // space for icon
}
.content {
border-radius: 0 @component-border-radius @component-border-radius 0;
}
.message {
max-height: @max-height-message;
overflow-y: auto;
}
.close-all.btn {
position: absolute;
top: 7px;
right: 38px;
background: none;
}
.close {
position: absolute;
top: 0;
right: 0;
width: 38px;
height: 38px;
line-height: 38px;
text-align: center;
font-size: 16px;
text-shadow: none;
color: black;
opacity: .4;
&:hover, &:focus {
opacity: 1;
}
&:active {
opacity: .2;
}
&:before {
margin: 0;
}
}
&.has-detail {
width: @width-detail;
}
.detail {
font-size: .8em;
background-color: hsla(0,0%,100%,.3);
background-clip: padding-box;
max-height: @max-height-detail;
overflow-y: auto;
.line {
font-family: @font-family-monospace;
}
.stack-toggle {
margin-top: @component-padding;
.icon:before {
margin: 0;
}
}
.detail-content {
.line {
white-space: pre-wrap;
}
}
.stack-container {
margin-top: @component-padding;
.line {
white-space: pre;
}
}
}
.description {
font-size: .8em;
p:last-child {
margin-bottom: 0;
}
}
.btn-toolbar.btn-toolbar {
margin-top: 10px;
margin-bottom: -5px;
margin-left: 0;
}
.btn-toolbar.btn-toolbar > .btn {
margin-left: 0;
margin-bottom: 5px;
}
.btn-copy-report {
vertical-align: middle;
}
.opening {
cursor: progress;
}
}
}
// Types -------------------------------
atom-notifications {
atom-notification.fatal {
.notification(@text-color-error; @background-color-error);
}
atom-notification.error {
.notification(@text-color-error; @background-color-error);
}
atom-notification.warning {
.notification(@text-color-warning; @background-color-warning);
}
atom-notification.info {
.notification(@text-color-info; @background-color-info);
}
atom-notification.success {
.notification(@text-color-success; @background-color-success);
}
}
// Mixins -------------------------------
.notification(@txt; @bg) {
.content {
color: darken(@txt, 40%);
background-color: lighten(@bg, 25%);
}
a {
color: darken(@txt, 20%);
}
code {
color: darken(@txt, 40%);
background-color: desaturate(lighten(@bg, 18%), 5%);
}
&.icon:before {
color: lighten(@bg, 36%);
background-color: @bg;
}
.close-all.btn {
border: 1px solid fadeout(darken(@txt, 40%), 70%);
color: fadeout(darken(@txt, 40%), 40%);
text-shadow: none;
&:hover {
background: none;
border-color: fadeout(darken(@txt, 40%), 20%);
color: darken(@txt, 40%);
}
}
}
// Animations -------------------------------
atom-notifications atom-notification {
-webkit-animation: notification-show .16s cubic-bezier(0.175, 0.885, 0.32, 1.27499);
&[type="fatal"] {
-webkit-animation: notification-show .16s cubic-bezier(0.175, 0.885, 0.32, 1.27499),
notification-shake 4s 2s;
-webkit-animation-iteration-count: 1, 3; // shake 3 times after showing
&:hover {
-webkit-animation-play-state: paused; // stop shaking when hovering
}
}
&.remove,
&.remove:hover {
-webkit-animation: notification-hide .12s cubic-bezier(.34,.07,1,.2),
notification-shrink .24s .12s cubic-bezier(0.5, 0, 0, 1);
-webkit-animation-fill-mode: forwards;
}
}
@-webkit-keyframes notification-show {
0% { opacity: 0; transform: perspective(@width) translate(0, -@icon-size) rotateX(90deg); }
100% { opacity: 1; transform: perspective(@width) translate(0, 0) rotateX( 0deg); }
}
@-webkit-keyframes notification-hide {
0% { opacity: 1; transform: scale( 1); }
100% { opacity: 0; transform: scale(.8); }
}
@-webkit-keyframes notification-shrink {
0% { opacity: 0; max-height: @max-height; transform: scale(.8); }
100% { opacity: 0; max-height: 0; transform: scale(.8); }
}
@-webkit-keyframes notification-shake {
0% { transform: translateX( 0); }
2% { transform: translateX(-4px); }
4% { transform: translateX( 8px); }
6% { transform: translateX(-4px); }
8% { transform: translateX( 0); }
100% { transform: translateX( 0); }
}

View File

@ -299,8 +299,9 @@ export default class PackageCard {
this.refs.downloadIcon.classList.add('icon-git-branch')
this.refs.downloadCount.textContent = this.pack.apmInstallSource.sha.substr(0, 8)
} else {
this.refs.stargazerCount.textContent = data.stargazers_count ? data.stargazers_count.toLocaleString() : ''
this.refs.downloadCount.textContent = data.downloads ? data.downloads.toLocaleString() : ''
this.refs.stargazerCount.textContent = data.stargazers_count ? parseInt(data.stargazers_count).toLocaleString() : ''
this.refs.downloadCount.textContent = data.downloads ? parseInt(data.downloads).toLocaleString() : ''
}
}
})

View File

@ -74,19 +74,6 @@ describe('Package', function() {
expect(pack.isCompatible()).toBe(false);
});
it('caches the incompatible native modules in local storage', function() {
const packagePath = atom.project
.getDirectories()[0]
.resolve('packages/package-with-incompatible-native-module');
expect(buildPackage(packagePath).isCompatible()).toBe(false);
expect(global.localStorage.getItem.callCount).toBe(1);
expect(global.localStorage.setItem.callCount).toBe(1);
expect(buildPackage(packagePath).isCompatible()).toBe(false);
expect(global.localStorage.getItem.callCount).toBe(2);
expect(global.localStorage.setItem.callCount).toBe(1);
});
it('logs an error to the console describing the problem', function() {
const packagePath = atom.project
.getDirectories()[0]
@ -166,7 +153,6 @@ describe('Package', function() {
// A different package instance has the same failure output (simulates reload)
const pack2 = buildPackage(packagePath);
expect(pack2.getBuildFailureOutput()).toBe('It is broken');
expect(pack2.isCompatible()).toBe(false);
// Clears the build failure after a successful build
pack.rebuild();
@ -175,25 +161,6 @@ describe('Package', function() {
expect(pack.getBuildFailureOutput()).toBeNull();
expect(pack2.getBuildFailureOutput()).toBeNull();
});
it('sets cached incompatible modules to an empty array when the rebuild completes (there may be a build error, but rebuilding *deletes* native modules)', function() {
const packagePath = __guard__(atom.project.getDirectories()[0], x =>
x.resolve('packages/package-with-incompatible-native-module')
);
const pack = buildPackage(packagePath);
expect(pack.getIncompatibleNativeModules().length).toBeGreaterThan(0);
const rebuildCallbacks = [];
spyOn(pack, 'runRebuildProcess').andCallFake(callback =>
rebuildCallbacks.push(callback)
);
pack.rebuild();
expect(pack.getIncompatibleNativeModules().length).toBeGreaterThan(0);
rebuildCallbacks[0]({ code: 0, stdout: 'It worked' });
expect(pack.getIncompatibleNativeModules().length).toBe(0);
});
});
describe('theme', function() {

View File

@ -1773,9 +1773,9 @@
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/node@^14.6.2":
version "14.18.33"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.33.tgz#8c29a0036771569662e4635790ffa9e057db379b"
integrity sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg==
version "14.18.42"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.42.tgz#fa39b2dc8e0eba61bdf51c66502f84e23b66e114"
integrity sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==
"@types/parse-json@^4.0.0":
version "4.0.0"
@ -7216,9 +7216,8 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
"notifications@https://codeload.github.com/atom/notifications/legacy.tar.gz/refs/tags/v0.72.1":
"notifications@file:./packages/notifications":
version "0.72.1"
resolved "https://codeload.github.com/atom/notifications/legacy.tar.gz/refs/tags/v0.72.1#4e5a155624b1189bdcc3416a9f736ed1e030b56e"
dependencies:
dompurify "^1.0.3"
fs-plus "^3.0.0"
@ -8451,9 +8450,9 @@ season@^6.0.2:
fs-plus "^3.0.0"
yargs "^3.23.0"
"second-mate@https://github.com/pulsar-edit/second-mate.git#14aa7bd":
"second-mate@https://github.com/pulsar-edit/second-mate.git#9686771":
version "8.0.0"
resolved "https://github.com/pulsar-edit/second-mate.git#14aa7bd94b90c47aa99f000394301b9573b8898b"
resolved "https://github.com/pulsar-edit/second-mate.git#9686771b4aa3159fe042528d60fcac3af3e1c655"
dependencies:
emissary "^1.3.3"
event-kit "^2.5.3"