Merge from vscode merge-base (#22780)

* Revert "Revert "Merge from vscode merge-base (#22769)" (#22779)"

This reverts commit 47a1745180.

* Fix notebook download task

* Remove done call from extensions-ci
This commit is contained in:
Karl Burtram
2023-04-19 21:48:46 -07:00
committed by GitHub
parent decbe8dded
commit e7d3d047ec
2389 changed files with 92155 additions and 42602 deletions

View File

@@ -0,0 +1,12 @@
.vscode/
.github/
out/test/
src/
.eslintrc.js
.gitignore
tsconfig*.json
*.tsbuildinfo
*.map
example.cjs
CODE_OF_CONDUCT.md
SECURITY.md

View File

@@ -0,0 +1,16 @@
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
"configurations": [
{
"name": "Attach",
"type": "node",
"request": "attach",
"port": 7997,
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/out/**/*.js"
]
}
]
}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,27 @@
{
"version": "2.0.0",
"command": "npm",
"args": [
"run",
"watch"
],
"isBackground": true,
"problemMatcher": "$tsc-watch",
"tasks": [
{
"label": "npm",
"type": "shell",
"command": "npm",
"args": [
"run",
"watch"
],
"isBackground": true,
"problemMatcher": "$tsc-watch",
"group": {
"_id": "build",
"isDefault": false
}
}
]
}

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

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withBrowserDefaults = require('../../shared.webpack.config').browser;
const path = require('path');
module.exports = withBrowserDefaults({
context: __dirname,
entry: {
extension: './src/browser/main.ts',
},
output: {
filename: 'main.js',
path: path.join(__dirname, 'dist', 'browser'),
libraryTarget: 'var',
library: 'serverExportVar'
}
});

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../../shared.webpack.config');
const path = require('path');
module.exports = withDefaults({
context: path.join(__dirname),
entry: {
extension: './src/node/main.ts',
},
output: {
filename: 'main.js',
path: path.join(__dirname, 'dist', 'node'),
}
});

View File

