Files
vscode/extensions/markdown-language-features/server/src/server.ts
T
Matt Bierner 03ada8a3e8 Rework how markdown server works with documents (#160948)
* Rework how markdown server works with documents

This rewrites how the markdown server works with documents. The goal is to better handle switching between in-memory versions of a doc (from `TextDocument`) and versions of the same doc on disk. From the markdown service's POV, there is only one type of document

As part of this, I've also adopted the newest markdown language service version

* Bump package-lock versions
2022-09-15 09:27:02 +02:00

285 lines
9.4 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, Connection, InitializeParams, InitializeResult, NotebookDocuments, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as lsp from 'vscode-languageserver-types';
import * as md from 'vscode-markdown-languageservice';
import * as nls from 'vscode-nls';
import { URI } from 'vscode-uri';
import { getLsConfiguration, LsConfiguration } from './config';
import { ConfigurationManager } from './configuration';
import { registerValidateSupport } from './languageFeatures/diagnostics';
import { LogFunctionLogger } from './logging';
import * as protocol from './protocol';
import { IDisposable } from './util/dispose';
import { VsCodeClientWorkspace } from './workspace';
const localize = nls.loadMessageBundle();
interface MdServerInitializationOptions extends LsConfiguration { }
const organizeLinkDefKind = 'source.organizeLinkDefinitions';
export async function startVsCodeServer(connection: Connection) {
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
const parser = new class implements md.IMdParser {
slugifier = md.githubSlugifier;
tokenize(document: md.ITextDocument): Promise<md.Token[]> {
return connection.sendRequest(protocol.parse, { uri: document.uri.toString() });
}
};
const documents = new TextDocuments(TextDocument);
const notebooks = new NotebookDocuments(documents);
const workspaceFactory: WorkspaceFactory = ({ connection, config, workspaceFolders }) => {
const workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks, logger);
workspace.workspaceFolders = (workspaceFolders ?? []).map(x => URI.parse(x.uri));
return workspace;
};
return startServer(connection, { documents, notebooks, logger, parser, workspaceFactory });
}
type WorkspaceFactory = (config: {
connection: Connection;
config: LsConfiguration;
workspaceFolders?: lsp.WorkspaceFolder[] | null;
}) => md.IWorkspace;
export async function startServer(connection: Connection, serverConfig: {
documents: TextDocuments<md.ITextDocument>;
notebooks?: NotebookDocuments<md.ITextDocument>;
logger: md.ILogger;
parser: md.IMdParser;
workspaceFactory: WorkspaceFactory;
}) {
const { documents, notebooks } = serverConfig;
let mdLs: md.IMdLanguageService | undefined;
connection.onInitialize((params: InitializeParams): InitializeResult => {
const initOptions = params.initializationOptions as MdServerInitializationOptions | undefined;
const config = getLsConfiguration(initOptions ?? {});
const configurationManager = new ConfigurationManager(connection);
const workspace = serverConfig.workspaceFactory({ connection, config, workspaceFolders: params.workspaceFolders });
mdLs = md.createLanguageService({
workspace,
parser: serverConfig.parser,
logger: serverConfig.logger,
markdownFileExtensions: config.markdownFileExtensions,
excludePaths: config.excludePaths,
});
registerCompletionsSupport(connection, documents, mdLs, configurationManager);
registerValidateSupport(connection, workspace, documents, mdLs, configurationManager, serverConfig.logger);
return {
capabilities: {
diagnosticProvider: {
documentSelector: null,
identifier: 'markdown',
interFileDependencies: true,
workspaceDiagnostics: false,
},
codeActionProvider: { resolveProvider: true },
completionProvider: { triggerCharacters: ['.', '/', '#'] },
definitionProvider: true,
documentLinkProvider: { resolveProvider: true },
documentSymbolProvider: true,
foldingRangeProvider: true,
referencesProvider: true,
renameProvider: { prepareProvider: true, },
selectionRangeProvider: true,
workspaceSymbolProvider: true,
workspace: {
workspaceFolders: {
supported: true,
changeNotifications: true,
},
}
}
};
});
connection.onDocumentLinks(async (params, token): Promise<lsp.DocumentLink[]> => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
return mdLs!.getDocumentLinks(document, token);
});
connection.onDocumentLinkResolve(async (link, token): Promise<lsp.DocumentLink | undefined> => {
return mdLs!.resolveDocumentLink(link, token);
});
connection.onDocumentSymbol(async (params, token): Promise<lsp.DocumentSymbol[]> => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
return mdLs!.getDocumentSymbols(document, { includeLinkDefinitions: true }, token);
});
connection.onFoldingRanges(async (params, token): Promise<lsp.FoldingRange[]> => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
return mdLs!.getFoldingRanges(document, token);
});
connection.onSelectionRanges(async (params, token): Promise<lsp.SelectionRange[] | undefined> => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
return mdLs!.getSelectionRanges(document, params.positions, token);
});
connection.onWorkspaceSymbol(async (params, token): Promise<lsp.WorkspaceSymbol[]> => {
return mdLs!.getWorkspaceSymbols(params.query, token);
});
connection.onReferences(async (params, token): Promise<lsp.Location[]> => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
}
return mdLs!.getReferences(document, params.position, params.context, token);
});
connection.onDefinition(async (params, token): Promise<lsp.Definition | undefined> => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return undefined;
}
return mdLs!.getDefinition(document, params.position, token);
});
connection.onPrepareRename(async (params, token) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return undefined;
}
return mdLs!.prepareRename(document, params.position, token);
});
connection.onRenameRequest(async (params, token) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return undefined;
}
return mdLs!.getRenameEdit(document, params.position, params.newName, token);
});
interface OrganizeLinkActionData {
readonly uri: string;
}
connection.onCodeAction(async (params, token) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return undefined;
}
if (params.context.only?.some(kind => kind === 'source' || kind.startsWith('source.'))) {
const action: lsp.CodeAction = {
title: localize('organizeLinkDefAction.title', "Organize link definitions"),
kind: organizeLinkDefKind,
data: <OrganizeLinkActionData>{ uri: document.uri }
};
return [action];
}
return mdLs!.getCodeActions(document, params.range, params.context, token);
});
connection.onCodeActionResolve(async (codeAction, token) => {
if (codeAction.kind === organizeLinkDefKind) {
const data = codeAction.data as OrganizeLinkActionData;
const document = documents.get(data.uri);
if (!document) {
return codeAction;
}
const edits = (await mdLs?.organizeLinkDefinitions(document, { removeUnused: true }, token)) || [];
codeAction.edit = {
changes: {
[data.uri]: edits
}
};
return codeAction;
}
return codeAction;
});
connection.onRequest(protocol.getReferencesToFileInWorkspace, (async (params: { uri: string }, token: CancellationToken) => {
return mdLs!.getFileReferences(URI.parse(params.uri), token);
}));
connection.onRequest(protocol.getEditForFileRenames, (async (params, token: CancellationToken) => {
return mdLs!.getRenameFilesInWorkspaceEdit(params.map(x => ({ oldUri: URI.parse(x.oldUri), newUri: URI.parse(x.newUri) })), token);
}));
connection.onRequest(protocol.resolveLinkTarget, (async (params, token: CancellationToken) => {
return mdLs!.resolveLinkTarget(params.linkText, URI.parse(params.uri), token);
}));
documents.listen(connection);
notebooks?.listen(connection);
connection.listen();
}
function registerCompletionsSupport(
connection: Connection,
documents: TextDocuments<md.ITextDocument>,
ls: md.IMdLanguageService,
config: ConfigurationManager,
): IDisposable {
// let registration: Promise<IDisposable> | undefined;
function update() {
// TODO: client still makes the request in this case. Figure our how to properly unregister.
return;
// const settings = config.getSettings();
// if (settings?.markdown.suggest.paths.enabled) {
// if (!registration) {
// registration = connection.client.register(CompletionRequest.type);
// }
// } else {
// registration?.then(x => x.dispose());
// registration = undefined;
// }
}
connection.onCompletion(async (params, token): Promise<lsp.CompletionItem[]> => {
try {
const settings = config.getSettings();
if (!settings?.markdown.suggest.paths.enabled) {
return [];
}
const document = documents.get(params.textDocument.uri);
if (document) {
return await ls.getCompletionItems(document, params.position, params.context!, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
update();
return config.onDidChangeConfiguration(() => update());
}