Skip to content

Implement base custom editor support #9812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jan 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
"--extensionDevelopmentPath=${workspaceFolder}",
"--enable-proposed-api",
"ms-python.python"
],
"stopOnEntry": false,
"smartStep": true,
Expand All @@ -19,7 +21,7 @@
"preLaunchTask": "Compile",
"skipFiles": [
"<node_internals>/**"
]
],
},
{
"name": "Extension inside container",
Expand Down
2 changes: 1 addition & 1 deletion build/ci/vscode-python-pr-validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: '$(Year:yyyy).$(Month).0.$(BuildID)-pr'
pr:
autoCancel: true
branches:
include: ["master", "release*"]
include: ["master", "release*", "ds*"]
paths:
exclude: ["/news/1 Enhancements", "/news/2 Fixes", "/news/3 Code Health", "/.vscode"]

Expand Down
1 change: 1 addition & 0 deletions news/1 Enhancements/9255.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement VS code's custom editor for opening notebooks.
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"version": "2020.2.0-dev",
"languageServerVersion": "0.4.114",
"publisher": "ms-python",
"enableProposedApi": true,
"author": {
"name": "Microsoft Corporation"
},
Expand Down Expand Up @@ -82,7 +83,8 @@
"onCommand:python.datascience.exportfileasnotebook",
"onCommand:python.datascience.exportfileandoutputasnotebook",
"onCommand:python.datascience.selectJupyterInterpreter",
"onCommand:python.enableSourceMapSupport"
"onCommand:python.enableSourceMapSupport",
"onWebviewEditor:NativeEditorProvider.ipynb"
],
"main": "./out/client/extension",
"contributes": {
Expand Down Expand Up @@ -2767,7 +2769,18 @@
"when": "testsDiscovered"
}
]
}
},
"webviewEditors": [
{
"viewType": "NativeEditorProvider.ipynb",
"displayName": "Jupyter Notebook",
"selector": [
{
"filenamePattern": "*.ipynb"
}
]
}
]
},
"scripts": {
"package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix",
Expand Down
16 changes: 15 additions & 1 deletion src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import { CancellationToken, Position, TextDocument, Uri } from 'vscode';
import { Commands as LSCommands } from '../../activation/languageServer/constants';
import { Commands as DSCommands } from '../../datascience/constants';
import { INotebook } from '../../datascience/types';
import { IEditCell, IInsertCell, ISwapCells } from '../../datascience/interactive-common/interactiveWindowTypes';
import { LiveKernelModel } from '../../datascience/jupyter/kernels/types';
import { ICell, IJupyterKernelSpec, INotebook } from '../../datascience/types';
import { PythonInterpreter } from '../../interpreter/contracts';
import { CommandSource } from '../../testing/common/constants';
import { TestFunction, TestsToRun } from '../../testing/common/types';
import { TestDataItem, TestWorkspaceFolder } from '../../testing/types';
Expand Down Expand Up @@ -87,6 +90,9 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
['python._loadLanguageServerExtension']: {}[];
['python.SelectAndInsertDebugConfiguration']: [TextDocument, Position, CancellationToken];
['python.viewLanguageServerOutput']: [];
['vscode.open']: [Uri];
['workbench.action.files.saveAs']: [Uri];
['workbench.action.files.save']: [Uri];
[Commands.Build_Workspace_Symbols]: [boolean, CancellationToken];
[Commands.Sort_Imports]: [undefined, Uri];
[Commands.Exec_In_Terminal]: [undefined, Uri];
Expand Down Expand Up @@ -142,4 +148,12 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
[DSCommands.ScrollToCell]: [string, string];
[DSCommands.ViewJupyterOutput]: [];
[DSCommands.SwitchJupyterKernel]: [INotebook | undefined];
[DSCommands.NotebookStorage_DeleteAllCells]: [Uri];
[DSCommands.NotebookStorage_ModifyCells]: [Uri, ICell[]];
[DSCommands.NotebookStorage_EditCell]: [Uri, IEditCell];
[DSCommands.NotebookStorage_InsertCell]: [Uri, IInsertCell];
[DSCommands.NotebookStorage_RemoveCell]: [Uri, string];
[DSCommands.NotebookStorage_SwapCells]: [Uri, ISwapCells];
[DSCommands.NotebookStorage_ClearCellOutputs]: [Uri];
[DSCommands.NotebookStorage_UpdateVersion]: [Uri, PythonInterpreter | undefined, IJupyterKernelSpec | LiveKernelModel | undefined];
}
21 changes: 21 additions & 0 deletions src/client/common/application/customEditorService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import { inject, injectable } from 'inversify';
import * as vscode from 'vscode';

import { ICommandManager, ICustomEditorService, WebviewCustomEditorProvider } from './types';

