Skip to content

Commit

Permalink
Clean up structure of markdown extension (#161148)
Browse files Browse the repository at this point in the history
- Move things related to the client under `client`
- Remove extra abstractions that are no longer used
- Add MdLanguageClient type
  • Loading branch information
mjbvz committed Sep 19, 2022
1 parent f4bf1f3 commit d03f015
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 226 deletions.
1 change: 0 additions & 1 deletion extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,6 @@
"morphdom": "^2.6.1",
"picomatch": "^2.3.1",
"vscode-languageclient": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.4",
"vscode-nls": "^5.1.0",
"vscode-uri": "^3.0.3"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,44 @@

import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient';
import { disposeAll, IDisposable } from 'vscode-markdown-languageservice/out/util/dispose';
import { ResourceMap } from 'vscode-markdown-languageservice/out/util/resourceMap';
import * as nls from 'vscode-nls';
import { Utils } from 'vscode-uri';
import { IMdParser } from './markdownEngine';
import { IMdParser } from '../markdownEngine';
import * as proto from './protocol';
import { looksLikeMarkdownPath, markdownFileExtensions } from './util/file';
import { Schemes } from './util/schemes';
import { IMdWorkspace } from './workspace';
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 {

export async function startClient(factory: LanguageClientConstructor, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
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(',')}}`;

Expand Down Expand Up @@ -59,6 +81,8 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
});
}

const workspace = new VsCodeMdWorkspace();

client.onRequest(proto.parse, async (e) => {
const uri = vscode.Uri.parse(e.uri);
const doc = await workspace.getOrLoadMarkdownDocument(uri);
Expand Down Expand Up @@ -125,93 +149,5 @@ export async function startClient(factory: LanguageClientConstructor, workspace:

await client.start();

return client;
}

type DirWatcherEntry = {
readonly uri: vscode.Uri;
readonly listeners: IDisposable[];
};

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);
}
return new MdLanguageClient(client, workspace);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,17 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { ITextDocument } from '../types/textDocument';

export class InMemoryDocument implements ITextDocument {

private readonly _doc: TextDocument;

constructor(
public readonly uri: vscode.Uri, contents: string,
public readonly uri: vscode.Uri,
private readonly contents: string,
public readonly version = 0,
) {

this._doc = TextDocument.create(uri.toString(), 'markdown', version, contents);
}

get lineCount(): number {
return this._doc.lineCount;
}

positionAt(offset: number): vscode.Position {
const pos = this._doc.positionAt(offset);
return new vscode.Position(pos.line, pos.character);
}
) { }

getText(range?: vscode.Range): string {
return this._doc.getText(range);
getText(): string {
return this.contents;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,18 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { ITextDocument } from './types/textDocument';
import { Disposable } from './util/dispose';
import { isMarkdownFile, looksLikeMarkdownPath } from './util/file';
import { InMemoryDocument } from './util/inMemoryDocument';
import { ResourceMap } from './util/resourceMap';

/**
* Provides set of markdown files in the current workspace.
*/
export interface IMdWorkspace {
getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined>;
}
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 implements IMdWorkspace {
export class VsCodeMdWorkspace extends Disposable {

private _watcher: vscode.FileSystemWatcher | undefined;

Expand Down
18 changes: 6 additions & 12 deletions extensions/markdown-language-features/src/extension.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { startClient } from './client';
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { MdLanguageClient, startClient } from './client/client';
import { activateShared } from './extension.shared';
import { VsCodeOutputLogger } from './logging';
import { IMdParser, MarkdownItEngine } from './markdownEngine';
import { getMarkdownExtensionContributions } from './markdownExtensions';
import { githubSlugifier } from './slugify';
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';

export async function activate(context: vscode.ExtensionContext) {
const contributions = getMarkdownExtensionContributions(context);
Expand All @@ -22,21 +21,16 @@ export async function activate(context: vscode.ExtensionContext) {

const engine = new MarkdownItEngine(contributions, githubSlugifier, logger);

const workspace = new VsCodeMdWorkspace();
context.subscriptions.push(workspace);

const client = await startServer(context, workspace, engine);
context.subscriptions.push({
dispose: () => client.stop()
});
const client = await startServer(context, engine);
context.subscriptions.push(client);
activateShared(context, client, engine, logger, contributions);
}

function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise<MdLanguageClient> {
const serverMain = vscode.Uri.joinPath(context.extensionUri, 'server/dist/browser/main.js');
const worker = new Worker(serverMain.toString());

return startClient((id: string, name: string, clientOptions: LanguageClientOptions) => {
return new LanguageClient(id, name, clientOptions, worker);
}, workspace, parser);
}, parser);
}

0 comments on commit d03f015

Please sign in to comment.