@@ -0,0 +1,26 @@
{
"name": "vscode-markdown-languageserver",
"description": "Markdown language server",
"version": "0.0.0-alpha-1",
"author": "Microsoft Corporation",
"license": "MIT",
"engines": {
"node": "*"
},
"main": "./out/node/main",
"browser": "./dist/browser/main",
"dependencies": {
"vscode-languageserver": "^8.0.2-next.5`",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-languageserver-types": "^3.17.1",
"vscode-markdown-languageservice": "^0.0.0-alpha.12",
"vscode-uri": "^3.0.3"
},
"devDependencies": {
"@types/node": "16.x"
},
"scripts": {
"compile": "gulp compile-extension:markdown-language-features-server",
"watch": "gulp watch-extension:markdown-language-features-server"
}
}

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { BrowserMessageReader, BrowserMessageWriter, createConnection } from 'vscode-languageserver/browser';
import { startServer } from '../server';
declare let self: any;
const messageReader = new BrowserMessageReader(self);
const messageWriter = new BrowserMessageWriter(self);
const connection = createConnection(messageReader, messageWriter);
startServer(connection);

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface LsConfiguration {
/**
* List of file extensions should be considered as markdown.
*
* These should not include the leading `.`.
*/
readonly markdownFileExtensions: readonly string[];
}
const defaultConfig: LsConfiguration = {
markdownFileExtensions: ['md'],
};
export function getLsConfiguration(overrides: Partial<LsConfiguration>): LsConfiguration {
return {
...defaultConfig,
...overrides,
};
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, Emitter } from 'vscode-languageserver';
import { Disposable } from './util/dispose';
export type ValidateEnabled = 'ignore' | 'warning' | 'error';
interface Settings {
readonly markdown: {
readonly suggest: {
readonly paths: {
readonly enabled: boolean;
};
};
readonly experimental: {
readonly validate: {
readonly enabled: true;
readonly referenceLinks: {
readonly enabled: ValidateEnabled;
};
readonly fragmentLinks: {
readonly enabled: ValidateEnabled;
};
readonly fileLinks: {
readonly enabled: ValidateEnabled;
readonly markdownFragmentLinks: ValidateEnabled;
};
readonly ignoreLinks: readonly string[];
};
};
};
}
export class ConfigurationManager extends Disposable {
private readonly _onDidChangeConfiguration = this._register(new Emitter<Settings>());
public readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event;
private _settings?: Settings;
constructor(connection: Connection) {
super();
// The settings have changed. Is send on server activation as well.
this._register(connection.onDidChangeConfiguration((change) => {
this._settings = change.settings;
this._onDidChangeConfiguration.fire(this._settings!);
}));
}
public getSettings(): Settings | undefined {
return this._settings;
}
}

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver';
import * as md from 'vscode-markdown-languageservice';
import { disposeAll } from 'vscode-markdown-languageservice/out/util/dispose';
import { Disposable } from 'vscode-notebook-renderer/events';
import { URI } from 'vscode-uri';
import { ConfigurationManager, ValidateEnabled } from '../configuration';
import { VsCodeClientWorkspace } from '../workspace';
const defaultDiagnosticOptions: md.DiagnosticOptions = {
validateFileLinks: md.DiagnosticLevel.ignore,
validateReferences: md.DiagnosticLevel.ignore,
validateFragmentLinks: md.DiagnosticLevel.ignore,
validateMarkdownFileLinkFragments: md.DiagnosticLevel.ignore,
ignoreLinks: [],
};
function convertDiagnosticLevel(enabled: ValidateEnabled): md.DiagnosticLevel | undefined {
switch (enabled) {
case 'error': return md.DiagnosticLevel.error;
case 'warning': return md.DiagnosticLevel.warning;
case 'ignore': return md.DiagnosticLevel.ignore;
default: return md.DiagnosticLevel.ignore;
}
}
function getDiagnosticsOptions(config: ConfigurationManager): md.DiagnosticOptions {
const settings = config.getSettings();
if (!settings) {
return defaultDiagnosticOptions;
}
return {
validateFileLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.enabled),
validateReferences: convertDiagnosticLevel(settings.markdown.experimental.validate.referenceLinks.enabled),
validateFragmentLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fragmentLinks.enabled),
validateMarkdownFileLinkFragments: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.markdownFragmentLinks),
ignoreLinks: settings.markdown.experimental.validate.ignoreLinks,
};
}
export function registerValidateSupport(
connection: Connection,
workspace: VsCodeClientWorkspace,
ls: md.IMdLanguageService,
config: ConfigurationManager,
): Disposable {
let diagnosticOptions: md.DiagnosticOptions = defaultDiagnosticOptions;
function updateDiagnosticsSetting(): void {
diagnosticOptions = getDiagnosticsOptions(config);
}
const subs: Disposable[] = [];
const manager = ls.createPullDiagnosticsManager();
subs.push(manager);
subs.push(manager.onLinkedToFileChanged(() => {
// TODO: We only need to refresh certain files
connection.languages.diagnostics.refresh();
}));
connection.languages.diagnostics.on(async (params, token): Promise<FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport> => {
if (!config.getSettings()?.markdown.experimental.validate.enabled) {
return { kind: 'full', items: [] };
}
const document = await workspace.openMarkdownDocument(URI.parse(params.textDocument.uri));
if (!document) {
return { kind: 'full', items: [] };
}
const diagnostics = await manager.computeDiagnostics(document, diagnosticOptions, token);
return {
kind: 'full',
items: diagnostics,
};
});
updateDiagnosticsSetting();
subs.push(config.onDidChangeConfiguration(() => {
updateDiagnosticsSetting();
connection.languages.diagnostics.refresh();
}));
return {
dispose: () => {
disposeAll(subs);
}
};
}

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILogger, LogLevel } from 'vscode-markdown-languageservice';
export class LogFunctionLogger implements ILogger {
private static now(): string {
const now = new Date();
return String(now.getUTCHours()).padStart(2, '0')
+ ':' + String(now.getMinutes()).padStart(2, '0')
+ ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0');
}
private static data2String(data: any): string {
if (data instanceof Error) {
if (typeof data.stack === 'string') {
return data.stack;
}
return data.message;
}
if (typeof data === 'string') {
return data;
}
return JSON.stringify(data, undefined, 2);
}
constructor(
private readonly _logFn: typeof console.log
) { }
public log(level: LogLevel, title: string, message: string, data?: any): void {
this.appendLine(`[${level} ${LogFunctionLogger.now()}] ${title}: ${message}`);
if (data) {
this.appendLine(LogFunctionLogger.data2String(data));
}
}
private appendLine(value: string): void {
this._logFn(value);
}
}
export const consoleLogger = new LogFunctionLogger(console.log);

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, createConnection } from 'vscode-languageserver/node';
import { startServer } from '../server';
// Create a connection for the server.
const connection: Connection = createConnection();
console.log = connection.console.log.bind(connection.console);
console.error = connection.console.error.bind(connection.console);
process.on('unhandledRejection', (e: any) => {
connection.console.error(`Unhandled exception ${e}`);
});
startServer(connection);

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RequestType } from 'vscode-languageserver';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
//#region From server
export const parse = new RequestType<{ uri: string }, md.Token[], any>('markdown/parse');
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');
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 fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
//#endregion

