@@ -9,7 +9,7 @@ import { IProcessServiceFactory } from '../../common/process/types';
99import { ITerminalServiceFactory } from '../../common/terminal/types';
1010import { IComponentAdapter, ICondaService } from '../../interpreter/contracts';
1111import { IServiceContainer } from '../../ioc/types';
12- import { IPackageManager, MessageEmitter, PackageKernel } from './types';
12+ import { IPackageManager, MessageEmitter, PackageSession } from './types';
1313
1414/** Package info returned by `conda search --json` */
1515interface CondaPackageInfo {
@@ -64,11 +64,11 @@ export class CondaPackageManager implements IPackageManager {
6464 private readonly _pythonPath: string,
6565 _messageEmitter: MessageEmitter,
6666 private readonly _serviceContainer: IServiceContainer,
67- private readonly _kernel: PackageKernel ,
67+ private readonly _session: PackageSession ,
6868 ) {}
6969
70- async getPackages(): Promise<positron.LanguageRuntimePackage[]> {
71- return this._kernel.callMethod ('getPackagesInstalled');
70+ async getPackages(token?: vscode.CancellationToken ): Promise<positron.LanguageRuntimePackage[]> {
71+ return this._callMethod<positron.LanguageRuntimePackage[]> ('getPackagesInstalled', token );
7272 }
7373
7474 /**
@@ -83,54 +83,70 @@ export class CondaPackageManager implements IPackageManager {
8383 }
8484 }
8585
86- async installPackages(packages: positron.PackageSpec[]): Promise<void> {
86+ async installPackages(packages: positron.PackageSpec[], token?: vscode.CancellationToken ): Promise<void> {
8787 if (packages.length === 0) {
8888 return;
8989 }
9090
91+ if (token?.isCancellationRequested) {
92+ throw new vscode.CancellationError();
93+ }
94+
9195 await this._ensureConda();
9296
9397 const packageSpecs = this._formatPackageSpecs(packages);
9498 const envPrefix = await this._getEnvironmentPrefix();
9599 const args = ['install', '--prefix', envPrefix, '-y', ...packageSpecs];
96100
97- await this._executeCondaInTerminal(args);
101+ await this._executeCondaInTerminal(args, token );
98102 }
99103
100- async uninstallPackages(packages: string[]): Promise<void> {
104+ async uninstallPackages(packages: string[], token?: vscode.CancellationToken ): Promise<void> {
101105 if (packages.length === 0) {
102106 return;
103107 }
104108
109+ if (token?.isCancellationRequested) {
110+ throw new vscode.CancellationError();
111+ }
112+
105113 await this._ensureConda();
106114
107115 const envPrefix = await this._getEnvironmentPrefix();
108116 const args = ['remove', '--prefix', envPrefix, '-y', ...packages];
109117
110- await this._executeCondaInTerminal(args);
118+ await this._executeCondaInTerminal(args, token );
111119 }
112120
113- async updatePackages(packages: positron.PackageSpec[]): Promise<void> {
121+ async updatePackages(packages: positron.PackageSpec[], token?: vscode.CancellationToken ): Promise<void> {
114122 // Use installPackages() because conda update doesn't support version specs.
115123 // conda install will update (or downgrade) to the specified version.
116- return this.installPackages(packages);
124+ return this.installPackages(packages, token );
117125 }
118126
119- async updateAllPackages(): Promise<void> {
127+ async updateAllPackages(token?: vscode.CancellationToken): Promise<void> {
128+ if (token?.isCancellationRequested) {
129+ throw new vscode.CancellationError();
130+ }
131+
120132 await this._ensureConda();
121133
122134 const envPrefix = await this._getEnvironmentPrefix();
123135 const args = ['update', '--prefix', envPrefix, '--all', '-y'];
124136
125- await this._executeCondaInTerminal(args);
137+ await this._executeCondaInTerminal(args, token );
126138 }
127139
128- async searchPackages(query: string): Promise<positron.LanguageRuntimePackage[]> {
140+ async searchPackages(query: string, token?: vscode.CancellationToken): Promise<positron.LanguageRuntimePackage[]> {
141+ if (token?.isCancellationRequested) {
142+ throw new vscode.CancellationError();
143+ }
144+
129145 await this._ensureConda();
130146
131147 try {
132148 // Use wildcard pattern for partial matching
133- const result = await this._executeCondaWithOutput(['search', `*${query}*`, '--json']);
149+ const result = await this._executeCondaWithOutput(['search', `*${query}*`, '--json'], token );
134150 const json = parseCondaSearchResult(result);
135151
136152 // Return unique package names with the latest version (sorted by timestamp)
@@ -144,17 +160,24 @@ export class CondaPackageManager implements IPackageManager {
144160 version: latest.version,
145161 };
146162 });
147- } catch {
163+ } catch (e) {
164+ if (e instanceof vscode.CancellationError) {
165+ throw e;
166+ }
148167 // Return empty array if search fails (e.g., no matches)
149168 return [];
150169 }
151170 }
152171
153- async searchPackageVersions(name: string): Promise<string[]> {
172+ async searchPackageVersions(name: string, token?: vscode.CancellationToken): Promise<string[]> {
173+ if (token?.isCancellationRequested) {
174+ throw new vscode.CancellationError();
175+ }
176+
154177 await this._ensureConda();
155178
156179 try {
157- const result = await this._executeCondaWithOutput(['search', name, '--json']);
180+ const result = await this._executeCondaWithOutput(['search', name, '--json'], token );
158181 const json = parseCondaSearchResult(result);
159182
160183 // Get all unique versions for this package
@@ -167,7 +190,10 @@ export class CondaPackageManager implements IPackageManager {
167190 const sorted = [...packageInfo].sort((a, b) => b.timestamp - a.timestamp);
168191 const versions = [...new Set(sorted.map((p) => p.version))];
169192 return versions;
170- } catch {
193+ } catch (e) {
194+ if (e instanceof vscode.CancellationError) {
195+ throw e;
196+ }
171197 return [];
172198 }
173199 }
@@ -224,31 +250,74 @@ export class CondaPackageManager implements IPackageManager {
224250
225251 /**
226252 * Execute a conda command in the terminal (visible to user).
253+ * @param args The conda arguments to execute
254+ * @param token Optional cancellation token
227255 */
228- private async _executeCondaInTerminal(args: string[]): Promise<void> {
256+ private async _executeCondaInTerminal(args: string[], token?: vscode.CancellationToken ): Promise<void> {
229257 const condaFile = await this._getCondaFile();
230258 const terminalService = this._serviceContainer
231259 .get<ITerminalServiceFactory>(ITerminalServiceFactory)
232260 .getTerminalService({});
233261 // Ensure terminal is created and ready before sending command
234262 await terminalService.show();
235- const tokenSource = new vscode.CancellationTokenSource();
263+
264+ const disposable = token?.onCancellationRequested(async () => {
265+ // Send Ctrl+C to interrupt the running command
266+ await terminalService.sendText('\x03');
267+ });
268+
236269 try {
237- await terminalService.sendCommand(condaFile, args, tokenSource. token);
270+ await terminalService.sendCommand(condaFile, args, token);
238271 } finally {
239- tokenSource .dispose();
272+ disposable? .dispose();
240273 }
241274 }
242275
243276 /**
244277 * Execute a conda command and capture stdout.
245278 */
246- private async _executeCondaWithOutput(args: string[]): Promise<string> {
279+ private async _executeCondaWithOutput(args: string[], token?: vscode.CancellationToken ): Promise<string> {
247280 const condaFile = await this._getCondaFile();
248281 const processServiceFactory = this._serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
249282 const processService = await processServiceFactory.create();
250283
251- const result = await processService.exec(condaFile, args);
284+ const result = await processService.exec(condaFile, args, { token } );
252285 return result.stdout;
253286 }
287+
288+ /**
289+ * Call a kernel method with cancellation support.
290+ * If the token is cancelled, interrupts the kernel (if supported).
291+ */
292+ private async _callMethod<T>(method: string, token?: vscode.CancellationToken, ...args: unknown[]): Promise<T> {
293+ if (token?.isCancellationRequested) {
294+ throw new vscode.CancellationError();
295+ }
296+
297+ const resultPromise = this._session.callMethod(method, ...args) as Promise<T>;
298+
299+ // If no token provided, just return the method result
300+ if (!token) {
301+ return resultPromise;
302+ }
303+
304+ // Wrap callMethod promise with cancellation handling
305+ return new Promise<T>((resolve, reject) => {
306+ const cancelDisp = token.onCancellationRequested(async () => {
307+ // Interrupt the session via the runtime service
308+ await positron.runtime.interruptSession(this._session.metadata.sessionId);
309+ reject(new vscode.CancellationError());
310+ });
311+
312+ resultPromise
313+ .then((result) => {
314+ cancelDisp.dispose();
315+ resolve(result);
316+ })
317+ .catch((err) => {
318+ cancelDisp.dispose();
319+ reject(err);
320+ });
321+ });
322+ }
254323}
0 commit comments