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,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';
}
}