notebook.contribution.ts 15.9 KB
Newer Older
P
Peng Lyu 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
R
rebornix 已提交
7 8
import { ResourceMap } from 'vs/base/common/map';
import { parse } from 'vs/base/common/marshalling';
R
rebornix 已提交
9
import { basename, isEqual } from 'vs/base/common/resources';
R
rebornix 已提交
10 11
import { assertType } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
12
import { ITextModel, ITextBufferFactory, DefaultEndOfLine, ITextBuffer } from 'vs/editor/common/model';
R
rebornix 已提交
13 14 15
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
R
rebornix 已提交
16
import * as nls from 'vs/nls';
R
rebornix 已提交
17
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
18
import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
P
Peng Lyu 已提交
19
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
20 21 22
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
P
Peng Lyu 已提交
23 24
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
25
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
R
rebornix 已提交
26
import { EditorInput, Extensions as EditorInputExtensions, IEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
R
rebornix 已提交
27
import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor';
28
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
29 30
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl';
R
rebornix 已提交
31 32
import { CellKind, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
33 34
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService';
R
rebornix 已提交
35 36 37 38
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorAssociationsSetting';
import { coalesce, distinct } from 'vs/base/common/arrays';
import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/customEditor';
R
rebornix 已提交
39

R
rebornix 已提交
40 41
// Editor Contribution

R
rebornix 已提交
42 43
import 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
import 'vs/workbench/contrib/notebook/browser/contrib/find/findController';
44 45
import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding';
import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting';
46
import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider';
47
import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider';
R
rebornix 已提交
48
import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus';
R
rebornix 已提交
49

R
rebornix 已提交
50 51
// Output renderers registration

R
rebornix 已提交
52 53 54
import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform';
import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform';
import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform';
R
rebornix 已提交
55
import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
56
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
R
rebornix 已提交
57

R
rebornix 已提交
58
/*--------------------------------------------------------------------------------------------- */
P
Peng Lyu 已提交
59 60 61 62 63 64 65 66 67 68 69 70

Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
	EditorDescriptor.create(
		NotebookEditor,
		NotebookEditor.ID,
		'Notebook Editor'
	),
	[
		new SyncDescriptor(NotebookEditorInput)
	]
);

71 72 73 74 75 76 77 78 79 80 81 82
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
	NotebookEditorInput.ID,
	class implements IEditorInputFactory {
		canSerialize(): boolean {
			return true;
		}
		serialize(input: EditorInput): string {
			assertType(input instanceof NotebookEditorInput);
			return JSON.stringify({
				resource: input.resource,
				name: input.name,
				viewType: input.viewType,
83
				group: input.group
84 85 86
			});
		}
		deserialize(instantiationService: IInstantiationService, raw: string) {
87
			type Data = { resource: URI, name: string, viewType: string, group: number };
88 89 90 91 92 93 94 95
			const data = <Data>parse(raw);
			if (!data) {
				return undefined;
			}
			const { resource, name, viewType } = data;
			if (!data || !URI.isUri(resource) || typeof name !== 'string' || typeof viewType !== 'string') {
				return undefined;
			}
96

97 98 99
			// if we have two editors open with the same resource (in different editor groups), we should then create two different
			// editor inputs, instead of `getOrCreate`.
			const input = NotebookEditorInput.create(instantiationService, resource, name, viewType);
100 101 102 103 104
			if (typeof data.group === 'number') {
				input.updateGroup(data.group);
			}

			return input;
105 106 107 108
		}
	}
);

J
Johannes Rieken 已提交
109 110 111 112
function getFirstNotebookInfo(notebookService: INotebookService, uri: URI): NotebookProviderInfo | undefined {
	return notebookService.getContributedNotebookProviders(uri)[0];
}

113
export class NotebookContribution extends Disposable implements IWorkbenchContribution {
114
	private _resourceMapping = new ResourceMap<NotebookEditorInput>();
P
Peng Lyu 已提交
115 116

