From 815ced856b49d1e8088f05f2632adaff545beb28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 9 May 2023 16:50:53 -0300 Subject: [PATCH 01/30] Disabled symbols-view for now --- .github/workflows/package-tests-linux.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/package-tests-linux.yml b/.github/workflows/package-tests-linux.yml index df8b9cf53..1ce4e95bc 100644 --- a/.github/workflows/package-tests-linux.yml +++ b/.github/workflows/package-tests-linux.yml @@ -1,4 +1,4 @@ -name: Package tests for Pulsar on Linux +name: Package tests on: - pull_request env: @@ -107,7 +107,7 @@ jobs: - package: "spell-check" - package: "status-bar" - package: "styleguide" - - package: "symbols-view" + # - package: "symbols-view" - package: "tabs" - package: "timecop" - package: "tree-view" From 0271b47908a8644a218551855449452074abc372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 9 May 2023 16:54:30 -0300 Subject: [PATCH 02/30] Fix flaky test on find-and-replace --- packages/find-and-replace/spec/results-view-spec.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/find-and-replace/spec/results-view-spec.js b/packages/find-and-replace/spec/results-view-spec.js index c318f9adf..6007e84dd 100644 --- a/packages/find-and-replace/spec/results-view-spec.js +++ b/packages/find-and-replace/spec/results-view-spec.js @@ -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'); }) }); From b8ad056f5e7b03ac14de5c81c02fe46fe2ee8503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 9 May 2023 16:59:21 -0300 Subject: [PATCH 03/30] Trigger builds on master too --- .github/workflows/documentation.yml | 2 +- .github/workflows/editor-tests.yml | 4 +++- .github/workflows/package-tests-linux.yml | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index e71f5fc29..a4805e2ff 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -4,7 +4,7 @@ on: push: branches: [ "master" ] workflow_dispatch: - + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/editor-tests.yml b/.github/workflows/editor-tests.yml index 4ab2179fe..c96f6dbe3 100644 --- a/.github/workflows/editor-tests.yml +++ b/.github/workflows/editor-tests.yml @@ -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 diff --git a/.github/workflows/package-tests-linux.yml b/.github/workflows/package-tests-linux.yml index 1ce4e95bc..5cfeedc7b 100644 --- a/.github/workflows/package-tests-linux.yml +++ b/.github/workflows/package-tests-linux.yml @@ -1,6 +1,8 @@ name: Package tests on: - - pull_request + pull_request: + push: + branches: ['master'] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ATOM_JASMINE_REPORTER: list From 9eba119fa8f477c11d97f14a19d8bf6491bdf3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 9 May 2023 19:54:59 -0300 Subject: [PATCH 04/30] Fixed regexp error message --- packages/find-and-replace/spec/find-view-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/find-and-replace/spec/find-view-spec.js b/packages/find-and-replace/spec/find-view-spec.js index 18250f3c5..0a3d132f2 100644 --- a/packages/find-and-replace/spec/find-view-spec.js +++ b/packages/find-and-replace/spec/find-view-spec.js @@ -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("expression is too large"); }); it("will be reset when there is no longer an error", () => { From cbe6ae2ffe9f8c6702178b660f7335c3ac464571 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 9 May 2023 16:11:19 -0700 Subject: [PATCH 05/30] Bump `second-mate` to 9686771 --- CHANGELOG.md | 1 + package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed82deb5f..d0d1c6793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ## [Unreleased] +- 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 diff --git a/package.json b/package.json index 70398b391..e113ea4e8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index c9d6b7a5a..2fe11cbb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -8456,9 +8456,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" From d4b5bc309005cd55e876f04ebc357d1fd12584c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 9 May 2023 21:07:44 -0300 Subject: [PATCH 06/30] Removed tests related to caching incompatible things --- spec/package-spec.js | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/spec/package-spec.js b/spec/package-spec.js index a31c2141c..a235c4e91 100644 --- a/spec/package-spec.js +++ b/spec/package-spec.js @@ -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() { From 99fbca95be2fea3784d24912feaded8c478082da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Wed, 10 May 2023 10:16:02 -0300 Subject: [PATCH 07/30] Fix another flaky test of find-and-replace --- packages/find-and-replace/spec/find-view-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/find-and-replace/spec/find-view-spec.js b/packages/find-and-replace/spec/find-view-spec.js index 0a3d132f2..d41ca02e3 100644 --- a/packages/find-and-replace/spec/find-view-spec.js +++ b/packages/find-and-replace/spec/find-view-spec.js @@ -559,7 +559,7 @@ describe("FindView", () => { it("displays the error", () => { expect(findView.refs.descriptionLabel).toHaveClass("text-error"); - expect(findView.refs.descriptionLabel.textContent).toContain("expression is too large"); + expect(findView.refs.descriptionLabel.textContent).toContain("too large"); }); it("will be reset when there is no longer an error", () => { From 3ed145afb3c5296a5d17b6fd8802d9ef056fcdf1 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 10 May 2023 12:18:01 -0700 Subject: [PATCH 08/30] =?UTF-8?q?Add=20bookmarks=20service=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …for consumption by other packages. This is nearly the simplest possible interface around the `bookmarks` package, except that it will fire events when the number of bookmarks has changed. --- packages/bookmarks/lib/bookmarks-provider.js | 31 ++++++++++++++ packages/bookmarks/lib/bookmarks.js | 40 +++++++++++++------ packages/bookmarks/lib/main.js | 12 ++++-- packages/bookmarks/package.json | 9 +++++ .../bookmarks/spec/bookmarks-view-spec.js | 28 ++++++++++++- 5 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 packages/bookmarks/lib/bookmarks-provider.js diff --git a/packages/bookmarks/lib/bookmarks-provider.js b/packages/bookmarks/lib/bookmarks-provider.js new file mode 100644 index 000000000..507471da2 --- /dev/null +++ b/packages/bookmarks/lib/bookmarks-provider.js @@ -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 diff --git a/packages/bookmarks/lib/bookmarks.js b/packages/bookmarks/lib/bookmarks.js index ffaf66257..b9b414de1 100644 --- a/packages/bookmarks/lib/bookmarks.js +++ b/packages/bookmarks/lib/bookmarks.js @@ -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)) diff --git a/packages/bookmarks/lib/main.js b/packages/bookmarks/lib/main.js index 272b8909c..a9ba90d72 100644 --- a/packages/bookmarks/lib/main.js +++ b/packages/bookmarks/lib/main.js @@ -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 } } diff --git a/packages/bookmarks/package.json b/packages/bookmarks/package.json index 5228364ad..d584eb4b4 100644 --- a/packages/bookmarks/package.json +++ b/packages/bookmarks/package.json @@ -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" + } + } } } diff --git a/packages/bookmarks/spec/bookmarks-view-spec.js b/packages/bookmarks/spec/bookmarks-view-spec.js index a1a0a5375..6b95c30ec 100644 --- a/packages/bookmarks/spec/bookmarks-view-spec.js +++ b/packages/bookmarks/spec/bookmarks-view-spec.js @@ -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) }) }) From d26fbe18b0e42d97905db14451f56886daa9f722 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 10 May 2023 18:19:19 -0700 Subject: [PATCH 09/30] Convert counts to integers, before attempting to convert to locale string --- packages/settings-view/lib/package-card.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/settings-view/lib/package-card.js b/packages/settings-view/lib/package-card.js index d6adc6771..15845201c 100644 --- a/packages/settings-view/lib/package-card.js +++ b/packages/settings-view/lib/package-card.js @@ -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() : '' } } }) From 3f9117765fe2d047e63df35f680f6f9355151696 Mon Sep 17 00:00:00 2001 From: confused_techie Date: Thu, 11 May 2023 10:31:51 -0700 Subject: [PATCH 10/30] Partial completion of `workspace-center` --- src/workspace-center.js | 109 +++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/src/workspace-center.js b/src/workspace-center.js index f803c1243..10293aa12 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -3,7 +3,7 @@ const TextEditor = require('./text-editor'); const PaneContainer = require('./pane-container'); -// Essential: Represents the workspace at the center of the entire window. +/** Essential: Represents the workspace at the center of the entire window. */ module.exports = class WorkspaceCenter { constructor(params) { params.location = 'center'; @@ -64,15 +64,20 @@ module.exports = class WorkspaceCenter { return this.onDidAddTextEditor(({ textEditor }) => callback(textEditor)); } - // Essential: Invoke the given callback with all current and future panes items - // in the workspace center. - // - // * `callback` {Function} to be called with current and future pane items. - // * `item` An item that is present in {::getPaneItems} at the time of - // subscription or that is added at some later time. - // - // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + /** + * Essential: Invoke the given callback with all current and future panes items + * in the workspace center. + * @params {WorkspaceCenter~observePaneItemsCallback} callback - + * Function to be called with current and future pane items. + * @returns {Disposable} Returns a Disposable on which `.dispose()` can be called + * to unsubscribe. + */ observePaneItems(callback) { + /** + * @callback observePaneItemsCallback + * @param {object} item - An item that is present in {::getPaneItems} at the + * time of subscription or that is added at some later time. + */ return this.paneContainer.observePaneItems(callback); } @@ -233,54 +238,58 @@ module.exports = class WorkspaceCenter { return this.paneContainer.onDidDestroyPaneItem(callback); } - // Extended: Invoke the given callback when a text editor is added to the - // workspace center. - // - // * `callback` {Function} to be called when panes are added. - // * `event` {Object} with the following keys: - // * `textEditor` {TextEditor} that was added. - // * `pane` {Pane} containing the added text editor. - // * `index` {Number} indicating the index of the added text editor in its - // pane. - // - // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + /** + * Extended: Invoke the given callback when a text editor is added to the + * workspace center. + * @params {WorkspaceCenter~onDidAddTextEditorCB} callback - Function to be + * called when panes are added. + * @returns {Disposable} on which `.dispose()` can be called to unsubscribe. + */ onDidAddTextEditor(callback) { return this.onDidAddPaneItem(({ item, pane, index }) => { if (item instanceof TextEditor) { + /** + * @callback onDidAddTextEditorCB + * @params {object} event - Object with following keys: + * @params {TextEditor} event.textEditor - The TextEditor that was added. + * @params {Pane} event.pane - Pane containing the added text editor. + * @params {integer} event.index - Number indicating the index of the added text + * editor in it's pane. + */ callback({ textEditor: item, pane, index }); } }); } - /* - Section: Pane Items - */ - - // Essential: Get all pane items in the workspace center. - // - // Returns an {Array} of items. + /** + * Essential: Get all pane items in the workspace center. + * @returns {array} Returns an array of items. + */ getPaneItems() { return this.paneContainer.getPaneItems(); } - // Essential: Get the active {Pane}'s active item. - // - // Returns an pane item {Object}. + /** + * Essential: Get the active {Pane}'s active item. + * @returns {object} A Pane Item + */ getActivePaneItem() { return this.paneContainer.getActivePaneItem(); } - // Essential: Get all text editors in the workspace center. - // - // Returns an {Array} of {TextEditor}s. + /** + * Essential: Get all text editors in the workspace center. + * @returns {TextEditor[]} + */ getTextEditors() { return this.getPaneItems().filter(item => item instanceof TextEditor); } - // Essential: Get the active item if it is an {TextEditor}. - // - // Returns an {TextEditor} or `undefined` if the current active item is not an - // {TextEditor}. + /** + * Essential: Get the active item if it is an {TextEditor}. + * @returns {TextEditor|undefined} Returns TextEditor or `undefined` if the + * current active item is not an {TextEditor} + */ getActiveTextEditor() { const activeItem = this.getActivePaneItem(); if (activeItem instanceof TextEditor) { @@ -288,7 +297,7 @@ module.exports = class WorkspaceCenter { } } - // Save all pane items. + /** Save all pane items. */ saveAll() { this.paneContainer.saveAll(); } @@ -297,30 +306,28 @@ module.exports = class WorkspaceCenter { return this.paneContainer.confirmClose(options); } - /* - Section: Panes - */ - - // Extended: Get all panes in the workspace center. - // - // Returns an {Array} of {Pane}s. + /** + * Extended: Get all panes in the workspace center. + * @returns {Pane[]} + */ getPanes() { return this.paneContainer.getPanes(); } - // Extended: Get the active {Pane}. - // - // Returns a {Pane}. + /** + * Extended: Get the active {Pane}. + * @returns {Pane} + */ getActivePane() { return this.paneContainer.getActivePane(); } - // Extended: Make the next pane active. + /** Extended: Make the next pane active. */ activateNextPane() { return this.paneContainer.activateNextPane(); } - // Extended: Make the previous pane active. + /** Extended: Make the previous pane active. */ activatePreviousPane() { return this.paneContainer.activatePreviousPane(); } @@ -333,7 +340,7 @@ module.exports = class WorkspaceCenter { return this.paneContainer.paneForItem(item); } - // Destroy (close) the active pane. + /** Destroy (close) the active pane. */ destroyActivePane() { const activePane = this.getActivePane(); if (activePane != null) { From 640c7c76a719b96af9ca4a72b1393a451677cd8c Mon Sep 17 00:00:00 2001 From: confused_techie Date: Thu, 11 May 2023 13:45:56 -0700 Subject: [PATCH 11/30] Convert `window.js` --- src/window.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/window.js b/src/window.js index a13a5e648..b7b4cdc64 100644 --- a/src/window.js +++ b/src/window.js @@ -1,10 +1,10 @@ -// Public: Measure how long a function takes to run. -// -// description - A {String} description that will be logged to the console when -// the function completes. -// fn - A {Function} to measure the duration of. -// -// Returns the value returned by the given function. +/** + * Public: Measure how long a function takes to run. + * @params {string} description - A string description that will be logged to the + * console when the function completes. + * @params {function} fn - A function to measure the duration of. + * @returns {*} Returns the value returned by the given function. + */ window.measure = function(description, fn) { let start = Date.now(); let value = fn(); @@ -13,13 +13,13 @@ window.measure = function(description, fn) { return value; }; -// Public: Create a dev tools profile for a function. -// -// description - A {String} description that will be available in the Profiles -// tab of the dev tools. -// fn - A {Function} to profile. -// -// Returns the value returned by the given function. +/** + * Public: Create a dev tools profile for a function. + * @params {string} description - A string description that will be available in + * the profiles tab of the dev tools. + * @params {function} fn - A Function to profile. + * @returns {*} Returns the value returned by the given function. + */ window.profile = function(description, fn) { window.measure(description, function() { console.profile(description); From 085c4dc756e02ed63eeae90d9c76312c1535132e Mon Sep 17 00:00:00 2001 From: confused_techie Date: Thu, 11 May 2023 14:36:51 -0700 Subject: [PATCH 12/30] Convert `view-registry` --- src/view-registry.js | 139 +++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 72 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index eaf00e18c..019f1a2bd 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -3,25 +3,27 @@ const { Disposable } = require('event-kit'); const AnyConstructor = Symbol('any-constructor'); -// Essential: `ViewRegistry` handles the association between model and view -// types in Pulsar. We call this association a View Provider. As in, for a given -// model, this class can provide a view via {::getView}, as long as the -// model/view association was registered via {::addViewProvider} -// -// If you're adding your own kind of pane item, a good strategy for all but the -// simplest items is to separate the model and the view. The model handles -// application logic and is the primary point of API interaction. The view -// just handles presentation. -// -// Note: Models can be any object, but must implement a `getTitle()` function -// if they are to be displayed in a {Pane} -// -// View providers inform the workspace how your model objects should be -// presented in the DOM. A view provider must always return a DOM node, which -// makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) -// an ideal tool for implementing views in Pulsar. -// -// You can access the `ViewRegistry` object via `atom.views`. +/** + * Essential: `ViewRegistry` handles the association between model and view + * types in Pulsar. We call this association a View Provider. As in, for a given + * model, this class can provide a view via {::getView}, as long as the + * model/view association was registered via {::addViewProvider}. + * + * If you're adding your own kind of pane item, a good strategy for all but the + * simplest items is to separate the model and the view. THe model handles + * application logic and is the primary point of the API interaction. The view + * just handles presentation. + + * Note: Modles can be any object, but must implement a `getTtile()` function + * if they are to be displayed in a {Pane}. + * + * View providers inform the workspace how your model objects should be + * presented in the DOM. A view provider must always return a DOM node, which + * makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) + * an ideal tool for implementing views in Pulsar. + * + * You can access the `ViewRegistry` object via `atom.views` + */ module.exports = class ViewRegistry { constructor(atomEnvironment) { this.animationFrameRequest = null; @@ -37,34 +39,30 @@ module.exports = class ViewRegistry { this.clearDocumentRequests(); } - // Essential: Add a provider that will be used to construct views in the - // workspace's view layer based on model objects in its model layer. - // - // ## Examples - // - // Text editors are divided into a model and a view layer, so when you interact - // with methods like `atom.workspace.getActiveTextEditor()` you're only going - // to get the model object. We display text editors on screen by teaching the - // workspace what view constructor it should use to represent them: - // - // ```coffee - // atom.views.addViewProvider TextEditor, (textEditor) -> - // textEditorElement = new TextEditorElement - // textEditorElement.initialize(textEditor) - // textEditorElement - // ``` - // - // * `modelConstructor` (optional) Constructor {Function} for your model. If - // a constructor is given, the `createView` function will only be used - // for model objects inheriting from that constructor. Otherwise, it will - // will be called for any object. - // * `createView` Factory {Function} that is passed an instance of your model - // and must return a subclass of `HTMLElement` or `undefined`. If it returns - // `undefined`, then the registry will continue to search for other view - // providers. - // - // Returns a {Disposable} on which `.dispose()` can be called to remove the - // added provider. + /** + * Essential: Add a provider that will be used to construct views in the + * workspace's view layer based on model objects in its model layer. + * + * Text editors are divided into a model and a view layer, so when you interact + * with methods like `atom.workspace.getActiveTextEditor()` you're only going + * to get the model object. We display text editors on screen by teaching the + * workspace what view constructor it should use to represent them + * @example + * atom.views.addViewProvider TextEditor, (textEditor) -> + * textEditorElement = new TextEditorElement + * textEditorElement.initialize(textEditor) + * textEditorElement + * @param {function} [modelConstructor] - Constructor Function for you model. + * If a constructor is given, the `createView` function will only be used for + * model objects inheriting from that constructor. Otherwise, it will + * will be called for any object. + * @param {function} createView - Factory function that is not passed an + * instance of your model and must return a subclass of `HTMLElement` or + * `undefined`. If it returns `undefined`, then the registry will continue to + * search for other viewproviders. + * returns {Disposable} Returns a `Disposable` on which `.dispose()` can be + * called to added provider. + */ addViewProvider(modelConstructor, createView) { let provider; if (arguments.length === 1) { @@ -98,31 +96,28 @@ module.exports = class ViewRegistry { return this.providers.length; } - // Essential: Get the view associated with an object in the workspace. - // - // If you're just *using* the workspace, you shouldn't need to access the view - // layer, but view layer access may be necessary if you want to perform DOM - // manipulation that isn't supported via the model API. - // - // ## View Resolution Algorithm - // - // The view associated with the object is resolved using the following - // sequence - // - // 1. Is the object an instance of `HTMLElement`? If true, return the object. - // 2. Does the object have a method named `getElement` that returns an - // instance of `HTMLElement`? If true, return that value. - // 3. Does the object have a property named `element` with a value which is - // an instance of `HTMLElement`? If true, return the property value. - // 4. Is the object a jQuery object, indicated by the presence of a `jquery` - // property? If true, return the root DOM element (i.e. `object[0]`). - // 5. Has a view provider been registered for the object? If true, use the - // provider to create a view associated with the object, and return the - // view. - // - // If no associated view is returned by the sequence an error is thrown. - // - // Returns a DOM element. + /** + * Essential: Get the view associated with an object in the workspace. + * + * If you're just *using* the workspace, you shouldn't need to access the view + * layer, but view layer access may be necessary if you want to perform DOM + * manipulation that isn't supported via the model API. + * ## View Resolution Algorithm + * The view associated with the object is resolved using the following sequence + * 1. Is the object an instance of `HTMLEelement`? If true, return the object. + * 2. Does the object have a method named `getElement` that returns an instance + * of `HTMLElement`? If true, return that value. + * 3. Does the object have a property named `element` with a value which is + * an instance of `HTMLElement`? If true, return the property value. + * 4. Is the object a jQuery object, indicated by the presence of a `jquery` + * property? If true, return the root DOM element (i.e. `object[0]`). + * 5. Has a view provider been registered for the object? If true, use the + * provider to create a view associated with the subject, and return + * the view. + * + * If no associated view is returned by the sequence an error is thrown. + * @returns {object} A DOM element. + */ getView(object) { if (object == null) { return; From 89c63926099237f4cf3607fac7e94d9b517015dd Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 11 May 2023 22:27:12 +0000 Subject: [PATCH 13/30] GH Action Documentation --- docs/Pulsar-API-Documentation.md | 27 +++++++++++++++++++++++++++ docs/Source-Code-Documentation.md | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/docs/Pulsar-API-Documentation.md b/docs/Pulsar-API-Documentation.md index df38ca986..3df5a3cd5 100644 --- a/docs/Pulsar-API-Documentation.md +++ b/docs/Pulsar-API-Documentation.md @@ -209,6 +209,15 @@ style: Exclusively used for the style attribute