View File

@@ -0,0 +1,252 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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 { IDisposable } from 'vscode-markdown-languageservice/out/util/dispose';
import { URI } from 'vscode-uri';
import { getLsConfiguration } from './config';
import { ConfigurationManager } from './configuration';
import { registerValidateSupport } from './languageFeatures/diagnostics';
import { LogFunctionLogger } from './logging';
import * as protocol from './protocol';
import { VsCodeClientWorkspace } from './workspace';
export async function startServer(connection: Connection) {
const documents = new TextDocuments(TextDocument);
const notebooks = new NotebookDocuments(documents);
const configurationManager = new ConfigurationManager(connection);
let provider: md.IMdLanguageService | undefined;
let workspace: VsCodeClientWorkspace | undefined;
connection.onInitialize((params: InitializeParams): InitializeResult => {
const parser = new class implements md.IMdParser {
slugifier = md.githubSlugifier;
async tokenize(document: md.ITextDocument): Promise<md.Token[]> {
return await connection.sendRequest(protocol.parse, { uri: document.uri.toString() });
}
};
const config = getLsConfiguration({
markdownFileExtensions: params.initializationOptions.markdownFileExtensions,
});
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks, logger);
provider = md.createLanguageService({
workspace,
parser,
logger,
markdownFileExtensions: config.markdownFileExtensions,
});
registerCompletionsSupport(connection, documents, provider, configurationManager);
registerValidateSupport(connection, workspace, provider, configurationManager);
workspace.workspaceFolders = (params.workspaceFolders ?? []).map(x => URI.parse(x.uri));
return {
capabilities: {
diagnosticProvider: {
documentSelector: null,
identifier: 'markdown',
interFileDependencies: true,
workspaceDiagnostics: false,
},
completionProvider: { triggerCharacters: ['.', '/', '#'] },
definitionProvider: true,
documentLinkProvider: { resolveProvider: true },
documentSymbolProvider: true,
foldingRangeProvider: true,
renameProvider: { prepareProvider: true, },
selectionRangeProvider: true,
workspaceSymbolProvider: true,
workspace: {
workspaceFolders: {
supported: true,
changeNotifications: true,
},
}
}
};
});
connection.onDocumentLinks(async (params, token): Promise<lsp.DocumentLink[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getDocumentLinks(document, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onDocumentLinkResolve(async (link, token): Promise<lsp.DocumentLink | undefined> => {
try {
return await provider!.resolveDocumentLink(link, token);
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onDocumentSymbol(async (params, token): Promise<lsp.DocumentSymbol[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getDocumentSymbols(document, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onFoldingRanges(async (params, token): Promise<lsp.FoldingRange[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getFoldingRanges(document, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onSelectionRanges(async (params, token): Promise<lsp.SelectionRange[] | undefined> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getSelectionRanges(document, params.positions, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onWorkspaceSymbol(async (params, token): Promise<lsp.WorkspaceSymbol[]> => {
try {
return await provider!.getWorkspaceSymbols(params.query, token);
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onReferences(async (params, token): Promise<lsp.Location[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getReferences(document, params.position, params.context, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onDefinition(async (params, token): Promise<lsp.Definition | undefined> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getDefinition(document, params.position, token);
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onPrepareRename(async (params, token) => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.prepareRename(document, params.position, token);
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onRenameRequest(async (params, token) => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
const edit = await provider!.getRenameEdit(document, params.position, params.newName, token);
console.log(JSON.stringify(edit));
return edit;
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onRequest(protocol.getReferencesToFileInWorkspace, (async (params: { uri: string }, token: CancellationToken) => {
try {
return await provider!.getFileReferences(URI.parse(params.uri), token);
} catch (e) {
console.error(e.stack);
}
return undefined;
}));
documents.listen(connection);
notebooks.listen(connection);
connection.listen();
}
function registerCompletionsSupport(
connection: Connection,
documents: TextDocuments<TextDocument>,
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());
}

View File

@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class MultiDisposeError extends Error {
constructor(
public readonly errors: any[]
) {
super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`);
}
}
export function disposeAll(disposables: Iterable<IDisposable>) {
const errors: any[] = [];
for (const disposable of disposables) {
try {
disposable.dispose();
} catch (e) {
errors.push(e);
}
}
if (errors.length === 1) {
throw errors[0];
} else if (errors.length > 1) {
throw new MultiDisposeError(errors);
}
}
export interface IDisposable {
dispose(): void;
}
export abstract class Disposable {
private _isDisposed = false;
protected _disposables: IDisposable[] = [];
public dispose(): any {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
disposeAll(this._disposables);
}
protected _register<T extends IDisposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed() {
return this._isDisposed;
}
}
export class DisposableStore extends Disposable {
private readonly items = new Set<IDisposable>();
public override dispose() {
super.dispose();
disposeAll(this.items);
this.items.clear();
}
public add<T extends IDisposable>(item: T): T {
if (this.isDisposed) {
console.warn('Adding to disposed store. Item will be leaked');
}
this.items.add(item);
return item;
}
}

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'vscode-languageserver-textdocument';
import { URI, Utils } from 'vscode-uri';
import { LsConfiguration } from '../config';
export function looksLikeMarkdownPath(config: LsConfiguration, resolvedHrefPath: URI) {
return config.markdownFileExtensions.includes(Utils.extname(URI.from(resolvedHrefPath)).toLowerCase().replace('.', ''));
}
export function isMarkdownFile(document: TextDocument) {
return document.languageId === 'markdown';
}

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
interface ILimitedTaskFactory<T> {
factory: ITask<Promise<T>>;
c: (value: T | Promise<T>) => void;
e: (error?: unknown) => void;
}
interface ITask<T> {
(): T;
}
/**
* A helper to queue N promises and run them all with a max degree of parallelism. The helper
* ensures that at any time no more than M promises are running at the same time.
*
* Taken from 'src/vs/base/common/async.ts'
*/
export class Limiter<T> {
private _size = 0;
private runningPromises: number;
private readonly maxDegreeOfParalellism: number;
private readonly outstandingPromises: ILimitedTaskFactory<T>[];
constructor(maxDegreeOfParalellism: number) {
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
this.outstandingPromises = [];
this.runningPromises = 0;
}
get size(): number {
return this._size;
}
queue(factory: ITask<Promise<T>>): Promise<T> {
this._size++;
return new Promise<T>((c, e) => {
this.outstandingPromises.push({ factory, c, e });
this.consume();
});
}
private consume(): void {
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
const iLimitedTask = this.outstandingPromises.shift()!;
this.runningPromises++;
const promise = iLimitedTask.factory();
promise.then(iLimitedTask.c, iLimitedTask.e);
promise.then(() => this.consumed(), () => this.consumed());
}
}
private consumed(): void {
this._size--;
this.runningPromises--;
if (this.outstandingPromises.length > 0) {
this.consume();
}
}
}

View File

@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vscode-uri';
type ResourceToKey = (uri: URI) => string;
const defaultResourceToKey = (resource: URI): string => resource.toString();
export class ResourceMap<T> {
private readonly map = new Map<string, { readonly uri: URI; readonly value: T }>();
private readonly toKey: ResourceToKey;
constructor(toKey: ResourceToKey = defaultResourceToKey) {
this.toKey = toKey;
}
public set(uri: URI, value: T): this {
this.map.set(this.toKey(uri), { uri, value });
return this;
}
public get(resource: URI): T | undefined {
return this.map.get(this.toKey(resource))?.value;
}
public has(resource: URI): boolean {
return this.map.has(this.toKey(resource));
}
public get size(): number {
return this.map.size;
}
public clear(): void {
this.map.clear();
}
public delete(resource: URI): boolean {
return this.map.delete(this.toKey(resource));
}
public *values(): IterableIterator<T> {
for (const entry of this.map.values()) {
yield entry.value;
}
}
public *keys(): IterableIterator<URI> {
for (const entry of this.map.values()) {
yield entry.uri;
}
}
public *entries(): IterableIterator<[URI, T]> {
for (const entry of this.map.values()) {
yield [entry.uri, entry.value];
}
}
public [Symbol.iterator](): IterableIterator<[URI, T]> {
return this.entries();
}
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const Schemes = Object.freeze({
notebookCell: 'vscode-notebook-cell',
});

View File

@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, Emitter, FileChangeType, NotebookDocuments, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as md from 'vscode-markdown-languageservice';
import { ContainingDocumentContext, FileWatcherOptions, IFileSystemWatcher } from 'vscode-markdown-languageservice/out/workspace';
import { URI } from 'vscode-uri';
import { LsConfiguration } from './config';
import * as protocol from './protocol';
import { coalesce } from './util/arrays';
import { isMarkdownFile, looksLikeMarkdownPath } from './util/file';
import { Limiter } from './util/limiter';
import { ResourceMap } from './util/resourceMap';
import { Schemes } from './util/schemes';
declare const TextDecoder: any;
export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
private readonly _onDidCreateMarkdownDocument = new Emitter<md.ITextDocument>();
public readonly onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocument.event;
private readonly _onDidChangeMarkdownDocument = new Emitter<md.ITextDocument>();
public readonly onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocument.event;
private readonly _onDidDeleteMarkdownDocument = new Emitter<URI>();
public readonly onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocument.event;
private readonly _documentCache = new ResourceMap<md.ITextDocument>();
private readonly _utf8Decoder = new TextDecoder('utf-8');
private _watcherPool = 0;
private readonly _watchers = new Map<number, {
readonly resource: URI;
readonly options: FileWatcherOptions;
readonly onDidChange: Emitter<URI>;
readonly onDidCreate: Emitter<URI>;
readonly onDidDelete: Emitter<URI>;
}>();
constructor(
private readonly connection: Connection,
private readonly config: LsConfiguration,
private readonly documents: TextDocuments<TextDocument>,
private readonly notebooks: NotebookDocuments<TextDocument>,
private readonly logger: md.ILogger,
) {
documents.onDidOpen(e => {
this._documentCache.delete(URI.parse(e.document.uri));
if (this.isRelevantMarkdownDocument(e.document)) {
this._onDidCreateMarkdownDocument.fire(e.document);
}
});
documents.onDidChangeContent(e => {
if (this.isRelevantMarkdownDocument(e.document)) {
this._onDidChangeMarkdownDocument.fire(e.document);
}
});
documents.onDidClose(e => {
this._documentCache.delete(URI.parse(e.document.uri));
});
connection.onDidChangeWatchedFiles(async ({ changes }) => {
for (const change of changes) {
const resource = URI.parse(change.uri);
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: onDidChangeWatchedFiles', `${change.type}: ${resource}`);
switch (change.type) {
case FileChangeType.Changed: {
this._documentCache.delete(resource);
const document = await this.openMarkdownDocument(resource);
if (document) {
this._onDidChangeMarkdownDocument.fire(document);
}
break;
}
case FileChangeType.Created: {
const document = await this.openMarkdownDocument(resource);
if (document) {
this._onDidCreateMarkdownDocument.fire(document);
}
break;
}
case FileChangeType.Deleted: {
this._documentCache.delete(resource);
this._onDidDeleteMarkdownDocument.fire(resource);
break;
}
}
}
});
connection.onRequest(protocol.fs_watcher_onChange, params => {
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: fs_watcher_onChange', `${params.kind}: ${params.uri}`);
const watcher = this._watchers.get(params.id);
if (!watcher) {
return;
}
switch (params.kind) {
case 'create': watcher.onDidCreate.fire(URI.parse(params.uri)); return;
case 'change': watcher.onDidChange.fire(URI.parse(params.uri)); return;
case 'delete': watcher.onDidDelete.fire(URI.parse(params.uri)); return;
}
});
}
public listen() {
this.connection.workspace.onDidChangeWorkspaceFolders(async () => {
this.workspaceFolders = (await this.connection.workspace.getWorkspaceFolders() ?? []).map(x => URI.parse(x.uri));
});
}
private _workspaceFolders: readonly URI[] = [];
get workspaceFolders(): readonly URI[] {
return this._workspaceFolders;
}
set workspaceFolders(value: readonly URI[]) {
this._workspaceFolders = value;
}
async getAllMarkdownDocuments(): Promise<Iterable<md.ITextDocument>> {
const maxConcurrent = 20;
const foundFiles = new ResourceMap<void>();
const limiter = new Limiter<md.ITextDocument | undefined>(maxConcurrent);
// Add files on disk
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);
const doc = await this.openMarkdownDocument(resource);
if (doc) {
foundFiles.set(resource);
}
return doc;
});
}));
// Add opened files (such as untitled files)
const openTextDocumentResults = await Promise.all(this.documents.all()
.filter(doc => !foundFiles.has(URI.parse(doc.uri)) && this.isRelevantMarkdownDocument(doc)));
return coalesce([...onDiskResults, ...openTextDocumentResults]);
}
hasMarkdownDocument(resource: URI): boolean {
return !!this.documents.get(resource.toString());
}
async openMarkdownDocument(resource: URI): Promise<md.ITextDocument | undefined> {
const existing = this._documentCache.get(resource);
if (existing) {
return existing;
}
const matchingDocument = this.documents.get(resource.toString());
if (matchingDocument) {
this._documentCache.set(resource, matchingDocument);
return matchingDocument;
}
if (!looksLikeMarkdownPath(this.config, resource)) {
return undefined;
}
try {
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);
// We assume that markdown is in UTF-8
const text = this._utf8Decoder.decode(bytes);
const doc = TextDocument.create(resource.toString(), 'markdown', 0, text);
this._documentCache.set(resource, doc);
return doc;
} catch (e) {
return undefined;
}
}
async stat(resource: URI): Promise<md.FileStat | undefined> {
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: stat', `${resource}`);
if (this._documentCache.has(resource) || this.documents.get(resource.toString())) {
return { isDirectory: false };
}
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.fs_readDirectory, { uri: resource.toString() });
}
getContainingDocument(resource: URI): ContainingDocumentContext | undefined {
if (resource.scheme === Schemes.notebookCell) {
const nb = this.notebooks.findNotebookDocumentForCell(resource.toString());
if (nb) {
return {
uri: URI.parse(nb.uri),
children: nb.cells.map(cell => ({ uri: URI.parse(cell.document) })),
};
}
}
return undefined;
}
watchFile(resource: URI, options: FileWatcherOptions): IFileSystemWatcher {
const id = this._watcherPool++;
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: watchFile', `(${id}) ${resource}`);
const entry = {
resource,
options,
onDidCreate: new Emitter<URI>(),
onDidChange: new Emitter<URI>(),
onDidDelete: new Emitter<URI>(),
};
this._watchers.set(id, entry);
this.connection.sendRequest(protocol.fs_watcher_create, {
id,
uri: resource.toString(),
options,
});
return {
onDidCreate: entry.onDidCreate.event,
onDidChange: entry.onDidChange.event,
onDidDelete: entry.onDidDelete.event,
dispose: () => {
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: disposeWatcher', `(${id}) ${resource}`);
this.connection.sendRequest(protocol.fs_watcher_delete, { id });
this._watchers.delete(id);
}
};
}
private isRelevantMarkdownDocument(doc: TextDocument) {
return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview';
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out"
},
"include": [
"src/**/*"
]
}

View File

@@ -0,0 +1,69 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@16.x":
version "16.11.43"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.43.tgz#555e5a743f76b6b897d47f945305b618525ddbe6"
integrity sha512-GqWykok+3uocgfAJM8imbozrqLnPyTrpFlrryURQlw1EesPUCx5XxTiucWDSFF9/NUEXDuD4bnvHm8xfVGWTpQ==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
vscode-jsonrpc@8.0.2-next.1:
version "8.0.2-next.1"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz#6bdc39fd194782032e34047eeefce562941259c6"
integrity sha512-sbbvGSWja7NVBLHPGawtgezc8DHYJaP4qfr/AaJiyDapWcSFtHyPtm18+LnYMLTmB7bhOUW/lf5PeeuLpP6bKA==
vscode-languageserver-protocol@3.17.2-next.6:
version "3.17.2-next.6"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2-next.6.tgz#8f1dc0fcb29366b85f623a3f9af726de433b5fcc"
integrity sha512-WtsebNOOkWyNn4oFYoAMPC8Q/ZDoJ/K7Ja53OzTixiitvrl/RpXZETrtzH79R8P5kqCyx6VFBPb6KQILJfkDkA==
dependencies:
vscode-jsonrpc "8.0.2-next.1"
vscode-languageserver-types "3.17.2-next.2"
vscode-languageserver-textdocument@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz#838769940ece626176ec5d5a2aa2d0aa69f5095c"
integrity sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg==
vscode-languageserver-types@3.17.2-next.2:
version "3.17.2-next.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2-next.2.tgz#af5d6978eee7682aab87c1419323f5b141ac6596"
integrity sha512-TiAkLABgqkVWdAlC3XlOfdhdjIAdVU4YntPUm9kKGbXr+MGwpVnKz2KZMNBcvG0CFx8Hi8qliL0iq+ndPB720w==
vscode-languageserver-types@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16"
integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ==
vscode-languageserver@^8.0.2-next.5`:
version "8.0.2-next.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz#39a2dd4c504fb88042375e7ac706a714bdaab4e5"
integrity sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==
dependencies:
vscode-languageserver-protocol "3.17.2-next.6"
vscode-markdown-languageservice@^0.0.0-alpha.12:
version "0.0.0-alpha.12"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.12.tgz#5a3c7559969c3cb455d508d48129c8e221589630"
integrity sha512-9dJ/GL6A9UUOcB1TpvgsbcwqsYjnxHx4jxDaqeZZEMWFSUySfp0PAn1ge2S2Qj00zypvsu0eCTGUNd56G1/BNQ==
dependencies:
picomatch "^2.3.1"
vscode-languageserver-textdocument "^1.0.5"
vscode-languageserver-types "^3.17.1"
vscode-nls "^5.0.1"
vscode-uri "^3.0.3"
vscode-nls@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2"
integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A==
vscode-uri@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84"
integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==