|
| 1 | +import * as fs from 'node:fs'; |
| 2 | + |
| 3 | +import * as vscode from 'vscode'; |
| 4 | + |
| 5 | +import { pineLsRoot } from '../pinels/installer'; |
| 6 | +import type { PineLsService } from '../pinels/service'; |
| 7 | +import { cliWorkdirPath } from '../plugins/installed'; |
| 8 | +import { SETUP_DOWNLOAD_MB } from './constants'; |
| 9 | +import type { EnvManager } from './manager'; |
| 10 | +import { |
| 11 | + formatBytes, |
| 12 | + removableComponents, |
| 13 | + removePaths, |
| 14 | + type RemovableComponent, |
| 15 | +} from './remove'; |
| 16 | + |
| 17 | +interface PickItem extends vscode.QuickPickItem { |
| 18 | + component: RemovableComponent; |
| 19 | +} |
| 20 | + |
| 21 | +/** |
| 22 | + * "Remove Environment" and "Reveal Environment Folder". |
| 23 | + * |
| 24 | + * Uninstalling the extension leaves globalStorage behind, so without a remove |
| 25 | + * command the only way to get the disk space back is to find the folder by |
| 26 | + * hand — which is also why Reveal sits next to it. |
| 27 | + */ |
| 28 | +export function registerRemoveEnvironment( |
| 29 | + context: vscode.ExtensionContext, |
| 30 | + manager: EnvManager, |
| 31 | + pineLs: PineLsService, |
| 32 | + output: vscode.OutputChannel |
| 33 | +): vscode.Disposable[] { |
| 34 | + const storageDir = context.globalStorageUri.fsPath; |
| 35 | + return [ |
| 36 | + vscode.commands.registerCommand('pyneide.removeEnvironment', () => |
| 37 | + removeEnvironment(storageDir, manager, pineLs, output) |
| 38 | + ), |
| 39 | + vscode.commands.registerCommand('pyneide.revealEnvironmentFolder', async () => { |
| 40 | + // globalStorage is created lazily, so it may not exist before a setup. |
| 41 | + await fs.promises.mkdir(storageDir, { recursive: true }); |
| 42 | + await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(storageDir)); |
| 43 | + }), |
| 44 | + ]; |
| 45 | +} |
| 46 | + |
| 47 | +async function removeEnvironment( |
| 48 | + storageDir: string, |
| 49 | + manager: EnvManager, |
| 50 | + pineLs: PineLsService, |
| 51 | + output: vscode.OutputChannel |
| 52 | +): Promise<void> { |
| 53 | + if (manager.state.kind === 'working') { |
| 54 | + void vscode.window.showInformationMessage( |
| 55 | + 'PyneIDE: the environment is busy. Wait for it to finish (or cancel it) before removing it.' |
| 56 | + ); |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + const components = await vscode.window.withProgress( |
| 61 | + { location: vscode.ProgressLocation.Window, title: 'PyneIDE: measuring the environment…' }, |
| 62 | + () => |
| 63 | + removableComponents(storageDir, { |
| 64 | + pineLsRoot: pineLsRoot(storageDir), |
| 65 | + cliWorkdir: cliWorkdirPath(storageDir), |
| 66 | + }) |
| 67 | + ); |
| 68 | + if (components.length === 0) { |
| 69 | + void vscode.window.showInformationMessage( |
| 70 | + 'PyneIDE: there is nothing to remove — no downloaded environment was found.' |
| 71 | + ); |
| 72 | + return; |
| 73 | + } |
| 74 | + |
| 75 | + const picked = await vscode.window.showQuickPick( |
| 76 | + components.map((component) => toItem(component)), |
| 77 | + { |
| 78 | + title: 'PyneIDE — Remove Environment', |
| 79 | + placeHolder: 'Select what to delete from PyneIDE storage', |
| 80 | + canPickMany: true, |
| 81 | + } |
| 82 | + ); |
| 83 | + if (!picked || picked.length === 0) return; |
| 84 | + |
| 85 | + const selected = picked.map((item) => item.component); |
| 86 | + const freed = selected.reduce((sum, component) => sum + component.bytes, 0); |
| 87 | + const lines = selected.map( |
| 88 | + (component) => `${component.label} — ${formatBytes(component.bytes)}` |
| 89 | + ); |
| 90 | + const reinstall = selected.some((component) => component.id === 'python') |
| 91 | + ? ` Setting the Python environment up again downloads about ${SETUP_DOWNLOAD_MB} MB.` |
| 92 | + : ''; |
| 93 | + const confirm = await vscode.window.showWarningMessage( |
| 94 | + 'Remove the downloaded PyneIDE environment?', |
| 95 | + { |
| 96 | + modal: true, |
| 97 | + detail: |
| 98 | + `${lines.join('\n')}\n\nFrees about ${formatBytes(freed)}. ` + |
| 99 | + `This cannot be undone.${reinstall}\n\n` + |
| 100 | + 'Your scripts, data and project files are not touched.', |
| 101 | + }, |
| 102 | + 'Remove' |
| 103 | + ); |
| 104 | + if (confirm !== 'Remove') return; |
| 105 | + |
| 106 | + const removingPineLs = selected.some((component) => component.id === 'pine-ls'); |
| 107 | + try { |
| 108 | + // The server runs from the directory about to be deleted; on Windows that |
| 109 | + // alone makes the removal fail. |
| 110 | + if (removingPineLs) await pineLs.stopServer(); |
| 111 | + await vscode.window.withProgress( |
| 112 | + { |
| 113 | + location: vscode.ProgressLocation.Notification, |
| 114 | + title: 'PyneIDE: removing the environment', |
| 115 | + cancellable: false, |
| 116 | + }, |
| 117 | + async () => { |
| 118 | + for (const component of selected) { |
| 119 | + await removePaths(component.paths, (message) => output.appendLine(message)); |
| 120 | + } |
| 121 | + } |
| 122 | + ); |
| 123 | + } catch (err) { |
| 124 | + const message = err instanceof Error ? err.message : String(err); |
| 125 | + output.appendLine(`Removing the environment failed: ${message}`); |
| 126 | + const choice = await vscode.window.showErrorMessage( |
| 127 | + `PyneIDE: removing the environment failed: ${message}. ` + |
| 128 | + 'A running PyneIDE process may still hold files open — reloading the window releases them.', |
| 129 | + 'Reload Window', |
| 130 | + 'Show Log' |
| 131 | + ); |
| 132 | + if (choice === 'Reload Window') { |
| 133 | + await vscode.commands.executeCommand('workbench.action.reloadWindow'); |
| 134 | + } else if (choice === 'Show Log') { |
| 135 | + output.show(); |
| 136 | + } |
| 137 | + return; |
| 138 | + } |
| 139 | + |
| 140 | + await manager.check(); |
| 141 | + if (removingPineLs) await pineLs.restart(); |
| 142 | + void vscode.window.showInformationMessage( |
| 143 | + `PyneIDE: removed ${selected.map((component) => component.label.toLowerCase()).join(' and ')}. ` + |
| 144 | + `Freed about ${formatBytes(freed)}.` |
| 145 | + ); |
| 146 | +} |
| 147 | + |
| 148 | +function toItem(component: RemovableComponent): PickItem { |
| 149 | + return { |
| 150 | + label: component.label, |
| 151 | + description: formatBytes(component.bytes), |
| 152 | + detail: component.note, |
| 153 | + // The Python environment is what the disk space is in, and reinstalling it |
| 154 | + // is one click; the Pine LS has to be downloaded again, so it is opt-in. |
| 155 | + picked: component.id === 'python', |
| 156 | + component, |
| 157 | + }; |
| 158 | +} |
0 commit comments