Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Event, Emitter } from '../../../../../base/common/event.js';
7
import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
8
import { INotebookKernelSourceAction, INotebookTextModel } from '../../common/notebookCommon.js';
9
import { INotebookKernel, ISelectedNotebooksChangeEvent, INotebookKernelMatchResult, INotebookKernelService, INotebookTextModelLike, ISourceAction, INotebookSourceActionChangeEvent, INotebookKernelDetectionTask, IKernelSourceActionProvider } from '../../common/notebookKernelService.js';
10
import { LRUCache, ResourceMap } from '../../../../../base/common/map.js';
11
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { INotebookService } from '../../common/notebookService.js';
14
import { IMenu, IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
15
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
16
import { IAction } from '../../../../../base/common/actions.js';
17
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
18
import { Schemas } from '../../../../../base/common/network.js';
19
import { getActiveWindow, runWhenWindowIdle } from '../../../../../base/browser/dom.js';
20
21
class KernelInfo {
22
23
private static _logicClock = 0;
24
25
readonly kernel: INotebookKernel;
26
public score: number;
27
readonly time: number;
28
29
readonly notebookPriorities = new ResourceMap<number>();
30
31
constructor(kernel: INotebookKernel) {
32
this.kernel = kernel;
33
this.score = -1;
34
this.time = KernelInfo._logicClock++;
35
}
36
}
37
38
class NotebookTextModelLikeId {
39
static str(k: INotebookTextModelLike): string {
40
return `${k.notebookType}/${k.uri.toString()}`;
41
}
42
static obj(s: string): INotebookTextModelLike {
43
const idx = s.indexOf('/');
44
return {
45
notebookType: s.substring(0, idx),
46
uri: URI.parse(s.substring(idx + 1))
47
};
48
}
49
}
50
51
class SourceAction extends Disposable implements ISourceAction {
52
execution: Promise<void> | undefined;
53
private readonly _onDidChangeState = this._register(new Emitter<void>());
54
readonly onDidChangeState = this._onDidChangeState.event;
55
56
constructor(
57
readonly action: IAction,
58
readonly model: INotebookTextModelLike,
59
readonly isPrimary: boolean
60
) {
61
super();
62
}
63
64
async runAction() {
65
if (this.execution) {
66
return this.execution;
67
}
68
69
this.execution = this._runAction();
70
this._onDidChangeState.fire();
71
await this.execution;
72
this.execution = undefined;
73
this._onDidChangeState.fire();
74
}
75
76
private async _runAction(): Promise<void> {
77
try {
78
await this.action.run({
79
uri: this.model.uri,
80
$mid: MarshalledId.NotebookActionContext
81
});
82
83
} catch (error) {
84
console.warn(`Kernel source command failed: ${error}`);
85
}
86
}
87
}
88
89
interface IKernelInfoCache {
90
menu: IMenu;
91
actions: [ISourceAction, IDisposable][];
92
93
}
94
95
export class NotebookKernelService extends Disposable implements INotebookKernelService {
96
97
declare _serviceBrand: undefined;
98
99
private readonly _kernels = new Map<string, KernelInfo>();
100
101
private readonly _notebookBindings = new LRUCache<string, string>(1000, 0.7);
102
103
private readonly _onDidChangeNotebookKernelBinding = this._register(new Emitter<ISelectedNotebooksChangeEvent>());
104
private readonly _onDidAddKernel = this._register(new Emitter<INotebookKernel>());
105
private readonly _onDidRemoveKernel = this._register(new Emitter<INotebookKernel>());
106
private readonly _onDidChangeNotebookAffinity = this._register(new Emitter<void>());
107
private readonly _onDidChangeSourceActions = this._register(new Emitter<INotebookSourceActionChangeEvent>());
108
private readonly _onDidNotebookVariablesChange = this._register(new Emitter<URI>());
109
private readonly _kernelSources = new Map<string, IKernelInfoCache>();
110
private readonly _kernelSourceActionsUpdates = new Map<string, IDisposable>();
111
private readonly _kernelDetectionTasks = new Map<string, INotebookKernelDetectionTask[]>();
112
private readonly _onDidChangeKernelDetectionTasks = this._register(new Emitter<string>());
113
private readonly _kernelSourceActionProviders = new Map<string, IKernelSourceActionProvider[]>();
114
115
readonly onDidChangeSelectedNotebooks: Event<ISelectedNotebooksChangeEvent> = this._onDidChangeNotebookKernelBinding.event;
116
readonly onDidAddKernel: Event<INotebookKernel> = this._onDidAddKernel.event;
117
readonly onDidRemoveKernel: Event<INotebookKernel> = this._onDidRemoveKernel.event;
118
readonly onDidChangeNotebookAffinity: Event<void> = this._onDidChangeNotebookAffinity.event;
119
readonly onDidChangeSourceActions: Event<INotebookSourceActionChangeEvent> = this._onDidChangeSourceActions.event;
120
readonly onDidChangeKernelDetectionTasks: Event<string> = this._onDidChangeKernelDetectionTasks.event;
121
readonly onDidNotebookVariablesUpdate: Event<URI> = this._onDidNotebookVariablesChange.event;
122
123
private static _storageNotebookBinding = 'notebook.controller2NotebookBindings';
124
125
126
constructor(
127
@INotebookService private readonly _notebookService: INotebookService,
128
@IStorageService private readonly _storageService: IStorageService,
129
@IMenuService private readonly _menuService: IMenuService,
130
@IContextKeyService private readonly _contextKeyService: IContextKeyService
131
) {
132
super();
133
134
// auto associate kernels to new notebook documents, also emit event when
135
// a notebook has been closed (but don't update the memento)
136
this._register(_notebookService.onDidAddNotebookDocument(this._tryAutoBindNotebook, this));
137
this._register(_notebookService.onWillRemoveNotebookDocument(notebook => {
138
const id = NotebookTextModelLikeId.str(notebook);
139
const kernelId = this._notebookBindings.get(id);
140
if (kernelId && notebook.uri.scheme === Schemas.untitled) {
141
this.selectKernelForNotebook(undefined, notebook);
142
}
143
this._kernelSourceActionsUpdates.get(id)?.dispose();
144
this._kernelSourceActionsUpdates.delete(id);
145
}));
146
147
// restore from storage
148
try {
149
const data = JSON.parse(this._storageService.get(NotebookKernelService._storageNotebookBinding, StorageScope.WORKSPACE, '[]'));
150
this._notebookBindings.fromJSON(data);
151
} catch {
152
// ignore
153
}
154
}
155
156
override dispose() {
157
this._kernels.clear();
158
this._kernelSources.forEach(v => {
159
v.menu.dispose();
160
v.actions.forEach(a => a[1].dispose());
161
});
162
this._kernelSourceActionsUpdates.forEach(v => {
163
v.dispose();
164
});
165
this._kernelSourceActionsUpdates.clear();
166
super.dispose();
167
}
168
169
private _persistSoonHandle?: IDisposable;
170
171
private _persistMementos(): void {
172
this._persistSoonHandle?.dispose();
173
this._persistSoonHandle = runWhenWindowIdle(getActiveWindow(), () => {
174
this._storageService.store(NotebookKernelService._storageNotebookBinding, JSON.stringify(this._notebookBindings), StorageScope.WORKSPACE, StorageTarget.MACHINE);
175
}, 100);
176
}
177
178
private static _score(kernel: INotebookKernel, notebook: INotebookTextModelLike): number {
179
if (kernel.viewType === '*') {
180
return 5;
181
} else if (kernel.viewType === notebook.notebookType) {
182
return 10;
183
} else {
184
return 0;
185
}
186
}
187
188
private _tryAutoBindNotebook(notebook: INotebookTextModel, onlyThisKernel?: INotebookKernel): void {
189
190
const id = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook));
191
if (!id) {
192
// no kernel associated
193
return;
194
}
195
const existingKernel = this._kernels.get(id);
196
if (!existingKernel || !NotebookKernelService._score(existingKernel.kernel, notebook)) {
197
// associated kernel not known, not matching
198
return;
199
}
200
if (!onlyThisKernel || existingKernel.kernel === onlyThisKernel) {
201
this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel: undefined, newKernel: existingKernel.kernel.id });
202
}
203
}
204
205
notifyVariablesChange(notebookUri: URI): void {
206
this._onDidNotebookVariablesChange.fire(notebookUri);
207
}
208
209
registerKernel(kernel: INotebookKernel): IDisposable {
210
if (this._kernels.has(kernel.id)) {
211
throw new Error(`NOTEBOOK CONTROLLER with id '${kernel.id}' already exists`);
212
}
213
214
this._kernels.set(kernel.id, new KernelInfo(kernel));
215
this._onDidAddKernel.fire(kernel);
216
217
// auto associate the new kernel to existing notebooks it was
218
// associated to in the past.
219
for (const notebook of this._notebookService.getNotebookTextModels()) {
220
this._tryAutoBindNotebook(notebook, kernel);
221
}
222
223
return toDisposable(() => {
224
if (this._kernels.delete(kernel.id)) {
225
this._onDidRemoveKernel.fire(kernel);
226
}
227
for (const [key, candidate] of Array.from(this._notebookBindings)) {
228
if (candidate === kernel.id) {
229
this._onDidChangeNotebookKernelBinding.fire({ notebook: NotebookTextModelLikeId.obj(key).uri, oldKernel: kernel.id, newKernel: undefined });
230
}
231
}
232
});
233
}
234
235
getMatchingKernel(notebook: INotebookTextModelLike): INotebookKernelMatchResult {
236
237
// all applicable kernels
238
const kernels: { kernel: INotebookKernel; instanceAffinity: number; score: number }[] = [];
239
for (const info of this._kernels.values()) {
240
const score = NotebookKernelService._score(info.kernel, notebook);
241
if (score) {
242
kernels.push({
243
score,
244
kernel: info.kernel,
245
instanceAffinity: info.notebookPriorities.get(notebook.uri) ?? 1 /* vscode.NotebookControllerPriority.Default */,
246
});
247
}
248
}
249
250
kernels
251
.sort((a, b) => b.instanceAffinity - a.instanceAffinity || a.score - b.score || a.kernel.label.localeCompare(b.kernel.label));
252
const all = kernels.map(obj => obj.kernel);
253
254
// bound kernel
255
const selectedId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook));
256
const selected = selectedId ? this._kernels.get(selectedId)?.kernel : undefined;
257
const suggestions = kernels.filter(item => item.instanceAffinity > 1).map(item => item.kernel);
258
const hidden = kernels.filter(item => item.instanceAffinity < 0).map(item => item.kernel);
259
return { all, selected, suggestions, hidden };
260
}
261
262
getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined {
263
const info = this.getMatchingKernel(notebook);
264
if (info.selected) {
265
return info.selected;
266
}
267
268
const preferred = info.all.filter(kernel => this._kernels.get(kernel.id)?.notebookPriorities.get(notebook.uri) === 2 /* vscode.NotebookControllerPriority.Preferred */);
269
if (preferred.length === 1) {
270
return preferred[0];
271
}
272
273
return info.all.length === 1 ? info.all[0] : undefined;
274
}
275
276
// a notebook has one kernel, a kernel has N notebooks
277
// notebook <-1----N-> kernel
278
selectKernelForNotebook(kernel: INotebookKernel | undefined, notebook: INotebookTextModelLike): void {
279
const key = NotebookTextModelLikeId.str(notebook);
280
const oldKernel = this._notebookBindings.get(key);
281
if (oldKernel !== kernel?.id) {
282
if (kernel) {
283
this._notebookBindings.set(key, kernel.id);
284
} else {
285
this._notebookBindings.delete(key);
286
}
287
this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel, newKernel: kernel?.id });
288
this._persistMementos();
289
}
290
}
291
292
preselectKernelForNotebook(kernel: INotebookKernel, notebook: INotebookTextModelLike): void {
293
const key = NotebookTextModelLikeId.str(notebook);
294
const oldKernel = this._notebookBindings.get(key);
295
if (oldKernel !== kernel?.id) {
296
this._notebookBindings.set(key, kernel.id);
297
this._persistMementos();
298
}
299
}
300
301
updateKernelNotebookAffinity(kernel: INotebookKernel, notebook: URI, preference: number | undefined): void {
302
const info = this._kernels.get(kernel.id);
303
if (!info) {
304
throw new Error(`UNKNOWN kernel '${kernel.id}'`);
305
}
306
if (preference === undefined) {
307
info.notebookPriorities.delete(notebook);
308
} else {
309
info.notebookPriorities.set(notebook, preference);
310
}
311
this._onDidChangeNotebookAffinity.fire();
312
}
313
314
getRunningSourceActions(notebook: INotebookTextModelLike) {
315
const id = NotebookTextModelLikeId.str(notebook);
316
const existingInfo = this._kernelSources.get(id);
317
if (existingInfo) {
318
return existingInfo.actions.filter(action => action[0].execution).map(action => action[0]);
319
}
320
321
return [];
322
}
323
324
getSourceActions(notebook: INotebookTextModelLike, contextKeyService: IContextKeyService | undefined): ISourceAction[] {
325
contextKeyService = contextKeyService ?? this._contextKeyService;
326
const id = NotebookTextModelLikeId.str(notebook);
327
const existingInfo = this._kernelSources.get(id);
328
329
if (existingInfo) {
330
return existingInfo.actions.map(a => a[0]);
331
}
332
333
const sourceMenu = this._register(this._menuService.createMenu(MenuId.NotebookKernelSource, contextKeyService));
334
const info: IKernelInfoCache = { menu: sourceMenu, actions: [] };
335
336
const loadActionsFromMenu = (menu: IMenu, document: INotebookTextModelLike) => {
337
const groups = menu.getActions({ shouldForwardArgs: true });
338
const sourceActions: [ISourceAction, IDisposable][] = [];
339
groups.forEach(group => {
340
const isPrimary = /^primary/.test(group[0]);
341
group[1].forEach(action => {
342
const sourceAction = new SourceAction(action, document, isPrimary);
343
const stateChangeListener = sourceAction.onDidChangeState(() => {
344
this._onDidChangeSourceActions.fire({
345
notebook: document.uri,
346
viewType: document.notebookType,
347
});
348
});
349
sourceActions.push([sourceAction, stateChangeListener]);
350
});
351
});
352
info.actions = sourceActions;
353
this._kernelSources.set(id, info);
354
this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.notebookType });
355
};
356
357
this._kernelSourceActionsUpdates.get(id)?.dispose();
358
this._kernelSourceActionsUpdates.set(id, sourceMenu.onDidChange(() => {
359
loadActionsFromMenu(sourceMenu, notebook);
360
}));
361
362
loadActionsFromMenu(sourceMenu, notebook);
363
364
return info.actions.map(a => a[0]);
365
}
366
367
registerNotebookKernelDetectionTask(task: INotebookKernelDetectionTask): IDisposable {
368
const notebookType = task.notebookType;
369
const all = this._kernelDetectionTasks.get(notebookType) ?? [];
370
all.push(task);
371
this._kernelDetectionTasks.set(notebookType, all);
372
this._onDidChangeKernelDetectionTasks.fire(notebookType);
373
return toDisposable(() => {
374
const all = this._kernelDetectionTasks.get(notebookType) ?? [];
375
const idx = all.indexOf(task);
376
if (idx >= 0) {
377
all.splice(idx, 1);
378
this._kernelDetectionTasks.set(notebookType, all);
379
this._onDidChangeKernelDetectionTasks.fire(notebookType);
380
}
381
});
382
}
383
384
getKernelDetectionTasks(notebook: INotebookTextModelLike): INotebookKernelDetectionTask[] {
385
return this._kernelDetectionTasks.get(notebook.notebookType) ?? [];
386
}
387
388
registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable {
389
const providers = this._kernelSourceActionProviders.get(viewType) ?? [];
390
providers.push(provider);
391
this._kernelSourceActionProviders.set(viewType, providers);
392
this._onDidChangeSourceActions.fire({ viewType: viewType });
393
394
const eventEmitterDisposable = provider.onDidChangeSourceActions?.(() => {
395
this._onDidChangeSourceActions.fire({ viewType: viewType });
396
});
397
398
return toDisposable(() => {
399
const providers = this._kernelSourceActionProviders.get(viewType) ?? [];
400
const idx = providers.indexOf(provider);
401
if (idx >= 0) {
402
providers.splice(idx, 1);
403
this._kernelSourceActionProviders.set(viewType, providers);
404
}
405
406
eventEmitterDisposable?.dispose();
407
});
408
}
409
410
/**
411
* Get kernel source actions from providers
412
*/
413
getKernelSourceActions2(notebook: INotebookTextModelLike): Promise<INotebookKernelSourceAction[]> {
414
const viewType = notebook.notebookType;
415
const providers = this._kernelSourceActionProviders.get(viewType) ?? [];
416
const promises = providers.map(provider => provider.provideKernelSourceActions());
417
return Promise.all(promises).then(actions => {
418
return actions.reduce((a, b) => a.concat(b), []);
419
});
420
}
421
}
422
423