Skip to content

Address Notebook breaking changes #2827

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
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
3 changes: 0 additions & 3 deletions .github/workflows/updateNotebookApi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ jobs:
# Remove the old file so it doesn't get picked up by tsc
Remove-Item ./old.vscode.proposed.d.ts -Force

- name: Compile the TypeScript to check for errors
Copy link
Member Author

Choose a reason for hiding this comment

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

Removed this because PRs should still get sent even if it doesn't compile.

run: npm run compile

- name: Create Pull Request
if: github.event_name == 'schedule'
id: cpr
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ npm-debug.log
*.DS_Store
test-results.xml
vscode.d.ts
test/.vscode/settings.json
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,17 +218,17 @@
}
},
{
"command": "PowerShell.ShowNotebookMode",
"title": "(Preview) Show Notebook Mode",
"command": "PowerShell.EnableNotebookMode",
"title": "(Preview) Enable Notebook Mode",
"category": "PowerShell",
"icon": {
"light": "resources/light/book.svg",
"dark": "resources/dark/book.svg"
}
},
{
"command": "PowerShell.HideNotebookMode",
"title": "Show Text Editor",
"command": "PowerShell.DisableNotebookMode",
"title": "(Preview) Disable Notebook Mode",
"category": "PowerShell",
"icon": {
"light": "resources/light/file-code.svg",
Expand Down Expand Up @@ -413,12 +413,12 @@
},
{
"when": "editorLangId == powershell && config.powershell.notebooks.showToggleButton",
"command": "PowerShell.ShowNotebookMode",
"command": "PowerShell.EnableNotebookMode",
"group": "navigation@102"
},
{
"when": "resourceLangId == powershell && notebookEditorFocused",
"command": "PowerShell.HideNotebookMode",
"command": "PowerShell.DisableNotebookMode",
"group": "navigation@102"
}
],
Expand Down
144 changes: 102 additions & 42 deletions src/features/PowerShellNotebooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,71 @@ import { EvaluateRequestType } from "./Console";
import { LanguageClientConsumer } from "../languageClientConsumer";
import Settings = require("../settings");
import { ILogger } from "../logging";
import { LanguageClient } from "vscode-languageclient";

