Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions spec/atom-environment-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});
});
54 changes: 53 additions & 1 deletion src/application-delegate.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,12 +363,64 @@ 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);
return new Disposable(() =>
ipcRenderer.removeListener('did-resolve-proxy', outerCallback)
);
}

// 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;
} 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.
Comment thread
savetheclocktower marked this conversation as resolved.
Outdated
return ipcRenderer.invoke('showItemInFolder', filePath).then(({ outcome, error, result }) => {
if (outcome === 'success') {
return result;
} else if (outcome === 'failure') {
return Promise.reject(error);
}
});
}
};
44 changes: 44 additions & 0 deletions src/atom-environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.openExternalDirect(url);
}

// Restore the window to its previous dimensions and show it.
//
// Restores the full screen and maximized state after the window has resized to
Expand Down
56 changes: 56 additions & 0 deletions src/main-process/atom-application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading