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

Fix markdown link diagnostics not updated when directories are renamed / deleted #157956

Merged
merged 1 commit into from Aug 12, 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
Expand Up @@ -14,7 +14,7 @@ export const fs_readFile = new RequestType<{ uri: string }, number[], any>('mark
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_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');
Expand Down
Expand Up @@ -236,6 +236,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
id,
uri: resource.toString(),
options,
watchParentDirs: true,
});

return {
Expand Down
111 changes: 104 additions & 7 deletions extensions/markdown-language-features/src/client.ts
Expand Up @@ -5,10 +5,14 @@

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 * as proto from './protocol';
import { looksLikeMarkdownPath, markdownFileExtensions } from './util/file';
import { Schemes } from './util/schemes';
import { IMdWorkspace } from './workspace';

const localize = nls.loadMessageBundle();
Expand Down Expand Up @@ -92,23 +96,116 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
});

const watchers = new Map<number, vscode.FileSystemWatcher>();
const watchers = new FileWatcherManager();

client.onRequest(proto.fs_watcher_create, async (params): Promise<void> => {
const id = params.id;
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(params.uri), '*'), params.options.ignoreCreate, params.options.ignoreChange, params.options.ignoreDelete);
watchers.set(id, watcher);
watcher.onDidCreate(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'create' }); });
watcher.onDidChange(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'change' }); });
watcher.onDidDelete(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'delete' }); });
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.get(params.id)?.dispose();
watchers.delete(params.id);
});

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);
}
}
4 changes: 2 additions & 2 deletions extensions/markdown-language-features/src/protocol.ts
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import Token = require('markdown-it/lib/token');
import type Token = require('markdown-it/lib/token');
import { RequestType } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
Expand All @@ -15,7 +15,7 @@ export const fs_readFile = new RequestType<{ uri: string }, number[], any>('mark
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_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');
Expand Down