+## Typedefs + +
+
observePaneItemsCallback : function
+
+
onDidAddTextEditorCB : function
+
+
+ ## AtomEnvironment @@ -648,3 +657,21 @@ This file aims to run some short simple tests against `update.js`. Focusing ## beforeEach() **Kind**: global function **Babel**: + + +## observePaneItemsCallback : function +**Kind**: global typedef + +| Param | Type | Description | +| --- | --- | --- | +| item | object | An item that is present in {::getPaneItems} at the time of subscription or that is added at some later time. | + + + +## onDidAddTextEditorCB : function +**Kind**: global typedef +**Params**: object event - Object with following keys: +**Params**: TextEditor event.textEditor - The TextEditor that was added. +**Params**: Pane event.pane - Pane containing the added text editor. +**Params**: integer event.index - Number indicating the index of the added text +editor in it's pane. diff --git a/docs/Source-Code-Documentation.md b/docs/Source-Code-Documentation.md index 4651c1d42..7a6381378 100644 --- a/docs/Source-Code-Documentation.md +++ b/docs/Source-Code-Documentation.md @@ -209,6 +209,15 @@ style: Exclusively used for the style attribute

+## Typedefs + +
+
observePaneItemsCallback : function
+
+
onDidAddTextEditorCB : function
+
+
+ ## AtomEnvironment @@ -656,3 +665,21 @@ This file aims to run some short simple tests against `update.js`. Focusing ## beforeEach() **Kind**: global function **Babel**: + + +## observePaneItemsCallback : function +**Kind**: global typedef + +| Param | Type | Description | +| --- | --- | --- | +| item | object | An item that is present in {::getPaneItems} at the time of subscription or that is added at some later time. | + + + +## onDidAddTextEditorCB : function +**Kind**: global typedef +**Params**: object event - Object with following keys: +**Params**: TextEditor event.textEditor - The TextEditor that was added. +**Params**: Pane event.pane - Pane containing the added text editor. +**Params**: integer event.index - Number indicating the index of the added text +editor in it's pane. From ba9cd05250a1d11b737917a67f0941306051e02e Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 11 May 2023 16:05:29 -0700 Subject: [PATCH 14/30] Revert "Partial completion of `workspace-center`" This reverts commit 3f9117765fe2d047e63df35f680f6f9355151696. --- src/workspace-center.js | 109 +++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/src/workspace-center.js b/src/workspace-center.js index 10293aa12..f803c1243 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -3,7 +3,7 @@ const TextEditor = require('./text-editor'); const PaneContainer = require('./pane-container'); -/** Essential: Represents the workspace at the center of the entire window. */ +// Essential: Represents the workspace at the center of the entire window. module.exports = class WorkspaceCenter { constructor(params) { params.location = 'center'; @@ -64,20 +64,15 @@ module.exports = class WorkspaceCenter { return this.onDidAddTextEditor(({ textEditor }) => callback(textEditor)); } - /** - * Essential: Invoke the given callback with all current and future panes items - * in the workspace center. - * @params {WorkspaceCenter~observePaneItemsCallback} callback - - * Function to be called with current and future pane items. - * @returns {Disposable} Returns a Disposable on which `.dispose()` can be called - * to unsubscribe. - */ + // Essential: Invoke the given callback with all current and future panes items + // in the workspace center. + // + // * `callback` {Function} to be called with current and future pane items. + // * `item` An item that is present in {::getPaneItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePaneItems(callback) { - /** - * @callback observePaneItemsCallback - * @param {object} item - An item that is present in {::getPaneItems} at the - * time of subscription or that is added at some later time. - */ return this.paneContainer.observePaneItems(callback); } @@ -238,58 +233,54 @@ module.exports = class WorkspaceCenter { return this.paneContainer.onDidDestroyPaneItem(callback); } - /** - * Extended: Invoke the given callback when a text editor is added to the - * workspace center. - * @params {WorkspaceCenter~onDidAddTextEditorCB} callback - Function to be - * called when panes are added. - * @returns {Disposable} on which `.dispose()` can be called to unsubscribe. - */ + // Extended: Invoke the given callback when a text editor is added to the + // workspace center. + // + // * `callback` {Function} to be called when panes are added. + // * `event` {Object} with the following keys: + // * `textEditor` {TextEditor} that was added. + // * `pane` {Pane} containing the added text editor. + // * `index` {Number} indicating the index of the added text editor in its + // pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddTextEditor(callback) { return this.onDidAddPaneItem(({ item, pane, index }) => { if (item instanceof TextEditor) { - /** - * @callback onDidAddTextEditorCB - * @params {object} event - Object with following keys: - * @params {TextEditor} event.textEditor - The TextEditor that was added. - * @params {Pane} event.pane - Pane containing the added text editor. - * @params {integer} event.index - Number indicating the index of the added text - * editor in it's pane. - */ callback({ textEditor: item, pane, index }); } }); } - /** - * Essential: Get all pane items in the workspace center. - * @returns {array} Returns an array of items. - */ + /* + Section: Pane Items + */ + + // Essential: Get all pane items in the workspace center. + // + // Returns an {Array} of items. getPaneItems() { return this.paneContainer.getPaneItems(); } - /** - * Essential: Get the active {Pane}'s active item. - * @returns {object} A Pane Item - */ + // Essential: Get the active {Pane}'s active item. + // + // Returns an pane item {Object}. getActivePaneItem() { return this.paneContainer.getActivePaneItem(); } - /** - * Essential: Get all text editors in the workspace center. - * @returns {TextEditor[]} - */ + // Essential: Get all text editors in the workspace center. + // + // Returns an {Array} of {TextEditor}s. getTextEditors() { return this.getPaneItems().filter(item => item instanceof TextEditor); } - /** - * Essential: Get the active item if it is an {TextEditor}. - * @returns {TextEditor|undefined} Returns TextEditor or `undefined` if the - * current active item is not an {TextEditor} - */ + // Essential: Get the active item if it is an {TextEditor}. + // + // Returns an {TextEditor} or `undefined` if the current active item is not an + // {TextEditor}. getActiveTextEditor() { const activeItem = this.getActivePaneItem(); if (activeItem instanceof TextEditor) { @@ -297,7 +288,7 @@ module.exports = class WorkspaceCenter { } } - /** Save all pane items. */ + // Save all pane items. saveAll() { this.paneContainer.saveAll(); } @@ -306,28 +297,30 @@ module.exports = class WorkspaceCenter { return this.paneContainer.confirmClose(options); } - /** - * Extended: Get all panes in the workspace center. - * @returns {Pane[]} - */ + /* + Section: Panes + */ + + // Extended: Get all panes in the workspace center. + // + // Returns an {Array} of {Pane}s. getPanes() { return this.paneContainer.getPanes(); } - /** - * Extended: Get the active {Pane}. - * @returns {Pane} - */ + // Extended: Get the active {Pane}. + // + // Returns a {Pane}. getActivePane() { return this.paneContainer.getActivePane(); } - /** Extended: Make the next pane active. */ + // Extended: Make the next pane active. activateNextPane() { return this.paneContainer.activateNextPane(); } - /** Extended: Make the previous pane active. */ + // Extended: Make the previous pane active. activatePreviousPane() { return this.paneContainer.activatePreviousPane(); } @@ -340,7 +333,7 @@ module.exports = class WorkspaceCenter { return this.paneContainer.paneForItem(item); } - /** Destroy (close) the active pane. */ + // Destroy (close) the active pane. destroyActivePane() { const activePane = this.getActivePane(); if (activePane != null) { From 7d28d50993b551f36c4cf7b82014e578410cf62d Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 11 May 2023 16:05:32 -0700 Subject: [PATCH 15/30] Revert "Convert `window.js`" This reverts commit 640c7c76a719b96af9ca4a72b1393a451677cd8c. --- src/window.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/window.js b/src/window.js index b7b4cdc64..a13a5e648 100644 --- a/src/window.js +++ b/src/window.js @@ -1,10 +1,10 @@ -/** - * Public: Measure how long a function takes to run. - * @params {string} description - A string description that will be logged to the - * console when the function completes. - * @params {function} fn - A function to measure the duration of. - * @returns {*} Returns the value returned by the given function. - */ +// Public: Measure how long a function takes to run. +// +// description - A {String} description that will be logged to the console when +// the function completes. +// fn - A {Function} to measure the duration of. +// +// Returns the value returned by the given function. window.measure = function(description, fn) { let start = Date.now(); let value = fn(); @@ -13,13 +13,13 @@ window.measure = function(description, fn) { return value; }; -/** - * Public: Create a dev tools profile for a function. - * @params {string} description - A string description that will be available in - * the profiles tab of the dev tools. - * @params {function} fn - A Function to profile. - * @returns {*} Returns the value returned by the given function. - */ +// Public: Create a dev tools profile for a function. +// +// description - A {String} description that will be available in the Profiles +// tab of the dev tools. +// fn - A {Function} to profile. +// +// Returns the value returned by the given function. window.profile = function(description, fn) { window.measure(description, function() { console.profile(description); From 519916272dee12ddc65d707d16fa968733364c7c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 11 May 2023 16:05:35 -0700 Subject: [PATCH 16/30] Revert "Convert `view-registry`" This reverts commit 085c4dc756e02ed63eeae90d9c76312c1535132e. --- src/view-registry.js | 139 ++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 67 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index 019f1a2bd..eaf00e18c 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -3,27 +3,25 @@ const { Disposable } = require('event-kit'); const AnyConstructor = Symbol('any-constructor'); -/** - * Essential: `ViewRegistry` handles the association between model and view - * types in Pulsar. We call this association a View Provider. As in, for a given - * model, this class can provide a view via {::getView}, as long as the - * model/view association was registered via {::addViewProvider}. - * - * If you're adding your own kind of pane item, a good strategy for all but the - * simplest items is to separate the model and the view. THe model handles - * application logic and is the primary point of the API interaction. The view - * just handles presentation. - - * Note: Modles can be any object, but must implement a `getTtile()` function - * if they are to be displayed in a {Pane}. - * - * View providers inform the workspace how your model objects should be - * presented in the DOM. A view provider must always return a DOM node, which - * makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) - * an ideal tool for implementing views in Pulsar. - * - * You can access the `ViewRegistry` object via `atom.views` - */ +// Essential: `ViewRegistry` handles the association between model and view +// types in Pulsar. We call this association a View Provider. As in, for a given +// model, this class can provide a view via {::getView}, as long as the +// model/view association was registered via {::addViewProvider} +// +// If you're adding your own kind of pane item, a good strategy for all but the +// simplest items is to separate the model and the view. The model handles +// application logic and is the primary point of API interaction. The view +// just handles presentation. +// +// Note: Models can be any object, but must implement a `getTitle()` function +// if they are to be displayed in a {Pane} +// +// View providers inform the workspace how your model objects should be +// presented in the DOM. A view provider must always return a DOM node, which +// makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) +// an ideal tool for implementing views in Pulsar. +// +// You can access the `ViewRegistry` object via `atom.views`. module.exports = class ViewRegistry { constructor(atomEnvironment) { this.animationFrameRequest = null; @@ -39,30 +37,34 @@ module.exports = class ViewRegistry { this.clearDocumentRequests(); } - /** - * Essential: Add a provider that will be used to construct views in the - * workspace's view layer based on model objects in its model layer. - * - * Text editors are divided into a model and a view layer, so when you interact - * with methods like `atom.workspace.getActiveTextEditor()` you're only going - * to get the model object. We display text editors on screen by teaching the - * workspace what view constructor it should use to represent them - * @example - * atom.views.addViewProvider TextEditor, (textEditor) -> - * textEditorElement = new TextEditorElement - * textEditorElement.initialize(textEditor) - * textEditorElement - * @param {function} [modelConstructor] - Constructor Function for you model. - * If a constructor is given, the `createView` function will only be used for - * model objects inheriting from that constructor. Otherwise, it will - * will be called for any object. - * @param {function} createView - Factory function that is not passed an - * instance of your model and must return a subclass of `HTMLElement` or - * `undefined`. If it returns `undefined`, then the registry will continue to - * search for other viewproviders. - * returns {Disposable} Returns a `Disposable` on which `.dispose()` can be - * called to added provider. - */ + // Essential: Add a provider that will be used to construct views in the + // workspace's view layer based on model objects in its model layer. + // + // ## Examples + // + // Text editors are divided into a model and a view layer, so when you interact + // with methods like `atom.workspace.getActiveTextEditor()` you're only going + // to get the model object. We display text editors on screen by teaching the + // workspace what view constructor it should use to represent them: + // + // ```coffee + // atom.views.addViewProvider TextEditor, (textEditor) -> + // textEditorElement = new TextEditorElement + // textEditorElement.initialize(textEditor) + // textEditorElement + // ``` + // + // * `modelConstructor` (optional) Constructor {Function} for your model. If + // a constructor is given, the `createView` function will only be used + // for model objects inheriting from that constructor. Otherwise, it will + // will be called for any object. + // * `createView` Factory {Function} that is passed an instance of your model + // and must return a subclass of `HTMLElement` or `undefined`. If it returns + // `undefined`, then the registry will continue to search for other view + // providers. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // added provider. addViewProvider(modelConstructor, createView) { let provider; if (arguments.length === 1) { @@ -96,28 +98,31 @@ module.exports = class ViewRegistry { return this.providers.length; } - /** - * Essential: Get the view associated with an object in the workspace. - * - * If you're just *using* the workspace, you shouldn't need to access the view - * layer, but view layer access may be necessary if you want to perform DOM - * manipulation that isn't supported via the model API. - * ## View Resolution Algorithm - * The view associated with the object is resolved using the following sequence - * 1. Is the object an instance of `HTMLEelement`? If true, return the object. - * 2. Does the object have a method named `getElement` that returns an instance - * of `HTMLElement`? If true, return that value. - * 3. Does the object have a property named `element` with a value which is - * an instance of `HTMLElement`? If true, return the property value. - * 4. Is the object a jQuery object, indicated by the presence of a `jquery` - * property? If true, return the root DOM element (i.e. `object[0]`). - * 5. Has a view provider been registered for the object? If true, use the - * provider to create a view associated with the subject, and return - * the view. - * - * If no associated view is returned by the sequence an error is thrown. - * @returns {object} A DOM element. - */ + // Essential: Get the view associated with an object in the workspace. + // + // If you're just *using* the workspace, you shouldn't need to access the view + // layer, but view layer access may be necessary if you want to perform DOM + // manipulation that isn't supported via the model API. + // + // ## View Resolution Algorithm + // + // The view associated with the object is resolved using the following + // sequence + // + // 1. Is the object an instance of `HTMLElement`? If true, return the object. + // 2. Does the object have a method named `getElement` that returns an + // instance of `HTMLElement`? If true, return that value. + // 3. Does the object have a property named `element` with a value which is + // an instance of `HTMLElement`? If true, return the property value. + // 4. Is the object a jQuery object, indicated by the presence of a `jquery` + // property? If true, return the root DOM element (i.e. `object[0]`). + // 5. Has a view provider been registered for the object? If true, use the + // provider to create a view associated with the object, and return the + // view. + // + // If no associated view is returned by the sequence an error is thrown. + // + // Returns a DOM element. getView(object) { if (object == null) { return; From 4b23e5fcc00abeabaad14b03150f192723f2ad40 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 11 May 2023 16:05:38 -0700 Subject: [PATCH 17/30] Revert "GH Action Documentation" This reverts commit 89c63926099237f4cf3607fac7e94d9b517015dd. --- docs/Pulsar-API-Documentation.md | 27 --------------------------- docs/Source-Code-Documentation.md | 27 --------------------------- 2 files changed, 54 deletions(-) diff --git a/docs/Pulsar-API-Documentation.md b/docs/Pulsar-API-Documentation.md index 3df5a3cd5..df38ca986 100644 --- a/docs/Pulsar-API-Documentation.md +++ b/docs/Pulsar-API-Documentation.md @@ -209,15 +209,6 @@ style: Exclusively used for the style attribute

