Skip to content

Commit 80cf8af

Browse files
authored
Implement base custom editor support (#9812)
* Setup usage of the new API * Partial ideas * Idea for splitting * Idea in place * Get all of the tests to build * Enable proposed api * Still not working but setting more options. * Get web views to load * Fix save and save as to use VS code commands * Fix unit tests * Fix a number of functional tests * REmove autoSave tests as not needed anymore * Add news entry * Review comments * Code review comments * Update PR validation list * Fix nyc compiler problems. * Try fixing the toggle markdown test
1 parent d5f153e commit 80cf8af

35 files changed

+1783
-2103
lines changed

.vscode/launch.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"request": "launch",
99
"runtimeExecutable": "${execPath}",
1010
"args": [
11-
"--extensionDevelopmentPath=${workspaceFolder}"
11+
"--extensionDevelopmentPath=${workspaceFolder}",
12+
"--enable-proposed-api",
13+
"ms-python.python"
1214
],
1315
"stopOnEntry": false,
1416
"smartStep": true,
@@ -19,7 +21,7 @@
1921
"preLaunchTask": "Compile",
2022
"skipFiles": [
2123
"<node_internals>/**"
22-
]
24+
],
2325
},
2426
{
2527
"name": "Extension inside container",

build/ci/vscode-python-pr-validation.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name: '$(Year:yyyy).$(Month).0.$(BuildID)-pr'
66
pr:
77
autoCancel: true
88
branches:
9-
include: ["master", "release*"]
9+
include: ["master", "release*", "ds*"]
1010
paths:
1111
exclude: ["/news/1 Enhancements", "/news/2 Fixes", "/news/3 Code Health", "/.vscode"]
1212

news/1 Enhancements/9255.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement VS code's custom editor for opening notebooks.

package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"version": "2020.2.0-dev",
66
"languageServerVersion": "0.4.114",
77
"publisher": "ms-python",
8+
"enableProposedApi": true,
89
"author": {
910
"name": "Microsoft Corporation"
1011
},
@@ -82,7 +83,8 @@
8283
"onCommand:python.datascience.exportfileasnotebook",
8384
"onCommand:python.datascience.exportfileandoutputasnotebook",
8485
"onCommand:python.datascience.selectJupyterInterpreter",
85-
"onCommand:python.enableSourceMapSupport"
86+
"onCommand:python.enableSourceMapSupport",
87+
"onWebviewEditor:NativeEditorProvider.ipynb"
8688
],
8789
"main": "./out/client/extension",
8890
"contributes": {
@@ -2767,7 +2769,18 @@
27672769
"when": "testsDiscovered"
27682770
}
27692771
]
2770-
}
2772+
},
2773+
"webviewEditors": [
2774+
{
2775+
"viewType": "NativeEditorProvider.ipynb",
2776+
"displayName": "Jupyter Notebook",
2777+
"selector": [
2778+
{
2779+
"filenamePattern": "*.ipynb"
2780+
}
2781+
]
2782+
}
2783+
]
27712784
},
27722785
"scripts": {
27732786
"package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix",

src/client/common/application/commands.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import { CancellationToken, Position, TextDocument, Uri } from 'vscode';
77
import { Commands as LSCommands } from '../../activation/languageServer/constants';
88
import { Commands as DSCommands } from '../../datascience/constants';
9-
import { INotebook } from '../../datascience/types';
9+
import { IEditCell, IInsertCell, ISwapCells } from '../../datascience/interactive-common/interactiveWindowTypes';
10+
import { LiveKernelModel } from '../../datascience/jupyter/kernels/types';
11+
import { ICell, IJupyterKernelSpec, INotebook } from '../../datascience/types';
12+
import { PythonInterpreter } from '../../interpreter/contracts';
1013
import { CommandSource } from '../../testing/common/constants';
1114
import { TestFunction, TestsToRun } from '../../testing/common/types';
1215
import { TestDataItem, TestWorkspaceFolder } from '../../testing/types';
@@ -87,6 +90,9 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
8790
['python._loadLanguageServerExtension']: {}[];
8891
['python.SelectAndInsertDebugConfiguration']: [TextDocument, Position, CancellationToken];
8992
['python.viewLanguageServerOutput']: [];
93+
['vscode.open']: [Uri];
94+
['workbench.action.files.saveAs']: [Uri];
95+
['workbench.action.files.save']: [Uri];
9096
[Commands.Build_Workspace_Symbols]: [boolean, CancellationToken];
9197
[Commands.Sort_Imports]: [undefined, Uri];
9298
[Commands.Exec_In_Terminal]: [undefined, Uri];
@@ -142,4 +148,12 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
142148
[DSCommands.ScrollToCell]: [string, string];
143149
[DSCommands.ViewJupyterOutput]: [];
144150
[DSCommands.SwitchJupyterKernel]: [INotebook | undefined];
151+
[DSCommands.NotebookStorage_DeleteAllCells]: [Uri];
152+
[DSCommands.NotebookStorage_ModifyCells]: [Uri, ICell[]];
153+
[DSCommands.NotebookStorage_EditCell]: [Uri, IEditCell];
154+
[DSCommands.NotebookStorage_InsertCell]: [Uri, IInsertCell];
155+
[DSCommands.NotebookStorage_RemoveCell]: [Uri, string];
156+
[DSCommands.NotebookStorage_SwapCells]: [Uri, ISwapCells];
157+
[DSCommands.NotebookStorage_ClearCellOutputs]: [Uri];
158+
[DSCommands.NotebookStorage_UpdateVersion]: [Uri, PythonInterpreter | undefined, IJupyterKernelSpec | LiveKernelModel | undefined];
145159
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
'use strict';
4+
import { inject, injectable } from 'inversify';
5+
import * as vscode from 'vscode';
6+
7+
import { ICommandManager, ICustomEditorService, WebviewCustomEditorProvider } from './types';
8+
9+
@injectable()
10+
export class CustomEditorService implements ICustomEditorService {
11+
constructor(@inject(ICommandManager) private commandManager: ICommandManager) {}
12+
13+
public registerWebviewCustomEditorProvider(viewType: string, provider: WebviewCustomEditorProvider, options?: vscode.WebviewPanelOptions): vscode.Disposable {
14+
// tslint:disable-next-line: no-any
15+
return (vscode.window as any).registerWebviewCustomEditorProvider(viewType, provider, options);
16+
}
17+
18+
public async openEditor(file: vscode.Uri): Promise<void> {
19+
await this.commandManager.executeCommand('vscode.open', file);
20+
}
21+
}

src/client/common/application/types.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import {
4949
TreeViewOptions,
5050
Uri,
5151
ViewColumn,
52+
WebviewPanel,
53+
WebviewPanelOptions,
5254
WindowState,
5355
WorkspaceConfiguration,
5456
WorkspaceEdit,
@@ -983,6 +985,8 @@ export interface IWebPanelOptions {
983985
cwd: string;
984986
// tslint:disable-next-line: no-any
985987
settings?: any;
988+
// Web panel to use if supplied by VS code instead
989+
webViewPanel?: WebviewPanel;
986990
}
987991

988992
// Wraps the VS Code api for creating a web panel
@@ -1042,3 +1046,102 @@ export const IActiveResourceService = Symbol('IActiveResourceService');
10421046
export interface IActiveResourceService {
10431047
getActiveResource(): Resource;
10441048
}
1049+
1050+
// Temporary hack to get the nyc compiler to find these types. vscode.proposed.d.ts doesn't work for some reason.
1051+
/**
1052+
* Defines the editing functionality of a webview editor. This allows the webview editor to hook into standard
1053+
* editor events such as `undo` or `save`.
1054+
*
1055+
* @param EditType Type of edits. Edit objects must be json serializable.
1056+
*/
1057+
// tslint:disable-next-line: interface-name
1058+
export interface WebviewCustomEditorEditingDelegate<EditType> {
1059+
/**
1060+
* Event triggered by extensions to signal to VS Code that an edit has occurred.
1061+
*/
1062+
readonly onEdit: Event<{ readonly resource: Uri; readonly edit: EditType }>;
1063+
/**
1064+
* Save a resource.
1065+
*
1066+
* @param resource Resource being saved.
1067+
*
1068+
* @return Thenable signaling that the save has completed.
1069+
*/
1070+
save(resource: Uri): Thenable<void>;
1071+
1072+
/**
1073+
* Save an existing resource at a new path.
1074+
*
1075+
* @param resource Resource being saved.
1076+
* @param targetResource Location to save to.
1077+
*
1078+
* @return Thenable signaling that the save has completed.
1079+
*/
1080+
saveAs(resource: Uri, targetResource: Uri): Thenable<void>;
1081+
1082+
/**
1083+
* Apply a set of edits.
1084+
*
1085+
* Note that is not invoked when `onEdit` is called as `onEdit` implies also updating the view to reflect the edit.
1086+
*
1087+
* @param resource Resource being edited.
1088+
* @param edit Array of edits. Sorted from oldest to most recent.
1089+
*
1090+
* @return Thenable signaling that the change has completed.
1091+
*/
1092+
applyEdits(resource: Uri, edits: readonly EditType[]): Thenable<void>;
1093+
1094+
/**
1095+
* Undo a set of edits.
1096+
*
1097+
* This is triggered when a user undoes an edit or when revert is called on a file.
1098+
*
1099+
* @param resource Resource being edited.
1100+
* @param edit Array of edits. Sorted from most recent to oldest.
1101+
*
1102+
* @return Thenable signaling that the change has completed.
1103+
*/
1104+
undoEdits(resource: Uri, edits: readonly EditType[]): Thenable<void>;
1105+
}
1106+
1107+
// tslint:disable-next-line: interface-name
1108+
export interface WebviewCustomEditorProvider {
1109+
/**
1110+
* Controls the editing functionality of a webview editor. This allows the webview editor to hook into standard
1111+
* editor events such as `undo` or `save`.
1112+
*
1113+
* WebviewEditors that do not have `editingCapability` are considered to be readonly. Users can still interact
1114+
* with readonly editors, but these editors will not integrate with VS Code's standard editor functionality.
1115+
*/
1116+
readonly editingDelegate?: WebviewCustomEditorEditingDelegate<unknown>;
1117+
/**
1118+
* Resolve a webview editor for a given resource.
1119+
*
1120+
* To resolve a webview editor, a provider must fill in its initial html content and hook up all
1121+
* the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`.
1122+
*
1123+
* @param resource Resource being resolved.
1124+
* @param webview Webview being resolved. The provider should take ownership of this webview.
1125+
*
1126+
* @return Thenable indicating that the webview editor has been resolved.
1127+
*/
1128+
resolveWebviewEditor(resource: Uri, webview: WebviewPanel): Thenable<void>;
1129+
}
1130+
1131+
export const ICustomEditorService = Symbol('ICustomEditorService');
1132+
export interface ICustomEditorService {
1133+
/**
1134+
* Register a new provider for webview editors of a given type.
1135+
*
1136+
* @param viewType Type of the webview editor provider.
1137+
* @param provider Resolves webview editors.
1138+
* @param options Content settings for a webview panels the provider is given.
1139+
*
1140+
* @return Disposable that unregisters the `WebviewCustomEditorProvider`.
1141+
*/
1142+
registerWebviewCustomEditorProvider(viewType: string, provider: WebviewCustomEditorProvider, options?: WebviewPanelOptions): Disposable;
1143+
/**
1144+
* Opens a file with a custom editor
1145+
*/
1146+
openEditor(file: Uri): Promise<void>;
1147+
}

src/client/common/application/webPanels/webPanel.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import '../../extensions';
55

66
import * as uuid from 'uuid/v4';
7-
import { Uri, Webview, WebviewPanel, window } from 'vscode';
7+
import { Uri, Webview, WebviewOptions, WebviewPanel, window } from 'vscode';
88

99
import { Identifiers } from '../../../datascience/constants';
1010
import { InteractiveWindowMessages } from '../../../datascience/interactive-common/interactiveWindowTypes';
@@ -31,18 +31,26 @@ export class WebPanel implements IWebPanel {
3131
private token: string | undefined,
3232
private options: IWebPanelOptions
3333
) {
34-
this.panel = window.createWebviewPanel(
35-
options.title.toLowerCase().replace(' ', ''),
36-
options.title,
37-
{ viewColumn: options.viewColumn, preserveFocus: true },
38-
{
39-
enableScripts: true,
40-
retainContextWhenHidden: true,
41-
localResourceRoots: [Uri.file(this.options.rootPath), Uri.file(this.options.cwd)],
42-
enableFindWidget: true,
43-
portMapping: port ? [{ webviewPort: RemappedPort, extensionHostPort: port }] : undefined
44-
}
45-
);
34+
const webViewOptions: WebviewOptions = {
35+
enableScripts: true,
36+
localResourceRoots: [Uri.file(this.options.rootPath), Uri.file(this.options.cwd)],
37+
portMapping: port ? [{ webviewPort: RemappedPort, extensionHostPort: port }] : undefined
38+
};
39+
if (options.webViewPanel) {
40+
this.panel = options.webViewPanel;
41+
this.panel.webview.options = webViewOptions;
42+
} else {
43+
this.panel = window.createWebviewPanel(
44+
options.title.toLowerCase().replace(' ', ''),
45+
options.title,
46+
{ viewColumn: options.viewColumn, preserveFocus: true },
47+
{
48+
retainContextWhenHidden: true,
49+
enableFindWidget: true,
50+
...webViewOptions
51+
}
52+
);
53+
}
4654
this.loadPromise = this.load();
4755
}
4856

src/client/common/serviceRegistry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IImportTracker } from '../telemetry/types';
99
import { ApplicationEnvironment } from './application/applicationEnvironment';
1010
import { ApplicationShell } from './application/applicationShell';
1111
import { CommandManager } from './application/commandManager';
12+
import { CustomEditorService } from './application/customEditorService';
1213
import { DebugService } from './application/debugService';
1314
import { DebugSessionTelemetry } from './application/debugSessionTelemetry';
1415
import { DocumentManager } from './application/documentManager';
@@ -19,6 +20,7 @@ import {
1920
IApplicationEnvironment,
2021
IApplicationShell,
2122
ICommandManager,
23+
ICustomEditorService,
2224
IDebugService,
2325
IDocumentManager,
2426
ILanguageService,
@@ -151,4 +153,5 @@ export function registerTypes(serviceManager: IServiceManager) {
151153
serviceManager.addSingleton<IExtensionChannelRule>(IExtensionChannelRule, ExtensionInsidersDailyChannelRule, ExtensionChannel.daily);
152154
serviceManager.addSingleton<IExtensionChannelRule>(IExtensionChannelRule, ExtensionInsidersWeeklyChannelRule, ExtensionChannel.weekly);
153155
serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, DebugSessionTelemetry);
156+
serviceManager.addSingleton<ICustomEditorService>(ICustomEditorService, CustomEditorService);
154157
}

src/client/datascience/constants.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ export namespace Commands {
6262
export const ScrollToCell = 'python.datascience.scrolltocell';
6363
export const CreateNewNotebook = 'python.datascience.createnewnotebook';
6464
export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput';
65+
66+
// Make sure to put these into the package .json
67+
export const NotebookStorage_DeleteAllCells = 'python.datascience.notebook.deleteall';
68+
export const NotebookStorage_ModifyCells = 'python.datascience.notebook.modifycells';
69+
export const NotebookStorage_EditCell = 'python.datascience.notebook.editcell';
70+
export const NotebookStorage_InsertCell = 'python.datascience.notebook.insertcell';
71+
export const NotebookStorage_RemoveCell = 'python.datascience.notebook.removecell';
72+
export const NotebookStorage_SwapCells = 'python.datascience.notebook.swapcells';
73+
export const NotebookStorage_ClearCellOutputs = 'python.datascience.notebook.clearoutputs';
74+
export const NotebookStorage_UpdateVersion = 'python.datascience.notebook.updateversion';
6575
}
6676

6777
export namespace CodeLensCommands {
@@ -238,7 +248,8 @@ export enum Telemetry {
238248
UserInstalledJupyter = 'DATASCIENCE.USER_INSTALLED_JUPYTER',
239249
UserDidNotInstallJupyter = 'DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER',
240250
OpenedInteractiveWindow = 'DATASCIENCE.OPENED_INTERACTIVE',
241-
FindKernelForLocalConnection = 'DATASCIENCE.FIND_KERNEL_FOR_LOCAL_CONNECTION'
251+
FindKernelForLocalConnection = 'DATASCIENCE.FIND_KERNEL_FOR_LOCAL_CONNECTION',
252+
OpenNotebookFailure = 'DS_INTERNAL.NATIVE.OPEN_NOTEBOOK_FAILURE'
242253
}
243254

244255
export enum NativeKeyboardCommandTelemetry {

0 commit comments

Comments
 (0)