@injectable()
export class CustomEditorService implements ICustomEditorService {
constructor(@inject(ICommandManager) private commandManager: ICommandManager) {}

public registerWebviewCustomEditorProvider(viewType: string, provider: WebviewCustomEditorProvider, options?: vscode.WebviewPanelOptions): vscode.Disposable {
// tslint:disable-next-line: no-any
return (vscode.window as any).registerWebviewCustomEditorProvider(viewType, provider, options);
}

public async openEditor(file: vscode.Uri): Promise<void> {
await this.commandManager.executeCommand('vscode.open', file);
}
}
103 changes: 103 additions & 0 deletions src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import {
TreeViewOptions,
Uri,
ViewColumn,
WebviewPanel,
WebviewPanelOptions,
WindowState,
WorkspaceConfiguration,
WorkspaceEdit,
Expand Down Expand Up @@ -983,6 +985,8 @@ export interface IWebPanelOptions {
cwd: string;
// tslint:disable-next-line: no-any
settings?: any;
// Web panel to use if supplied by VS code instead
webViewPanel?: WebviewPanel;
}

// Wraps the VS Code api for creating a web panel
Expand Down Expand Up @@ -1042,3 +1046,102 @@ export const IActiveResourceService = Symbol('IActiveResourceService');
export interface IActiveResourceService {
getActiveResource(): Resource;
}

// Temporary hack to get the nyc compiler to find these types. vscode.proposed.d.ts doesn't work for some reason.
/**
* Defines the editing functionality of a webview editor. This allows the webview editor to hook into standard
* editor events such as `undo` or `save`.
*
* @param EditType Type of edits. Edit objects must be json serializable.
*/
// tslint:disable-next-line: interface-name
export interface WebviewCustomEditorEditingDelegate<EditType> {
/**
* Event triggered by extensions to signal to VS Code that an edit has occurred.
*/
readonly onEdit: Event<{ readonly resource: Uri; readonly edit: EditType }>;
/**
* Save a resource.
*
* @param resource Resource being saved.
*
* @return Thenable signaling that the save has completed.
*/
save(resource: Uri): Thenable<void>;

/**
* Save an existing resource at a new path.
*
* @param resource Resource being saved.
* @param targetResource Location to save to.
*
* @return Thenable signaling that the save has completed.
*/
saveAs(resource: Uri, targetResource: Uri): Thenable<void>;

/**
* Apply a set of edits.
*
* Note that is not invoked when `onEdit` is called as `onEdit` implies also updating the view to reflect the edit.
*
* @param resource Resource being edited.
* @param edit Array of edits. Sorted from oldest to most recent.
*
* @return Thenable signaling that the change has completed.
*/
applyEdits(resource: Uri, edits: readonly EditType[]): Thenable<void>;

/**
* Undo a set of edits.
*
* This is triggered when a user undoes an edit or when revert is called on a file.
*
* @param resource Resource being edited.
* @param edit Array of edits. Sorted from most recent to oldest.
*
* @return Thenable signaling that the change has completed.
*/
undoEdits(resource: Uri, edits: readonly EditType[]): Thenable<void>;
}

// tslint:disable-next-line: interface-name
export interface WebviewCustomEditorProvider {
/**
* Controls the editing functionality of a webview editor. This allows the webview editor to hook into standard
* editor events such as `undo` or `save`.
*
* WebviewEditors that do not have `editingCapability` are considered to be readonly. Users can still interact
* with readonly editors, but these editors will not integrate with VS Code's standard editor functionality.
*/
readonly editingDelegate?: WebviewCustomEditorEditingDelegate<unknown>;
/**
* Resolve a webview editor for a given resource.
*
* To resolve a webview editor, a provider must fill in its initial html content and hook up all
* the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`.
*
* @param resource Resource being resolved.
* @param webview Webview being resolved. The provider should take ownership of this webview.
*
* @return Thenable indicating that the webview editor has been resolved.
*/
resolveWebviewEditor(resource: Uri, webview: WebviewPanel): Thenable<void>;
}

export const ICustomEditorService = Symbol('ICustomEditorService');
export interface ICustomEditorService {
/**
* Register a new provider for webview editors of a given type.
*
* @param viewType Type of the webview editor provider.
* @param provider Resolves webview editors.
* @param options Content settings for a webview panels the provider is given.
*
* @return Disposable that unregisters the `WebviewCustomEditorProvider`.
*/
registerWebviewCustomEditorProvider(viewType: string, provider: WebviewCustomEditorProvider, options?: WebviewPanelOptions): Disposable;
/**
* Opens a file with a custom editor
*/
openEditor(file: Uri): Promise<void>;
}
34 changes: 21 additions & 13 deletions src/client/common/application/webPanels/webPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import '../../extensions';

import * as uuid from 'uuid/v4';
import { Uri, Webview, WebviewPanel, window } from 'vscode';
import { Uri, Webview, WebviewOptions, WebviewPanel, window } from 'vscode';

import { Identifiers } from '../../../datascience/constants';
import { InteractiveWindowMessages } from '../../../datascience/interactive-common/interactiveWindowTypes';
Expand All @@ -31,18 +31,26 @@ export class WebPanel implements IWebPanel {
private token: string | undefined,
private options: IWebPanelOptions
) {
this.panel = window.createWebviewPanel(
options.title.toLowerCase().replace(' ', ''),
options.title,
{ viewColumn: options.viewColumn, preserveFocus: true },
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [Uri.file(this.options.rootPath), Uri.file(this.options.cwd)],
enableFindWidget: true,
portMapping: port ? [{ webviewPort: RemappedPort, extensionHostPort: port }] : undefined
}
);
const webViewOptions: WebviewOptions = {
enableScripts: true,
localResourceRoots: [Uri.file(this.options.rootPath), Uri.file(this.options.cwd)],
portMapping: port ? [{ webviewPort: RemappedPort, extensionHostPort: port }] : undefined
};
if (options.webViewPanel) {
this.panel = options.webViewPanel;
this.panel.webview.options = webViewOptions;
} else {
this.panel = window.createWebviewPanel(
options.title.toLowerCase().replace(' ', ''),
options.title,
{ viewColumn: options.viewColumn, preserveFocus: true },
{
retainContextWhenHidden: true,
enableFindWidget: true,
...webViewOptions
}
);
}
this.loadPromise = this.load();
}

