mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-28 04:23:32 +01:00
Clean up structure of markdown extension (#161148)
- Move things related to the client under `client` - Remove extra abstractions that are no longer used - Add MdLanguageClient type
This commit is contained in:
153
extensions/markdown-language-features/src/client/client.ts
Normal file
153
extensions/markdown-language-features/src/client/client.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { IMdParser } from '../markdownEngine';
|
||||
import * as proto from './protocol';
|
||||
import { looksLikeMarkdownPath, markdownFileExtensions } from '../util/file';
|
||||
import { VsCodeMdWorkspace } from './workspace';
|
||||
import { FileWatcherManager } from './fileWatchingManager';
|
||||
import { IDisposable } from '../util/dispose';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
|
||||
|
||||
export class MdLanguageClient implements IDisposable {
|
||||
|
||||
constructor(
|
||||
private readonly _client: BaseLanguageClient,
|
||||
private readonly _workspace: VsCodeMdWorkspace,
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
this._client.stop();
|
||||
this._workspace.dispose();
|
||||
}
|
||||
|
||||
resolveLinkTarget(linkText: string, uri: vscode.Uri): Promise<proto.ResolvedDocumentLinkTarget> {
|
||||
return this._client.sendRequest(proto.resolveLinkTarget, { linkText, uri: uri.toString() });
|
||||
}
|
||||
|
||||
getEditForFileRenames(files: ReadonlyArray<{ oldUri: string; newUri: string }>, token: vscode.CancellationToken) {
|
||||
return this._client.sendRequest(proto.getEditForFileRenames, files, token);
|
||||
}
|
||||
|
||||
getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken) {
|
||||
return this._client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startClient(factory: LanguageClientConstructor, parser: IMdParser): Promise<MdLanguageClient> {
|
||||
|
||||
const mdFileGlob = `**/*.{${markdownFileExtensions.join(',')}}`;
|
||||
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: [{ language: 'markdown' }],
|
||||
synchronize: {
|
||||
configurationSection: ['markdown'],
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher(mdFileGlob),
|
||||
},
|
||||
initializationOptions: {
|
||||
markdownFileExtensions,
|
||||
},
|
||||
diagnosticPullOptions: {
|
||||
onChange: true,
|
||||
onTabs: true,
|
||||
match(_documentSelector, resource) {
|
||||
return looksLikeMarkdownPath(resource);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const client = factory('markdown', localize('markdownServer.name', 'Markdown Language Server'), clientOptions);
|
||||
|
||||
client.registerProposedFeatures();
|
||||
|
||||
const notebookFeature = client.getFeature(NotebookDocumentSyncRegistrationType.method);
|
||||
if (notebookFeature !== undefined) {
|
||||
notebookFeature.register({
|
||||
id: String(Date.now()),
|
||||
registerOptions: {
|
||||
notebookSelector: [{
|
||||
notebook: '*',
|
||||
cells: [{ language: 'markdown' }]
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = new VsCodeMdWorkspace();
|
||||
|
||||
client.onRequest(proto.parse, async (e) => {
|
||||
const uri = vscode.Uri.parse(e.uri);
|
||||
const doc = await workspace.getOrLoadMarkdownDocument(uri);
|
||||
if (doc) {
|
||||
return parser.tokenize(doc);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
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.fs_stat, async (e): Promise<{ isDirectory: boolean } | undefined> => {
|
||||
const uri = vscode.Uri.parse(e.uri);
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(uri);
|
||||
return { isDirectory: stat.type === vscode.FileType.Directory };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
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.findMarkdownFilesInWorkspace, async (): Promise<string[]> => {
|
||||
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
|
||||
});
|
||||
|
||||
const watchers = new FileWatcherManager();
|
||||
|
||||
client.onRequest(proto.fs_watcher_create, async (params): Promise<void> => {
|
||||
const id = params.id;
|
||||
const uri = vscode.Uri.parse(params.uri);
|
||||
|
||||
const sendWatcherChange = (kind: 'create' | 'change' | 'delete') => {
|
||||
client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind });
|
||||
};
|
||||
|
||||
watchers.create(id, uri, params.watchParentDirs, {
|
||||
create: params.options.ignoreCreate ? undefined : () => sendWatcherChange('create'),
|
||||
change: params.options.ignoreChange ? undefined : () => sendWatcherChange('change'),
|
||||
delete: params.options.ignoreDelete ? undefined : () => sendWatcherChange('delete'),
|
||||
});
|
||||
});
|
||||
|
||||
client.onRequest(proto.fs_watcher_delete, async (params): Promise<void> => {
|
||||
watchers.delete(params.id);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('vscodeMarkdownLanguageservice.open', (uri, args) => {
|
||||
return vscode.commands.executeCommand('vscode.open', uri, args);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('vscodeMarkdownLanguageservice.rename', (uri, pos) => {
|
||||
return vscode.commands.executeCommand('editor.action.rename', [vscode.Uri.from(uri), new vscode.Position(pos.line, pos.character)]);
|
||||
});
|
||||
|
||||
await client.start();
|
||||
|
||||
return new MdLanguageClient(client, workspace);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { disposeAll } from 'vscode-markdown-languageservice/out/util/dispose';
|
||||
import { ResourceMap } from 'vscode-markdown-languageservice/out/util/resourceMap';
|
||||
import { Utils } from 'vscode-uri';
|
||||
import { IDisposable } from '../util/dispose';
|
||||
import { Schemes } from '../util/schemes';
|
||||
|
||||
type DirWatcherEntry = {
|
||||
readonly uri: vscode.Uri;
|
||||
readonly listeners: IDisposable[];
|
||||
};
|
||||
|
||||
|
||||
export class FileWatcherManager {
|
||||
|
||||
private readonly fileWatchers = new Map<number, {
|
||||
readonly watcher: vscode.FileSystemWatcher;
|
||||
readonly dirWatchers: DirWatcherEntry[];
|
||||
}>();
|
||||
|
||||
private readonly dirWatchers = new ResourceMap<{
|
||||
readonly watcher: vscode.FileSystemWatcher;
|
||||
refCount: number;
|
||||
}>();
|
||||
|
||||
create(id: number, uri: vscode.Uri, watchParentDirs: boolean, listeners: { create?: () => void; change?: () => void; delete?: () => void }): void {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '*'), !listeners.create, !listeners.change, !listeners.delete);
|
||||
const parentDirWatchers: DirWatcherEntry[] = [];
|
||||
this.fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers });
|
||||
|
||||
if (listeners.create) { watcher.onDidCreate(listeners.create); }
|
||||
if (listeners.change) { watcher.onDidChange(listeners.change); }
|
||||
if (listeners.delete) { watcher.onDidDelete(listeners.delete); }
|
||||
|
||||
if (watchParentDirs && uri.scheme !== Schemes.untitled) {
|
||||
// We need to watch the parent directories too for when these are deleted / created
|
||||
for (let dirUri = Utils.dirname(uri); dirUri.path.length > 1; dirUri = Utils.dirname(dirUri)) {
|
||||
const dirWatcher: DirWatcherEntry = { uri: dirUri, listeners: [] };
|
||||
|
||||
let parentDirWatcher = this.dirWatchers.get(dirUri);
|
||||
if (!parentDirWatcher) {
|
||||
const glob = new vscode.RelativePattern(Utils.dirname(dirUri), Utils.basename(dirUri));
|
||||
const parentWatcher = vscode.workspace.createFileSystemWatcher(glob, !listeners.create, true, !listeners.delete);
|
||||
parentDirWatcher = { refCount: 0, watcher: parentWatcher };
|
||||
this.dirWatchers.set(dirUri, parentDirWatcher);
|
||||
}
|
||||
parentDirWatcher.refCount++;
|
||||
|
||||
if (listeners.create) {
|
||||
dirWatcher.listeners.push(parentDirWatcher.watcher.onDidCreate(async () => {
|
||||
// Just because the parent dir was created doesn't mean our file was created
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(uri);
|
||||
if (stat.type === vscode.FileType.File) {
|
||||
listeners.create!();
|
||||
}
|
||||
} catch {
|
||||
// Noop
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (listeners.delete) {
|
||||
// When the parent dir is deleted, consider our file deleted too
|
||||
// TODO: this fires if the file previously did not exist and then the parent is deleted
|
||||
dirWatcher.listeners.push(parentDirWatcher.watcher.onDidDelete(listeners.delete));
|
||||
}
|
||||
|
||||
parentDirWatchers.push(dirWatcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete(id: number): void {
|
||||
const entry = this.fileWatchers.get(id);
|
||||
if (entry) {
|
||||
for (const dirWatcher of entry.dirWatchers) {
|
||||
disposeAll(dirWatcher.listeners);
|
||||
|
||||
const dirWatcherEntry = this.dirWatchers.get(dirWatcher.uri);
|
||||
if (dirWatcherEntry) {
|
||||
if (--dirWatcherEntry.refCount <= 0) {
|
||||
dirWatcherEntry.watcher.dispose();
|
||||
this.dirWatchers.delete(dirWatcher.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry.watcher.dispose();
|
||||
}
|
||||
|
||||
this.fileWatchers.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ITextDocument } from '../types/textDocument';
|
||||
|
||||
export class InMemoryDocument implements ITextDocument {
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
private readonly contents: string,
|
||||
public readonly version = 0,
|
||||
) { }
|
||||
|
||||
getText(): string {
|
||||
return this.contents;
|
||||
}
|
||||
}
|
||||
38
extensions/markdown-language-features/src/client/protocol.ts
Normal file
38
extensions/markdown-language-features/src/client/protocol.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { RequestType } from 'vscode-languageclient';
|
||||
import type * as lsp from 'vscode-languageserver-types';
|
||||
import type * as md from 'vscode-markdown-languageservice';
|
||||
|
||||
|
||||
export type ResolvedDocumentLinkTarget =
|
||||
| { readonly kind: 'file'; readonly uri: vscode.Uri; position?: lsp.Position; fragment?: string }
|
||||
| { readonly kind: 'folder'; readonly uri: vscode.Uri }
|
||||
| { readonly kind: 'external'; readonly uri: vscode.Uri };
|
||||
|
||||
//#region From server
|
||||
export const parse = new RequestType<{ uri: string }, 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; watchParentDirs: boolean }, 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 getEditForFileRenames = new RequestType<Array<{ oldUri: string; newUri: string }>, lsp.WorkspaceEdit, any>('markdown/getEditForFileRenames');
|
||||
|
||||
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
|
||||
|
||||
export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget');
|
||||
//#endregion
|
||||
@@ -0,0 +1,80 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ITextDocument } from '../types/textDocument';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile, looksLikeMarkdownPath } from '../util/file';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { ResourceMap } from '../util/resourceMap';
|
||||
|
||||
/**
|
||||
* Provides set of markdown files known to VS Code.
|
||||
*
|
||||
* This includes both opened text documents and markdown files in the workspace.
|
||||
*/
|
||||
export class VsCodeMdWorkspace extends Disposable {
|
||||
|
||||
private _watcher: vscode.FileSystemWatcher | undefined;
|
||||
|
||||
private readonly _documentCache = new ResourceMap<ITextDocument>();
|
||||
|
||||
private readonly utf8Decoder = new TextDecoder('utf-8');
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md'));
|
||||
|
||||
this._register(this._watcher.onDidChange(async resource => {
|
||||
this._documentCache.delete(resource);
|
||||
}));
|
||||
|
||||
this._register(this._watcher.onDidDelete(resource => {
|
||||
this._documentCache.delete(resource);
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidOpenTextDocument(e => {
|
||||
this._documentCache.delete(e.uri);
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidCloseTextDocument(e => {
|
||||
this._documentCache.delete(e.uri);
|
||||
}));
|
||||
}
|
||||
|
||||
private isRelevantMarkdownDocument(doc: vscode.TextDocument) {
|
||||
return isMarkdownFile(doc) && doc.uri.scheme !== 'vscode-bulkeditpreview';
|
||||
}
|
||||
|
||||
public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
|
||||
const existing = this._documentCache.get(resource);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const matchingDocument = vscode.workspace.textDocuments.find((doc) => this.isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString());
|
||||
if (matchingDocument) {
|
||||
this._documentCache.set(resource, matchingDocument);
|
||||
return matchingDocument;
|
||||
}
|
||||
|
||||
if (!looksLikeMarkdownPath(resource)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const bytes = await vscode.workspace.fs.readFile(resource);
|
||||
|
||||
// We assume that markdown is in UTF-8
|
||||
const text = this.utf8Decoder.decode(bytes);
|
||||
const doc = new InMemoryDocument(resource, text, 0);
|
||||
this._documentCache.set(resource, doc);
|
||||
return doc;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user