-## Typedefs - -
-
observePaneItemsCallback : function
-
-
onDidAddTextEditorCB : function
-
-
- ## AtomEnvironment @@ -657,21 +648,3 @@ This file aims to run some short simple tests against `update.js`. Focusing ## beforeEach() **Kind**: global function **Babel**: - - -## observePaneItemsCallback : function -**Kind**: global typedef - -| Param | Type | Description | -| --- | --- | --- | -| item | object | An item that is present in {::getPaneItems} at the time of subscription or that is added at some later time. | - - - -## onDidAddTextEditorCB : function -**Kind**: global typedef -**Params**: object event - Object with following keys: -**Params**: TextEditor event.textEditor - The TextEditor that was added. -**Params**: Pane event.pane - Pane containing the added text editor. -**Params**: integer event.index - Number indicating the index of the added text -editor in it's pane. diff --git a/docs/Source-Code-Documentation.md b/docs/Source-Code-Documentation.md index 7a6381378..4651c1d42 100644 --- a/docs/Source-Code-Documentation.md +++ b/docs/Source-Code-Documentation.md @@ -209,15 +209,6 @@ style: Exclusively used for the style attribute

-## Typedefs - -
-
observePaneItemsCallback : function
-
-
onDidAddTextEditorCB : function
-
-
- ## AtomEnvironment @@ -665,21 +656,3 @@ This file aims to run some short simple tests against `update.js`. Focusing ## beforeEach() **Kind**: global function **Babel**: - - -## observePaneItemsCallback : function -**Kind**: global typedef - -| Param | Type | Description | -| --- | --- | --- | -| item | object | An item that is present in {::getPaneItems} at the time of subscription or that is added at some later time. | - - - -## onDidAddTextEditorCB : function -**Kind**: global typedef -**Params**: object event - Object with following keys: -**Params**: TextEditor event.textEditor - The TextEditor that was added. -**Params**: Pane event.pane - Pane containing the added text editor. -**Params**: integer event.index - Number indicating the index of the added text -editor in it's pane. From a94ffa17dfa66569138edf421bcaab9459c19434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 11 May 2023 22:31:05 -0300 Subject: [PATCH 18/30] Bundle notifications --- packages/notifications/.coffeelintignore | 1 + packages/notifications/.gitignore | 3 + packages/notifications/LICENSE.md | 20 + .../notifications/PULL_REQUEST_TEMPLATE.md | 28 + packages/notifications/README.md | 10 + packages/notifications/keymaps/messages.cson | 11 + packages/notifications/lib/command-logger.js | 214 ++++ packages/notifications/lib/main.js | 192 ++++ .../notifications/lib/notification-element.js | 365 +++++++ .../notifications/lib/notification-issue.js | 314 ++++++ .../lib/notifications-log-item.js | 110 ++ .../notifications/lib/notifications-log.js | 148 +++ packages/notifications/lib/template-helper.js | 17 + packages/notifications/lib/user-utilities.js | 199 ++++ packages/notifications/package.json | 39 + .../spec/command-logger-spec.coffee | 141 +++ packages/notifications/spec/helper.coffee | 41 + .../spec/notifications-log-spec.coffee | 323 ++++++ .../spec/notifications-spec.coffee | 966 ++++++++++++++++++ .../styles/notifications-log.less | 193 ++++ .../notifications/styles/notifications.less | 336 ++++++ 21 files changed, 3671 insertions(+) create mode 100644 packages/notifications/.coffeelintignore create mode 100644 packages/notifications/.gitignore create mode 100644 packages/notifications/LICENSE.md create mode 100644 packages/notifications/PULL_REQUEST_TEMPLATE.md create mode 100644 packages/notifications/README.md create mode 100644 packages/notifications/keymaps/messages.cson create mode 100644 packages/notifications/lib/command-logger.js create mode 100644 packages/notifications/lib/main.js create mode 100644 packages/notifications/lib/notification-element.js create mode 100644 packages/notifications/lib/notification-issue.js create mode 100644 packages/notifications/lib/notifications-log-item.js create mode 100644 packages/notifications/lib/notifications-log.js create mode 100644 packages/notifications/lib/template-helper.js create mode 100644 packages/notifications/lib/user-utilities.js create mode 100644 packages/notifications/package.json create mode 100644 packages/notifications/spec/command-logger-spec.coffee create mode 100644 packages/notifications/spec/helper.coffee create mode 100644 packages/notifications/spec/notifications-log-spec.coffee create mode 100644 packages/notifications/spec/notifications-spec.coffee create mode 100644 packages/notifications/styles/notifications-log.less create mode 100644 packages/notifications/styles/notifications.less diff --git a/packages/notifications/.coffeelintignore b/packages/notifications/.coffeelintignore new file mode 100644 index 000000000..1db51fed7 --- /dev/null +++ b/packages/notifications/.coffeelintignore @@ -0,0 +1 @@ +spec/fixtures diff --git a/packages/notifications/.gitignore b/packages/notifications/.gitignore new file mode 100644 index 000000000..ade14b919 --- /dev/null +++ b/packages/notifications/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +npm-debug.log +node_modules diff --git a/packages/notifications/LICENSE.md b/packages/notifications/LICENSE.md new file mode 100644 index 000000000..4d231b456 --- /dev/null +++ b/packages/notifications/LICENSE.md @@ -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. diff --git a/packages/notifications/PULL_REQUEST_TEMPLATE.md b/packages/notifications/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..cdaa94a86 --- /dev/null +++ b/packages/notifications/PULL_REQUEST_TEMPLATE.md @@ -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 + + + +### Alternate Designs + + + +### Benefits + + + +### Possible Drawbacks + + + +### Applicable Issues + + diff --git a/packages/notifications/README.md b/packages/notifications/README.md new file mode 100644 index 000000000..8fbc571e4 --- /dev/null +++ b/packages/notifications/README.md @@ -0,0 +1,10 @@ +##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) + # Notifications package +[![CI](https://github.com/atom/notifications/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/notifications/actions/workflows/ci.yml) + +![notifications](https://cloud.githubusercontent.com/assets/69169/5176406/350d0e80-73fd-11e4-8101-1776b9d6d8bf.gif) + +### Docs + +Notifications are available for use in your Atom 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. diff --git a/packages/notifications/keymaps/messages.cson b/packages/notifications/keymaps/messages.cson new file mode 100644 index 000000000..beacf16b9 --- /dev/null +++ b/packages/notifications/keymaps/messages.cson @@ -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' diff --git a/packages/notifications/lib/command-logger.js b/packages/notifications/lib/command-logger.js new file mode 100644 index 000000000..85d7f29ce --- /dev/null +++ b/packages/notifications/lib/command-logger.js @@ -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 Atom 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; +} \ No newline at end of file diff --git a/packages/notifications/lib/main.js b/packages/notifications/lib/main.js new file mode 100644 index 000000000..16b1b0df4 --- /dev/null +++ b/packages/notifications/lib/main.js @@ -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 === '') || fs.isAbsolute(file)) { + return true; + } + } + return false; +}; + +module.exports = Notifications; diff --git a/packages/notifications/lib/notification-element.js b/packages/notifications/lib/notification-element.js new file mode 100644 index 000000000..469629c05 --- /dev/null +++ b/packages/notifications/lib/notification-element.js @@ -0,0 +1,365 @@ +/* + * 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 = `\ +
+
+
+
+ +
+
+
+
+
+
Close All
\ +`; + +const FatalMetaNotificationTemplate = `\ +
+
+ + +
\ +`; + +const MetaNotificationTemplate = `\ +
\ +`; + +const ButtonListTemplate = `\ +
\ +`; + +const ButtonTemplate = `\ +\ +`; + +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 ${packageName} package. `; + } 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 atom/atom"; + } + + const promises = []; + promises.push(this.issue.findSimilarIssues()); + promises.push(UserUtilities.checkAtomUpToDate()); + 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 += `\ +${packageName} 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 += `\ +

