diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..010f659 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,59 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": true, // set this to true to hide the "out" folder with the compiled JS files + "**/*.pyc": true, + ".nyc_output": true, + "obj": true, + "bin": true, + "**/__pycache__": true, + "**/node_modules": false, + ".vscode-test": true, + ".vscode test": true, + ".venv": true, + "**/.pytest_cache/**": true, + "languageServer.*/**": true, + "**/.mypy_cache/**": true, + "**/.ropeproject/**": true + }, + "search.exclude": { + "out": true, // set this to false to include "out" folder in search results + "**/node_modules": true, + "coverage": true, + "languageServer*/**": true, + ".vscode-test": true, + ".vscode test": true + }, + "[python]": { + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.formatOnSave": true + }, + "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version + "python.linting.enabled": false, + "python.testing.promptToConfigure": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.provider": "black", + "typescript.preferences.quoteStyle": "single", + "javascript.preferences.quoteStyle": "single", + "typescriptHero.imports.stringQuoteStyle": "'", + "prettier.printWidth": 120, + "prettier.singleQuote": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.fixAll.tslint": true + }, + "python.languageServer": "Pylance", + "python.analysis.logLevel": "Trace", + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": [ + // Match what black does. + "--max-line-length=88" + ], + "typescript.preferences.importModuleSpecifier": "relative" +} diff --git a/package-lock.json b/package-lock.json index 06b3bed..2093999 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@vscode/jupyter-lsp-middleware", - "version": "0.2.17", + "version": "0.2.18", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6ba6bd9..0b68833 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vscode/jupyter-lsp-middleware", - "version": "0.2.17", + "version": "0.2.18", "description": "VS Code Python Language Server Middleware for Jupyter Notebook", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/index.ts b/src/index.ts index f2f3bbc..f4d746e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { LanguageClient, Middleware } from 'vscode-languageclient/node'; import { IVSCodeNotebook } from './common/types'; import { HidingMiddlewareAddon } from './hidingMiddlewareAddon'; import { NotebookMiddlewareAddon } from './notebookMiddlewareAddon'; +import { PylanceMiddlewareAddon } from './pylanceMiddlewareAddon'; export type NotebookMiddleware = Middleware & Disposable & { stopWatching(notebook: NotebookDocument): void; @@ -40,3 +41,17 @@ export function createNotebookMiddleware( isDocumentAllowed ); } + +export function createPylanceMiddleware( + getClient: () => LanguageClient | undefined, + pythonPath: string, + isDocumentAllowed: (uri: Uri) => boolean +): NotebookMiddleware { + // LanguageClients are created per interpreter (as they start) with a selector for all notebooks + // Middleware swallows all requests for notebooks that don't match itself (isDocumentAllowed returns false) + return new PylanceMiddlewareAddon( + getClient, + pythonPath, + isDocumentAllowed + ); +} diff --git a/src/pylanceMiddlewareAddon.ts b/src/pylanceMiddlewareAddon.ts new file mode 100644 index 0000000..9ba327f --- /dev/null +++ b/src/pylanceMiddlewareAddon.ts @@ -0,0 +1,577 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { + CallHierarchyIncomingCall, + CallHierarchyItem, + CallHierarchyOutgoingCall, + CancellationToken, + CodeAction, + CodeActionContext, + CodeLens, + Color, + ColorInformation, + ColorPresentation, + Command, + CompletionContext, + CompletionItem, + Declaration, + Definition, + DefinitionLink, + Diagnostic, + Disposable, + DocumentHighlight, + DocumentLink, + DocumentSymbol, + FoldingContext, + FoldingRange, + FormattingOptions, + LinkedEditingRanges, + Location, + NotebookDocument, + Position, + Position as VPosition, + ProviderResult, + Range, + SelectionRange, + SemanticTokens, + SemanticTokensEdits, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + TextEdit, + Uri, + WorkspaceEdit +} from 'vscode'; +import { + ConfigurationParams, + ConfigurationRequest, + DidCloseTextDocumentNotification, + DidOpenTextDocumentNotification, + HandleDiagnosticsSignature, + LanguageClient, + Middleware, + PrepareRenameSignature, + ProvideCodeActionsSignature, + ProvideCodeLensesSignature, + ProvideCompletionItemsSignature, + ProvideDefinitionSignature, + ProvideDocumentFormattingEditsSignature, + ProvideDocumentHighlightsSignature, + ProvideDocumentLinksSignature, + ProvideDocumentRangeFormattingEditsSignature, + ProvideDocumentSymbolsSignature, + ProvideHoverSignature, + ProvideOnTypeFormattingEditsSignature, + ProvideReferencesSignature, + ProvideRenameEditsSignature, + ProvideSignatureHelpSignature, + ProvideWorkspaceSymbolsSignature, + ResolveCodeLensSignature, + ResolveCompletionItemSignature, + ResolveDocumentLinkSignature, + ResponseError +} from 'vscode-languageclient/node'; + +import { ProvideDeclarationSignature } from 'vscode-languageclient/lib/common/declaration'; +import { isThenable } from './common/utils'; +import { ProvideTypeDefinitionSignature } from 'vscode-languageclient/lib/common/typeDefinition'; +import { ProvideImplementationSignature } from 'vscode-languageclient/lib/common/implementation'; +import { + ProvideDocumentColorsSignature, + ProvideColorPresentationSignature +} from 'vscode-languageclient/lib/common/colorProvider'; +import { ProvideFoldingRangeSignature } from 'vscode-languageclient/lib/common/foldingRange'; +import { ProvideSelectionRangeSignature } from 'vscode-languageclient/lib/common/selectionRange'; +import { + PrepareCallHierarchySignature, + CallHierarchyIncomingCallsSignature, + CallHierarchyOutgoingCallsSignature +} from 'vscode-languageclient/lib/common/callHierarchy'; +import { + DocumentRangeSemanticTokensSignature, + DocumentSemanticsTokensEditsSignature, + DocumentSemanticsTokensSignature +} from 'vscode-languageclient/lib/common/semanticTokens'; +import { ProvideLinkedEditingRangeSignature } from 'vscode-languageclient/lib/common/linkedEditingRange'; + +/** + * This class is a temporary solution to handling intellisense and diagnostics in python based notebooks. + * + * It is responsible for sending requests to pylance if they are allowed. + */ +export class PylanceMiddlewareAddon implements Middleware, Disposable { + constructor( + private readonly getClient: () => LanguageClient | undefined, + private readonly pythonPath: string, + private readonly isDocumentAllowed: (uri: Uri) => boolean + ) { + // Make sure a bunch of functions are bound to this. VS code can call them without a this context + this.handleDiagnostics = this.handleDiagnostics.bind(this); + this.didOpen = this.didOpen.bind(this); + } + + public workspace = { + configuration: async ( + params: ConfigurationParams, + token: CancellationToken, + next: ConfigurationRequest.HandlerSignature + ) => { + // Handle workspace/configuration requests. + let settings = next(params, token); + if (isThenable(settings)) { + settings = await settings; + } + if (settings instanceof ResponseError) { + return settings; + } + + for (const [i, item] of params.items.entries()) { + if (item.section === 'python') { + settings[i].pythonPath = this.pythonPath; + } + } + + return settings; + } + }; + + public dispose(): void { + // Nothing to dispose at the moment + } + + public stopWatching(notebook: NotebookDocument): void { + // Close all of the cells. This should cause diags and other things to be cleared + const client = this.getClient(); + if (client && notebook.cellCount > 0) { + notebook.getCells().forEach((c) => { + const params = client.code2ProtocolConverter.asCloseTextDocumentParams(c.document); + client.sendNotification(DidCloseTextDocumentNotification.type, params); + }); + + // Set the diagnostics to nothing for all the cells + if (client.diagnostics) { + notebook.getCells().forEach((c) => { + client.diagnostics?.set(c.document.uri, []); + }); + } + } + } + + public startWatching(notebook: NotebookDocument): void { + // We need to talk directly to the language client here. + const client = this.getClient(); + + // Mimic a document open for all cells + if (client && notebook.cellCount > 0) { + notebook.getCells().forEach((c) => { + this.didOpen(c.document, (ev) => { + const params = client.code2ProtocolConverter.asOpenTextDocumentParams(ev); + client.sendNotification(DidOpenTextDocumentNotification.type, params); + }); + }); + } + } + + public didOpen(document: TextDocument, next: (ev: TextDocument) => void) { + next(document); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + public provideCompletionItem( + document: TextDocument, + position: Position, + context: CompletionContext, + token: CancellationToken, + next: ProvideCompletionItemsSignature + ) { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, context, token); + } + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + public provideHover( + document: TextDocument, + position: Position, + token: CancellationToken, + next: ProvideHoverSignature + ) { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public resolveCompletionItem( + item: CompletionItem, + token: CancellationToken, + next: ResolveCompletionItemSignature + ): ProviderResult { + // Range should have already been remapped. + + // TODO: What if the LS needs to read the range? It won't make sense. This might mean + // doing this at the extension level is not possible. + return next(item, token); + } + + public provideSignatureHelp( + document: TextDocument, + position: Position, + context: SignatureHelpContext, + token: CancellationToken, + next: ProvideSignatureHelpSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, context, token); + } + } + + public provideDefinition( + document: TextDocument, + position: Position, + token: CancellationToken, + next: ProvideDefinitionSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + public provideReferences( + document: TextDocument, + position: Position, + options: { + includeDeclaration: boolean; + }, + token: CancellationToken, + next: ProvideReferencesSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, options, token); + } + } + + public provideDocumentHighlights( + document: TextDocument, + position: Position, + token: CancellationToken, + next: ProvideDocumentHighlightsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + public provideDocumentSymbols( + document: TextDocument, + token: CancellationToken, + next: ProvideDocumentSymbolsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, token); + } + } + + public provideWorkspaceSymbols( + query: string, + token: CancellationToken, + next: ProvideWorkspaceSymbolsSignature + ): ProviderResult { + // Is this one possible to check? + return next(query, token); + } + + // eslint-disable-next-line class-methods-use-this + public provideCodeActions( + document: TextDocument, + range: Range, + context: CodeActionContext, + token: CancellationToken, + next: ProvideCodeActionsSignature + ): ProviderResult<(Command | CodeAction)[]> { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, range, context, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public provideCodeLenses( + document: TextDocument, + token: CancellationToken, + next: ProvideCodeLensesSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public resolveCodeLens( + codeLens: CodeLens, + token: CancellationToken, + next: ResolveCodeLensSignature + ): ProviderResult { + // Range should have already been remapped. + + // TODO: What if the LS needs to read the range? It won't make sense. This might mean + // doing this at the extension level is not possible. + return next(codeLens, token); + } + + // eslint-disable-next-line class-methods-use-this + public provideDocumentFormattingEdits( + document: TextDocument, + options: FormattingOptions, + token: CancellationToken, + next: ProvideDocumentFormattingEditsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, options, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public provideDocumentRangeFormattingEdits( + document: TextDocument, + range: Range, + options: FormattingOptions, + token: CancellationToken, + next: ProvideDocumentRangeFormattingEditsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, range, options, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public provideOnTypeFormattingEdits( + document: TextDocument, + position: Position, + ch: string, + options: FormattingOptions, + token: CancellationToken, + next: ProvideOnTypeFormattingEditsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, ch, options, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public provideRenameEdits( + document: TextDocument, + position: Position, + newName: string, + token: CancellationToken, + next: ProvideRenameEditsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, newName, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public prepareRename( + document: TextDocument, + position: Position, + token: CancellationToken, + next: PrepareRenameSignature + ): ProviderResult< + | Range + | { + range: Range; + placeholder: string; + } + > { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public provideDocumentLinks( + document: TextDocument, + token: CancellationToken, + next: ProvideDocumentLinksSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, token); + } + } + + // eslint-disable-next-line class-methods-use-this + public resolveDocumentLink( + link: DocumentLink, + token: CancellationToken, + next: ResolveDocumentLinkSignature + ): ProviderResult { + // Range should have already been remapped. + + // TODO: What if the LS needs to read the range? It won't make sense. This might mean + // doing this at the extension level is not possible. + return next(link, token); + } + + public handleDiagnostics(uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature): void { + if (this.shouldProvideIntellisense(uri)) { + return next(uri, diagnostics); + } else { + // Swallow all other diagnostics + next(uri, []); + } + } + + public provideTypeDefinition( + document: TextDocument, + position: Position, + token: CancellationToken, + next: ProvideTypeDefinitionSignature + ) { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + public provideImplementation( + document: TextDocument, + position: VPosition, + token: CancellationToken, + next: ProvideImplementationSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + public provideDocumentColors( + document: TextDocument, + token: CancellationToken, + next: ProvideDocumentColorsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, token); + } + } + public provideColorPresentations( + color: Color, + context: { + document: TextDocument; + range: Range; + }, + token: CancellationToken, + next: ProvideColorPresentationSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(context.document.uri)) { + return next(color, context, token); + } + } + + public provideFoldingRanges( + document: TextDocument, + context: FoldingContext, + token: CancellationToken, + next: ProvideFoldingRangeSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, context, token); + } + } + + public provideDeclaration( + document: TextDocument, + position: Position, + token: CancellationToken, + next: ProvideDeclarationSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + public provideSelectionRanges( + document: TextDocument, + positions: Position[], + token: CancellationToken, + next: ProvideSelectionRangeSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, positions, token); + } + } + + public prepareCallHierarchy( + document: TextDocument, + positions: Position, + token: CancellationToken, + next: PrepareCallHierarchySignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, positions, token); + } + } + public provideCallHierarchyIncomingCalls( + item: CallHierarchyItem, + token: CancellationToken, + next: CallHierarchyIncomingCallsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(item.uri)) { + return next(item, token); + } + } + public provideCallHierarchyOutgoingCalls( + item: CallHierarchyItem, + token: CancellationToken, + next: CallHierarchyOutgoingCallsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(item.uri)) { + return next(item, token); + } + } + + public provideDocumentSemanticTokens( + document: TextDocument, + token: CancellationToken, + next: DocumentSemanticsTokensSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, token); + } + } + public provideDocumentSemanticTokensEdits( + document: TextDocument, + previousResultId: string, + token: CancellationToken, + next: DocumentSemanticsTokensEditsSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, previousResultId, token); + } + } + public provideDocumentRangeSemanticTokens( + document: TextDocument, + range: Range, + token: CancellationToken, + next: DocumentRangeSemanticTokensSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, range, token); + } + } + + public provideLinkedEditingRange( + document: TextDocument, + position: Position, + token: CancellationToken, + next: ProvideLinkedEditingRangeSignature + ): ProviderResult { + if (this.shouldProvideIntellisense(document.uri)) { + return next(document, position, token); + } + } + + private shouldProvideIntellisense(uri: Uri): boolean { + // Make sure document is allowed + return this.isDocumentAllowed(uri); + } +} diff --git a/src/test/suite/helper.ts b/src/test/suite/helper.ts index 14b45df..23837c4 100644 --- a/src/test/suite/helper.ts +++ b/src/test/suite/helper.ts @@ -23,7 +23,7 @@ import { ServerCapabilities, StaticFeature } from 'vscode-languageclient/node'; -import { createNotebookMiddleware, createHidingMiddleware } from '../..'; +import { createNotebookMiddleware, createHidingMiddleware, createPylanceMiddleware } from '../..'; import { FileBasedCancellationStrategy } from '../../fileBasedCancellationStrategy'; import * as uuid from 'uuid/v4'; @@ -870,10 +870,40 @@ export class LanguageServer implements vscode.Disposable { } } +export type MiddlewareType = 'pylance' | 'hiding' | 'notebook'; + +function createMiddleware( + middlewareType: MiddlewareType, + notebookApi: IVSCodeNotebook, + getClient: () => LanguageClient | undefined, + traceInfo: (...args: any[]) => void, + cellSelector: DocumentSelector, + notebookFileRegex: RegExp, + pythonPath: string, + isDocumentAllowed: (uri: vscode.Uri) => boolean +) { + switch (middlewareType) { + case 'hiding': + return createHidingMiddleware(); + case 'pylance': + return createPylanceMiddleware(getClient, pythonPath, isDocumentAllowed); + case 'notebook': + return createNotebookMiddleware( + notebookApi, + getClient, + traceInfo, + cellSelector, + notebookFileRegex, + pythonPath, + isDocumentAllowed + ); + } +} + async function startLanguageServer( outputChannel: string, languageServerFolder: string, - hidingMiddleware: boolean, + middlewareType: MiddlewareType, pythonPath: string, selector: DocumentSelector, shouldProvideIntellisense: (uri: vscode.Uri) => boolean @@ -902,7 +932,8 @@ async function startLanguageServer( } }; - const middleware = hidingMiddleware ? createHidingMiddleware() : createNotebookMiddleware( + const middleware = createMiddleware( + middlewareType, notebookApi, () => languageClient, traceInfo, @@ -952,7 +983,7 @@ async function startLanguageServer( export async function createLanguageServer( outputChannel: string, selector: DocumentSelector, - hidingMiddleware: boolean, + middlewareType: MiddlewareType, shouldProvideIntellisense: (uri: vscode.Uri) => boolean ): Promise { // Python should be installed too. @@ -972,7 +1003,7 @@ export async function createLanguageServer( return startLanguageServer( outputChannel, path.join(pylance.extensionPath, 'dist'), - hidingMiddleware, + middlewareType, pythonPath, selector, shouldProvideIntellisense diff --git a/src/test/suite/hiding.test.ts b/src/test/suite/hiding.test.ts index 66f9069..87ee3d3 100644 --- a/src/test/suite/hiding.test.ts +++ b/src/test/suite/hiding.test.ts @@ -3,15 +3,7 @@ /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ import { assert } from 'chai'; -import { - Position, - Disposable, - languages, - Range, - WorkspaceEdit, - workspace, - Uri -} from 'vscode'; +import { Position, Disposable, languages, Range, WorkspaceEdit, workspace, Uri } from 'vscode'; import { DocumentFilter } from 'vscode-languageserver-protocol'; import { canRunNotebookTests, @@ -56,7 +48,7 @@ suite('Hiding tests', function () { languageServer = await createLanguageServer( 'lsp-middleware-test', NOTEBOOK_SELECTOR, - true, + 'hiding', shouldProvideIntellisense ); }); diff --git a/src/test/suite/notebook.test.ts b/src/test/suite/notebook.test.ts index f3a431a..f0322f7 100644 --- a/src/test/suite/notebook.test.ts +++ b/src/test/suite/notebook.test.ts @@ -63,7 +63,7 @@ suite('Notebook tests', function () { languageServer = await createLanguageServer( 'lsp-middleware-test', NOTEBOOK_SELECTOR, - false, + 'notebook', shouldProvideIntellisense ); }); @@ -230,7 +230,7 @@ suite('Notebook tests', function () { ); }); test('Make sure diags are skipped when not allowing', async function () { - this.skip(); // Skip for now. Requires jupyter to not be providing intellisense too + this.skip(); // Skip for now. Requires jupyter extension to not be providing intellisense too allowIntellisense = false; await insertCodeCell('import sys\nprint(sys.executable)'); await insertCodeCell('import sys\nprint(sys.executable)'); diff --git a/src/test/suite/pylance.test.ts b/src/test/suite/pylance.test.ts new file mode 100644 index 0000000..7215147 --- /dev/null +++ b/src/test/suite/pylance.test.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { assert } from 'chai'; +import { Position, Disposable, languages, Range, WorkspaceEdit, workspace, Uri } from 'vscode'; +import { DocumentFilter } from 'vscode-languageserver-protocol'; +import { + canRunNotebookTests, + closeNotebooksAndCleanUpAfterTests, + insertCodeCell, + createEmptyPythonNotebook, + traceInfo, + createLanguageServer, + focusCell, + captureScreenShot, + captureOutputMessages, + LanguageServer, + waitForDiagnostics +} from './helper'; + +export const PYTHON_LANGUAGE = 'python'; +export const NotebookCellScheme = 'vscode-notebook-cell'; +export const InteractiveInputScheme = 'vscode-interactive-input'; +export const NOTEBOOK_SELECTOR: DocumentFilter[] = [ + { scheme: NotebookCellScheme, language: PYTHON_LANGUAGE }, + { scheme: InteractiveInputScheme, language: PYTHON_LANGUAGE } +]; + +/* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ +suite('Pylance tests', function () { + const disposables: Disposable[] = []; + let languageServer: LanguageServer | undefined = undefined; + let allowIntellisense = true; + let emptyNotebookUri: Uri | undefined; + const shouldProvideIntellisense = (uri: Uri) => { + if (emptyNotebookUri?.fsPath === uri.fsPath) { + return allowIntellisense; + } + return false; + }; + this.timeout(120_000); + suiteSetup(async function () { + this.timeout(120_000); + if (!canRunNotebookTests()) { + return this.skip(); + } + languageServer = await createLanguageServer( + 'lsp-middleware-test', + NOTEBOOK_SELECTOR, + 'pylance', + shouldProvideIntellisense + ); + }); + // Use same notebook without starting kernel in every single test (use one for whole suite). + setup(async function () { + traceInfo(`Start Test ${this.currentTest?.title}`); + allowIntellisense = true; + emptyNotebookUri = await createEmptyPythonNotebook(disposables); + traceInfo(`Start Test (completed) ${this.currentTest?.title}`); + }); + teardown(async function () { + traceInfo(`Ended Test ${this.currentTest?.title}`); + if (this.currentTest && this.currentTest.state === 'failed') { + await captureScreenShot(this.currentTest.title); + await captureOutputMessages(); + } + await closeNotebooksAndCleanUpAfterTests(disposables); + traceInfo(`Ended Test (completed) ${this.currentTest?.title}`); + }); + suiteTeardown(async () => { + closeNotebooksAndCleanUpAfterTests(disposables); + await languageServer?.dispose(); + }); + test('Edit a cell and make sure diagnostics do show up', async () => { + // Pylance should definitely be able to handle a single cell + const cell = await insertCodeCell('import sys\nprint(sys.executable)\na = 1'); + // Should be no diagnostics yet + let diagnostics = languages.getDiagnostics(cell.document.uri); + assert.isEmpty(diagnostics, 'No diagnostics should be found in the first cell'); + + // Edit the cell + await focusCell(cell); + const edit = new WorkspaceEdit(); + edit.replace(cell.document.uri, new Range(new Position(0, 7), new Position(0, 10)), 'system'); + await workspace.applyEdit(edit); + + // There should be diagnostics now + await waitForDiagnostics(cell.document.uri); + }); +}); diff --git a/vscode.d.ts b/vscode.d.ts index a6704f6..88b9794 100644 --- a/vscode.d.ts +++ b/vscode.d.ts @@ -766,8 +766,9 @@ declare module 'vscode' { preserveFocus?: boolean; /** - * An optional flag that controls if an {@link TextEditor editor}-tab will be replaced - * with the next editor or if it will be kept. + * An optional flag that controls if an {@link TextEditor editor}-tab shows as preview. Preview tabs will + * be replaced and reused until set to stay - either explicitly or through editing. The default behaviour depends + * on the `workbench.editor.enablePreview`-setting. */ preview?: boolean; @@ -2566,11 +2567,12 @@ declare module 'vscode' { } /** - * The MarkdownString represents human-readable text that supports formatting via the - * markdown syntax. Standard markdown is supported, also tables, but no embedded html. + * Human-readable text that supports formatting via the [markdown syntax](https://commonmark.org). * * Rendering of {@link ThemeIcon theme icons} via the `$()`-syntax is supported - * when the {@linkcode MarkdownString.supportThemeIcons supportThemeIcons} is set to `true`. + * when the {@linkcode supportThemeIcons} is set to `true`. + * + * Rendering of embedded html is supported when {@linkcode supportHtml} is set to `true`. */ export class MarkdownString { @@ -2591,7 +2593,7 @@ declare module 'vscode' { supportThemeIcons?: boolean; /** - * Indicates that this markdown string can contain raw html tags. Defaults to false. + * Indicates that this markdown string can contain raw html tags. Defaults to `false`. * * When `supportHtml` is false, the markdown renderer will strip out any raw html tags * that appear in the markdown text. This means you can only use markdown syntax for rendering. @@ -3550,7 +3552,7 @@ declare module 'vscode' { * * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). */ - readonly resultId?: string; + readonly resultId: string | undefined; /** * The actual tokens data. * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format. @@ -3570,7 +3572,7 @@ declare module 'vscode' { * * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). */ - readonly resultId?: string; + readonly resultId: string | undefined; /** * The edits to the tokens data. * All edits refer to the initial data state. @@ -3596,7 +3598,7 @@ declare module 'vscode' { /** * The elements to insert. */ - readonly data?: Uint32Array; + readonly data: Uint32Array | undefined; constructor(start: number, deleteCount: number, data?: Uint32Array); } @@ -5745,7 +5747,7 @@ declare module 'vscode' { * The priority of this item. Higher value means the item should * be shown more to the left. */ - readonly priority?: number; + readonly priority: number | undefined; /** * The name of the entry, like 'Python Language Indicator', 'Git Status' etc. @@ -5801,7 +5803,7 @@ declare module 'vscode' { /** * Accessibility information used when a screen reader interacts with this StatusBar item */ - accessibilityInformation?: AccessibilityInformation; + accessibilityInformation: AccessibilityInformation | undefined; /** * Shows the entry in the status bar. @@ -8421,7 +8423,7 @@ declare module 'vscode' { /** * The detected default shell for the extension host, this is overridden by the - * `terminal.integrated.shell` setting for the extension host's platform. Note that in + * `terminal.integrated.defaultProfile` setting for the extension host's platform. Note that in * environments that do not support a shell the value is the empty string. */ export const shell: string; @@ -9723,6 +9725,8 @@ declare module 'vscode' { * Note writing `\n` will just move the cursor down 1 row, you need to write `\r` as well * to move the cursor to the left-most cell. * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * **Example:** Write red text to the terminal * ```typescript * const writeEmitter = new vscode.EventEmitter(); @@ -9748,6 +9752,8 @@ declare module 'vscode' { * bar). Set to `undefined` for the terminal to go back to the regular dimensions (fit to * the size of the panel). * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * **Example:** Override the dimensions of a terminal to 20 columns and 10 rows * ```typescript * const dimensionsEmitter = new vscode.EventEmitter(); @@ -9770,6 +9776,8 @@ declare module 'vscode' { /** * An event that when fired will signal that the pty is closed and dispose of the terminal. * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * A number can be used to provide an exit code for the terminal. Exit codes must be * positive and a non-zero exit codes signals failure which shows a notification for a * regular terminal and allows dependent tasks to proceed when used with the @@ -9799,6 +9807,8 @@ declare module 'vscode' { /** * An event that when fired allows changing the name of the terminal. * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * **Example:** Change the terminal name to "My new terminal". * ```typescript * const writeEmitter = new vscode.EventEmitter(); @@ -10835,8 +10845,8 @@ declare module 'vscode' { * will be matched against the file paths of resulting matches relative to their workspace. Use a {@link RelativePattern relative pattern} * to restrict the search results to a {@link WorkspaceFolder workspace folder}. * @param exclude A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default excludes and the user's - * configured excludes will apply. When `null`, no excludes will apply. + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default file-excludes (e.g. the `files.exclude`-setting + * but not `search.exclude`) will apply. When `null`, no excludes will apply. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. Will return no results if no @@ -12733,8 +12743,9 @@ declare module 'vscode' { * The UI-visible count of {@link SourceControlResourceState resource states} of * this source control. * - * Equals to the total number of {@link SourceControlResourceState resource state} - * of this source control, if undefined. + * If undefined, this source control will + * - display its UI-visible count as zero, and + * - contribute the count of its {@link SourceControlResourceState resource states} to the UI-visible aggregated count for all source controls */ count?: number; @@ -14101,7 +14112,7 @@ declare module 'vscode' { * Associated tag for the profile. If this is set, only {@link TestItem} * instances with the same tag will be eligible to execute in this profile. */ - tag?: TestTag; + tag: TestTag | undefined; /** * If this method is present, a configuration gear will be present in the @@ -14109,7 +14120,7 @@ declare module 'vscode' { * you can take other editor actions, such as showing a quick pick or * opening a configuration file. */ - configureHandler?: () => void; + configureHandler: (() => void) | undefined; /** * Handler called to start a test run. When invoked, the function should call @@ -14258,7 +14269,7 @@ declare module 'vscode' { * The process of running tests should resolve the children of any test * items who have not yet been resolved. */ - readonly include?: TestItem[]; + readonly include: TestItem[] | undefined; /** * An array of tests the user has marked as excluded from the test included @@ -14267,14 +14278,14 @@ declare module 'vscode' { * May be omitted if no exclusions were requested. Test controllers should * not run excluded tests or any children of excluded tests. */ - readonly exclude?: TestItem[]; + readonly exclude: TestItem[] | undefined; /** * The profile used for this request. This will always be defined * for requests issued from the editor UI, though extensions may * programmatically create requests not associated with any profile. */ - readonly profile?: TestRunProfile; + readonly profile: TestRunProfile | undefined; /** * @param tests Array of specific tests to run, or undefined to run all tests @@ -14293,7 +14304,7 @@ declare module 'vscode' { * disambiguate multiple sets of results in a test run. It is useful if * tests are run across multiple platforms, for example. */ - readonly name?: string; + readonly name: string | undefined; /** * A cancellation token which will be triggered when the test run is @@ -14433,7 +14444,7 @@ declare module 'vscode' { /** * URI this `TestItem` is associated with. May be a file or directory. */ - readonly uri?: Uri; + readonly uri: Uri | undefined; /** * The children of this test item. For a test suite, this may contain the @@ -14446,7 +14457,7 @@ declare module 'vscode' { * top-level items in the {@link TestController.items} and for items that * aren't yet included in another item's {@link children}. */ - readonly parent?: TestItem; + readonly parent: TestItem | undefined; /** * Tags associated with this test item. May be used in combination with @@ -14488,7 +14499,7 @@ declare module 'vscode' { * * This is only meaningful if the `uri` points to a file. */ - range?: Range; + range: Range | undefined; /** * Optional error encountered while loading the test. @@ -14496,7 +14507,7 @@ declare module 'vscode' { * Note that this is not a test result and should only be used to represent errors in * test discovery, such as syntax errors. */ - error?: string | MarkdownString; + error: string | MarkdownString | undefined; } /** diff --git a/vscode.proposed.d.ts b/vscode.proposed.d.ts index 4fae0a8..b4af49f 100644 --- a/vscode.proposed.d.ts +++ b/vscode.proposed.d.ts @@ -1575,6 +1575,10 @@ declare module 'vscode' { } export interface NotebookController { + /** + * The human-readable label used to categorise controllers. + */ + kind?: string; // todo@API allow add, not remove readonly rendererScripts: NotebookRendererScript[]; @@ -1822,6 +1826,7 @@ declare module 'vscode' { /** * The text of the hint. */ + // todo@API label? text: string; /** * The position of this hint.