Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up structure of markdown extension #161148

Merged
merged 2 commits into from Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion extensions/markdown-language-features/package.json
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
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);
}
@@ -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);
}
}
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;
}
}
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
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);
}