Expand Down
3 changes: 3 additions & 0 deletions src/client/common/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IImportTracker } from '../telemetry/types';
import { ApplicationEnvironment } from './application/applicationEnvironment';
import { ApplicationShell } from './application/applicationShell';
import { CommandManager } from './application/commandManager';
import { CustomEditorService } from './application/customEditorService';
import { DebugService } from './application/debugService';
import { DebugSessionTelemetry } from './application/debugSessionTelemetry';
import { DocumentManager } from './application/documentManager';
Expand All @@ -19,6 +20,7 @@ import {
IApplicationEnvironment,
IApplicationShell,
ICommandManager,
ICustomEditorService,
IDebugService,
IDocumentManager,
ILanguageService,
Expand Down Expand Up @@ -151,4 +153,5 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IExtensionChannelRule>(IExtensionChannelRule, ExtensionInsidersDailyChannelRule, ExtensionChannel.daily);
serviceManager.addSingleton<IExtensionChannelRule>(IExtensionChannelRule, ExtensionInsidersWeeklyChannelRule, ExtensionChannel.weekly);
serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, DebugSessionTelemetry);
serviceManager.addSingleton<ICustomEditorService>(ICustomEditorService, CustomEditorService);
}
13 changes: 12 additions & 1 deletion src/client/datascience/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ export namespace Commands {
export const ScrollToCell = 'python.datascience.scrolltocell';
export const CreateNewNotebook = 'python.datascience.createnewnotebook';
export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput';

// Make sure to put these into the package .json
Copy link
Member

@IanMatthewHuff IanMatthewHuff Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to happen now? #WontFix

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if we need to expose them externally.


In reply to: 372540362 [](ancestors = 372540362)

Copy link

@DonJayamanne DonJayamanne Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users would need to run VSCode with a special CLI argument as well. At least that's what I thought. #ByDesign

export const NotebookStorage_DeleteAllCells = 'python.datascience.notebook.deleteall';
export const NotebookStorage_ModifyCells = 'python.datascience.notebook.modifycells';
export const NotebookStorage_EditCell = 'python.datascience.notebook.editcell';
export const NotebookStorage_InsertCell = 'python.datascience.notebook.insertcell';
export const NotebookStorage_RemoveCell = 'python.datascience.notebook.removecell';
export const NotebookStorage_SwapCells = 'python.datascience.notebook.swapcells';
export const NotebookStorage_ClearCellOutputs = 'python.datascience.notebook.clearoutputs';
export const NotebookStorage_UpdateVersion = 'python.datascience.notebook.updateversion';
}

export namespace CodeLensCommands {
Expand Down Expand Up @@ -238,7 +248,8 @@ export enum Telemetry {
UserInstalledJupyter = 'DATASCIENCE.USER_INSTALLED_JUPYTER',
UserDidNotInstallJupyter = 'DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER',
OpenedInteractiveWindow = 'DATASCIENCE.OPENED_INTERACTIVE',
FindKernelForLocalConnection = 'DATASCIENCE.FIND_KERNEL_FOR_LOCAL_CONNECTION'
FindKernelForLocalConnection = 'DATASCIENCE.FIND_KERNEL_FOR_LOCAL_CONNECTION',
OpenNotebookFailure = 'DS_INTERNAL.NATIVE.OPEN_NOTEBOOK_FAILURE'
}

export enum NativeKeyboardCommandTelemetry {
Expand Down
Loading