	constructor(
117
		@IEditorService private readonly editorService: EditorServiceImpl,
R
rebornix 已提交
118
		@INotebookService private readonly notebookService: INotebookService,
R
rebornix 已提交
119 120
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IConfigurationService private readonly configurationService: IConfigurationService
121

P
Peng Lyu 已提交
122
	) {
123 124 125
		super();

		this._register(this.editorService.overrideOpenEditor({
126 127
			getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => {
				const currentEditorForResource = group?.editors.find(editor => isEqual(editor.resource, resource));
R
rebornix 已提交
128

R
rebornix 已提交
129 130 131 132
				const associatedEditors = distinct([
					...this.getUserAssociatedNotebookEditors(resource),
					...this.getContributedEditors(resource)
				], editor => editor.id);
R
rebornix 已提交
133

R
rebornix 已提交
134
				return associatedEditors.map(info => {
R
rebornix 已提交
135 136 137
					return {
						label: info.displayName,
						id: info.id,
138
						active: currentEditorForResource instanceof NotebookEditorInput && currentEditorForResource.viewType === info.id,
R
rebornix 已提交
139 140 141 142 143
						detail: info.providerDisplayName
					};
				});
			},
			open: (editor, options, group, id) => this.onEditorOpening(editor, options, group, id)
144
		}));
R
rebornix 已提交
145

146
		this._register(this.editorService.onDidActiveEditorChange(() => {
R
rebornix 已提交
147 148 149 150
			const activeEditorPane = editorService.activeEditorPane as any | undefined;
			const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined;
			if (notebookEditor) {
				this.notebookService.updateActiveNotebookEditor(notebookEditor);
R
rebornix 已提交
151
			}
152 153 154 155 156 157 158 159 160 161 162
		}));

		this._register(this.editorService.onDidCloseEditor(({ editor }) => {
			if (!(editor instanceof NotebookEditorInput)) {
				return;
			}

			if (!this.editorService.editors.some(other => other === editor)) {
				editor.dispose();
			}
		}));
P
Peng Lyu 已提交
163 164
	}

R
rebornix 已提交
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
	getUserAssociatedEditors(resource: URI) {
		const rawAssociations = this.configurationService.getValue<CustomEditorsAssociations>(customEditorsAssociationsSettingId) || [];

		return coalesce(rawAssociations
			.filter(association => CustomEditorInfo.selectorMatches(association, resource)));
	}

	getUserAssociatedNotebookEditors(resource: URI) {
		const rawAssociations = this.configurationService.getValue<CustomEditorsAssociations>(customEditorsAssociationsSettingId) || [];

		return coalesce(rawAssociations
			.filter(association => CustomEditorInfo.selectorMatches(association, resource))
			.map(association => this.notebookService.getContributedNotebookProvider(association.viewType)));
	}

	getContributedEditors(resource: URI) {
		return this.notebookService.getContributedNotebookProviders(resource);
	}

R
rebornix 已提交
184
	private onEditorOpening(originalInput: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, id: string | undefined): IOpenEditorOverride | undefined {
185 186 187 188
		if (originalInput instanceof NotebookEditorInput) {
			if ((originalInput.group === group.id || originalInput.group === undefined) && (originalInput.viewType === id || typeof id !== 'string')) {
				// No need to do anything
				originalInput.updateGroup(group.id);
189 190 191
				return {
					override: this.editorService.openEditor(originalInput, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group)
				};
192 193 194 195 196 197
			} else {
				// Create a copy of the input.
				// Unlike normal editor inputs, we do not want to share custom editor inputs
				// between multiple editors / groups.
				const copiedInput = this.instantiationService.createInstance(NotebookEditorInput, originalInput.resource, originalInput.name, originalInput.viewType);
				copiedInput.updateGroup(group.id);
198 199 200 201 202 203 204 205

				// transfer ownership of editor widget
				// const widgetRef = NotebookRegistry.getNotebookEditorWidget(originalInput);
				// if (widgetRef) {
				// 	NotebookRegistry.releaseNotebookEditorWidget(originalInput);
				// 	NotebookRegistry.claimNotebookEditorWidget(copiedInput, widgetRef);
				// }

206 207 208 209 210 211
				return {
					override: this.editorService.openEditor(copiedInput, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group)
				};
			}
		}

212 213 214 215
		let resource = originalInput.resource;
		if (!resource) {
			return undefined;
		}
216

R
rebornix 已提交
217
		if (id === undefined) {
R
rebornix 已提交
218
			const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, resource) && !(editor instanceof NotebookEditorInput));
R
rebornix 已提交
219 220

			if (existingEditors.length) {
R
rebornix 已提交
221
				return undefined;
R
rebornix 已提交
222 223
			}

R
rebornix 已提交
224 225 226 227 228 229 230
			const userAssociatedEditors = this.getUserAssociatedEditors(resource);
			const notebookEditor = userAssociatedEditors.filter(association => this.notebookService.getContributedNotebookProvider(association.viewType));

			if (userAssociatedEditors.length && !notebookEditor.length) {
				// user pick a non-notebook editor for this resource
				return undefined;
			}
231 232 233 234 235 236 237
		} else {
			const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, resource) && (editor instanceof NotebookEditorInput) && editor.viewType === id);

			if (existingEditors.length) {
				// switch to this cell
				return { override: this.editorService.openEditor(existingEditors[0], new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) };
			}
R
rebornix 已提交
238 239
		}

240 241 242 243
		if (this._resourceMapping.has(resource)) {
			const input = this._resourceMapping.get(resource);

			if (!input!.isDisposed()) {
244
				input?.updateGroup(group.id);
245
				return { override: this.editorService.openEditor(input!, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) };
246 247
			} else {
				this._resourceMapping.delete(resource);
248 249 250
			}
		}

J
Johannes Rieken 已提交
251 252
		let info: NotebookProviderInfo | undefined;
		const data = CellUri.parse(resource);
