diff --git a/extensions/markdown-language-features/server/src/protocol.ts b/extensions/markdown-language-features/server/src/protocol.ts index 9aa467d2b1e..efb723d5f67 100644 --- a/extensions/markdown-language-features/server/src/protocol.ts +++ b/extensions/markdown-language-features/server/src/protocol.ts @@ -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'); diff --git a/extensions/markdown-language-features/server/src/workspace.ts b/extensions/markdown-language-features/server/src/workspace.ts index e1dca9f20c5..3cd75cf5e84 100644 --- a/extensions/markdown-language-features/server/src/workspace.ts +++ b/extensions/markdown-language-features/server/src/workspace.ts @@ -236,6 +236,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { id, uri: resource.toString(), options, + watchParentDirs: true, }); return { diff --git a/extensions/markdown-language-features/src/client.ts b/extensions/markdown-language-features/src/client.ts index 553859bb541..0695e275e25 100644 --- a/extensions/markdown-language-features/src/client.ts +++ b/extensions/markdown-language-features/src/client.ts @@ -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(); @@ -92,19 +96,24 @@ export async function startClient(factory: LanguageClientConstructor, workspace: return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString()); }); - const watchers = new Map(); + const watchers = new FileWatcherManager(); client.onRequest(proto.fs_watcher_create, async (params): Promise => { 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 => { - watchers.get(params.id)?.dispose(); watchers.delete(params.id); }); @@ -112,3 +121,91 @@ export async function startClient(factory: LanguageClientConstructor, workspace: return client; } + +type DirWatcherEntry = { + readonly uri: vscode.Uri; + readonly listeners: IDisposable[]; +}; + +class FileWatcherManager { + + private readonly fileWatchers = new Map(); + + 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); + } +} diff --git a/extensions/markdown-language-features/src/protocol.ts b/extensions/markdown-language-features/src/protocol.ts index 51fcc8ab2d8..61a13a8bd88 100644 --- a/extensions/markdown-language-features/src/protocol.ts +++ b/extensions/markdown-language-features/src/protocol.ts @@ -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'; @@ -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');