From 12e7879014fb1a8d19f865ad220f634586c8d865 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 18 May 2026 18:25:58 -0700 Subject: [PATCH 1/7] Add missing `shell` methods on `atom` global --- src/application-delegate.js | 45 ++++++++++++++++++++++ src/atom-environment.js | 44 ++++++++++++++++++++++ src/main-process/atom-application.js | 56 ++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) diff --git a/src/application-delegate.js b/src/application-delegate.js index 21c125932a..bf49003278 100644 --- a/src/application-delegate.js +++ b/src/application-delegate.js @@ -371,4 +371,49 @@ module.exports = class ApplicationDelegate { ipcRenderer.removeListener('did-resolve-proxy', outerCallback) ); } + openExternal(url) { + return ipcRenderer.invoke('openExternal', url).then(({ outcome, error, result }) => { + if (outcome === 'success') { + return result; + } else if (outcome === 'failure') { + return Promise.reject(error); + } + }); + } + + openPath (filePath) { + return ipcRenderer.invoke('openPath', filePath).then(({ outcome, error, result }) => { + if (outcome === 'success') { + return result; + } else if (outcome === 'failure') { + return Promise.reject(error); + } + }); + } + + trashItem(filePath) { + // A simple wrapper around `shell.trashItem`, since the main process is the + // most reliable place from which to call this method. + return ipcRenderer.invoke('trashItem', filePath).then(({ outcome, error, result }) => { + if (outcome === 'success') { + // `result` is undefined, but we might as well guard against an + // Electron API change in the future. + return result; + } else if (outcome === 'failure') { + return Promise.reject(error); + } + }); + } + + showItemInFolder(filePath) { + // A simple wrapper around `shell.trashItem`, which currently can only be + // called from the main process. + return ipcRenderer.invoke('showItemInFolder', filePath).then(({ outcome, error, result }) => { + if (outcome === 'success') { + return result; + } else if (outcome === 'failure') { + return Promise.reject(error); + } + }); + } }; diff --git a/src/atom-environment.js b/src/atom-environment.js index a9084b3c57..786f79d025 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -800,6 +800,50 @@ class AtomEnvironment { return this.setFullScreen(!this.isFullScreen()); } + // Extended: Moves an item to the trash. + // + // Returns a {Promise} that resolves when the operation has completed or + // rejects in the event of failure. + trashItem(filePath) { + return this.applicationDelegate.trashItem(filePath); + } + + // Extended: Reveals the given path in the system’s file browser, selecting + // it if possible. + // + // Returns a {Promise} that resolves when the operation has completed or + // rejects in the event of failure. + showItemInFolder(filePath) { + return this.applicationDelegate.showItemInFolder(filePath); + } + + // Extended: Opens the given path in the default manner for the operating + // system. + // + // For instance: if you pass the path to a directory, will likely open that + // directory in a file browser. If you pass the path to an image file, will + // likely open that image file in a web browser or an image editing + // application. + // + // Returns a {Promise} that resolves when the operation has completed or + // rejects in the event of failure. + openPath (filePath) { + return this.applicationDelegate.openPath(filePath); + } + + // Extended: Opens the given URL in the default manner for the operating + // system. + // + // For instance: passing an `https:` URI will open it in the default web + // browser, and passing a `mailto:` link will open it in the default mail + // client. + // + // Returns a {Promise} that resolves when the operation has completed or + // rejects in the event of failure. + openExternal (url) { + return this.applicationDelegate.openExternal(url); + } + // Restore the window to its previous dimensions and show it. // // Restores the full screen and maximized state after the window has resized to diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 0652659c50..1e3f51e71e 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -136,6 +136,62 @@ ipcMain.handle('setAsDefaultProtocolClient', (_, { protocol, path, args }) => { return app.setAsDefaultProtocolClient(protocol, path, args); }); +// Handle file deletion requests. +// +// Works around https://github.com/electron/electron/issues/29598, which seems +// to be the cause of failed deletion attempts on Windows. +ipcMain.handle('trashItem', async (_, filePath) => { + // We can't toss a promise over the wall, so we'll `await` it on our side and + // report the progress back to the renderer. + // + // If we return an `Error` object from this handler in the case of error, + // `ipcRenderer.invoke` will detect it and wrap it with its own explanation. + // We want to preserve the original error and hide the implementation + // details, so we instead return an object with an explicit `outcome` + // property to avoid this behavior. + try { + // `shell.trashItem` resolves with an empty value on success… + let result = await shell.trashItem(filePath); + return { outcome: 'success', result }; + } catch (error) { + // …and rejects on failure. + return { outcome: 'failure', error }; + } +}); + +ipcMain.handle('showItemInFolder', async (_, filePath) => { + try { + // Result will be `undefined`, but might as well return it in case of a + // future Electron API change. + // + // Unlike the others, this method does not return a promise; but we must go + // async anyway. + let result = shell.showItemInFolder(filePath); + return { outcome: 'success', result }; + } catch (error) { + // Not sure whether this can even fail, but might as well handle it. + return { outcome: 'failure', error }; + } +}); + +ipcMain.handle('openPath', async (_, filePath) => { + try { + let result = await shell.openPath(filePath); + return { outcome: 'success', result }; + } catch (error) { + return { outcome: 'failure', error }; + } +}); + +ipcMain.handle('openExternal', async (_, url) => { + try { + let result = await shell.openExternal(url); + return { outcome: 'success', result }; + } catch (error) { + return { outcome: 'failure', error }; + } +}); + // The application's singleton class. // // It's the entry point into the Pulsar application and maintains the global state From 1301aa16f8510f8f590f89ab58d1c91c601b10bb Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 18 May 2026 19:15:00 -0700 Subject: [PATCH 2/7] Add specs for `atom.trashItem` --- spec/atom-environment-spec.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index d2ae0cfba3..048d997c54 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -940,4 +940,35 @@ describe('AtomEnvironment', () => { expect(atom.getReleaseChannel()).toBe('dev'); }); }); + + describe('::trashItem()', () => { + let fileToBeTrashed, tempDir; + beforeEach(() => { + tempDir = temp.mkdirSync('trash-item-'); + fileToBeTrashed = path.join(tempDir, 'file-1.txt'); + fs.writeFileSync(fileToBeTrashed, 'test file'); + }); + + it('trashes the file', async () => { + expect(fs.existsSync(fileToBeTrashed)).toBe(true); + await atom.trashItem(fileToBeTrashed); + expect(fs.existsSync(fileToBeTrashed)).toBe(false); + }); + + it('rejects when asked to trash a nonexistent file', async () => { + let nonexistentFile = path.join(tempDir, 'zzyzx.txt'); + expect(fs.existsSync(nonexistentFile)).toBe(false); + let outcome = undefined; + // Assert that the `catch` clause was hit. (`expect().toThrow()` does not + // work with async functions.) + try { + await atom.trashItem(nonexistentFile); + outcome = 'success'; + } catch (error) { + outcome = 'failure'; + } finally { + expect(outcome).toBe('failure'); + } + }); + }); }); From 5224aa16b0e75788de1edc559eb98491ea049952 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 18 May 2026 19:45:22 -0700 Subject: [PATCH 3/7] Fix failing specs --- src/application-delegate.js | 13 ++++++++++--- src/atom-environment.js | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/application-delegate.js b/src/application-delegate.js index bf49003278..3e3b736133 100644 --- a/src/application-delegate.js +++ b/src/application-delegate.js @@ -363,7 +363,7 @@ module.exports = class ApplicationDelegate { } onDidResolveProxy(callback) { - const outerCallback = (event, requestId, proxy) => + const outerCallback = (_event, requestId, proxy) => callback(requestId, proxy); ipcRenderer.on('did-resolve-proxy', outerCallback); @@ -371,7 +371,14 @@ module.exports = class ApplicationDelegate { ipcRenderer.removeListener('did-resolve-proxy', outerCallback) ); } - openExternal(url) { + + // We already have an `openExternal` method that calls `shell.openExternal` + // directly from the renderer. This version proxies to the main process to + // call the same method. + // + // We'll leave `openExternal` in place above because some existing specs + // rely on being able to mock it. + openExternalDirect(url) { return ipcRenderer.invoke('openExternal', url).then(({ outcome, error, result }) => { if (outcome === 'success') { return result; @@ -390,7 +397,7 @@ module.exports = class ApplicationDelegate { } }); } - + trashItem(filePath) { // A simple wrapper around `shell.trashItem`, since the main process is the // most reliable place from which to call this method. diff --git a/src/atom-environment.js b/src/atom-environment.js index 786f79d025..784630d2d3 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -841,7 +841,7 @@ class AtomEnvironment { // Returns a {Promise} that resolves when the operation has completed or // rejects in the event of failure. openExternal (url) { - return this.applicationDelegate.openExternal(url); + return this.applicationDelegate.openExternalDirect(url); } // Restore the window to its previous dimensions and show it. From dfe998e7a618b1e71065e1a99ffa064ac63d3630 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 18 May 2026 20:08:12 -0700 Subject: [PATCH 4/7] Fix comment --- src/application-delegate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/application-delegate.js b/src/application-delegate.js index 3e3b736133..c18ff25dc3 100644 --- a/src/application-delegate.js +++ b/src/application-delegate.js @@ -413,8 +413,8 @@ module.exports = class ApplicationDelegate { } showItemInFolder(filePath) { - // A simple wrapper around `shell.trashItem`, which currently can only be - // called from the main process. + // A simple wrapper around `shell.showItemInFolder`, which currently can + // only be called from the main process. return ipcRenderer.invoke('showItemInFolder', filePath).then(({ outcome, error, result }) => { if (outcome === 'success') { return result; From a9d7184bdb310e68639117a38be1948fdc716794 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 10 Jun 2026 11:41:36 -0700 Subject: [PATCH 5/7] Replace calls to `shell` within `tree-view` --- packages/tree-view/lib/tree-view.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/tree-view/lib/tree-view.js b/packages/tree-view/lib/tree-view.js index 38d68c85d7..3e73c12eba 100644 --- a/packages/tree-view/lib/tree-view.js +++ b/packages/tree-view/lib/tree-view.js @@ -1,5 +1,4 @@ const path = require('path'); -const { shell } = require('@electron/remote'); const _ = require('underscore-plus'); const fs = require('fs-plus'); const { CompositeDisposable, Emitter } = require('atom'); @@ -825,7 +824,7 @@ class TreeView { `Unable to show ${filePath} in ${this.getFileManagerName()}` ); } - return shell.showItemInFolder(filePath); + return atom.showItemInFolder(filePath); } showCurrentFileInFileManager() { @@ -837,7 +836,7 @@ class TreeView { `Unable to show ${filePath} in ${this.getFileManagerName()}` ); } - return shell.showItemInFolder(filePath); + return atom.showItemInFolder(filePath); } getFileManagerName() { @@ -932,7 +931,7 @@ class TreeView { this.emitter.emit('will-delete-entry', meta); - let promise = shell.trashItem(selectedPath).then(() => { + let promise = atom.trashItem(selectedPath).then(() => { this.emitter.emit('entry-deleted', meta); }).catch(() => { this.emitter.emit('delete-entry-failed', meta); From 155a74b441c0cfbbe0db1cae21f48a8b29447121 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 11 Jun 2026 09:14:52 -0700 Subject: [PATCH 6/7] Remove all usages of `shell` within core packages --- packages/about/lib/about.js | 3 +- packages/about/lib/components/about-view.js | 7 +-- .../lib/autocomplete-manager.js | 2 +- .../autocomplete-manager-integration-spec.js | 10 ++-- packages/link/lib/link.js | 3 +- packages/link/spec/link-spec.js | 60 +++++++++---------- .../notifications/lib/notification-element.js | 4 +- packages/open-on-github/lib/github-file.js | 3 +- packages/pulsar-updater/src/main.js | 3 +- packages/settings-view/lib/badge-view.js | 3 +- packages/settings-view/lib/install-panel.js | 5 +- packages/settings-view/lib/package-card.js | 5 +- .../settings-view/lib/package-detail-view.js | 9 ++- .../settings-view/spec/package-card-spec.js | 5 +- .../spec/package-detail-view-spec.js | 25 ++++---- packages/symbols-view/lib/main.js | 5 +- .../tree-view/spec/tree-view-package-spec.js | 17 +++--- spec/window-event-handler-spec.js | 22 ++++--- src/application-delegate.js | 19 +++--- 19 files changed, 95 insertions(+), 115 deletions(-) diff --git a/packages/about/lib/about.js b/packages/about/lib/about.js index f9b9b06c78..da3422f85c 100644 --- a/packages/about/lib/about.js +++ b/packages/about/lib/about.js @@ -24,8 +24,7 @@ module.exports = class About { this.subscriptions.add( atom.commands.add('atom-workspace', 'about:view-release-notes', () => { - shell = shell || require('electron').shell; - shell.openExternal( + atom.openExternal( this.state.updateManager.getReleaseNotesURLForCurrentVersion() ); }) diff --git a/packages/about/lib/components/about-view.js b/packages/about/lib/components/about-view.js index 0e2d0f2049..79940dbf89 100644 --- a/packages/about/lib/components/about-view.js +++ b/packages/about/lib/components/about-view.js @@ -1,6 +1,5 @@ const { Disposable } = require('atom'); const etch = require('etch'); -const { shell } = require('electron'); const AtomLogo = require('./atom-logo'); const EtchComponent = require('../etch-component'); @@ -29,7 +28,7 @@ module.exports = class AboutView extends EtchComponent { handleReleaseNotesClick(e) { e.preventDefault(); - shell.openExternal( + atom.openExternal( this.props.updateManager.getReleaseNotesURLForCurrentVersion() ); } @@ -44,13 +43,13 @@ module.exports = class AboutView extends EtchComponent { handleTermsOfUseClick(e) { e.preventDefault(); - shell.openExternal('https://atom.io/terms'); //If we use this then this URL will need updating but button disabled (L#182) + atom.openExternal('https://atom.io/terms'); //If we use this then this URL will need updating but button disabled (L#182) // TODO Update to Privacy Policy once `pulsar-edit.github.io` #161 is resolved } handleHowToUpdateClick(e) { e.preventDefault(); - shell.openExternal( + atom.openExternal( 'https://github.com/pulsar-edit/pulsar/tree/master/packages/pulsar-updater#readme' ); } diff --git a/packages/autocomplete-plus/lib/autocomplete-manager.js b/packages/autocomplete-plus/lib/autocomplete-manager.js index 99abdd0e93..ae826062af 100644 --- a/packages/autocomplete-plus/lib/autocomplete-manager.js +++ b/packages/autocomplete-plus/lib/autocomplete-manager.js @@ -247,7 +247,7 @@ class AutocompleteManager { let descriptionContainer = suggestionListView.querySelector('.suggestion-description') if (descriptionContainer !== null && descriptionContainer.style.display === 'block') { let descriptionMoreLink = descriptionContainer.querySelector('.suggestion-description-more-link') - require('electron').shell.openExternal(descriptionMoreLink.href) + atom.openExternal(descriptionMoreLink.href) } } })) diff --git a/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js b/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js index b3736e1783..afc21e5450 100644 --- a/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js +++ b/packages/autocomplete-plus/spec/autocomplete-manager-integration-spec.js @@ -1997,29 +1997,27 @@ defm` it('triggers openExternal on keybind if there is a description', async () => { spyOn(provider, 'getSuggestions').andCallFake(() => [{text: 'ab', description: 'it is ab'}]) // eslint-disable-next-line node/no-extraneous-require - let shell = require('electron').shell - spyOn(shell, 'openExternal') + spyOn(atom, 'openExternal') triggerAutocompletion(editor, true, 'a') await waitForAutocomplete(editor) expect(editorView.querySelector('.autocomplete-plus')).toExist() atom.commands.dispatch(editorView, 'autocomplete-plus:navigate-to-description-more-link') - expect(shell.openExternal).toHaveBeenCalled() + expect(atom.openExternal).toHaveBeenCalled() }) it('does not trigger openExternal on keybind if there is not a description', async () => { spyOn(provider, 'getSuggestions').andCallFake(() => [{text: 'ab'}]) // eslint-disable-next-line node/no-extraneous-require - let shell = require('electron').shell - spyOn(shell, 'openExternal') + spyOn(atom, 'openExternal') triggerAutocompletion(editor, true, 'a') await waitForAutocomplete(editor) expect(editorView.querySelector('.autocomplete-plus')).toExist() atom.commands.dispatch(editorView, 'autocomplete-plus:navigate-to-description-more-link') - expect(shell.openExternal).not.toHaveBeenCalled() + expect(atom.openExternal).not.toHaveBeenCalled() }) }) }) diff --git a/packages/link/lib/link.js b/packages/link/lib/link.js index 4590456abb..78ab2e17ec 100644 --- a/packages/link/lib/link.js +++ b/packages/link/lib/link.js @@ -1,5 +1,4 @@ const url = require('url'); -const { shell } = require('electron'); const _ = require('underscore-plus'); const LINK_SCOPE_REGEX = /markup\.underline\.link/; @@ -30,7 +29,7 @@ module.exports = { const { protocol } = url.parse(link); if (protocol === 'http:' || protocol === 'https:' || protocol === 'atom:') { - shell.openExternal(link); + atom.openExternal(link); } }, diff --git a/packages/link/spec/link-spec.js b/packages/link/spec/link-spec.js index 09d63bd9ad..9c4943e235 100644 --- a/packages/link/spec/link-spec.js +++ b/packages/link/spec/link-spec.js @@ -1,5 +1,3 @@ -const { shell } = require('electron'); - describe('link package', () => { beforeEach(async () => { await atom.packages.activatePackage('language-hyperlink'); @@ -20,29 +18,29 @@ describe('link package', () => { editor.setText('// http://github.com '); await languageMode.atTransactionEnd(); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).not.toHaveBeenCalled(); + expect(atom.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 4]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.argsForCall[0][0]).toBe('http://github.com'); - shell.openExternal.reset(); + atom.openExternal.reset(); editor.setCursorBufferPosition([0, 8]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.argsForCall[0][0]).toBe('http://github.com'); - shell.openExternal.reset(); + atom.openExternal.reset(); editor.setCursorBufferPosition([0, 20]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.argsForCall[0][0]).toBe('http://github.com'); }); // NOTE: I don't think anyone realized that this was a feature. Our @@ -57,33 +55,33 @@ describe('link package', () => { '// atom://core/open/file?filename=sample.js&line=1&column=2 ' ); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).not.toHaveBeenCalled(); + expect(atom.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 4]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.argsForCall[0][0]).toBe( + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.argsForCall[0][0]).toBe( 'atom://core/open/file?filename=sample.js&line=1&column=2' ); - shell.openExternal.reset(); + atom.openExternal.reset(); editor.setCursorBufferPosition([0, 8]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.argsForCall[0][0]).toBe( + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.argsForCall[0][0]).toBe( 'atom://core/open/file?filename=sample.js&line=1&column=2' ); - shell.openExternal.reset(); + atom.openExternal.reset(); editor.setCursorBufferPosition([0, 59]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.argsForCall[0][0]).toBe( + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.argsForCall[0][0]).toBe( 'atom://core/open/file?filename=sample.js&line=1&column=2' ); }); @@ -106,22 +104,22 @@ you should not [click][her] // Allow for time for injections to populate await languageMode.atTransactionEnd(); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); editor.setCursorBufferPosition([0, 0]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).not.toHaveBeenCalled(); + expect(atom.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 19]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.argsForCall[0][0]).toBe('http://github.com'); - shell.openExternal.reset(); + atom.openExternal.reset(); editor.setCursorBufferPosition([1, 24]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).not.toHaveBeenCalled(); + expect(atom.openExternal).not.toHaveBeenCalled(); })); it('does not open non http/https/atom links', async () => { @@ -130,14 +128,14 @@ you should not [click][her] const editor = atom.workspace.getActiveTextEditor(); editor.setText('// ftp://github.com\n'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).not.toHaveBeenCalled(); + expect(atom.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 5]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); - expect(shell.openExternal).not.toHaveBeenCalled(); + expect(atom.openExternal).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/notifications/lib/notification-element.js b/packages/notifications/lib/notification-element.js index 4d3ef9be49..935c8fffca 100644 --- a/packages/notifications/lib/notification-element.js +++ b/packages/notifications/lib/notification-element.js @@ -9,8 +9,6 @@ */ let NotificationElement; const fs = require('fs-plus'); -const path = require('path'); -const {shell} = require('electron'); const NotificationIssue = require('./notification-issue'); const TemplateHelper = require('./template-helper'); @@ -264,7 +262,7 @@ Upgrading to the { e.preventDefault(); - shell ??= shell || require("electron").shell; let latestVersion = this.cache.getCacheItem("last-update-check")?.latestVersion; let tagSegment = latestVersion ? `tag/${latestVersion}` : ""; - shell.openExternal(`https://github.com/pulsar-edit/pulsar/releases/${tagSegment}`); + atom.openExternal(`https://github.com/pulsar-edit/pulsar/releases/${tagSegment}`); }; switch (installMethod.installMethod) { diff --git a/packages/settings-view/lib/badge-view.js b/packages/settings-view/lib/badge-view.js index 0c5a4160a4..1ac731136d 100644 --- a/packages/settings-view/lib/badge-view.js +++ b/packages/settings-view/lib/badge-view.js @@ -3,7 +3,6 @@ import {CompositeDisposable, Disposable} from 'atom' import etch from 'etch' -import {shell} from 'electron' export default class BadgeView { constructor(badge) { @@ -18,7 +17,7 @@ export default class BadgeView { if (!anchor) return event.stopPropagation() event.preventDefault() - shell.openExternal(anchor.href) + atom.openExternal(anchor.href) } if (this.hasLink()) { diff --git a/packages/settings-view/lib/install-panel.js b/packages/settings-view/lib/install-panel.js index 0d1a53081c..bf0043944f 100644 --- a/packages/settings-view/lib/install-panel.js +++ b/packages/settings-view/lib/install-panel.js @@ -2,7 +2,6 @@ /** @jsx etch.dom */ import path from 'path' -import electron from 'electron' import etch from 'etch' import hostedGitInfo from 'hosted-git-info' @@ -28,7 +27,7 @@ export default class InstallPanel { this.refs.searchEditor.setPlaceholderText('Search packages') this.searchType = 'packages' this.disposables.add( - this.packageManager.on('package-install-failed', ({pack, error}) => { + this.packageManager.on('package-install-failed', ({error}) => { this.refs.searchErrors.appendChild(new ErrorView(this.packageManager, error).element) }) ) @@ -303,7 +302,7 @@ export default class InstallPanel { didClickOpenAtomIo (event) { event.preventDefault() - electron.shell.openExternal(this.atomIoURL) + atom.openExternal(this.atomIoURL) } didClickSearchPackagesButton () { diff --git a/packages/settings-view/lib/package-card.js b/packages/settings-view/lib/package-card.js index 11a412f004..ad06ef9fd7 100644 --- a/packages/settings-view/lib/package-card.js +++ b/packages/settings-view/lib/package-card.js @@ -2,7 +2,6 @@ /** @jsx etch.dom */ import {CompositeDisposable, Disposable} from 'atom' -import {shell} from 'electron' import etch from 'etch' import BadgeView from './badge-view' import path from 'path' @@ -233,14 +232,14 @@ export default class PackageCard { const packageNameClickHandler = (event) => { event.stopPropagation() - shell.openExternal(`https://web.pulsar-edit.dev/packages/${this.pack.name}`) + atom.openExternal(`https://web.pulsar-edit.dev/packages/${this.pack.name}`) } this.refs.packageName.addEventListener('click', packageNameClickHandler) this.disposables.add(new Disposable(() => { this.refs.packageName.removeEventListener('click', packageNameClickHandler) })) const packageAuthorClickHandler = (event) => { event.stopPropagation() - shell.openExternal(`https://web.pulsar-edit.dev/users/${ownerFromRepository(this.pack.repository)}`) //TODO: Fix - This does not current exist but this will at least be more accurate + atom.openExternal(`https://web.pulsar-edit.dev/users/${ownerFromRepository(this.pack.repository)}`) //TODO: Fix - This does not current exist but this will at least be more accurate } this.refs.loginLink.addEventListener('click', packageAuthorClickHandler) this.disposables.add(new Disposable(() => { this.refs.loginLink.removeEventListener('click', packageAuthorClickHandler) })) diff --git a/packages/settings-view/lib/package-detail-view.js b/packages/settings-view/lib/package-detail-view.js index cb7be2b07f..3242c1415d 100644 --- a/packages/settings-view/lib/package-detail-view.js +++ b/packages/settings-view/lib/package-detail-view.js @@ -6,7 +6,6 @@ import url from 'url' import _ from 'underscore-plus' import fs from 'fs-plus' -import {shell} from 'electron' import {CompositeDisposable, Disposable} from 'atom' import etch from 'etch' @@ -49,9 +48,9 @@ export default class PackageDetailView { const repoUrl = this.packageManager.getRepositoryUrl(this.pack) if (typeof repoUrl === 'string') { if (url.parse(repoUrl).pathname === '/pulsar-edit/pulsar') { - shell.openExternal(`${repoUrl}/tree/master/packages/${this.pack.name}`) + atom.openExternal(`${repoUrl}/tree/master/packages/${this.pack.name}`) } else { - shell.openExternal(repoUrl) + atom.openExternal(repoUrl) } } } @@ -62,7 +61,7 @@ export default class PackageDetailView { event.preventDefault() let bugUri = this.packageManager.getRepositoryBugUri(this.pack) if (bugUri) { - shell.openExternal(bugUri) + atom.openExternal(bugUri) } } this.refs.issueButton.addEventListener('click', issueButtonClickHandler) @@ -97,7 +96,7 @@ export default class PackageDetailView { const learnMoreButtonClickHandler = (event) => { event.preventDefault() - shell.openExternal(`https://web.pulsar-edit.dev/packages/${this.pack.name}`) + atom.openExternal(`https://web.pulsar-edit.dev/packages/${this.pack.name}`) } this.refs.learnMoreButton.addEventListener('click', learnMoreButtonClickHandler) this.disposables.add(new Disposable(() => { this.refs.learnMoreButton.removeEventListener('click', learnMoreButtonClickHandler) })) diff --git a/packages/settings-view/spec/package-card-spec.js b/packages/settings-view/spec/package-card-spec.js index c7290c5a13..7e5ed072b8 100644 --- a/packages/settings-view/spec/package-card-spec.js +++ b/packages/settings-view/spec/package-card-spec.js @@ -3,7 +3,6 @@ const path = require('path'); const PackageCard = require('../lib/package-card'); const PackageManager = require('../lib/package-manager'); const SettingsView = require('../lib/settings-view'); -const {shell} = require('electron'); describe("PackageCard", function() { const setPackageStatusSpies = function(opts) { @@ -91,12 +90,12 @@ describe("PackageCard", function() { }; card = new PackageCard(pack, new SettingsView(), packageManager); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); jasmine.attachToDOM(card.element); let badge = card.element.querySelector('.badge'); expect(badge).toExist(); badge?.click(); - expect(shell.openExternal).toHaveBeenCalled(); + expect(atom.openExternal).toHaveBeenCalled(); }) it("shows the author details", function() { diff --git a/packages/settings-view/spec/package-detail-view-spec.js b/packages/settings-view/spec/package-detail-view-spec.js index 9caa9bb291..285de3a7d2 100644 --- a/packages/settings-view/spec/package-detail-view-spec.js +++ b/packages/settings-view/spec/package-detail-view-spec.js @@ -1,7 +1,6 @@ const fs = require('fs'); const path = require('path'); -const {shell} = require('electron'); const PackageDetailView = require('../lib/package-detail-view'); const PackageManager = require('../lib/package-manager'); @@ -129,30 +128,30 @@ describe("PackageDetailView", function() { it("triggers a report issue button click and checks that the fallback repository issue tracker URL was opened", function() { loadCustomPackageFromRemote('package-without-bugs-property'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); view.refs.issueButton.click(); - expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/example/package-without-bugs-property/issues/new'); + expect(atom.openExternal).toHaveBeenCalledWith('https://github.com/example/package-without-bugs-property/issues/new'); }); it("triggers a report issue button click and checks that the bugs URL string was opened", function() { loadCustomPackageFromRemote('package-with-bugs-property-url-string'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); view.refs.issueButton.click(); - expect(shell.openExternal).toHaveBeenCalledWith('https://example.com/custom-issue-tracker/new'); + expect(atom.openExternal).toHaveBeenCalledWith('https://example.com/custom-issue-tracker/new'); }); it("triggers a report issue button click and checks that the bugs URL was opened", function() { loadCustomPackageFromRemote('package-with-bugs-property-url'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); view.refs.issueButton.click(); - expect(shell.openExternal).toHaveBeenCalledWith('https://example.com/custom-issue-tracker/new'); + expect(atom.openExternal).toHaveBeenCalledWith('https://example.com/custom-issue-tracker/new'); }); it("triggers a report issue button click and checks that the bugs email link was opened", function() { loadCustomPackageFromRemote('package-with-bugs-property-email'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); view.refs.issueButton.click(); - expect(shell.openExternal).toHaveBeenCalledWith('mailto:issues@example.com'); + expect(atom.openExternal).toHaveBeenCalledWith('mailto:issues@example.com'); }); it("should show 'Install' as the first breadcrumb by default", function() { @@ -162,15 +161,15 @@ describe("PackageDetailView", function() { it("should open repository url", function() { loadPackageFromRemote('package-with-readme'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); view.refs.packageRepo.click(); - expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/example/package-with-readme'); + expect(atom.openExternal).toHaveBeenCalledWith('https://github.com/example/package-with-readme'); }); it("should open internal package repository url", function() { loadPackageFromRemote('package-internal'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); view.refs.packageRepo.click(); - expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/pulsar-edit/pulsar/tree/master/packages/package-internal'); + expect(atom.openExternal).toHaveBeenCalledWith('https://github.com/pulsar-edit/pulsar/tree/master/packages/package-internal'); }); }); diff --git a/packages/symbols-view/lib/main.js b/packages/symbols-view/lib/main.js index a8aedeed78..593407ab0d 100644 --- a/packages/symbols-view/lib/main.js +++ b/packages/symbols-view/lib/main.js @@ -1,8 +1,7 @@ -const { Disposable, TextEditor } = require('atom'); +const { Disposable } = require('atom'); const Config = require('./config'); const ProviderBroker = require('./provider-broker'); const Path = require('path'); -const { shell } = require('electron'); const { migrateOldConfigIfNeeded } = require('./util'); const NO_PROVIDERS_MESSAGE = `You don’t have any symbol providers installed.`; @@ -18,7 +17,7 @@ const NO_PROVIDERS_BUTTONS = [ { text: 'Find providers', onDidClick: () => { - shell.openExternal(`https://web.pulsar-edit.dev/packages?service=symbol.provider&serviceType=provided`); + atom.openExternal(`https://web.pulsar-edit.dev/packages?service=symbol.provider&serviceType=provided`); } } ]; diff --git a/packages/tree-view/spec/tree-view-package-spec.js b/packages/tree-view/spec/tree-view-package-spec.js index 238f9a5572..aa8a6e2661 100644 --- a/packages/tree-view/spec/tree-view-package-spec.js +++ b/packages/tree-view/spec/tree-view-package-spec.js @@ -6,7 +6,6 @@ const path = require('path'); const temp = require('temp').track(); const os = require('os'); const remote = require('@electron/remote'); -const {shell} = remote; const Directory = require('../lib/directory'); const eventHelpers = require("./event-helpers"); const { setDebug } = require('../lib/helpers'); @@ -3227,7 +3226,7 @@ describe("TreeView", function () { fileView.dispatchEvent(new MouseEvent('click', { bubbles: true, detail: 1 })); treeView.focus(); - spyOn(shell, 'trashItem').andCallFake(() => { + spyOn(atom, 'trashItem').andCallFake(() => { return Promise.reject(false); }); @@ -4410,17 +4409,17 @@ describe("TreeView", function () { }); describe("showSelectedEntryInFileManager()", function () { - beforeEach(() => spyOn(shell, 'showItemInFolder').andReturn(false)); + beforeEach(() => spyOn(atom, 'showItemInFolder').andReturn(false)); it("does nothing if no entry is selected", function () { treeView.deselect(); treeView.showSelectedEntryInFileManager(); - expect(shell.showItemInFolder).not.toHaveBeenCalled(); + expect(atom.showItemInFolder).not.toHaveBeenCalled(); }); it("shows the selected entry in the OS's file manager", function () { treeView.showSelectedEntryInFileManager(); - expect(shell.showItemInFolder).toHaveBeenCalled(); + expect(atom.showItemInFolder).toHaveBeenCalled(); }); it("displays a notification if showing the file fails", function () { @@ -4432,13 +4431,13 @@ describe("TreeView", function () { }); describe("showCurrentFileInFileManager()", function () { - beforeEach(() => spyOn(shell, 'showItemInFolder').andReturn(false)); + beforeEach(() => spyOn(atom, 'showItemInFolder').andReturn(false)); it("does nothing when no file is opened", function () { expect(atom.workspace.getCenter().getPaneItems().length).toBe(0); treeView.showCurrentFileInFileManager(); - expect(shell.showItemInFolder).not.toHaveBeenCalled(); + expect(atom.showItemInFolder).not.toHaveBeenCalled(); }); it("does nothing when only an untitled tab is opened", function () { @@ -4447,7 +4446,7 @@ describe("TreeView", function () { return runs(function () { workspaceElement.focus(); treeView.showCurrentFileInFileManager(); - expect(shell.showItemInFolder).not.toHaveBeenCalled(); + expect(atom.showItemInFolder).not.toHaveBeenCalled(); }); }); @@ -4458,7 +4457,7 @@ describe("TreeView", function () { return runs(function () { treeView.showCurrentFileInFileManager(); - expect(shell.showItemInFolder).toHaveBeenCalled(); + expect(atom.showItemInFolder).toHaveBeenCalled(); }); }); diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index 40fd09e4c2..2749c7fcd5 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -1,6 +1,5 @@ const KeymapManager = require('@pulsar-edit/atom-keymap'); const WindowEventHandler = require('../src/window-event-handler'); -const { conditionPromise } = require('./helpers/async-spec-helpers'); describe('WindowEventHandler', () => { let windowEventHandler; @@ -69,8 +68,7 @@ describe('WindowEventHandler', () => { describe('when a link is clicked', () => { it('opens the http/https links in an external application', () => { - const { shell } = require('electron'); - spyOn(shell, 'openExternal'); + spyOn(atom, 'openExternal'); const link = document.createElement('a'); const linkChild = document.createElement('span'); @@ -84,24 +82,24 @@ describe('WindowEventHandler', () => { }; windowEventHandler.handleLinkClick(fakeEvent); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.calls.argsFor(0)[0]).toBe('http://github.com'); - shell.openExternal.calls.reset(); + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.calls.argsFor(0)[0]).toBe('http://github.com'); + atom.openExternal.calls.reset(); link.href = 'https://github.com'; windowEventHandler.handleLinkClick(fakeEvent); - expect(shell.openExternal).toHaveBeenCalled(); - expect(shell.openExternal.calls.argsFor(0)[0]).toBe('https://github.com'); - shell.openExternal.calls.reset(); + expect(atom.openExternal).toHaveBeenCalled(); + expect(atom.openExternal.calls.argsFor(0)[0]).toBe('https://github.com'); + atom.openExternal.calls.reset(); link.href = ''; windowEventHandler.handleLinkClick(fakeEvent); - expect(shell.openExternal).not.toHaveBeenCalled(); - shell.openExternal.calls.reset(); + expect(atom.openExternal).not.toHaveBeenCalled(); + atom.openExternal.calls.reset(); link.href = '#scroll-me'; windowEventHandler.handleLinkClick(fakeEvent); - expect(shell.openExternal).not.toHaveBeenCalled(); + expect(atom.openExternal).not.toHaveBeenCalled(); }); it('opens the "atom://" links with URL handler', () => { diff --git a/src/application-delegate.js b/src/application-delegate.js index c18ff25dc3..a74140a60e 100644 --- a/src/application-delegate.js +++ b/src/application-delegate.js @@ -13,7 +13,7 @@ module.exports = class ApplicationDelegate { ipcMessageEmitter() { if (!this._ipcMessageEmitter) { this._ipcMessageEmitter = new Emitter(); - ipcRenderer.on('message', (event, message, detail) => { + ipcRenderer.on('message', (_event, message, detail) => { this._ipcMessageEmitter.emit(message, detail); }); } @@ -34,7 +34,7 @@ module.exports = class ApplicationDelegate { pickFolder(callback) { const responseChannel = 'atom-pick-folder-response'; - ipcRenderer.on(responseChannel, function (event, path) { + ipcRenderer.on(responseChannel, function (_event, path) { ipcRenderer.removeAllListeners(responseChannel); return callback(path); }); @@ -271,7 +271,7 @@ module.exports = class ApplicationDelegate { } } - showMessageDialog(params) {} + showMessageDialog(_params) {} showSaveDialog(options, callback) { if (typeof callback === 'function') { @@ -287,6 +287,7 @@ module.exports = class ApplicationDelegate { } playBeepSound() { + // TODO: Delegate to main. return shell.beep(); } @@ -295,7 +296,7 @@ module.exports = class ApplicationDelegate { } onApplicationMenuCommand(handler) { - const outerCallback = (event, ...args) => handler(...args); + const outerCallback = (_event, ...args) => handler(...args); ipcRenderer.on('command', outerCallback); return new Disposable(() => @@ -304,7 +305,7 @@ module.exports = class ApplicationDelegate { } onContextMenuCommand(handler) { - const outerCallback = (event, ...args) => handler(...args); + const outerCallback = (_event, ...args) => handler(...args); ipcRenderer.on('context-command', outerCallback); return new Disposable(() => @@ -313,7 +314,7 @@ module.exports = class ApplicationDelegate { } onURIMessage(handler) { - const outerCallback = (event, ...args) => handler(...args); + const outerCallback = (_event, ...args) => handler(...args); ipcRenderer.on('uri-message', outerCallback); return new Disposable(() => @@ -322,7 +323,7 @@ module.exports = class ApplicationDelegate { } onDidRequestUnload(callback) { - const outerCallback = async (event, message) => { + const outerCallback = async (event, _message) => { const shouldUnload = await callback(event); ipcRenderer.send('did-prepare-to-unload', shouldUnload); }; @@ -334,7 +335,7 @@ module.exports = class ApplicationDelegate { } onDidChangeHistoryManager(callback) { - const outerCallback = (event, message) => callback(event); + const outerCallback = (event, _message) => callback(event); ipcRenderer.on('did-change-history-manager', outerCallback); return new Disposable(() => @@ -347,7 +348,7 @@ module.exports = class ApplicationDelegate { } openExternal(url) { - return shell.openExternal(url); + return this.openExternalDirect(url); } emitWillSavePath(path) { From 80dad19ceb76b541cc58d2580cfd6873f4d57ad9 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 11 Jun 2026 09:37:52 -0700 Subject: [PATCH 7/7] Fix spec --- spec/window-event-handler-spec.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index 2749c7fcd5..010a40e85f 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -68,7 +68,7 @@ describe('WindowEventHandler', () => { describe('when a link is clicked', () => { it('opens the http/https links in an external application', () => { - spyOn(atom, 'openExternal'); + spyOn(atom.applicationDelegate, 'openExternal'); const link = document.createElement('a'); const linkChild = document.createElement('span'); @@ -82,24 +82,24 @@ describe('WindowEventHandler', () => { }; windowEventHandler.handleLinkClick(fakeEvent); - expect(atom.openExternal).toHaveBeenCalled(); - expect(atom.openExternal.calls.argsFor(0)[0]).toBe('http://github.com'); - atom.openExternal.calls.reset(); + expect(atom.applicationDelegate.openExternal).toHaveBeenCalled(); + expect(atom.applicationDelegate.openExternal.calls.argsFor(0)[0]).toBe('http://github.com'); + atom.applicationDelegate.openExternal.calls.reset(); link.href = 'https://github.com'; windowEventHandler.handleLinkClick(fakeEvent); - expect(atom.openExternal).toHaveBeenCalled(); - expect(atom.openExternal.calls.argsFor(0)[0]).toBe('https://github.com'); - atom.openExternal.calls.reset(); + expect(atom.applicationDelegate.openExternal).toHaveBeenCalled(); + expect(atom.applicationDelegate.openExternal.calls.argsFor(0)[0]).toBe('https://github.com'); + atom.applicationDelegate.openExternal.calls.reset(); link.href = ''; windowEventHandler.handleLinkClick(fakeEvent); - expect(atom.openExternal).not.toHaveBeenCalled(); - atom.openExternal.calls.reset(); + expect(atom.applicationDelegate.openExternal).not.toHaveBeenCalled(); + atom.applicationDelegate.openExternal.calls.reset(); link.href = '#scroll-me'; windowEventHandler.handleLinkClick(fakeEvent); - expect(atom.openExternal).not.toHaveBeenCalled(); + expect(atom.applicationDelegate.openExternal).not.toHaveBeenCalled(); }); it('opens the "atom://" links with URL handler', () => {