R
rebornix 已提交
253
		if (data) {
R
rebornix 已提交
254
			const infos = this.getContributedEditors(data.notebook);
R
rebornix 已提交
255 256 257 258 259

			if (infos.length) {
				const info = id === undefined ? infos[0] : (infos.find(info => info.id === id) || infos[0]);
				// cell-uri -> open (container) notebook
				const name = basename(data.notebook);
260 261
				let input = this._resourceMapping.get(data.notebook);
				if (!input || input.isDisposed()) {
262
					input = NotebookEditorInput.create(this.instantiationService, data.notebook, name, info.id);
263 264
					this._resourceMapping.set(data.notebook, input);
				}
265 266

				input.updateGroup(group.id);
R
rebornix 已提交
267 268
				return { override: this.editorService.openEditor(input, new NotebookEditorOptions({ ...options, forceReload: true, cellOptions: { resource, options } }), group) };
			}
269 270
		}

R
rebornix 已提交
271 272 273
		const infos = this.notebookService.getContributedNotebookProviders(resource);
		info = id === undefined ? infos[0] : infos.find(info => info.id === id);

J
Johannes Rieken 已提交
274
		if (!info) {
R
rebornix 已提交
275
			return undefined;
R
rebornix 已提交
276 277
		}

278
		const input = NotebookEditorInput.create(this.instantiationService, resource, originalInput.getName(), info.id);
279
		input.updateGroup(group.id);
280
		this._resourceMapping.set(resource, input);
281

282 283 284 285 286 287 288 289 290 291
		/**
		 * Scenario: we are reopening a file editor input which is pinned, we should open in a new editor tab.
		 */
		let index = undefined;
		if (group.activeEditor === originalInput && isEqual(originalInput.resource, resource)) {
			const originalEditorIndex = group.getIndexOfEditor(originalInput);
			index = group.isPinned(originalInput) ? originalEditorIndex + 1 : originalEditorIndex;
		}

		return { override: this.editorService.openEditor(input, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true, index }), group) };
P
Peng Lyu 已提交
292 293 294
	}
}

295 296 297 298 299 300 301 302 303 304
class CellContentProvider implements ITextModelContentProvider {

	private readonly _registration: IDisposable;

	constructor(
		@ITextModelService textModelService: ITextModelService,
		@IModelService private readonly _modelService: IModelService,
		@IModeService private readonly _modeService: IModeService,
		@INotebookService private readonly _notebookService: INotebookService,
	) {
J
Johannes Rieken 已提交
305
		this._registration = textModelService.registerTextModelContentProvider(CellUri.scheme, this);
306 307 308 309 310 311 312
	}

	dispose(): void {
		this._registration.dispose();
	}

	async provideTextContent(resource: URI): Promise<ITextModel | null> {
J
Johannes Rieken 已提交
313 314 315 316
		const existing = this._modelService.getModel(resource);
		if (existing) {
			return existing;
		}
J
Johannes Rieken 已提交
317 318
		const data = CellUri.parse(resource);
		// const data = parseCellUri(resource);
319 320 321
		if (!data) {
			return null;
		}
J
Johannes Rieken 已提交
322 323 324 325
		const info = getFirstNotebookInfo(this._notebookService, data.notebook);
		if (!info) {
			return null;
		}
326

J
Johannes Rieken 已提交
327
		const editorModel = this._notebookService.modelManager.get(data.notebook);
328
		if (!editorModel) {
329 330
			return null;
		}
331 332

		for (let cell of editorModel.notebook.cells) {
333
			if (cell.uri.toString() === resource.toString()) {
334 335 336 337 338 339 340 341 342 343 344
				const bufferFactory: ITextBufferFactory = {
					create: (defaultEOL) => {
						const newEOL = (defaultEOL === DefaultEndOfLine.CRLF ? '\r\n' : '\n');
						(cell.textBuffer as ITextBuffer).setEOL(newEOL);
						return cell.textBuffer as ITextBuffer;
					},
					getFirstLineText: (limit: number) => {
						return cell.textBuffer.getLineContent(1).substr(0, limit);
					}
				};
				const language = cell.cellKind === CellKind.Markdown ? this._modeService.create('markdown') : (cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.textBuffer.getLineContent(1)));
345
				return this._modelService.createModel(
R
rebornix 已提交
346
					bufferFactory,
347
					language,
348 349 350 351 352 353 354 355 356
					resource
				);
			}
		}

		return null;
	}
}

357 358
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting);
359
workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting);
R
rebornix 已提交
360 361

registerSingleton(INotebookService, NotebookService);
R
rebornix 已提交
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379

const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration({
	id: 'notebook',
	order: 100,
	title: nls.localize('notebookConfigurationTitle', "Notebook"),
	type: 'object',
	properties: {
		'notebook.displayOrder': {
			markdownDescription: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"),
			type: ['array'],
			items: {
				type: 'string'
			},
			default: []
		}
	}
});