Files
Viper_VSCode/packages/pyright-internal/src/languageServerBase.ts
TermiNexus e9e4693333
Some checks failed
Run mypy_primer on push / Run mypy_primer on push (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
初始化上传
2026-07-24 17:08:39 +08:00

1635 lines
68 KiB
TypeScript

/*
* languageServerBase.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Implements common language server functionality.
* This is split out as a base class to allow for
* different language server variants to be created
* from the same core functionality.
*/
import './common/extensions';
import {
AbstractCancellationTokenSource,
CallHierarchyIncomingCallsParams,
CallHierarchyItem,
CallHierarchyOutgoingCall,
CallHierarchyOutgoingCallsParams,
CallHierarchyPrepareParams,
CancellationToken,
CodeAction,
CodeActionParams,
Command,
CompletionItem,
CompletionList,
CompletionParams,
CompletionTriggerKind,
ConfigurationItem,
Connection,
Declaration,
DeclarationLink,
Definition,
DefinitionLink,
Diagnostic,
DiagnosticRefreshRequest,
DiagnosticRelatedInformation,
DiagnosticSeverity,
DiagnosticTag,
DidChangeConfigurationParams,
DidChangeTextDocumentParams,
DidChangeWatchedFilesParams,
DidCloseTextDocumentParams,
DidOpenTextDocumentParams,
Disposable,
DocumentDiagnosticParams,
DocumentDiagnosticReport,
DocumentHighlight,
DocumentHighlightParams,
DocumentSymbol,
DocumentSymbolParams,
ExecuteCommandParams,
HoverParams,
InitializeParams,
InitializeResult,
LSPObject,
Location,
MarkupKind,
PrepareRenameParams,
PublishDiagnosticsParams,
ReferenceParams,
RemoteWindow,
RenameParams,
ResultProgressReporter,
SignatureHelp,
SignatureHelpParams,
SymbolInformation,
TextDocumentPositionParams,
TextDocumentSyncKind,
WorkDoneProgressReporter,
WorkspaceDiagnosticParams,
WorkspaceDiagnosticReport,
WorkspaceDiagnosticReportPartialResult,
WorkspaceEdit,
WorkspaceFoldersChangeEvent,
WorkspaceSymbol,
WorkspaceSymbolParams,
} from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { AnalysisResults } from './analyzer/analysis';
import { BackgroundAnalysisProgram, InvalidatedReason } from './analyzer/backgroundAnalysisProgram';
import { ImportResolver } from './analyzer/importResolver';
import { MaxAnalysisTime } from './analyzer/program';
import { AnalyzerService, LibraryReanalysisTimeProvider, getNextServiceId } from './analyzer/service';
import { IPythonMode } from './analyzer/sourceFile';
import type { IBackgroundAnalysis } from './backgroundAnalysisBase';
import { CommandResult } from './commands/commandResult';
import { CancelAfter } from './common/cancellationUtils';
import { CaseSensitivityDetector } from './common/caseSensitivityDetector';
import { getNestedProperty } from './common/collectionUtils';
import { DiagnosticSeverityOverrides, getDiagnosticSeverityOverrides } from './common/commandLineOptions';
import { ConfigOptions, getDiagLevelDiagnosticRules, parseDiagLevel } from './common/configOptions';
import { ConsoleInterface, ConsoleWithLogLevel, LogLevel } from './common/console';
import { Diagnostic as AnalyzerDiagnostic, DiagnosticCategory } from './common/diagnostic';
import { DiagnosticRule } from './common/diagnosticRules';
import { FileDiagnostics } from './common/diagnosticSink';
import { DocumentRange } from './common/docRange';
import { FileSystem, ReadOnlyFileSystem } from './common/fileSystem';
import { FileWatcherEventType } from './common/fileWatcher';
import { Host } from './common/host';
import {
LanguageServerInterface,
ServerOptions,
ServerSettings,
WorkspaceServices,
} from './common/languageServerInterface';
import { fromLSPAny, isNullProgressReporter } from './common/lspUtils';
import { ProgressReportTracker, ProgressReporter } from './common/progressReporter';
import { ServiceKeys } from './common/serviceKeys';
import { ServiceProvider } from './common/serviceProvider';
import { Position, Range } from './common/textRange';
import { Uri } from './common/uri/uri';
import { convertUriToLspUriString } from './common/uri/uriUtils';
import { hasWorkspaceEditChanges } from './common/workspaceEditUtils';
import { AnalyzerServiceExecutor } from './languageService/analyzerServiceExecutor';
import { CallHierarchyProvider } from './languageService/callHierarchyProvider';
import { CompletionItemData, CompletionProvider } from './languageService/completionProvider';
import { DefinitionFilter, DefinitionProvider, TypeDefinitionProvider } from './languageService/definitionProvider';
import { DocumentHighlightProvider } from './languageService/documentHighlightProvider';
import { CollectionResult } from './languageService/documentSymbolCollector';
import { DocumentSymbolProvider } from './languageService/documentSymbolProvider';
import { DynamicFeature, DynamicFeatures } from './languageService/dynamicFeature';
import { FileWatcherDynamicFeature } from './languageService/fileWatcherDynamicFeature';
import { HoverProvider } from './languageService/hoverProvider';
import { canNavigateToFile } from './languageService/navigationUtils';
import { ReferencesProvider } from './languageService/referencesProvider';
import { RenameProvider } from './languageService/renameProvider';
import { SignatureHelpProvider } from './languageService/signatureHelpProvider';
import { WorkspaceSymbolProvider } from './languageService/workspaceSymbolProvider';
import { Localizer, setLocaleOverride } from './localization/localize';
import { ParseFileResults } from './parser/parser';
import { ClientCapabilities, InitializationOptions } from './types';
import {
InitStatus,
IWorkspaceFactory,
WellKnownWorkspaceKinds,
Workspace,
WorkspaceFactory,
} from './workspaceFactory';
import { PullDiagnosticsDynamicFeature } from './languageService/pullDiagnosticsDynamicFeature';
const UncomputedDiagnosticsVersion = -1;
export function wrapProgressReporter(reporter: WorkDoneProgressReporter): ProgressReporter {
let isDisplayingProgress = false;
return {
isDisplayingProgress: () => {
return isDisplayingProgress;
},
isEnabled: () => {
return true;
},
begin: () => {
isDisplayingProgress = true;
reporter.begin('', /* percentage */ undefined, /* message */ undefined, /* cancellable */ false);
},
report: (message) => {
reporter.report(message);
},
end: () => {
isDisplayingProgress = false;
reporter.done();
},
};
}
export abstract class LanguageServerBase implements LanguageServerInterface, Disposable {
// We support running only one "find all reference" at a time.
private _pendingFindAllRefsCancellationSource: AbstractCancellationTokenSource | undefined;
// We support running only one command at a time.
private _pendingCommandCancellationSource: AbstractCancellationTokenSource | undefined;
private _progressReporter: ProgressReporter;
private _progressReportCounter = 0;
private _lastTriggerKind: CompletionTriggerKind | undefined = CompletionTriggerKind.Invoked;
private _initialized = false;
private _workspaceFoldersChangedDisposable: Disposable | undefined;
private _workspaceDiagnosticsReporter: ResultProgressReporter<WorkspaceDiagnosticReportPartialResult> | undefined;
private _workspaceDiagnosticsProgressReporter: ProgressReporter | undefined;
private _workspaceDiagnosticsResolve: ((value: WorkspaceDiagnosticReport) => void) | undefined;
protected isDisposed = false;
protected client: ClientCapabilities = {
hasConfigurationCapability: false,
hasVisualStudioExtensionsCapability: false,
hasWorkspaceFoldersCapability: false,
hasWatchFileCapability: false,
hasWatchFileRelativePathCapability: false,
hasActiveParameterCapability: false,
hasSignatureLabelOffsetCapability: false,
hasHierarchicalDocumentSymbolCapability: false,
hasWindowProgressCapability: false,
hasGoToDeclarationCapability: false,
hasDocumentChangeCapability: false,
hasDocumentAnnotationCapability: false,
hasCompletionCommitCharCapability: false,
hasCompletionItemDataDefaultCapability: false,
hoverContentFormat: MarkupKind.PlainText,
completionDocFormat: MarkupKind.PlainText,
completionSupportsSnippet: false,
signatureDocFormat: MarkupKind.PlainText,
supportsDeprecatedDiagnosticTag: false,
supportsUnnecessaryDiagnosticTag: false,
supportsTaskItemDiagnosticTag: false,
completionItemResolveSupportsAdditionalTextEdits: false,
supportsPullDiagnostics: false,
requiresPullRelatedInformationCapability: false,
};
protected defaultClientConfig: any;
protected readonly workspaceFactory: IWorkspaceFactory;
protected readonly openFileMap = new Map<string, TextDocument>();
protected readonly fs: FileSystem;
protected readonly caseSensitiveDetector: CaseSensitivityDetector;
// The URIs for which diagnostics are reported
protected readonly documentsWithDiagnostics = new Set<string>();
protected readonly dynamicFeatures = new DynamicFeatures();
constructor(protected serverOptions: ServerOptions, protected connection: Connection) {
// Stash the base directory into a global variable.
// This must happen before fs.getModulePath().
(global as any).__rootDirectory = serverOptions.rootDirectory.getFilePath();
this.console.info(
`${serverOptions.productName} language server ${
serverOptions.version && serverOptions.version + ' '
}starting`
);
this.console.info(`Server root directory: ${serverOptions.rootDirectory}`);
this.fs = this.serverOptions.serviceProvider.fs();
this.caseSensitiveDetector = this.serverOptions.serviceProvider.get(ServiceKeys.caseSensitivityDetector);
this.workspaceFactory = this.createWorkspaceFactory();
// Set the working directory to a known location within
// the extension directory. Otherwise the execution of
// python can have unintended and surprising results.
const moduleDirectory = this.fs.getModulePath();
if (moduleDirectory && this.fs.existsSync(moduleDirectory)) {
this.fs.chdir(moduleDirectory);
}
// Set up callbacks.
this.setupConnection(serverOptions.supportedCommands ?? [], serverOptions.supportedCodeActions ?? []);
this._progressReporter = new ProgressReportTracker(this.createProgressReporter());
// Listen on the connection.
this.connection.listen();
}
get console(): ConsoleInterface {
return this.serverOptions.serviceProvider.console();
}
// Provides access to the client's window.
get window(): RemoteWindow {
return this.connection.window;
}
get supportAdvancedEdits(): boolean {
return this.client.hasDocumentChangeCapability && this.client.hasDocumentAnnotationCapability;
}
get serviceProvider() {
return this.serverOptions.serviceProvider;
}
dispose() {
this.isDisposed = true;
this.workspaceFactory.clear();
this.openFileMap.clear();
this.dynamicFeatures.unregister();
this._workspaceFoldersChangedDisposable?.dispose();
}
abstract createBackgroundAnalysis(serviceId: string, workspaceRoot: Uri): IBackgroundAnalysis | undefined;
abstract getSettings(workspace: Workspace): Promise<ServerSettings>;
// Creates a service instance that's used for analyzing a
// program within a workspace.
createAnalyzerService(
name: string,
workspaceRoot: Uri,
services?: WorkspaceServices,
libraryReanalysisTimeProvider?: LibraryReanalysisTimeProvider
): AnalyzerService {
this.console.info(`Starting service instance "${name}"`);
const serviceId = getNextServiceId(name);
const service = new AnalyzerService(name, this.serverOptions.serviceProvider, {
console: this.console,
hostFactory: this.createHost.bind(this),
importResolverFactory: this.createImportResolver.bind(this),
backgroundAnalysis: services
? services.backgroundAnalysis
: this.createBackgroundAnalysis(serviceId, workspaceRoot),
maxAnalysisTime: this.serverOptions.maxAnalysisTimeInForeground,
backgroundAnalysisProgramFactory: this.createBackgroundAnalysisProgram.bind(this),
libraryReanalysisTimeProvider,
serviceId,
fileSystem: services?.fs ?? this.serverOptions.serviceProvider.fs(),
onInvalidated: (reason) => {
// Don't send requests if the server is disposed.
if (this.isDisposed) {
return;
}
// If we're in openFilesOnly mode and the client supports pull diagnostics, request a refresh. In
// workspace mode we just use the 'push' notification to respond to the workspace diagnostics
if (this.client.supportsPullDiagnostics && service.checkOnlyOpenFiles) {
void this.connection.sendRequest(DiagnosticRefreshRequest.type);
}
},
shouldRunAnalysis: () => {
// We should run analysis if:
// The client doesn't support pull diagnostics (meaning we have to run analysis ourselves)
// or
// We have a workspace partial result callback (meaning in pull mode, we're waiting for workspace results)
return !this.client.supportsPullDiagnostics || this._workspaceDiagnosticsReporter !== undefined;
},
});
service.setCompletionCallback((results) => this.onAnalysisCompletedHandler(service.fs, results));
return service;
}
async getWorkspaces(): Promise<Workspace[]> {
const workspaces = this.workspaceFactory.items();
for (const workspace of workspaces) {
await workspace.isInitialized.promise;
}
return workspaces;
}
async getWorkspaceForFile(fileUri: Uri, pythonPath?: Uri): Promise<Workspace> {
return this.workspaceFactory.getWorkspaceForFile(fileUri, pythonPath);
}
async getContainingWorkspacesForFile(fileUri: Uri): Promise<Workspace[]> {
return this.workspaceFactory.getContainingWorkspacesForFile(fileUri);
}
reanalyze() {
this.workspaceFactory.items().forEach((workspace) => {
workspace.service.invalidateAndForceReanalysis(InvalidatedReason.Reanalyzed);
});
}
restart() {
this.workspaceFactory.items().forEach((workspace) => {
workspace.service.restart();
});
}
updateSettingsForAllWorkspaces(): void {
const tasks: Promise<void>[] = [];
this.workspaceFactory.items().forEach((workspace) => {
// Updating settings can change workspace's file ownership. Make workspace uninitialized so that
// features can wait until workspace gets new settings.
// the file's ownership can also changed by `pyrightconfig.json` changes, but those are synchronous
// operation, so it won't affect this.
workspace.isInitialized = workspace.isInitialized.reset();
tasks.push(this.updateSettingsForWorkspace(workspace, workspace.isInitialized));
});
Promise.all(tasks).then(() => {
this.dynamicFeatures.register();
});
}
async updateSettingsForWorkspace(
workspace: Workspace,
status: InitStatus | undefined,
serverSettings?: ServerSettings
): Promise<void> {
try {
status?.markCalled();
serverSettings = serverSettings ?? (await this.getSettings(workspace));
// Set logging level first.
(this.console as ConsoleWithLogLevel).level = serverSettings.logLevel ?? LogLevel.Info;
this.dynamicFeatures.update(serverSettings);
// If the workspace mode has changed, we may need to resolve the workspace diagnostics promise.
if (serverSettings.openFilesOnly && this._workspaceDiagnosticsResolve) {
this._workspaceDiagnosticsResolve({ items: [] });
this._workspaceDiagnosticsResolve = undefined;
}
// Then use the updated settings to restart the service.
this.updateOptionsAndRestartService(workspace, serverSettings);
workspace.disableLanguageServices = !!serverSettings.disableLanguageServices;
workspace.disableTaggedHints = !!serverSettings.disableTaggedHints;
workspace.disableOrganizeImports = !!serverSettings.disableOrganizeImports;
} finally {
// Don't use workspace.isInitialized directly since it might have been
// reset due to pending config change event.
// The workspace is now open for business.
status?.resolve();
}
}
updateOptionsAndRestartService(
workspace: Workspace,
serverSettings: ServerSettings,
typeStubTargetImportName?: string
) {
AnalyzerServiceExecutor.runWithOptions(workspace, serverSettings, { typeStubTargetImportName });
workspace.searchPathsToWatch = workspace.service.librarySearchUrisToWatch ?? [];
}
protected get workspaceDiagnosticsReporter() {
return this._workspaceDiagnosticsReporter;
}
protected abstract executeCommand(params: ExecuteCommandParams, token: CancellationToken): Promise<any>;
protected abstract isLongRunningCommand(command: string): boolean;
protected abstract isRefactoringCommand(command: string): boolean;
protected abstract executeCodeAction(
params: CodeActionParams,
token: CancellationToken
): Promise<(Command | CodeAction)[] | undefined | null>;
protected async getConfiguration(scopeUri: Uri | undefined, section: string) {
if (this.client.hasConfigurationCapability) {
const item: ConfigurationItem = {};
if (scopeUri !== undefined) {
item.scopeUri = scopeUri.toString();
}
if (section !== undefined) {
item.section = section;
}
return this.connection.workspace.getConfiguration(item);
}
if (this.defaultClientConfig) {
return getNestedProperty(this.defaultClientConfig, section);
}
return undefined;
}
protected isOpenFilesOnly(diagnosticMode: string): boolean {
return diagnosticMode !== 'workspace';
}
protected getSeverityOverrides(value: string | boolean): DiagnosticSeverityOverrides | undefined {
const enumValue = parseDiagLevel(value);
if (!enumValue) {
return undefined;
}
if (getDiagnosticSeverityOverrides().includes(enumValue)) {
return enumValue;
}
return undefined;
}
protected getDiagnosticRuleName(value: string): DiagnosticRule | undefined {
const enumValue = value as DiagnosticRule;
if (getDiagLevelDiagnosticRules().includes(enumValue)) {
return enumValue;
}
return undefined;
}
protected abstract createHost(): Host;
protected abstract createImportResolver(
serviceProvider: ServiceProvider,
options: ConfigOptions,
host: Host
): ImportResolver;
protected createBackgroundAnalysisProgram(
serviceId: string,
serviceProvider: ServiceProvider,
configOptions: ConfigOptions,
importResolver: ImportResolver,
backgroundAnalysis?: IBackgroundAnalysis,
maxAnalysisTime?: MaxAnalysisTime
): BackgroundAnalysisProgram {
return new BackgroundAnalysisProgram(
serviceId,
serviceProvider,
configOptions,
importResolver,
backgroundAnalysis,
maxAnalysisTime,
/* disableChecker */ undefined
);
}
protected createWorkspaceFactory(): IWorkspaceFactory {
return new WorkspaceFactory(
this.console,
this.createAnalyzerServiceForWorkspace.bind(this),
this.onWorkspaceCreated.bind(this),
this.onWorkspaceRemoved.bind(this),
this.serviceProvider
);
}
protected setupConnection(supportedCommands: string[], supportedCodeActions: string[]): void {
// After the server has started the client sends an initialize request. The server receives
// in the passed params the rootPath of the workspace plus the client capabilities.
this.connection.onInitialize((params) => this.initialize(params, supportedCommands, supportedCodeActions));
this.connection.onInitialized(() => this.onInitialized());
this.connection.onDidChangeConfiguration((params) => this.onDidChangeConfiguration(params));
this.connection.onCodeAction((params, token) => this.executeCodeAction(params, token));
this.connection.onDefinition(async (params, token) => this.onDefinition(params, token));
this.connection.onDeclaration(async (params, token) => this.onDeclaration(params, token));
this.connection.onTypeDefinition(async (params, token) => this.onTypeDefinition(params, token));
this.connection.onReferences(async (params, token, workDoneReporter, resultReporter) =>
this.onReferences(params, token, workDoneReporter, resultReporter)
);
this.connection.onDocumentSymbol(async (params, token) => this.onDocumentSymbol(params, token));
this.connection.onWorkspaceSymbol(async (params, token, _, resultReporter) =>
this.onWorkspaceSymbol(params, token, resultReporter)
);
this.connection.onHover(async (params, token) => this.onHover(params, token));
this.connection.onDocumentHighlight(async (params, token) => this.onDocumentHighlight(params, token));
this.connection.onSignatureHelp(async (params, token) => this.onSignatureHelp(params, token));
this.connection.onCompletion((params, token) => this.onCompletion(params, token));
this.connection.onCompletionResolve(async (params, token) => this.onCompletionResolve(params, token));
this.connection.onPrepareRename(async (params, token) => this.onPrepareRenameRequest(params, token));
this.connection.onRenameRequest(async (params, token) => this.onRenameRequest(params, token));
const callHierarchy = this.connection.languages.callHierarchy;
callHierarchy.onPrepare(async (params, token) => this.onCallHierarchyPrepare(params, token));
callHierarchy.onIncomingCalls(async (params, token) => this.onCallHierarchyIncomingCalls(params, token));
callHierarchy.onOutgoingCalls(async (params, token) => this.onCallHierarchyOutgoingCalls(params, token));
this.connection.onDidOpenTextDocument(async (params) => this.onDidOpenTextDocument(params));
this.connection.onDidChangeTextDocument(async (params) => this.onDidChangeTextDocument(params));
this.connection.onDidCloseTextDocument(async (params) => this.onDidCloseTextDocument(params));
this.connection.onDidChangeWatchedFiles((params) => this.onDidChangeWatchedFiles(params));
this.connection.languages.diagnostics.on(async (params, token) => this.onDiagnostics(params, token));
this.connection.languages.diagnostics.onWorkspace(async (params, token, progress, reporter) =>
this.onWorkspaceDiagnostics(params, token, progress, reporter)
);
this.connection.onExecuteCommand(async (params, token, reporter) =>
this.onExecuteCommand(params, token, reporter)
);
this.connection.onShutdown(async (token) => this.onShutdown(token));
}
protected async initialize(
params: InitializeParams,
supportedCommands: string[],
supportedCodeActions: string[]
): Promise<InitializeResult> {
if (params.locale) {
setLocaleOverride(params.locale);
}
const initializationOptions = (params.initializationOptions ?? {}) as LSPObject & InitializationOptions;
const capabilities = params.capabilities;
this.client.hasConfigurationCapability = !!capabilities.workspace?.configuration;
this.client.hasWatchFileCapability = !!capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration;
this.client.hasWatchFileRelativePathCapability =
!!capabilities.workspace?.didChangeWatchedFiles?.relativePatternSupport;
this.client.hasWorkspaceFoldersCapability = !!capabilities.workspace?.workspaceFolders;
this.client.hasVisualStudioExtensionsCapability = !!(capabilities as any)._vs_supportsVisualStudioExtensions;
this.client.hasActiveParameterCapability =
!!capabilities.textDocument?.signatureHelp?.signatureInformation?.activeParameterSupport;
this.client.hasSignatureLabelOffsetCapability =
!!capabilities.textDocument?.signatureHelp?.signatureInformation?.parameterInformation?.labelOffsetSupport;
this.client.hasHierarchicalDocumentSymbolCapability =
!!capabilities.textDocument?.documentSymbol?.hierarchicalDocumentSymbolSupport;
this.client.hasDocumentChangeCapability =
!!capabilities.workspace?.workspaceEdit?.documentChanges &&
!!capabilities.workspace.workspaceEdit?.resourceOperations;
this.client.hasDocumentAnnotationCapability = !!capabilities.workspace?.workspaceEdit?.changeAnnotationSupport;
this.client.hasCompletionCommitCharCapability =
!!capabilities.textDocument?.completion?.completionList?.itemDefaults &&
!!capabilities.textDocument?.completion?.completionItem?.commitCharactersSupport;
this.client.hasCompletionItemDataDefaultCapability =
!!capabilities.textDocument?.completion?.completionList?.itemDefaults?.includes('data') &&
!!capabilities.textDocument?.completion?.completionList?.applyKindSupport;
this.client.hoverContentFormat = this._getCompatibleMarkupKind(capabilities.textDocument?.hover?.contentFormat);
this.client.completionDocFormat = this._getCompatibleMarkupKind(
capabilities.textDocument?.completion?.completionItem?.documentationFormat
);
this.client.completionSupportsSnippet = !!capabilities.textDocument?.completion?.completionItem?.snippetSupport;
this.client.signatureDocFormat = this._getCompatibleMarkupKind(
capabilities.textDocument?.signatureHelp?.signatureInformation?.documentationFormat
);
const supportedDiagnosticTags = capabilities.textDocument?.publishDiagnostics?.tagSupport?.valueSet || [];
this.client.supportsUnnecessaryDiagnosticTag = supportedDiagnosticTags.some(
(tag) => tag === DiagnosticTag.Unnecessary
);
this.client.supportsDeprecatedDiagnosticTag = supportedDiagnosticTags.some(
(tag) => tag === DiagnosticTag.Deprecated
);
// if the client is running in VS, it always supports task item diagnostics
this.client.supportsTaskItemDiagnosticTag = this.client.hasVisualStudioExtensionsCapability;
this.client.hasWindowProgressCapability = !!capabilities.window?.workDoneProgress;
this.client.hasGoToDeclarationCapability = !!capabilities.textDocument?.declaration;
this.client.completionItemResolveSupportsAdditionalTextEdits =
!!capabilities.textDocument?.completion?.completionItem?.resolveSupport?.properties.some(
(p) => p === 'additionalTextEdits'
);
this.client.supportsPullDiagnostics =
!!capabilities.textDocument?.diagnostic?.dynamicRegistration &&
initializationOptions?.disablePullDiagnostics !== true;
this.client.requiresPullRelatedInformationCapability =
!!capabilities.textDocument?.diagnostic?.relatedInformation &&
initializationOptions?.disablePullDiagnostics !== true;
// Create a service instance for each of the workspace folders.
this.workspaceFactory.handleInitialize(params);
if (this.client.hasWatchFileCapability) {
this.addDynamicFeature(
new FileWatcherDynamicFeature(
this.connection,
this.client.hasWatchFileRelativePathCapability,
this.fs,
this.workspaceFactory
)
);
}
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
definitionProvider: { workDoneProgress: true },
declarationProvider: { workDoneProgress: true },
typeDefinitionProvider: { workDoneProgress: true },
referencesProvider: { workDoneProgress: true },
documentSymbolProvider: { workDoneProgress: true },
workspaceSymbolProvider: { workDoneProgress: true },
hoverProvider: { workDoneProgress: true },
documentHighlightProvider: { workDoneProgress: true },
renameProvider: { prepareProvider: true, workDoneProgress: true },
completionProvider: {
triggerCharacters: this.client.hasVisualStudioExtensionsCapability
? ['.', '[', '@', '"', "'"]
: ['.', '[', '"', "'"],
resolveProvider: true,
workDoneProgress: true,
completionItem: {
labelDetailsSupport: true,
},
},
signatureHelpProvider: {
triggerCharacters: ['(', ',', ')'],
workDoneProgress: true,
},
codeActionProvider: {
codeActionKinds: supportedCodeActions,
workDoneProgress: true,
},
executeCommandProvider: {
commands: supportedCommands,
workDoneProgress: true,
},
callHierarchyProvider: true,
workspace: {
workspaceFolders: {
supported: true,
changeNotifications: true,
},
},
},
};
if (this.client.supportsPullDiagnostics) {
this.addDynamicFeature(new PullDiagnosticsDynamicFeature(this.connection, this.serverOptions.productName));
}
return result;
}
protected onInitialized() {
this.handleInitialized((event) => {
this.workspaceFactory.handleWorkspaceFoldersChanged(event, null);
this.dynamicFeatures.register();
});
}
protected handleInitialized(changeWorkspaceFolderHandler: (e: WorkspaceFoldersChangeEvent) => any) {
// Mark as initialized. We need this to make sure to
// not send config updates before this point.
this._initialized = true;
if (this.client.hasWorkspaceFoldersCapability) {
this._workspaceFoldersChangedDisposable =
this.connection.workspace.onDidChangeWorkspaceFolders(changeWorkspaceFolderHandler);
}
this.dynamicFeatures.register();
this.updateSettingsForAllWorkspaces();
}
protected onDidChangeConfiguration(params: DidChangeConfigurationParams) {
this.console.log(`Received updated settings`);
if (params?.settings) {
this.defaultClientConfig = params?.settings;
}
this.updateSettingsForAllWorkspaces();
}
protected async onDefinition(
params: TextDocumentPositionParams,
token: CancellationToken
): Promise<Definition | DefinitionLink[] | undefined | null> {
return this.getDefinitions(
params,
token,
this.client.hasGoToDeclarationCapability ? DefinitionFilter.PreferSource : DefinitionFilter.All,
(workspace, filePath, position, filter, token) =>
workspace.service.run((program) => {
return new DefinitionProvider(program, filePath, position, filter, token).getDefinitions();
}, token)
);
}
protected async onDeclaration(
params: TextDocumentPositionParams,
token: CancellationToken
): Promise<Declaration | DeclarationLink[] | undefined | null> {
return this.getDefinitions(
params,
token,
this.client.hasGoToDeclarationCapability ? DefinitionFilter.PreferStubs : DefinitionFilter.All,
(workspace, filePath, position, filter, token) =>
workspace.service.run((program) => {
return new DefinitionProvider(program, filePath, position, filter, token).getDefinitions();
}, token)
);
}
protected async onTypeDefinition(
params: TextDocumentPositionParams,
token: CancellationToken
): Promise<Definition | DefinitionLink[] | undefined | null> {
return this.getDefinitions(params, token, DefinitionFilter.All, (workspace, filePath, position, _, token) =>
workspace.service.run((program) => {
return new TypeDefinitionProvider(program, filePath, position, token).getDefinitions();
}, token)
);
}
protected async getDefinitions(
params: TextDocumentPositionParams,
token: CancellationToken,
filter: DefinitionFilter,
getDefinitionsFunc: (
workspace: Workspace,
fileUri: Uri,
position: Position,
filter: DefinitionFilter,
token: CancellationToken
) => DocumentRange[] | undefined
) {
this.recordUserInteractionTime();
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return undefined;
}
const locations = getDefinitionsFunc(workspace, uri, params.position, filter, token);
if (!locations) {
return undefined;
}
return locations
.filter((loc) => this.canNavigateToFile(loc.uri, workspace.service.fs))
.map((loc) => Location.create(convertUriToLspUriString(workspace.service.fs, loc.uri), loc.range));
}
protected async onReferences(
params: ReferenceParams,
token: CancellationToken,
workDoneReporter: WorkDoneProgressReporter,
resultReporter: ResultProgressReporter<Location[]> | undefined,
createDocumentRange?: (uri: Uri, result: CollectionResult, parseResults: ParseFileResults) => DocumentRange,
convertToLocation?: (fs: ReadOnlyFileSystem, ranges: DocumentRange) => Location | undefined
): Promise<Location[] | null | undefined> {
if (this._pendingFindAllRefsCancellationSource) {
this._pendingFindAllRefsCancellationSource.cancel();
this._pendingFindAllRefsCancellationSource = undefined;
}
// VS Code doesn't support cancellation of "find all references".
// We provide a progress bar a cancellation button so the user can cancel
// any long-running actions.
const progress = await this.getProgressReporter(
workDoneReporter,
Localizer.CodeAction.findingReferences(),
token
);
const source = progress.source;
this._pendingFindAllRefsCancellationSource = source;
try {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return;
}
return workspace.service.run((program) => {
return new ReferencesProvider(
program,
source.token,
createDocumentRange,
convertToLocation
).reportReferences(uri, params.position, params.context.includeDeclaration, resultReporter);
}, token);
} finally {
progress.reporter.done();
source.dispose();
}
}
protected async onDocumentSymbol(
params: DocumentSymbolParams,
token: CancellationToken
): Promise<DocumentSymbol[] | SymbolInformation[] | null | undefined> {
this.recordUserInteractionTime();
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return undefined;
}
return workspace.service.run((program) => {
return new DocumentSymbolProvider(
program,
uri,
this.client.hasHierarchicalDocumentSymbolCapability,
{ includeAliases: false },
token
).getSymbols();
}, token);
}
protected onWorkspaceSymbol(
params: WorkspaceSymbolParams,
token: CancellationToken,
resultReporter: ResultProgressReporter<SymbolInformation[]> | undefined
): Promise<SymbolInformation[] | WorkspaceSymbol[] | null | undefined> {
const result = new WorkspaceSymbolProvider(
this.workspaceFactory.items(),
resultReporter,
params.query,
token
).reportSymbols();
return Promise.resolve(result);
}
protected async onHover(params: HoverParams, token: CancellationToken) {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return undefined;
}
return workspace.service.run((program) => {
return new HoverProvider(program, uri, params.position, this.client.hoverContentFormat, token).getHover();
}, token);
}
protected async onDocumentHighlight(
params: DocumentHighlightParams,
token: CancellationToken
): Promise<DocumentHighlight[] | null | undefined> {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
return workspace.service.run((program) => {
return new DocumentHighlightProvider(program, uri, params.position, token).getDocumentHighlight();
}, token);
}
protected async onSignatureHelp(
params: SignatureHelpParams,
token: CancellationToken
): Promise<SignatureHelp | undefined | null> {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return;
}
return workspace.service.run((program) => {
return new SignatureHelpProvider(
program,
uri,
params.position,
this.client.signatureDocFormat,
this.client.hasSignatureLabelOffsetCapability,
this.client.hasActiveParameterCapability,
params.context,
program.serviceProvider.docStringService(),
token
).getSignatureHelp();
}, token);
}
protected setCompletionIncomplete(params: CompletionParams, completions: CompletionList | null) {
// We set completion incomplete for the first invocation and next consecutive call,
// but after that we mark it as completed so the client doesn't repeatedly call back.
// We mark the first one as incomplete because completion could be invoked without
// any meaningful character provided, such as an explicit completion invocation (ctrl+space)
// or a period. That might cause us to not include some items (e.g., auto-imports).
// The next consecutive call provides some characters to help us to pick
// better completion items. After that, we are not going to introduce new items,
// so we can let the client to do the filtering and caching.
const completionIncomplete =
this._lastTriggerKind !== CompletionTriggerKind.TriggerForIncompleteCompletions ||
params.context?.triggerKind !== CompletionTriggerKind.TriggerForIncompleteCompletions;
this._lastTriggerKind = params.context?.triggerKind;
if (completions) {
completions.isIncomplete = completionIncomplete;
}
}
protected async onCompletion(params: CompletionParams, token: CancellationToken): Promise<CompletionList | null> {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return null;
}
return await workspace.service.run(async (program) => {
const completions = await new CompletionProvider(
program,
uri,
params.position,
{
format: this.client.completionDocFormat,
snippet: this.client.completionSupportsSnippet,
lazyEdit: false,
triggerCharacter: params?.context?.triggerCharacter,
completionItemDataDefault: this.client.hasCompletionItemDataDefaultCapability,
},
token
).getCompletions();
this.setCompletionIncomplete(params, completions);
return completions;
}, token);
}
// Cancellation bugs in vscode and LSP:
// https://github.com/microsoft/vscode-languageserver-node/issues/615
// https://github.com/microsoft/vscode/issues/95485
//
// If resolver throws cancellation exception, LSP and VSCode
// cache that result and never call us back.
protected async onCompletionResolve(params: CompletionItem, token: CancellationToken): Promise<CompletionItem> {
const completionItemData = fromLSPAny<CompletionItemData>(params.data);
if (completionItemData && completionItemData.uri) {
const uri = Uri.parse(completionItemData.uri, this.caseSensitiveDetector);
const workspace = await this.getWorkspaceForFile(uri);
workspace.service.run((program) => {
return new CompletionProvider(
program,
uri,
completionItemData.position,
{
format: this.client.completionDocFormat,
snippet: this.client.completionSupportsSnippet,
lazyEdit: false,
},
token
).resolveCompletionItem(params);
}, token);
}
return params;
}
protected async onPrepareRenameRequest(
params: PrepareRenameParams,
token: CancellationToken
): Promise<Range | { range: Range; placeholder: string } | null> {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const isUntitled = uri.isUntitled();
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return null;
}
return workspace.service.run((program) => {
return new RenameProvider(program, uri, params.position, token).canRenameSymbol(
workspace.kinds.includes(WellKnownWorkspaceKinds.Default),
isUntitled
);
}, token);
}
protected async onRenameRequest(
params: RenameParams,
token: CancellationToken
): Promise<WorkspaceEdit | null | undefined> {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const isUntitled = uri.isUntitled();
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return;
}
return workspace.service.run((program) => {
return new RenameProvider(program, uri, params.position, token).renameSymbol(
params.newName,
workspace.kinds.includes(WellKnownWorkspaceKinds.Default),
isUntitled
);
}, token);
}
protected async onCallHierarchyPrepare(
params: CallHierarchyPrepareParams,
token: CancellationToken
): Promise<CallHierarchyItem[] | null> {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return null;
}
return workspace.service.run((program) => {
return new CallHierarchyProvider(program, uri, params.position, token).onPrepare();
}, token);
}
protected async onCallHierarchyIncomingCalls(params: CallHierarchyIncomingCallsParams, token: CancellationToken) {
const uri = this.convertLspUriStringToUri(params.item.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return null;
}
return workspace.service.run((program) => {
return new CallHierarchyProvider(program, uri, params.item.range.start, token).getIncomingCalls();
}, token);
}
protected async onCallHierarchyOutgoingCalls(
params: CallHierarchyOutgoingCallsParams,
token: CancellationToken
): Promise<CallHierarchyOutgoingCall[] | null> {
const uri = this.convertLspUriStringToUri(params.item.uri);
const workspace = await this.getWorkspaceForFile(uri);
if (workspace.disableLanguageServices) {
return null;
}
return workspace.service.run((program) => {
return new CallHierarchyProvider(program, uri, params.item.range.start, token).getOutgoingCalls();
}, token);
}
protected async onDidOpenTextDocument(params: DidOpenTextDocumentParams, ipythonMode = IPythonMode.None) {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
let doc = this.openFileMap.get(uri.key);
if (doc) {
// We shouldn't get an open text document request for an already-opened doc.
this.console.error(`Received redundant open text document command for ${uri}`);
TextDocument.update(doc, [{ text: params.textDocument.text }], params.textDocument.version);
} else {
doc = TextDocument.create(
params.textDocument.uri,
'python',
params.textDocument.version,
params.textDocument.text
);
}
this.openFileMap.set(uri.key, doc);
// Send this open to all the workspaces that might contain this file.
const workspaces = await this.getContainingWorkspacesForFile(uri);
workspaces.forEach((w) => {
w.service.setFileOpened(uri, params.textDocument.version, params.textDocument.text, ipythonMode);
});
}
protected async onDidChangeTextDocument(params: DidChangeTextDocumentParams, ipythonMode = IPythonMode.None) {
this.recordUserInteractionTime();
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const doc = this.openFileMap.get(uri.key);
if (!doc) {
// We shouldn't get a change text request for a closed doc.
this.console.error(`Received change text document command for closed file ${uri}`);
return;
}
TextDocument.update(doc, params.contentChanges, params.textDocument.version);
const newContents = doc.getText();
// Send this change to all the workspaces that might contain this file.
const workspaces = await this.getContainingWorkspacesForFile(uri);
workspaces.forEach((w) => {
w.service.updateOpenFileContents(uri, params.textDocument.version, newContents, ipythonMode);
});
}
protected async onDidCloseTextDocument(params: DidCloseTextDocumentParams) {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
// Stop tracking the document as open before any async work so that a request handled
// immediately after this close (e.g. a pull-diagnostics re-pull) observes the file as closed.
this.openFileMap.delete(uri.key);
// Send this close to all the workspaces that might contain this file.
const workspaces = await this.getContainingWorkspacesForFile(uri);
workspaces.forEach((w) => {
w.service.setFileClosed(uri);
});
}
protected async onDiagnostics(params: DocumentDiagnosticParams, token: CancellationToken) {
const uri = this.convertLspUriStringToUri(params.textDocument.uri);
const workspace = await this.getWorkspaceForFile(uri);
let sourceFile = workspace.service.getSourceFile(uri);
let diagnosticsVersion = sourceFile?.isCheckingRequired()
? UncomputedDiagnosticsVersion
: sourceFile?.getDiagnosticVersion() ?? UncomputedDiagnosticsVersion;
const result: DocumentDiagnosticReport = {
kind: 'full',
resultId: sourceFile?.getDiagnosticVersion()?.toString(),
items: [],
};
if (
workspace.disableLanguageServices ||
!canNavigateToFile(workspace.service.fs, uri) ||
token.isCancellationRequested
) {
return result;
}
// In open-files-only mode, only report diagnostics for files the client currently has open.
// A library/out-of-workspace file may have been transiently opened (e.g. via go-to-definition)
// and analyzed; once the client closes it, a re-pull must clear those diagnostics by returning
// an empty `full` report rather than re-analyzing the now-closed file.
if (workspace.service.checkOnlyOpenFiles && !this.openFileMap.has(uri.key)) {
return result;
}
// Send a progress message to the client.
this.incrementAnalysisProgress();
try {
// Reanalyze the file if it's not up to date.
if (params.previousResultId !== diagnosticsVersion.toString() && sourceFile) {
let diagnosticsVersionAfter = UncomputedDiagnosticsVersion - 1; // Just has to be different
let serverDiagnostics: AnalyzerDiagnostic[] = [];
// Loop until we analyze the same version that we started with.
while (diagnosticsVersion !== diagnosticsVersionAfter && !token.isCancellationRequested && sourceFile) {
// Reset the version we're analyzing
sourceFile = workspace.service.getSourceFile(uri);
diagnosticsVersion = sourceFile?.getDiagnosticVersion() ?? UncomputedDiagnosticsVersion;
// Then reanalyze the file (this should go to the background thread so this thread can handle other requests).
if (sourceFile) {
serverDiagnostics = await workspace.service.analyzeFileAndGetDiagnostics(uri, token);
}
// If any text edits came in, make sure we reanalyze the file. Diagnostics version should be reset to zero
// if a text edit comes in.
const sourceFileAfter = workspace.service.getSourceFile(uri);
diagnosticsVersionAfter = sourceFileAfter?.getDiagnosticVersion() ?? UncomputedDiagnosticsVersion;
}
// Then convert the diagnostics to the LSP format.
const lspDiagnostics = this._convertDiagnostics(workspace.service.fs, serverDiagnostics).filter(
(d) => d !== undefined
) as Diagnostic[];
result.resultId =
diagnosticsVersionAfter === UncomputedDiagnosticsVersion
? undefined
: diagnosticsVersionAfter.toString();
result.items = lspDiagnostics;
} else {
(result as any).kind = 'unchanged';
result.resultId =
diagnosticsVersion === UncomputedDiagnosticsVersion ? undefined : diagnosticsVersion.toString();
delete (result as any).items;
}
} finally {
this.decrementAnalysisProgress();
}
return result;
}
protected async onWorkspaceDiagnostics(
params: WorkspaceDiagnosticParams,
token: CancellationToken,
workDoneProgress: WorkDoneProgressReporter,
resultReporter?: ResultProgressReporter<WorkspaceDiagnosticReportPartialResult>
) {
// Resolve any pending workspace diagnostics. We only allow one at a time.
this._workspaceDiagnosticsResolve?.({ items: [] });
this._workspaceDiagnosticsResolve = undefined;
// Save the progress reporters and force a refresh of analysis.
this._workspaceDiagnosticsProgressReporter = !isNullProgressReporter(workDoneProgress)
? wrapProgressReporter(workDoneProgress)
: undefined;
this._workspaceDiagnosticsReporter = resultReporter;
this.workspaceFactory.getNonDefaultWorkspaces().forEach((workspace) => {
workspace.service.invalidateAndScheduleReanalysis(InvalidatedReason.Reanalyzed);
});
return new Promise<WorkspaceDiagnosticReport>((resolve, reject) => {
// We never resolve as this should be a continually occurring process. Scheduling analysis
// should cause a new workspace diagnostic to be generated.
// Save the resolve callback to be used during shutdown so that tests don't crash
// on the unresolved promise for the workspace diagnostics.
this._workspaceDiagnosticsResolve = resolve;
});
}
protected onDidChangeWatchedFiles(params: DidChangeWatchedFilesParams) {
params.changes.forEach((change) => {
const filePath = this.fs.realCasePath(this.convertLspUriStringToUri(change.uri));
const eventType: FileWatcherEventType = change.type === 1 ? 'add' : 'change';
this.serverOptions.fileWatcherHandler.onFileChange(eventType, filePath);
});
}
protected async onExecuteCommand(
params: ExecuteCommandParams,
token: CancellationToken,
reporter: WorkDoneProgressReporter
) {
// Cancel running command if there is one.
if (this._pendingCommandCancellationSource) {
this._pendingCommandCancellationSource.cancel();
this._pendingCommandCancellationSource = undefined;
}
const executeCommand = async (token: CancellationToken) => {
const result = await this.executeCommand(params, token);
if (WorkspaceEdit.is(result) && hasWorkspaceEditChanges(result)) {
// Tell client to apply edits.
// Do not await; the client isn't expecting a result.
this.connection.workspace.applyEdit({
label: `Command '${params.command}'`,
edit: result,
metadata: { isRefactoring: this.isRefactoringCommand(params.command) },
});
}
if (CommandResult.is(result) && hasWorkspaceEditChanges(result.edits)) {
// Tell client to apply edits.
// Await so that we return after the edit is complete.
await this.connection.workspace.applyEdit({
label: result.label,
edit: result.edits,
metadata: { isRefactoring: this.isRefactoringCommand(params.command) },
});
}
return result;
};
if (this.isLongRunningCommand(params.command)) {
// Create a progress dialog for long-running commands.
const progress = await this.getProgressReporter(reporter, Localizer.CodeAction.executingCommand(), token);
const source = progress.source;
this._pendingCommandCancellationSource = source;
try {
const result = await executeCommand(source.token);
return result;
} finally {
progress.reporter.done();
source.dispose();
}
} else {
const result = await executeCommand(token);
return result;
}
}
protected onShutdown(token: CancellationToken) {
// Shutdown remaining workspaces.
this._workspaceDiagnosticsResolve?.({ items: [] });
this.workspaceFactory.clear();
// Stop tracking all open files.
this.openFileMap.clear();
this.serviceProvider.dispose();
return Promise.resolve();
}
protected convertDiagnostics(fs: FileSystem, fileDiagnostics: FileDiagnostics): PublishDiagnosticsParams[] {
return [
{
uri: convertUriToLspUriString(fs, fileDiagnostics.fileUri),
version: fileDiagnostics.version,
diagnostics: this._convertDiagnostics(fs, fileDiagnostics.diagnostics),
},
];
}
protected getDiagCode(_diag: AnalyzerDiagnostic, rule: string | undefined): string | undefined {
return rule;
}
protected onAnalysisCompletedHandler(fs: FileSystem, results: AnalysisResults): void {
// Send the computed diagnostics to the client.
results.diagnostics.forEach((fileDiag) => {
if (!this.canNavigateToFile(fileDiag.fileUri, fs)) {
return;
}
this.sendDiagnostics(this.convertDiagnostics(fs, fileDiag));
});
const reporter = this.getAnalysisProgressReporter();
if (!reporter.isEnabled(results)) {
// Make sure to disable progress bar if it is currently active.
// This can happen if a user changes typeCheckingMode in the middle
// of analysis.
// end() is noop if there is no active progress bar.
reporter.end();
return;
}
// Update progress.
this.sendProgressMessage(results.requiringAnalysisCount.files, results.requiringAnalysisCount.cells);
}
protected incrementAnalysisProgress() {
this._progressReportCounter += 1;
this.sendProgressMessage(this._progressReportCounter);
}
protected decrementAnalysisProgress() {
this._progressReportCounter -= 1;
if (this._progressReportCounter < 0) {
this._progressReportCounter = 0;
}
this.sendProgressMessage(this._progressReportCounter);
}
protected getAnalysisProgressReporter(): ProgressReporter {
if (this._workspaceDiagnosticsProgressReporter) {
return this._workspaceDiagnosticsProgressReporter;
}
return this._progressReporter;
}
protected sendProgressMessage(fileCount: number, cellCount?: number) {
const reporter = this.getAnalysisProgressReporter();
if (fileCount <= 0) {
reporter.end();
return;
}
const progressMessage =
fileCount === 1
? Localizer.CodeAction.filesToAnalyzeOne()
: Localizer.CodeAction.filesToAnalyzeCount().format({
count: fileCount,
});
// Update progress.
if (!reporter.isDisplayingProgress()) {
reporter.begin();
}
reporter.report(progressMessage);
}
protected onWorkspaceCreated(workspace: Workspace) {
// Update settings on this workspace (but only if initialize has happened)
if (this._initialized) {
this.updateSettingsForWorkspace(workspace, workspace.isInitialized).catch(() => {});
}
// Otherwise the initialize completion should cause settings to be updated on all workspaces.
}
protected onWorkspaceRemoved(workspace: Workspace) {
const documentsWithDiagnosticsList = [...this.documentsWithDiagnostics];
const otherWorkspaces = this.workspaceFactory.items().filter((w) => w !== workspace);
for (const uri of documentsWithDiagnosticsList) {
const fileUri = this.convertLspUriStringToUri(uri);
if (workspace.service.isTracked(fileUri)) {
// Do not clean up diagnostics for files tracked by multiple workspaces
if (otherWorkspaces.some((w) => w.service.isTracked(fileUri))) {
continue;
}
this.sendDiagnostics([
{
uri: uri,
diagnostics: [],
},
]);
}
}
}
protected createAnalyzerServiceForWorkspace(
name: string,
workspaceRoot: Uri | undefined,
kinds: string[],
services?: WorkspaceServices
): AnalyzerService {
// 5 seconds default
const defaultBackOffTime = 5 * 1000;
return this.createAnalyzerService(name, workspaceRoot || Uri.empty(), services, () => defaultBackOffTime);
}
protected recordUserInteractionTime() {
// Tell all of the services that the user is actively
// interacting with one or more editors, so they should
// back off from performing any work.
this.workspaceFactory.items().forEach((workspace: { service: { recordUserInteractionTime: () => void } }) => {
workspace.service.recordUserInteractionTime();
});
}
protected getDocumentationUrlForDiagnostic(diag: AnalyzerDiagnostic): string | undefined {
const rule = diag.getRule();
if (rule) {
// Configuration.md is configured to have a link for every rule name.
return `https://github.com/microsoft/pyright/blob/main/docs/configuration.md#${rule}`;
}
return undefined;
}
protected abstract createProgressReporter(): ProgressReporter;
protected canNavigateToFile(path: Uri, fs: FileSystem): boolean {
return canNavigateToFile(fs, path);
}
protected async getProgressReporter(reporter: WorkDoneProgressReporter, title: string, token: CancellationToken) {
// This is a bit ugly, but we need to determine whether the provided reporter
// is an actual client-side progress reporter or a dummy (null) progress reporter
// created by the LSP library. If it's the latter, we'll create a server-initiated
// progress reporter.
if (!isNullProgressReporter(reporter)) {
return { reporter: reporter, source: CancelAfter(this.serviceProvider.cancellationProvider(), token) };
}
const serverInitiatedReporter = await this.connection.window.createWorkDoneProgress();
serverInitiatedReporter.begin(
title,
/* percentage */ undefined,
/* message */ undefined,
/* cancellable */ true
);
return {
reporter: serverInitiatedReporter,
source: CancelAfter(this.serviceProvider.cancellationProvider(), token, serverInitiatedReporter.token),
};
}
protected sendDiagnostics(params: PublishDiagnosticsParams[]) {
for (const param of params) {
if (param.diagnostics.length === 0) {
this.documentsWithDiagnostics.delete(param.uri);
} else {
this.documentsWithDiagnostics.add(param.uri);
}
// If we're waiting for a pending workspace diagnostic, send a partial result.
if (this._workspaceDiagnosticsReporter) {
// Skip storing previous result ids, just send new results every time.
this._workspaceDiagnosticsReporter.report({
items: [{ ...param, kind: 'full', version: param.version || null, items: param.diagnostics }],
});
} else {
// Otherwise send a publish diagnostic notification.
this.connection.sendDiagnostics(param);
}
}
}
protected convertLspUriStringToUri(uri: string) {
return Uri.parse(uri, this.serverOptions.serviceProvider);
}
protected addDynamicFeature(feature: DynamicFeature) {
this.dynamicFeatures.add(feature);
}
private _getCompatibleMarkupKind(clientSupportedFormats: MarkupKind[] | undefined) {
const serverSupportedFormats = [MarkupKind.PlainText, MarkupKind.Markdown];
for (const format of clientSupportedFormats ?? []) {
if (serverSupportedFormats.includes(format)) {
return format;
}
}
return MarkupKind.PlainText;
}
private _convertDiagnostics(fs: FileSystem, diags: AnalyzerDiagnostic[]): Diagnostic[] {
const convertedDiags: Diagnostic[] = [];
diags.forEach((diag) => {
const severity = convertCategoryToSeverity(diag.category);
const rule = diag.getRule();
const code = this.getDiagCode(diag, rule);
const vsDiag = Diagnostic.create(diag.range, diag.message, severity, code, this.serverOptions.productName);
// Save all of the actions in the data.
const actions = diag.getActions();
if (actions?.length) {
vsDiag.data = { ...vsDiag.data, actions: actions };
}
if (
diag.category === DiagnosticCategory.UnusedCode ||
diag.category === DiagnosticCategory.UnreachableCode
) {
vsDiag.tags = [DiagnosticTag.Unnecessary];
vsDiag.severity = DiagnosticSeverity.Hint;
vsDiag.data = { ...vsDiag.data, category: diag.category, rule: rule };
// If the client doesn't support "unnecessary" tags, don't report unused code.
if (!this.client.supportsUnnecessaryDiagnosticTag) {
return;
}
} else if (diag.category === DiagnosticCategory.Deprecated) {
vsDiag.tags = [DiagnosticTag.Deprecated];
vsDiag.severity = DiagnosticSeverity.Hint;
// If the client doesn't support "deprecated" tags, don't report.
if (!this.client.supportsDeprecatedDiagnosticTag) {
return;
}
} else if (diag.category === DiagnosticCategory.TaskItem) {
// TaskItem is not supported.
return;
}
if (rule) {
const ruleDocUrl = this.getDocumentationUrlForDiagnostic(diag);
if (ruleDocUrl) {
vsDiag.codeDescription = {
href: ruleDocUrl,
};
}
}
const relatedInfo = diag.getRelatedInfo();
if (relatedInfo.length > 0) {
vsDiag.relatedInformation = relatedInfo
.filter((info) => this.canNavigateToFile(info.uri, fs))
.map((info) =>
DiagnosticRelatedInformation.create(
Location.create(convertUriToLspUriString(fs, info.uri), info.range),
info.message
)
);
}
convertedDiags.push(vsDiag);
});
function convertCategoryToSeverity(category: DiagnosticCategory) {
switch (category) {
case DiagnosticCategory.Error:
return DiagnosticSeverity.Error;
case DiagnosticCategory.Warning:
return DiagnosticSeverity.Warning;
case DiagnosticCategory.Information:
case DiagnosticCategory.TaskItem: // task items only show up in the task list if they are information or above.
return DiagnosticSeverity.Information;
case DiagnosticCategory.UnusedCode:
case DiagnosticCategory.UnreachableCode:
case DiagnosticCategory.Deprecated:
return DiagnosticSeverity.Hint;
}
}
return convertedDiags;
}
}