+Locally installed core Pulsar package ${packageName} is out of date: ${packageCheck.installedVersion} installed locally; +${packageCheck.versionShippedWithAtom} 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 += `\ +

+Use: apm unlink ${packagePath}\ +`; + } + } else if ((atomCheck != null) && !atomCheck.upToDate) { + issueButton.remove(); + + fatalNotification.innerHTML += `\ +Pulsar is out of date: ${atomCheck.installedVersion} installed; +${atomCheck.latestVersion} latest. +Upgrading to the latest version 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 = 'Hide Stack Trace'; + return container.style.display = 'block'; + } else { + e.currentTarget.innerHTML = '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; +} diff --git a/packages/notifications/lib/notification-issue.js b/packages/notifications/lib/notification-issue.js new file mode 100644 index 000000000..2bf083528 --- /dev/null +++ b/packages/notifications/lib/notification-issue.js @@ -0,0 +1,314 @@ +/* + * 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 + * 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 = +(NotificationIssue = class NotificationIssue { + constructor(notification) { + this.normalizedStackPaths = this.normalizedStackPaths.bind(this); + this.notification = notification; + } + + findSimilarIssues() { + let repoUrl = this.getRepoUrl(); + if (repoUrl == null) { repoUrl = 'atom/atom'; } + 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(e => 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/atom/atom'; } + 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 = __guard__(__guard__(atom.packages.getLoadedPackage(packageName), x1 => x1.metadata), x => x.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 = 'Atom Core'; + } + + this.issueBody = `\ + + +### Prerequisites + +* [ ] Put an X between the brackets on this line if you have done all of the following: + * Reproduced the problem in Safe Mode: + * Followed all applicable steps in the debugging guide: + * Checked the FAQs on the message board for common solutions: + * Checked that your issue isn't already filed: + * Checked that there is not already an Atom package that provides the described functionality: + +### Description + + + +### Steps to Reproduce + +1. +2. +3. + +**Expected behavior:** + + + +**Actual behavior:** + + + +### 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 + + +${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 = __guard__(__guard__(atom.packages.getLoadedPackage(packageName), x1 => x1.metadata), x => x.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 = __guard__(JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'))), x2 => x2.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; +} diff --git a/packages/notifications/lib/notifications-log-item.js b/packages/notifications/lib/notifications-log-item.js new file mode 100644 index 000000000..cb87c0038 --- /dev/null +++ b/packages/notifications/lib/notifications-log-item.js @@ -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; +})()); diff --git a/packages/notifications/lib/notifications-log.js b/packages/notifications/lib/notifications-log.js new file mode 100644 index 000000000..f5b79c05c --- /dev/null +++ b/packages/notifications/lib/notifications-log.js @@ -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; +})()); diff --git a/packages/notifications/lib/template-helper.js b/packages/notifications/lib/template-helper.js new file mode 100644 index 000000000..52f923f50 --- /dev/null +++ b/packages/notifications/lib/template-helper.js @@ -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); + } +}; diff --git a/packages/notifications/lib/user-utilities.js b/packages/notifications/lib/user-utilities.js new file mode 100644 index 000000000..7bfed7019 --- /dev/null +++ b/packages/notifications/lib/user-utilities.js @@ -0,0 +1,199 @@ +/* + * 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 + * 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)' : ''}`)); + }); + }, + + getLatestAtomData() { + const githubHeaders = new Headers({ + accept: 'application/vnd.github.v3+json', + contentType: "application/json" + }); + return fetch('https://atom.io/api/updates', {headers: githubHeaders}) + .then(function(r) { if (r.ok) { return r.json(); } else { return Promise.reject(r.statusCode); } }); + }, + + checkAtomUpToDate() { + return this.getLatestAtomData().then(function(latestAtomData) { + const installedVersion = __guard__(atom.getVersion(), x => x.replace(/-.*$/, '')); + const latestVersion = latestAtomData.name; + const upToDate = (installedVersion != null) && semver.gte(installedVersion, latestVersion); + return {upToDate, latestVersion, installedVersion};}); + }, + + getPackageVersion(packageName) { + const pack = atom.packages.getLoadedPackage(packageName); + return (pack != null ? pack.metadata.version : undefined); + }, + + getPackageVersionShippedWithAtom(packageName) { + return require(path.join(atom.getLoadSettings().resourcePath, 'package.json')).packageDependencies[packageName]; + }, + + getLatestPackageData(packageName) { + const githubHeaders = new Headers({ + accept: 'application/vnd.github.v3+json', + contentType: "application/json" + }); + return fetch(`https://atom.io/api/packages/${packageName}`, {headers: githubHeaders}) + .then(function(r) { if (r.ok) { return r.json(); } else { return Promise.reject(r.statusCode); } }); + }, + + 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 versionShippedWithAtom = this.getPackageVersionShippedWithAtom(packageName); + + if (isCore = (versionShippedWithAtom != 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 Atom which is running. This will happen when there's a locally + // installed version of the package with a lower version than Atom's. + upToDate = (installedVersion != null) && semver.gte(installedVersion, versionShippedWithAtom); + } + + return {isCore, upToDate, latestVersion, installedVersion, versionShippedWithAtom}; + }); + } +}; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/packages/notifications/package.json b/packages/notifications/package.json new file mode 100644 index 000000000..2f36fc4ff --- /dev/null +++ b/packages/notifications/package.json @@ -0,0 +1,39 @@ +{ + "name": "notifications", + "main": "./lib/main", + "version": "0.72.1", + "description": "A tidy way to display Atom notifications.", + "repository": "https://github.com/atom/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 Atom 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" + } +} diff --git a/packages/notifications/spec/command-logger-spec.coffee b/packages/notifications/spec/command-logger-spec.coffee new file mode 100644 index 000000000..7a583bfb4 --- /dev/null +++ b/packages/notifications/spec/command-logger-spec.coffee @@ -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 """ + ``` + ``` + """ diff --git a/packages/notifications/spec/helper.coffee b/packages/notifications/spec/helper.coffee new file mode 100644 index 000000000..e3541ca8f --- /dev/null +++ b/packages/notifications/spec/helper.coffee @@ -0,0 +1,41 @@ + +### +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('is.gd') > -1 + return textPromise options?.shortenerResponse ? 'http://is.gd/cats' + + if url.indexOf('atom.io/api/packages') > -1 + return jsonPromise(options?.packageResponse ? { + repository: url: 'https://github.com/atom/notifications' + releases: latest: '0.0.0' + }) + + if url.indexOf('atom.io/api/updates') > -1 + return(jsonPromise options?.atomResponse ? {name: atom.getVersion()}) + + 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} diff --git a/packages/notifications/spec/notifications-log-spec.coffee b/packages/notifications/spec/notifications-log-spec.coffee new file mode 100644 index 000000000..3ac2f21b7 --- /dev/null +++ b/packages/notifications/spec/notifications-log-spec.coffee @@ -0,0 +1,323 @@ +{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' + } + ] + 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() diff --git a/packages/notifications/spec/notifications-spec.coffee b/packages/notifications/spec/notifications-spec.coffee new file mode 100644 index 000000000..2f55e715b --- /dev/null +++ b/packages/notifications/spec/notifications-spec.coffee @@ -0,0 +1,966 @@ +fs = require 'fs-plus' +path = require 'path' +temp = require('temp').track() +{Notification} = require 'atom' +NotificationElement = require '../lib/notification-element' +NotificationIssue = require '../lib/notification-issue' +{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 Atom + 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 html ') + notification = notificationContainer.childNodes[0] + expect(notification.querySelector('.message').innerHTML).toContain( + 'test html 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 Atom or an Atom 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/Atom.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/Atom.app/Contents/Resources/app/src/pane.js:442:18) + at HTMLDivElement. (/Applications/Atom.app/Contents/Resources/app/node_modules/tabs/lib/tab-bar-view.js:174:22) + at space-pen-ul.jQuery.event.dispatch (/Applications/Atom.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/Atom.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/Atom.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() + 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 "notifications package" + 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 /Atom\*\*: [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/atom/notifications) 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/atom/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/Atom.app/Contents/Resources/app/src/command-registry.js:238:29) + at /Applications/Atom.app/Contents/Resources/app/src/command-registry.js:3:61 + at CommandPaletteView.module.exports.CommandPaletteView.confirmed (/Applications/Atom.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 "linked-package package" + 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/atom/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 "unloaded package" + 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/atom/notifications" + } + """ + + stack = "TypeError: Cannot read property 'prototype' of undefined\n at __extends (:1:1)\n at Object.defineProperty.value [as .coffee] (/Applications/Atom.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 "broken-load package" + 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/atom/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.:1 + "#\\{" "end": "\\}" + ^^^^^ + Unexpected string in #{path.join('packageDir', 'grammars', 'broken-grammar.cson')} + + SyntaxError: Syntax error on line 241, column 18: evalmachine.: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 "language-broken-grammar package" + 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/atom/notifications" + } + """ + + stack = "TypeError: Cannot read property 'command' of undefined\n at Object.module.exports.activate (:7:23)\n at Package.module.exports.Package.activateNow (/Applications/Atom.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 "broken-activation package" + 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') + + it "detects the package name from the URL", -> + waitsForPromise -> fatalError.getRenderPromise() + + runs -> + expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined' + expect(fatalError.innerHTML).toContain "notifications package" + 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'), '').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 Atom' + expect(fatalError.issue.getPackageName()).toBeUndefined() + + button = fatalError.querySelector('.btn') + expect(button.textContent).toContain 'Create issue on atom/atom' + + expect(issueBody).toContain 'ReferenceError: a is not defined' + expect(issueBody).toContain '**Thrown From**: Atom 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 'You can help by creating an issue' + + 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. (/Applications/Atom.app/Contents/Resources/atom/browser/lib/rpc-server.js:128:79) at EventEmitter.emit (events.js:119:17) at EventEmitter. (/Applications/Atom.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/atom/sort-lines' + releases: latest: '0.10.0' + spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "sort-lines" + spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/atom/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/atom/notifications' + releases: latest: '0.11.0' + + describe "when the locally installed version is lower than Atom's version", -> + beforeEach -> + versionShippedWithAtom = '0.10.0' + UserUtilities = require '../lib/user-utilities' + spyOn(UserUtilities, 'getPackageVersionShippedWithAtom').andCallFake -> versionShippedWithAtom + + 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 Atom package' + expect(fatalNotification.textContent).toContain 'is out of date' + + describe "when the locally installed version matches Atom's version", -> + beforeEach -> + versionShippedWithAtom = '0.9.0' + UserUtilities = require '../lib/user-utilities' + spyOn(UserUtilities, 'getPackageVersionShippedWithAtom').andCallFake -> versionShippedWithAtom + + 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' + + describe "when Atom 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 Atom is out of date", -> + fatalNotification = fatalError.querySelector('.fatal-notification') + expect(fatalNotification.textContent).toContain 'Atom is out of date' + + it "provides a link to the latest released version", -> + fatalNotification = fatalError.querySelector('.fatal-notification') + expect(fatalNotification.innerHTML).toContain 'latest version' + + 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' + } + ] + 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('atom/notifications') + + 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' + } + ] + 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/Atom 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() diff --git a/packages/notifications/styles/notifications-log.less b/packages/notifications/styles/notifications-log.less new file mode 100644 index 000000000..e7c23b49d --- /dev/null +++ b/packages/notifications/styles/notifications-log.less @@ -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; + } +} diff --git a/packages/notifications/styles/notifications.less b/packages/notifications/styles/notifications.less new file mode 100644 index 000000000..de3df6f59 --- /dev/null +++ b/packages/notifications/styles/notifications.less @@ -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); } +} From 7234f929739e25508cc300158cc941fb1a18bc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 11 May 2023 22:31:58 -0300 Subject: [PATCH 19/30] Atom => Pulsar --- packages/notifications/README.md | 4 +- packages/notifications/lib/command-logger.js | 2 +- .../notifications/lib/notification-element.js | 4 +- .../notifications/lib/notification-issue.js | 8 +-- packages/notifications/lib/user-utilities.js | 22 ++++---- packages/notifications/package.json | 4 +- .../spec/notifications-spec.coffee | 56 +++++++++---------- 7 files changed, 50 insertions(+), 50 deletions(-) diff --git a/packages/notifications/README.md b/packages/notifications/README.md index 8fbc571e4..ff7a141db 100644 --- a/packages/notifications/README.md +++ b/packages/notifications/README.md @@ -1,4 +1,4 @@ -##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) +##### Pulsar and all repositories under Pulsar will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) # Notifications package [![CI](https://github.com/atom/notifications/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/notifications/actions/workflows/ci.yml) @@ -6,5 +6,5 @@ ### Docs -Notifications are available for use in your Atom packages via the `atom.notifications` `NotificationManager` object. See +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. diff --git a/packages/notifications/lib/command-logger.js b/packages/notifications/lib/command-logger.js index 85d7f29ce..1d90f6a6e 100644 --- a/packages/notifications/lib/command-logger.js +++ b/packages/notifications/lib/command-logger.js @@ -25,7 +25,7 @@ const ignoredCommands = { // Ten minutes in milliseconds. const tenMinutes = 10 * 60 * 1000; -// Public: Handles logging all of the Atom commands for the automatic repro steps feature. +// 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 = diff --git a/packages/notifications/lib/notification-element.js b/packages/notifications/lib/notification-element.js index 469629c05..0cdb99dc8 100644 --- a/packages/notifications/lib/notification-element.js +++ b/packages/notifications/lib/notification-element.js @@ -212,7 +212,7 @@ module.exports = const promises = []; promises.push(this.issue.findSimilarIssues()); - promises.push(UserUtilities.checkAtomUpToDate()); + promises.push(UserUtilities.checkPulsarUpToDate()); if (packageName != null) { promises.push(UserUtilities.checkPackageUpToDate(packageName)); } return Promise.all(promises).then(allData => { @@ -244,7 +244,7 @@ Upgrading to the latest version may fix this issue.\ fatalNotification.innerHTML += `\

Locally installed core Pulsar package ${packageName} is out of date: ${packageCheck.installedVersion} installed locally; -${packageCheck.versionShippedWithAtom} included with the version of Pulsar you're running. +${packageCheck.versionShippedWithPulsar} included with the version of Pulsar you're running. Removing the locally installed version may fix this issue.\ `; diff --git a/packages/notifications/lib/notification-issue.js b/packages/notifications/lib/notification-issue.js index 2bf083528..fd15cd0c6 100644 --- a/packages/notifications/lib/notification-issue.js +++ b/packages/notifications/lib/notification-issue.js @@ -122,14 +122,14 @@ module.exports = } else if (packageName != null) { packageMessage = `'${packageName}' package v${packageVersion}`; } else { - packageMessage = 'Atom Core'; + packageMessage = 'Pulsar Core'; } this.issueBody = `\ ### Prerequisites @@ -139,7 +139,7 @@ Do you want to ask a question? Are you looking for support? The Atom message boa * Followed all applicable steps in the debugging guide: * Checked the FAQs on the message board for common solutions: * Checked that your issue isn't already filed: - * Checked that there is not already an Atom package that provides the described functionality: + * Checked that there is not already an Pulsar package that provides the described functionality: ### Description diff --git a/packages/notifications/lib/user-utilities.js b/packages/notifications/lib/user-utilities.js index 7bfed7019..a246e4235 100644 --- a/packages/notifications/lib/user-utilities.js +++ b/packages/notifications/lib/user-utilities.js @@ -138,7 +138,7 @@ module.exports = { }); }, - getLatestAtomData() { + getLatestPulsarData() { const githubHeaders = new Headers({ accept: 'application/vnd.github.v3+json', contentType: "application/json" @@ -147,10 +147,10 @@ module.exports = { .then(function(r) { if (r.ok) { return r.json(); } else { return Promise.reject(r.statusCode); } }); }, - checkAtomUpToDate() { - return this.getLatestAtomData().then(function(latestAtomData) { + checkPulsarUpToDate() { + return this.getLatestPulsarData().then(function(latestPulsarData) { const installedVersion = __guard__(atom.getVersion(), x => x.replace(/-.*$/, '')); - const latestVersion = latestAtomData.name; + const latestVersion = latestPulsarData.name; const upToDate = (installedVersion != null) && semver.gte(installedVersion, latestVersion); return {upToDate, latestVersion, installedVersion};}); }, @@ -160,7 +160,7 @@ module.exports = { return (pack != null ? pack.metadata.version : undefined); }, - getPackageVersionShippedWithAtom(packageName) { + getPackageVersionShippedWithPulsar(packageName) { return require(path.join(atom.getLoadSettings().resourcePath, 'package.json')).packageDependencies[packageName]; }, @@ -179,17 +179,17 @@ module.exports = { const installedVersion = this.getPackageVersion(packageName); let upToDate = (installedVersion != null) && semver.gte(installedVersion, latestPackageData.releases.latest); const latestVersion = latestPackageData.releases.latest; - const versionShippedWithAtom = this.getPackageVersionShippedWithAtom(packageName); + const versionShippedWithPulsar = this.getPackageVersionShippedWithPulsar(packageName); - if (isCore = (versionShippedWithAtom != null)) { + 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 Atom which is running. This will happen when there's a locally - // installed version of the package with a lower version than Atom's. - upToDate = (installedVersion != null) && semver.gte(installedVersion, versionShippedWithAtom); + // 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, versionShippedWithAtom}; + return {isCore, upToDate, latestVersion, installedVersion, versionShippedWithPulsar}; }); } }; diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 2f36fc4ff..6fa4977ad 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -2,7 +2,7 @@ "name": "notifications", "main": "./lib/main", "version": "0.72.1", - "description": "A tidy way to display Atom notifications.", + "description": "A tidy way to display Pulsar notifications.", "repository": "https://github.com/atom/notifications", "license": "MIT", "engines": { @@ -24,7 +24,7 @@ "showErrorsInDevMode": { "type": "boolean", "default": false, - "description": "Show notifications for uncaught exceptions even if Atom 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." + "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", diff --git a/packages/notifications/spec/notifications-spec.coffee b/packages/notifications/spec/notifications-spec.coffee index 2f55e715b..a34bcbf38 100644 --- a/packages/notifications/spec/notifications-spec.coffee +++ b/packages/notifications/spec/notifications-spec.coffee @@ -24,7 +24,7 @@ describe "Notifications", -> describe "when there are notifications before activation", -> beforeEach -> waitsForPromise -> - # Wrapped in Promise.resolve so this test continues to work on earlier versions of Atom + # 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", -> @@ -304,7 +304,7 @@ describe "Notifications", -> atom.onWillThrowError(handler) # Fake an unhandled error with a call stack located outside of the source - # of Atom or an Atom package + # 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)' @@ -348,12 +348,12 @@ describe "Notifications", -> beforeEach -> stack = """ TypeError: undefined is not a function - at Object.module.exports.Pane.promptToSaveItem [as defaultSavePrompt] (/Applications/Atom.app/Contents/Resources/app/src/pane.js:490:23) + 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/Atom.app/Contents/Resources/app/src/pane.js:442:18) - at HTMLDivElement. (/Applications/Atom.app/Contents/Resources/app/node_modules/tabs/lib/tab-bar-view.js:174:22) - at space-pen-ul.jQuery.event.dispatch (/Applications/Atom.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/Atom.app/Contents/Resources/app/node_modules/archive-view/node_modules/atom-space-pen-views/node_modules/space-pen/vendor/jquery.js:4360:46) + at Pane.module.exports.Pane.destroyItem (/Applications/Pulsar.app/Contents/Resources/app/src/pane.js:442:18) + at HTMLDivElement. (/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' @@ -364,7 +364,7 @@ describe "Notifications", -> spyOn(fs, 'realpathSync').andCallFake (p) -> p spyOn(fatalError.issue, 'getPackagePathsByPackageName').andCallFake -> 'save-session': '/Users/someguy/.atom/packages/save-session' - 'tabs': '/Applications/Atom.app/Contents/Resources/app/node_modules/tabs' + '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' @@ -398,7 +398,7 @@ describe "Notifications", -> expect(issueTitle).toContain '$ATOM_HOME' expect(issueTitle).not.toContain process.env.ATOM_HOME - expect(issueBody).toMatch /Atom\*\*: [0-9].[0-9]+.[0-9]+/ig + 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/atom/notifications) package ' @@ -468,9 +468,9 @@ describe "Notifications", -> 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/Atom.app/Contents/Resources/app/src/command-registry.js:238:29) - at /Applications/Atom.app/Contents/Resources/app/src/command-registry.js:3:61 - at CommandPaletteView.module.exports.CommandPaletteView.confirmed (/Applications/Atom.app/Contents/Resources/app/node_modules/command-palette/lib/command-palette-view.js:159:32) + 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" @@ -540,7 +540,7 @@ describe "Notifications", -> } """ - stack = "TypeError: Cannot read property 'prototype' of undefined\n at __extends (:1:1)\n at Object.defineProperty.value [as .coffee] (/Applications/Atom.app/Contents/Resources/app.asar/src/compile-cache.js:169:21)" + stack = "TypeError: Cannot read property 'prototype' of undefined\n at __extends (: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}) @@ -621,7 +621,7 @@ describe "Notifications", -> } """ - stack = "TypeError: Cannot read property 'command' of undefined\n at Object.module.exports.activate (:7:23)\n at Package.module.exports.Package.activateNow (/Applications/Atom.app/Contents/Resources/app.asar/src/package.js:232:19)" + stack = "TypeError: Cannot read property 'command' of undefined\n at Object.module.exports.activate (: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}) @@ -688,14 +688,14 @@ describe "Notifications", -> expect(fatalError).toBeDefined() expect(fatalError).toHaveClass 'has-close' expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined' - expect(fatalError.innerHTML).toContain 'bug in Atom' + expect(fatalError.innerHTML).toContain 'bug in Pulsar' expect(fatalError.issue.getPackageName()).toBeUndefined() button = fatalError.querySelector('.btn') expect(button.textContent).toContain 'Create issue on atom/atom' expect(issueBody).toContain 'ReferenceError: a is not defined' - expect(issueBody).toContain '**Thrown From**: Atom Core' + 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' @@ -732,7 +732,7 @@ describe "Notifications", -> 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. (/Applications/Atom.app/Contents/Resources/atom/browser/lib/rpc-server.js:128:79) at EventEmitter.emit (events.js:119:17) at EventEmitter. (/Applications/Atom.app/Contents/Resources/atom/browser/api/lib/web-contents.js:99:23) at EventEmitter.emit (events.js:119:17)" + 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. (/Applications/Pulsar.app/Contents/Resources/atom/browser/lib/rpc-server.js:128:79) at EventEmitter.emit (events.js:119:17) at EventEmitter. (/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 -> @@ -812,11 +812,11 @@ describe "Notifications", -> repository: url: 'https://github.com/atom/notifications' releases: latest: '0.11.0' - describe "when the locally installed version is lower than Atom's version", -> + describe "when the locally installed version is lower than Pulsar's version", -> beforeEach -> - versionShippedWithAtom = '0.10.0' + versionShippedWithPulsar = '0.10.0' UserUtilities = require '../lib/user-utilities' - spyOn(UserUtilities, 'getPackageVersionShippedWithAtom').andCallFake -> versionShippedWithAtom + spyOn(UserUtilities, 'getPackageVersionShippedWithPulsar').andCallFake -> versionShippedWithPulsar generateException() fatalError = notificationContainer.querySelector('atom-notification.fatal') @@ -829,14 +829,14 @@ describe "Notifications", -> 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 Atom package' + expect(fatalNotification.textContent).toContain 'Locally installed core Pulsar package' expect(fatalNotification.textContent).toContain 'is out of date' - describe "when the locally installed version matches Atom's version", -> + describe "when the locally installed version matches Pulsar's version", -> beforeEach -> - versionShippedWithAtom = '0.9.0' + versionShippedWithPulsar = '0.9.0' UserUtilities = require '../lib/user-utilities' - spyOn(UserUtilities, 'getPackageVersionShippedWithAtom').andCallFake -> versionShippedWithAtom + spyOn(UserUtilities, 'getPackageVersionShippedWithPulsar').andCallFake -> versionShippedWithPulsar generateException() fatalError = notificationContainer.querySelector('atom-notification.fatal') @@ -848,7 +848,7 @@ describe "Notifications", -> button = fatalError.querySelector('.btn') expect(button.textContent).toContain 'Create issue' - describe "when Atom is out of date", -> + describe "when Pulsar is out of date", -> beforeEach -> installedVersion = '0.179.0' spyOn(atom, 'getVersion').andCallFake -> installedVersion @@ -868,9 +868,9 @@ describe "Notifications", -> button = fatalError.querySelector('.btn-issue') expect(button).not.toExist() - it "tells the user that Atom is out of date", -> + it "tells the user that Pulsar is out of date", -> fatalNotification = fatalError.querySelector('.fatal-notification') - expect(fatalNotification.textContent).toContain 'Atom is out of date' + expect(fatalNotification.textContent).toContain 'Pulsar is out of date' it "provides a link to the latest released version", -> fatalNotification = fatalError.querySelector('.fatal-notification') @@ -957,7 +957,7 @@ describe "Notifications", -> a + 1 catch e e.code = 'ENOENT' - message = 'Error: spawn /opt/atom/Atom Helper (deleted) ENOENT' + message = 'Error: spawn /opt/atom/Pulsar Helper (deleted) ENOENT' window.onerror.call(window, message, 'abc', 2, 3, e) it "displays a fatal error", -> From 4726b4f1530fc9d6f77fe0b0713d7b0f2f8a7b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 11 May 2023 22:32:28 -0300 Subject: [PATCH 20/30] Bundle notifications on core --- package.json | 4 ++-- yarn.lock | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e113ea4e8..b6f7d2539 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/yarn.lock b/yarn.lock index 2fe11cbb1..26f5039fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7219,9 +7219,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" From 9c8d9944cc11704d9efd0a8d0608a724b357bd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 11 May 2023 23:10:51 -0300 Subject: [PATCH 21/30] Fixed some instances of Atom to Pulsar --- .../notifications/lib/notification-element.js | 6 ++- .../notifications/lib/notification-issue.js | 19 +++++----- packages/notifications/lib/user-utilities.js | 38 ++++++++----------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/notifications/lib/notification-element.js b/packages/notifications/lib/notification-element.js index 0cdb99dc8..e3cecedae 100644 --- a/packages/notifications/lib/notification-element.js +++ b/packages/notifications/lib/notification-element.js @@ -207,13 +207,15 @@ module.exports = if ((packageName != null) && (repoUrl != null)) { issueButton.textContent = `Create issue on the ${packageName} package`; } else { - issueButton.textContent = "Create issue on atom/atom"; + 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)); } + if (packageName != null) { + promises.push(UserUtilities.checkPackageUpToDate(packageName)); + } return Promise.all(promises).then(allData => { let issue; diff --git a/packages/notifications/lib/notification-issue.js b/packages/notifications/lib/notification-issue.js index fd15cd0c6..927a39ad0 100644 --- a/packages/notifications/lib/notification-issue.js +++ b/packages/notifications/lib/notification-issue.js @@ -2,7 +2,6 @@ * 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 * DS202: Simplify dynamic range loops * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md @@ -19,8 +18,7 @@ const TITLE_CHAR_LIMIT = 100; // Truncate issue title to 100 characters (includi const FileURLRegExp = new RegExp('file://\w*/(.*)'); -module.exports = -(NotificationIssue = class NotificationIssue { +module.exports = class NotificationIssue { constructor(notification) { this.normalizedStackPaths = this.normalizedStackPaths.bind(this); this.notification = notification; @@ -28,7 +26,7 @@ module.exports = findSimilarIssues() { let repoUrl = this.getRepoUrl(); - if (repoUrl == null) { repoUrl = 'atom/atom'; } + 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}`; @@ -51,7 +49,8 @@ module.exports = if ((issues.open != null) || (issues.closed != null)) { return issues; } } - return null;}).catch(e => null); + return null; + }).catch(_ => null); } getIssueUrlForSystem() { @@ -108,7 +107,9 @@ module.exports = const options = this.notification.getOptions(); const repoUrl = this.getRepoUrl(); const packageName = this.getPackageName(); - if (packageName != null) { packageVersion = __guard__(__guard__(atom.packages.getLoadedPackage(packageName), x1 => x1.metadata), x => x.version); } + if (packageName != null) { + packageVersion = atom.packages.getLoadedPackage(packageName)?.metadata?.version; + } const copyText = ''; const systemUser = process.env.USER; let rootUserStatus = ''; @@ -214,13 +215,13 @@ ${copyText}\ getRepoUrl() { const packageName = this.getPackageName(); if (packageName == null) { return; } - let repo = __guard__(__guard__(atom.packages.getLoadedPackage(packageName), x1 => x1.metadata), x => x.repository); + 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 = __guard__(JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'))), x2 => x2.repository); + 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) {} } @@ -307,7 +308,7 @@ ${copyText}\ } return packagePathsByPackageName; } -}); +} function __guard__(value, transform) { return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; diff --git a/packages/notifications/lib/user-utilities.js b/packages/notifications/lib/user-utilities.js index a246e4235..5ab9c8948 100644 --- a/packages/notifications/lib/user-utilities.js +++ b/packages/notifications/lib/user-utilities.js @@ -2,7 +2,6 @@ * 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 * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ @@ -138,21 +137,13 @@ module.exports = { }); }, - getLatestPulsarData() { - const githubHeaders = new Headers({ - accept: 'application/vnd.github.v3+json', - contentType: "application/json" - }); - return fetch('https://atom.io/api/updates', {headers: githubHeaders}) - .then(function(r) { if (r.ok) { return r.json(); } else { return Promise.reject(r.statusCode); } }); - }, - checkPulsarUpToDate() { - return this.getLatestPulsarData().then(function(latestPulsarData) { - const installedVersion = __guard__(atom.getVersion(), x => x.replace(/-.*$/, '')); - const latestVersion = latestPulsarData.name; - const upToDate = (installedVersion != null) && semver.gte(installedVersion, latestVersion); - return {upToDate, latestVersion, installedVersion};}); + const installedVersion = atom.getVersion().replace(/-.*$/, ''); + return { + upToDate: true, + latestVersion: installedVersion, + installedVersion + } }, getPackageVersion(packageName) { @@ -166,11 +157,18 @@ module.exports = { getLatestPackageData(packageName) { const githubHeaders = new Headers({ - accept: 'application/vnd.github.v3+json', + accept: 'application/json', contentType: "application/json" }); - return fetch(`https://atom.io/api/packages/${packageName}`, {headers: githubHeaders}) - .then(function(r) { if (r.ok) { return r.json(); } else { return Promise.reject(r.statusCode); } }); + 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(r.statusCode); + } + }); }, checkPackageUpToDate(packageName) { @@ -193,7 +191,3 @@ module.exports = { }); } }; - -function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file From 4d4aca3272c94122fbb11639a16e23b566bc49b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 11 May 2023 23:19:53 -0300 Subject: [PATCH 22/30] Repo update for notifications --- packages/notifications/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 6fa4977ad..f89f157d0 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -1,9 +1,9 @@ { "name": "notifications", "main": "./lib/main", - "version": "0.72.1", + "version": "0.73.0", + "repository": "https://github.com/pulsar-edit/pulsar", "description": "A tidy way to display Pulsar notifications.", - "repository": "https://github.com/atom/notifications", "license": "MIT", "engines": { "atom": ">0.50.0" From 4ad1b7c56323e8970c6a850cae1e157a3b9e679b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 12 May 2023 00:17:15 -0300 Subject: [PATCH 23/30] README fix --- packages/notifications/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/notifications/README.md b/packages/notifications/README.md index ff7a141db..a3702b0de 100644 --- a/packages/notifications/README.md +++ b/packages/notifications/README.md @@ -1,6 +1,4 @@ -##### Pulsar and all repositories under Pulsar will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) # Notifications package -[![CI](https://github.com/atom/notifications/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/notifications/actions/workflows/ci.yml) ![notifications](https://cloud.githubusercontent.com/assets/69169/5176406/350d0e80-73fd-11e4-8101-1776b9d6d8bf.gif) From f9697a8de0f2cb68f2637b00a02ff9b689753daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 12 May 2023 00:19:29 -0300 Subject: [PATCH 24/30] Formatting --- packages/notifications/lib/notification-issue.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/notifications/lib/notification-issue.js b/packages/notifications/lib/notification-issue.js index 927a39ad0..3bfd14c12 100644 --- a/packages/notifications/lib/notification-issue.js +++ b/packages/notifications/lib/notification-issue.js @@ -68,7 +68,9 @@ module.exports = class NotificationIssue { getIssueUrl() { return this.getIssueBody().then(issueBody => { let repoUrl = this.getRepoUrl(); - if (repoUrl == null) { repoUrl = 'https://github.com/atom/atom'; } + if (repoUrl == null) { + repoUrl = 'https://github.com/pulsar-edit/pulsar'; + } return `${repoUrl}/issues/new?title=${this.encodeURI(this.getIssueTitle())}&body=${this.encodeURI(issueBody)}`; }); } From 89e7800027fb049fa5cf45c66a21a5ea2751b343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 12 May 2023 00:19:51 -0300 Subject: [PATCH 25/30] Fixed validations of semver --- packages/notifications/lib/user-utilities.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/notifications/lib/user-utilities.js b/packages/notifications/lib/user-utilities.js index 5ab9c8948..64923d648 100644 --- a/packages/notifications/lib/user-utilities.js +++ b/packages/notifications/lib/user-utilities.js @@ -175,8 +175,8 @@ module.exports = { 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; + let upToDate = (installedVersion != null) && semver.gte(installedVersion, latestPackageData?.releases?.latest); + const latestVersion = latestPackageData?.releases?.latest; const versionShippedWithPulsar = this.getPackageVersionShippedWithPulsar(packageName); if (isCore = (versionShippedWithPulsar != null)) { From 2f32e56e9eeb0b9685c2cedf7e47bdc523d198b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 12 May 2023 00:19:57 -0300 Subject: [PATCH 26/30] Rebrand to Pulsar --- packages/notifications/lib/notification-element.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notifications/lib/notification-element.js b/packages/notifications/lib/notification-element.js index e3cecedae..88fa63d14 100644 --- a/packages/notifications/lib/notification-element.js +++ b/packages/notifications/lib/notification-element.js @@ -263,7 +263,7 @@ Use: apm unlink ${packagePath}\ fatalNotification.innerHTML += `\ Pulsar is out of date: ${atomCheck.installedVersion} installed; ${atomCheck.latestVersion} latest. -Upgrading to the latest version may fix this issue.\ +Upgrading to the latest version may fix this issue.\ `; } else { fatalNotification.innerHTML += " You can help by creating an issue. Please explain what actions triggered this error."; From 5f308e17e635a5e94e75d2b1fc05db066f3fe322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 12 May 2023 00:20:03 -0300 Subject: [PATCH 27/30] Fixed more tests --- packages/notifications/spec/helper.coffee | 10 +- .../spec/notifications-log-spec.coffee | 2 + .../spec/notifications-spec.coffee | 102 ++++++++++-------- 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/packages/notifications/spec/helper.coffee b/packages/notifications/spec/helper.coffee index e3541ca8f..72011e665 100644 --- a/packages/notifications/spec/helper.coffee +++ b/packages/notifications/spec/helper.coffee @@ -20,18 +20,12 @@ module.exports = spyOn(window, 'fetch') unless window.fetch.isSpy fetch.andCallFake (url) -> - if url.indexOf('is.gd') > -1 - return textPromise options?.shortenerResponse ? 'http://is.gd/cats' - - if url.indexOf('atom.io/api/packages') > -1 + if url.indexOf('api.pulsar-edit.dev/api') > -1 return jsonPromise(options?.packageResponse ? { - repository: url: 'https://github.com/atom/notifications' + repository: url: 'https://github.com/pulsar-edit/notifications' releases: latest: '0.0.0' }) - if url.indexOf('atom.io/api/updates') > -1 - return(jsonPromise options?.atomResponse ? {name: atom.getVersion()}) - if options?.issuesErrorResponse? return Promise.reject(options?.issuesErrorResponse) diff --git a/packages/notifications/spec/notifications-log-spec.coffee b/packages/notifications/spec/notifications-log-spec.coffee index 3ac2f21b7..ab9d8a0a0 100644 --- a/packages/notifications/spec/notifications-log-spec.coffee +++ b/packages/notifications/spec/notifications-log-spec.coffee @@ -171,6 +171,8 @@ describe "Notifications Log", -> 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 -> diff --git a/packages/notifications/spec/notifications-spec.coffee b/packages/notifications/spec/notifications-spec.coffee index a34bcbf38..920da3d73 100644 --- a/packages/notifications/spec/notifications-spec.coffee +++ b/packages/notifications/spec/notifications-spec.coffee @@ -4,6 +4,7 @@ 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", -> @@ -375,6 +376,7 @@ describe "Notifications", -> 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') @@ -390,7 +392,7 @@ describe "Notifications", -> expect(notificationContainer.childNodes.length).toBe 1 expect(fatalError).toHaveClass 'has-close' expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined' - expect(fatalError.innerHTML).toContain "notifications package" + expect(fatalError.innerHTML).toContain "notifications package" expect(fatalError.issue.getPackageName()).toBe 'notifications' button = fatalError.querySelector('.btn') @@ -401,7 +403,7 @@ describe "Notifications", -> 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/atom/notifications) package ' + 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. @@ -459,7 +461,7 @@ describe "Notifications", -> { "name": "linked-package", "version": "1.0.0", - "repository": "https://github.com/atom/notifications" + "repository": "https://github.com/pulsar-edit/notifications" } """ atom.packages.enablePackage('linked-package') @@ -486,7 +488,7 @@ describe "Notifications", -> 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 "linked-package package" + expect(fatalError.innerHTML).toContain "linked-package package" expect(fatalError.issue.getPackageName()).toBe 'linked-package' describe "when an exception is thrown from an unloaded package", -> @@ -502,7 +504,7 @@ describe "Notifications", -> { "name": "unloaded", "version": "1.0.0", - "repository": "https://github.com/atom/notifications" + "repository": "https://github.com/pulsar-edit/notifications" } """ @@ -521,7 +523,7 @@ describe "Notifications", -> expect(notificationContainer.childNodes.length).toBe 1 expect(fatalError).toHaveClass 'has-close' expect(fatalError.innerHTML).toContain 'ReferenceError: unloaded error' - expect(fatalError.innerHTML).toContain "unloaded package" + expect(fatalError.innerHTML).toContain "unloaded package" expect(fatalError.issue.getPackageName()).toBe 'unloaded' describe "when an exception is thrown from a package trying to load", -> @@ -536,7 +538,7 @@ describe "Notifications", -> { "name": "broken-load", "version": "1.0.0", - "repository": "https://github.com/atom/notifications" + "repository": "https://github.com/pulsar-edit/notifications" } """ @@ -555,7 +557,7 @@ describe "Notifications", -> 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 "broken-load package" + expect(fatalError.innerHTML).toContain "broken-load package" expect(fatalError.issue.getPackageName()).toBe 'broken-load' describe "when an exception is thrown from a package trying to load a grammar", -> @@ -570,7 +572,7 @@ describe "Notifications", -> { "name": "language-broken-grammar", "version": "1.0.0", - "repository": "https://github.com/atom/notifications" + "repository": "https://github.com/pulsar-edit/notifications" } """ @@ -602,7 +604,7 @@ describe "Notifications", -> 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 "language-broken-grammar package" + expect(fatalError.innerHTML).toContain "language-broken-grammar package" expect(fatalError.issue.getPackageName()).toBe 'language-broken-grammar' describe "when an exception is thrown from a package trying to activate", -> @@ -617,7 +619,7 @@ describe "Notifications", -> { "name": "broken-activation", "version": "1.0.0", - "repository": "https://github.com/atom/notifications" + "repository": "https://github.com/pulsar-edit/notifications" } """ @@ -636,7 +638,7 @@ describe "Notifications", -> 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 "broken-activation package" + expect(fatalError.innerHTML).toContain "broken-activation package" expect(fatalError.issue.getPackageName()).toBe 'broken-activation' describe "when an exception is thrown from a package without a trace, but with a URL", -> @@ -654,12 +656,13 @@ describe "Notifications", -> notificationContainer = workspaceElement.querySelector('atom-notifications') fatalError = notificationContainer.querySelector('atom-notification.fatal') - it "detects the package name from the URL", -> + # 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 "notifications package" + expect(fatalError.innerHTML).toContain "notifications package" expect(fatalError.issue.getPackageName()).toBe 'notifications' describe "when an exception is thrown from core", -> @@ -692,7 +695,7 @@ describe "Notifications", -> expect(fatalError.issue.getPackageName()).toBeUndefined() button = fatalError.querySelector('.btn') - expect(button.textContent).toContain 'Create issue on atom/atom' + 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' @@ -725,7 +728,7 @@ describe "Notifications", -> button = fatalError.querySelector('.btn') fatalNotification = fatalError.querySelector('.fatal-notification') expect(button.textContent).toContain 'Create issue' - expect(fatalNotification.textContent).toContain 'You can help by creating an issue' + expect(fatalNotification.textContent).toContain 'The error was thrown from the notifications package.' describe "when the error has not been reported", -> beforeEach -> @@ -787,10 +790,10 @@ describe "Notifications", -> beforeEach -> generateFakeFetchResponses packageResponse: - repository: url: 'https://github.com/atom/sort-lines' + 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/atom/sort-lines" + spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/pulsar-edit/sort-lines" generateException() fatalError = notificationContainer.querySelector('atom-notification.fatal') @@ -809,7 +812,7 @@ describe "Notifications", -> beforeEach -> generateFakeFetchResponses packageResponse: - repository: url: 'https://github.com/atom/notifications' + 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", -> @@ -848,33 +851,34 @@ describe "Notifications", -> button = fatalError.querySelector('.btn') expect(button.textContent).toContain 'Create issue' - 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 'latest version' + # 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 'latest version' describe "when the error has been reported", -> beforeEach -> @@ -891,6 +895,8 @@ describe "Notifications", -> 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 -> @@ -902,7 +908,7 @@ describe "Notifications", -> 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('atom/notifications') + expect(fetch.calls[0].args[0]).toContain encodeURIComponent('someguy/somepackage') describe "when the issue is closed", -> beforeEach -> @@ -915,6 +921,8 @@ describe "Notifications", -> 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 -> From 34f744a1b5f39cc183bf1526a2dea4efb8199cf8 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 11 May 2023 20:20:32 -0700 Subject: [PATCH 28/30] Add encrypted secret API Token --- .cirrus.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index c5ea734c4..71814c89b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -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 From ff36697dac7f878c730eba808707a79790294e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 12 May 2023 00:24:09 -0300 Subject: [PATCH 29/30] CHANGELOG update --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d1c6793..bd7174d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ## [Unreleased] +- 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 From 0891a29dac581e15a8b7e77bca14dc6d2403cd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 12 May 2023 00:41:01 -0300 Subject: [PATCH 30/30] Reject with error (co-authored with @Sertonix) --- packages/notifications/lib/user-utilities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notifications/lib/user-utilities.js b/packages/notifications/lib/user-utilities.js index 64923d648..c82583633 100644 --- a/packages/notifications/lib/user-utilities.js +++ b/packages/notifications/lib/user-utilities.js @@ -166,7 +166,7 @@ module.exports = { if (r.ok) { return r.json(); } else { - return Promise.reject(r.statusCode); + return Promise.reject(new Error(`Fetching updates resulted in status ${r.status}`)); } }); },