export class PowerShellNotebooksFeature extends LanguageClientConsumer implements vscode.NotebookContentProvider, vscode.NotebookKernel {
export class PowerShellNotebooksFeature extends LanguageClientConsumer {

private readonly showNotebookModeCommand: vscode.Disposable;
private readonly hideNotebookModeCommand: vscode.Disposable;
private readonly disposables: vscode.Disposable[];
private readonly notebookContentProvider: vscode.NotebookContentProvider;
private readonly notebookKernel: PowerShellNotebookKernel;

private _onDidChangeNotebook = new vscode.EventEmitter<vscode.NotebookDocumentEditEvent>();
public onDidChangeNotebook: vscode.Event<vscode.NotebookDocumentEditEvent> = this._onDidChangeNotebook.event;
public kernel?: vscode.NotebookKernel;
public constructor(logger: ILogger, skipRegisteringCommands?: boolean) {
super();
this.disposables = [];
if(!skipRegisteringCommands) {
this.disposables.push(vscode.commands.registerCommand(
"PowerShell.EnableNotebookMode",
PowerShellNotebooksFeature.EnableNotebookMode));

public label: string = "PowerShell";
public preloads?: vscode.Uri[];
this.disposables.push(vscode.commands.registerCommand(
"PowerShell.DisableNotebookMode",
PowerShellNotebooksFeature.DisableNotebookMode));
}

public constructor(private logger: ILogger, skipRegisteringCommands?: boolean) {
super();
// VS Code Notebook API uses this property for handling cell execution.
this.kernel = this;
this.notebookContentProvider = new PowerShellNotebookContentProvider(logger);
this.notebookKernel = new PowerShellNotebookKernel();
}

if(!skipRegisteringCommands) {
this.showNotebookModeCommand = vscode.commands.registerCommand(
"PowerShell.ShowNotebookMode",
PowerShellNotebooksFeature.showNotebookMode);
public registerNotebookProviders() {
this.disposables.push(vscode.notebook.registerNotebookKernelProvider({
viewType: "PowerShellNotebookMode"
}, this.notebookKernel));

this.disposables.push(vscode.notebook.registerNotebookContentProvider(
"PowerShellNotebookMode",
this.notebookContentProvider));
}

this.hideNotebookModeCommand = vscode.commands.registerCommand(
"PowerShell.HideNotebookMode",
PowerShellNotebooksFeature.hideNotebookMode);
public dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
}

public setLanguageClient(languageClient: LanguageClient) {
this.notebookKernel.setLanguageClient(languageClient);
}

private static async EnableNotebookMode() {
const uri = vscode.window.activeTextEditor.document.uri;
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
await vscode.commands.executeCommand("vscode.openWith", uri, "PowerShellNotebookMode");
}

private static async DisableNotebookMode() {
const uri = vscode.notebook.activeNotebookEditor.document.uri;
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
await vscode.commands.executeCommand("vscode.openWith", uri, "default");
}
}

class PowerShellNotebookContentProvider implements vscode.NotebookContentProvider {
private _onDidChangeNotebook = new vscode.EventEmitter<vscode.NotebookDocumentEditEvent>();
public onDidChangeNotebook: vscode.Event<vscode.NotebookDocumentEditEvent> = this._onDidChangeNotebook.event;

public constructor(private logger: ILogger) {
}

public async openNotebook(uri: vscode.Uri, context: vscode.NotebookDocumentOpenContext): Promise<vscode.NotebookData> {
// load from backup if needed.
const actualUri = context.backupId ? vscode.Uri.parse(context.backupId) : uri;
Expand Down Expand Up @@ -186,11 +222,6 @@ export class PowerShellNotebooksFeature extends LanguageClientConsumer implement
};
}

public dispose() {
this.showNotebookModeCommand.dispose();
this.hideNotebookModeCommand.dispose();
}

private async _save(document: vscode.NotebookDocument, targetResource: vscode.Uri, _token: vscode.CancellationToken): Promise<void> {
this.logger.writeDiagnostic(`Saving Notebook: ${targetResource.toString()}`);

Expand All @@ -215,35 +246,64 @@ export class PowerShellNotebooksFeature extends LanguageClientConsumer implement

await vscode.workspace.fs.writeFile(targetResource, new TextEncoder().encode(retArr.join("\n")));
}
}

private static async showNotebookMode() {
const uri = vscode.window.activeTextEditor.document.uri;
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
await vscode.commands.executeCommand("vscode.openWith", uri, "PowerShellNotebookMode");
class PowerShellNotebookKernel implements vscode.NotebookKernel, vscode.NotebookKernelProvider {
private static informationMessage = "PowerShell extension has not finished starting up yet. Please try again in a few moments.";

public id?: string;
public label: string = "PowerShell";
public description?: string = "The PowerShell Notebook Mode kernel that runs commands in the PowerShell Integrated Console.";
public isPreferred?: boolean;
public preloads?: vscode.Uri[];

private _languageClient: LanguageClient;
private get languageClient(): LanguageClient {
if (!this._languageClient) {
vscode.window.showInformationMessage(
PowerShellNotebookKernel.informationMessage);
}
return this._languageClient;
}

private static async hideNotebookMode() {
const uri = vscode.notebook.activeNotebookEditor.document.uri;
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
await vscode.commands.executeCommand("vscode.openWith", uri, "default");
private set languageClient(value: LanguageClient) {
this._languageClient = value;
}

/*
`vscode.NotebookKernel` implementations
*/
public async executeAllCells(document: vscode.NotebookDocument, token: vscode.CancellationToken): Promise<void> {
public async executeAllCells(document: vscode.NotebookDocument): Promise<void> {
for (const cell of document.cells) {
await this.executeCell(document, cell, token);
if (cell.cellKind === vscode.CellKind.Code) {
await this.executeCell(document, cell);
}
}
}

public async executeCell(document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined, token: vscode.CancellationToken): Promise<void> {
if (token.isCancellationRequested) {
return;
}

public async executeCell(document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined): Promise<void> {
await this.languageClient.sendRequest(EvaluateRequestType, {
expression: cell.document.getText(),
});
}

// Since executing a cell is a "fire and forget", there's no time for the user to cancel
// any of the executing cells. We can bring this in after PSES has a better API for executing code.
public cancelCellExecution(document: vscode.NotebookDocument, cell: vscode.NotebookCell): void {
return;
}

// Since executing a cell is a "fire and forget", there's no time for the user to cancel
// any of the executing cells. We can bring this in after PSES has a better API for executing code.
public cancelAllCellsExecution(document: vscode.NotebookDocument): void {
return;
}

public setLanguageClient(languageClient: LanguageClient) {
this.languageClient = languageClient;
}

/*
vscode.NotebookKernelProvider implementation
*/
public provideKernels(document: vscode.NotebookDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.NotebookKernel[]> {
return [this];
}
}
6 changes: 5 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,17 @@ export function activate(context: vscode.ExtensionContext): void {
const powerShellNotebooksFeature = new PowerShellNotebooksFeature(logger);

try {
context.subscriptions.push(vscode.notebook.registerNotebookContentProvider("PowerShellNotebookMode", powerShellNotebooksFeature));
powerShellNotebooksFeature.registerNotebookProviders();
languageClientConsumers.push(powerShellNotebooksFeature);
} catch (e) {
// This would happen if VS Code changes their API.
powerShellNotebooksFeature.dispose();
logger.writeVerbose("Failed to register NotebookContentProvider", e);
}
} else {
vscode.commands.registerCommand(
"PowerShell.EnableNotebookMode",
() => vscode.window.showWarningMessage("Notebook Mode only works in Visual Studio Code Insiders. To get it, go to: aka.ms/vscode-insiders"));
}

sessionManager.setLanguageClientConsumers(languageClientConsumers);
Expand Down
8 changes: 5 additions & 3 deletions test/features/PowerShellNotebooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,12 @@ suite("PowerShellNotebooks tests", () => {
]);

const feature = new PowerShellNotebooksFeature(new MockLogger(), true);
// `notebookContentProvider` is a private property so cast the feature as `any` so we can access it.
const notebookContentProvider: vscode.NotebookContentProvider = (feature as any).notebookContentProvider;

for (const [uri, expectedCells] of notebookTestData) {
test(`Can open a notebook with expected cells - ${uri.fsPath}`, async () => {
const actualNotebookData = await feature.openNotebook(uri, {});
const actualNotebookData = await notebookContentProvider.openNotebook(uri, {});
compareCells(actualNotebookData.cells, expectedCells);
});
}
Expand All @@ -218,8 +220,8 @@ suite("PowerShellNotebooks tests", () => {
notebookSimpleMixedComments.toString());

// Save it as testFile.ps1 and reopen it using the feature.
await feature.saveNotebookAs(uri, vscode.notebook.activeNotebookEditor.document, null);
const newNotebook = await feature.openNotebook(uri, {});
await notebookContentProvider.saveNotebookAs(uri, vscode.notebook.activeNotebookEditor.document, null);
const newNotebook = await notebookContentProvider.openNotebook(uri, {});

// Compare that saving as a file results in the same cell data as the existing one.
const expectedCells = notebookTestData.get(notebookSimpleMixedComments);
Expand Down
16 changes: 14 additions & 2 deletions vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ declare module 'vscode' {
Error = 4
}

export enum NotebookRunState {
Running = 1,
Idle = 2
}

export interface NotebookCellMetadata {
/**
* Controls if the content of a cell is editable or not.
Expand Down Expand Up @@ -191,6 +196,11 @@ declare module 'vscode' {
* Additional attributes of the document metadata.
*/
custom?: { [key: string]: any };

/**
* The document's current run state
*/
runState?: NotebookRunState;
}

export interface NotebookDocument {
Expand Down Expand Up @@ -498,8 +508,10 @@ declare module 'vscode' {
description?: string;
isPreferred?: boolean;
preloads?: Uri[];
executeCell(document: NotebookDocument, cell: NotebookCell, token: CancellationToken): Promise<void>;
executeAllCells(document: NotebookDocument, token: CancellationToken): Promise<void>;
executeCell(document: NotebookDocument, cell: NotebookCell): void;
cancelCellExecution(document: NotebookDocument, cell: NotebookCell): void;
executeAllCells(document: NotebookDocument): void;
cancelAllCellsExecution(document: NotebookDocument): void;
}

export interface NotebookDocumentFilter {
Expand Down