Documenting markdown LS (#155789)

This commit is contained in:
Matt Bierner 2022-07-21 02:48:30 -07:00 committed by GitHub
parent e7bf4c6a75
commit 924dde5c1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 168 additions and 40 deletions

View file

@ -0,0 +1,120 @@
# Markdown Language Server
> **❗ Import** This is still in development. While the language server is being used by VS Code, it has not yet been tested with other clients.
The Markdown language server powers VS Code's built-in markdown support, providing tools for writing and browsing Markdown files. It runs as a separate executable and implements the [language server protocol](https://microsoft.github.io/language-server-protocol/overview).
This server uses the [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) to implement almost all of the language features. You can use that library if you need a library for working with Markdown instead of a full language server.
## Server capabilities
- [Completions](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) for Markdown links.
- [Folding](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange) of Markdown regions, block elements, and header sections.
- [Smart selection](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange) for inline elements, block elements, and header sections.
- [Document Symbols](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for quick navigation to headers in a document.
- [Workspace Symbols](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) for quick navigation to headers in the workspace
- [Document links](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentLink) for making Markdown links in a document clickable.
- [Find all references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) to headers and links across all Markdown files in the workspace.
- [Rename](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename) of headers and links across all Markdown files in the workspace.
- [Go to definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) from links to headers or link definitions.
- (experimental) [Pull diagnostics (validation)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) for links.
## Client requirements
### Initialization options
The client can send the following initialization options to the server:
- `markdownFileExtensions` Array file extensions that should be considered as Markdown. These should not include the leading `.`. For example: `['md', 'mdown', 'markdown']`.
### Settings
Clients may send a `workspace/didChangeConfiguration` notification to notify the server of settings changes.
The server supports the following settings:
- `markdown`
- `suggest`
- `paths`
- `enabled` — Enable/disable path suggestions.
- `experimental`
- `validate`
- `enabled` Enable/disable all validation.
- `referenceLinks`
- `enabled` Enable/disable validation of reference links: `[text][ref]`
- `fragmentLinks`
- `enabled` Enable/disable validation of links to fragments in the current files: `[text](#head)`
- `fileLinks`
- `enabled` Enable/disable validation of links to file in the workspace.
- `markdownFragmentLinks` Enable/disable validation of links to headers in other Markdown files.
- `ignoreLinks` Array of glob patterns for files that should not be validated.
### Custom requests
To support all of the features of the language server, the client needs to implement a few custom request types. The definitions of these request types can be found in [`protocol.ts`](./src/protocol.ts)
#### `markdown/parse`
Get the tokens for a Markdown file. Clients are expected to use [Markdown-it](https://github.com/markdown-it/markdown-it) for this.
We require that clients bring their own version of Markdown-it so that they can customize/extend Markdown-it.
#### `markdown/fs/readFile`
Read the contents of a file in the workspace.
#### `markdown/fs/readDirectory`
Read the contents of a directory in the workspace.
#### `markdown/fs/stat`
Check if a given file/directory exists in the workspace.
#### `markdown/fs/watcher/create`
Create a file watcher. This is needed for diagnostics support.
#### `markdown/fs/watcher/delete`
Delete a previously created file watcher.
#### `markdown/findMarkdownFilesInWorkspace`
Get a list of all markdown files in the workspace.
## Contribute
The source code of the Markdown language server can be found in the [VSCode repository](https://github.com/microsoft/vscode) at [extensions/markdown-language-features/server](https://github.com/microsoft/vscode/tree/master/extensions/markdown-language-features/server).
File issues and pull requests in the [VSCode GitHub Issues](https://github.com/microsoft/vscode/issues). See the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) on how to build and run from source.
Most of the functionality of the server is located in libraries:
- [vscode-markdown-languageservice](https://github.com/microsoft/vscode-markdown-languageservice) contains the implementation of all features as a reusable library.
- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) contains the implementation of language server for NodeJS.
Help on any of these projects is very welcome.
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## License
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [MIT](https://github.com/microsoft/vscode/blob/master/LICENSE.txt) License.

View file

@ -4,20 +4,24 @@
*--------------------------------------------------------------------------------------------*/
import { RequestType } from 'vscode-languageserver';
import * as lsp from 'vscode-languageserver-types';
import * as md from 'vscode-markdown-languageservice';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
// From server
export const parseRequestType: RequestType<{ uri: string }, md.Token[], any> = new RequestType('markdown/parse');
export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
export const statFileRequestType: RequestType<{ uri: string }, md.FileStat | undefined, any> = new RequestType('markdown/statFile');
export const readDirectoryRequestType: RequestType<{ uri: string }, [string, md.FileStat][], any> = new RequestType('markdown/readDirectory');
export const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles');
//#region From server
export const parse = new RequestType<{ uri: string }, md.Token[], any>('markdown/parse');
export const createFileWatcher: RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any> = new RequestType('markdown/createFileWatcher');
export const deleteFileWatcher: RequestType<{ id: number }, void, any> = new RequestType('markdown/deleteFileWatcher');
export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile');
export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory');
export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat');
// To server
export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create');
export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete');
export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace');
//#endregion
//#region To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const onWatcherChange: RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any> = new RequestType('markdown/onWatcherChange');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
//#endregion

View file

@ -30,7 +30,7 @@ export async function startServer(connection: Connection) {
slugifier = md.githubSlugifier;
async tokenize(document: md.ITextDocument): Promise<md.Token[]> {
return await connection.sendRequest(protocol.parseRequestType, { uri: document.uri.toString() });
return await connection.sendRequest(protocol.parse, { uri: document.uri.toString() });
}
};

View file

@ -94,7 +94,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
}
});
connection.onRequest(protocol.onWatcherChange, params => {
connection.onRequest(protocol.fs_watcher_onChange, params => {
const watcher = this._watchers.get(params.id);
if (!watcher) {
return;
@ -130,7 +130,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
const limiter = new Limiter<md.ITextDocument | undefined>(maxConcurrent);
// Add files on disk
const resources = await this.connection.sendRequest(protocol.findFilesRequestTypes, {});
const resources = await this.connection.sendRequest(protocol.findMarkdownFilesInWorkspace, {});
const onDiskResults = await Promise.all(resources.map(strResource => {
return limiter.queue(async () => {
const resource = URI.parse(strResource);
@ -170,7 +170,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
}
try {
const response = await this.connection.sendRequest(protocol.readFileRequestType, { uri: resource.toString() });
const response = await this.connection.sendRequest(protocol.fs_readFile, { uri: resource.toString() });
// TODO: LSP doesn't seem to handle Array buffers well
const bytes = new Uint8Array(response);
@ -189,12 +189,12 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
if (this._documentCache.has(resource) || this.documents.get(resource.toString())) {
return { isDirectory: false };
}
return this.connection.sendRequest(protocol.statFileRequestType, { uri: resource.toString() });
return this.connection.sendRequest(protocol.fs_stat, { uri: resource.toString() });
}
async readDirectory(resource: URI): Promise<[string, md.FileStat][]> {
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: readDir', `${resource}`);
return this.connection.sendRequest(protocol.readDirectoryRequestType, { uri: resource.toString() });
return this.connection.sendRequest(protocol.fs_readDirectory, { uri: resource.toString() });
}
getContainingDocument(resource: URI): ContainingDocumentContext | undefined {
@ -221,7 +221,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
const id = this._watcherPool++;
this._watchers.set(id, entry);
this.connection.sendRequest(protocol.createFileWatcher, {
this.connection.sendRequest(protocol.fs_watcher_create, {
id,
uri: resource.toString(),
options,
@ -232,7 +232,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
onDidChange: entry.onDidChange.event,
onDidDelete: entry.onDidDelete.event,
dispose: () => {
this.connection.sendRequest(protocol.deleteFileWatcher, { id });
this.connection.sendRequest(protocol.fs_watcher_delete, { id });
this._watchers.delete(id);
}
};

View file

@ -57,7 +57,7 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
});
}
client.onRequest(proto.parseRequestType, async (e) => {
client.onRequest(proto.parse, async (e) => {
const uri = vscode.Uri.parse(e.uri);
const doc = await workspace.getOrLoadMarkdownDocument(uri);
if (doc) {
@ -67,12 +67,12 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
}
});
client.onRequest(proto.readFileRequestType, async (e): Promise<number[]> => {
client.onRequest(proto.fs_readFile, async (e): Promise<number[]> => {
const uri = vscode.Uri.parse(e.uri);
return Array.from(await vscode.workspace.fs.readFile(uri));
});
client.onRequest(proto.statFileRequestType, async (e): Promise<{ isDirectory: boolean } | undefined> => {
client.onRequest(proto.fs_stat, async (e): Promise<{ isDirectory: boolean } | undefined> => {
const uri = vscode.Uri.parse(e.uri);
try {
const stat = await vscode.workspace.fs.stat(uri);
@ -82,28 +82,28 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
}
});
client.onRequest(proto.readDirectoryRequestType, async (e): Promise<[string, { isDirectory: boolean }][]> => {
client.onRequest(proto.fs_readDirectory, async (e): Promise<[string, { isDirectory: boolean }][]> => {
const uri = vscode.Uri.parse(e.uri);
const result = await vscode.workspace.fs.readDirectory(uri);
return result.map(([name, type]) => [name, { isDirectory: type === vscode.FileType.Directory }]);
});
client.onRequest(proto.findFilesRequestTypes, async (): Promise<string[]> => {
client.onRequest(proto.findMarkdownFilesInWorkspace, async (): Promise<string[]> => {
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
});
const watchers = new Map<number, vscode.FileSystemWatcher>();
client.onRequest(proto.createFileWatcher, async (params): Promise<void> => {
client.onRequest(proto.fs_watcher_create, async (params): Promise<void> => {
const id = params.id;
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(params.uri), '*'), params.options.ignoreCreate, params.options.ignoreChange, params.options.ignoreDelete);
watchers.set(id, watcher);
watcher.onDidCreate(() => { client.sendRequest(proto.onWatcherChange, { id, uri: params.uri, kind: 'create' }); });
watcher.onDidChange(() => { client.sendRequest(proto.onWatcherChange, { id, uri: params.uri, kind: 'change' }); });
watcher.onDidDelete(() => { client.sendRequest(proto.onWatcherChange, { id, uri: params.uri, kind: 'delete' }); });
watcher.onDidCreate(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'create' }); });
watcher.onDidChange(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'change' }); });
watcher.onDidDelete(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'delete' }); });
});
client.onRequest(proto.deleteFileWatcher, async (params): Promise<void> => {
client.onRequest(proto.fs_watcher_delete, async (params): Promise<void> => {
watchers.get(params.id)?.dispose();
watchers.delete(params.id);
});

View file

@ -8,17 +8,21 @@ import { RequestType } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
// From server
export const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse');
export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
export const statFileRequestType: RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any> = new RequestType('markdown/statFile');
export const readDirectoryRequestType: RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any> = new RequestType('markdown/readDirectory');
export const findFilesRequestTypes = new RequestType<{}, string[], any>('markdown/findFiles');
//#region From server
export const parse = new RequestType<{ uri: string }, Token[], any>('markdown/parse');
export const createFileWatcher: RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any> = new RequestType('markdown/createFileWatcher');
export const deleteFileWatcher: RequestType<{ id: number }, void, any> = new RequestType('markdown/deleteFileWatcher');
export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile');
export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory');
export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat');
// To server
export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create');
export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete');
export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace');
//#endregion
//#region To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const onWatcherChange: RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any> = new RequestType('markdown/onWatcherChange');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
//#endregion