Add diagnostics for markdown links (#148578)

* Initial work on md link diagnostics

* Adding settings to enable/disable validation

* Add delay for recomputing diagnostics

* 💄

* Split test on diagnostics compute vs management

* Validate on file open

* Remove dianostics on file close

* Allow paths to folders

* Add validation configuration option
This commit is contained in:
Matt Bierner 2022-05-02 16:06:00 -07:00 committed by GitHub
parent 2108837fc1
commit eba8ef0547
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 589 additions and 9 deletions

View file

@ -413,6 +413,45 @@
"default": true,
"markdownDescription": "%configuration.markdown.editor.drop.enabled%",
"scope": "resource"
},
"markdown.experimental.validate.enabled": {
"type": "boolean",
"scope": "resource",
"description": "%configuration.markdown.experimental.validate.enabled.description%",
"default": false
},
"markdown.experimental.validate.referenceLinks": {
"type": "string",
"scope": "resource",
"description": "%configuration.markdown.experimental.validate.referenceLinks.description%",
"default": "warning",
"enum": [
"ignore",
"warning",
"error"
]
},
"markdown.experimental.validate.headerLinks": {
"type": "string",
"scope": "resource",
"description": "%configuration.markdown.experimental.validate.headerLinks.description%",
"default": "warning",
"enum": [
"ignore",
"warning",
"error"
]
},
"markdown.experimental.validate.fileLinks": {
"type": "string",
"scope": "resource",
"description": "%configuration.markdown.experimental.validate.fileLinks.description%",
"default": "warning",
"enum": [
"ignore",
"warning",
"error"
]
}
}
},

View file

@ -29,5 +29,9 @@
"configuration.markdown.links.openLocation.beside": "Open links beside the active editor.",
"configuration.markdown.suggest.paths.enabled.description": "Enable/disable path suggestions for markdown links",
"configuration.markdown.editor.drop.enabled": "Enable/disable dropping into the markdown editor to insert shift. Requires enabling `#workbenck.experimental.editor.dropIntoEditor.enabled#`.",
"configuration.markdown.experimental.validate.enabled.description": "Enable/disable all error reporting in Markdown files.",
"configuration.markdown.experimental.validate.referenceLinks.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.experimental.validate.enabled#`.",
"configuration.markdown.experimental.validate.headerLinks.description": "Validate links to headers in Markdown files, e.g. `[link](#header)`. Requires enabling `#markdown.experimental.validate.enabled#`.",
"configuration.markdown.experimental.validate.fileLinks.description": "Validate links to other files in Markdown files, e.g. `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.experimental.validate.enabled#`.",
"workspaceTrust": "Required for loading styles configured in the workspace."
}

View file

@ -6,6 +6,7 @@
import * as vscode from 'vscode';
import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { register as registerDiagnostics } from './languageFeatures/diagnostics';
import { MdDefinitionProvider } from './languageFeatures/definitionProvider';
import { MdLinkProvider } from './languageFeatures/documentLinkProvider';
import { MdDocumentSymbolProvider } from './languageFeatures/documentSymbolProvider';
@ -75,6 +76,7 @@ function registerMarkdownLanguageFeatures(
vscode.languages.registerRenameProvider(selector, new MdRenameProvider(referencesProvider, workspaceContents, githubSlugifier)),
vscode.languages.registerDefinitionProvider(selector, new MdDefinitionProvider(referencesProvider)),
MdPathCompletionProvider.register(selector, engine, linkProvider),
registerDiagnostics(engine, workspaceContents, linkProvider),
registerDropIntoEditor(selector),
registerFindFileReferences(commandManager, referencesProvider),
);

View file

@ -0,0 +1,298 @@
/*---------------------------------------------------------------------------------------------
* 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 * as nls from 'vscode-nls';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { noopToken } from '../test/util';
import { Delayer } from '../util/async';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider } from './documentLinkProvider';
import { tryFindMdDocumentForLink } from './references';
const localize = nls.loadMessageBundle();
export interface DiagnosticConfiguration {
/**
* Fired when the configuration changes.
*/
readonly onDidChange: vscode.Event<void>;
getOptions(resource: vscode.Uri): DiagnosticOptions;
}
export enum DiagnosticLevel {
ignore = 'ignore',
warning = 'warning',
error = 'error',
}
export interface DiagnosticOptions {
readonly enabled: boolean;
readonly validateReferences: DiagnosticLevel;
readonly validateOwnHeaders: DiagnosticLevel;
readonly validateFilePaths: DiagnosticLevel;
}
function toSeverity(level: DiagnosticLevel): vscode.DiagnosticSeverity | undefined {
switch (level) {
case DiagnosticLevel.error: return vscode.DiagnosticSeverity.Error;
case DiagnosticLevel.warning: return vscode.DiagnosticSeverity.Warning;
case DiagnosticLevel.ignore: return undefined;
}
}
class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConfiguration {
private readonly _onDidChange = this._register(new vscode.EventEmitter<void>());
public readonly onDidChange = this._onDidChange.event;
constructor() {
super();
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('markdown.experimental.validate.enabled')) {
this._onDidChange.fire();
}
}));
}
public getOptions(resource: vscode.Uri): DiagnosticOptions {
const config = vscode.workspace.getConfiguration('markdown', resource);
return {
enabled: config.get<boolean>('experimental.validate.enabled', false),
validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks', DiagnosticLevel.ignore),
validateOwnHeaders: config.get<DiagnosticLevel>('experimental.validate.headerLinks', DiagnosticLevel.ignore),
validateFilePaths: config.get<DiagnosticLevel>('experimental.validate.fileLinks', DiagnosticLevel.ignore),
};
}
}
export class DiagnosticManager extends Disposable {
private readonly collection: vscode.DiagnosticCollection;
private readonly pendingDiagnostics = new Set<vscode.Uri>();
private readonly diagnosticDelayer: Delayer<void>;
constructor(
private readonly computer: DiagnosticComputer,
private readonly configuration: DiagnosticConfiguration,
) {
super();
this.diagnosticDelayer = new Delayer(300);
this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown'));
this._register(this.configuration.onDidChange(() => {
this.rebuild();
}));
const onDocUpdated = (doc: vscode.TextDocument) => {
if (isMarkdownFile(doc)) {
this.pendingDiagnostics.add(doc.uri);
this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics());
}
};
this._register(vscode.workspace.onDidOpenTextDocument(doc => {
onDocUpdated(doc);
}));
this._register(vscode.workspace.onDidChangeTextDocument(e => {
onDocUpdated(e.document);
}));
this._register(vscode.workspace.onDidCloseTextDocument(doc => {
this.pendingDiagnostics.delete(doc.uri);
this.collection.delete(doc.uri);
}));
this.rebuild();
}
private recomputePendingDiagnostics(): void {
const pending = [...this.pendingDiagnostics];
this.pendingDiagnostics.clear();
for (const resource of pending) {
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === resource.fsPath);
if (doc) {
this.update(doc);
}
}
}
private async rebuild() {
this.collection.clear();
const allOpenedTabResources = this.getAllTabResources();
await Promise.all(
vscode.workspace.textDocuments
.filter(doc => allOpenedTabResources.has(doc.uri.toString()) && isMarkdownFile(doc))
.map(doc => this.update(doc)));
}
private getAllTabResources() {
const openedTabDocs = new Map<string, vscode.Uri>();
for (const group of vscode.window.tabGroups.all) {
for (const tab of group.tabs) {
if (tab.input instanceof vscode.TabInputText) {
openedTabDocs.set(tab.input.uri.toString(), tab.input.uri);
}
}
}
return openedTabDocs;
}
private async update(doc: vscode.TextDocument): Promise<void> {
const diagnostics = await this.getDiagnostics(doc, noopToken);
this.collection.set(doc.uri, diagnostics);
}
public async getDiagnostics(doc: SkinnyTextDocument, token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const config = this.configuration.getOptions(doc.uri);
if (!config.enabled) {
return [];
}
return this.computer.getDiagnostics(doc, config, token);
}
}
export class DiagnosticComputer {
constructor(
private readonly engine: MarkdownEngine,
private readonly workspaceContents: MdWorkspaceContents,
private readonly linkProvider: MdLinkProvider,
) { }
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const links = await this.linkProvider.getAllLinks(doc, token);
if (token.isCancellationRequested) {
return [];
}
return (await Promise.all([
this.validateFileLinks(doc, options, links, token),
Array.from(this.validateReferenceLinks(options, links)),
this.validateOwnHeaderLinks(doc, options, links, token),
])).flat();
}
private async validateOwnHeaderLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateOwnHeaders);
if (typeof severity === 'undefined') {
return [];
}
const toc = await TableOfContents.create(this.engine, doc);
if (token.isCancellationRequested) {
return [];
}
const diagnostics: vscode.Diagnostic[] = [];
for (const link of links) {
if (link.href.kind === 'internal'
&& link.href.path.toString() === doc.uri.toString()
&& link.href.fragment
&& !toc.lookup(link.href.fragment)
) {
diagnostics.push(new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
severity));
}
}
return diagnostics;
}
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[]): Iterable<vscode.Diagnostic> {
const severity = toSeverity(options.validateReferences);
if (typeof severity === 'undefined') {
return [];
}
const definitionSet = new LinkDefinitionSet(links);
for (const link of links) {
if (link.href.kind === 'reference' && !definitionSet.lookup(link.href.ref)) {
yield new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidReferenceLink', 'No link reference found: \'{0}\'', link.href.ref),
severity);
}
}
}
private async validateFileLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateFilePaths);
if (typeof severity === 'undefined') {
return [];
}
const tocs = new Map<string, TableOfContents>();
// TODO: cache links so we don't recompute duplicate hrefs
// TODO: parallelize
const diagnostics: vscode.Diagnostic[] = [];
for (const link of links) {
if (token.isCancellationRequested) {
return [];
}
if (link.href.kind !== 'internal') {
continue;
}
const hrefDoc = await tryFindMdDocumentForLink(link.href, this.workspaceContents);
if (hrefDoc && hrefDoc.uri.toString() === doc.uri.toString()) {
continue;
}
if (!hrefDoc && !await this.workspaceContents.pathExists(link.href.path)) {
diagnostics.push(
new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidPathLink', 'File does not exist at path: {0}', (link.href as InternalHref).path.toString(true)),
severity));
} else if (hrefDoc) {
if (link.href.fragment) {
// validate fragment looks valid
let hrefDocToc = tocs.get(link.href.path.toString());
if (!hrefDocToc) {
hrefDocToc = await TableOfContents.create(this.engine, hrefDoc);
tocs.set(link.href.path.toString(), hrefDocToc);
}
if (!hrefDocToc.lookup(link.href.fragment)) {
diagnostics.push(
new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', (link.href as InternalHref).path.fragment),
severity));
}
}
}
}
return diagnostics;
}
}
export function register(
engine: MarkdownEngine,
workspaceContents: MdWorkspaceContents,
linkProvider: MdLinkProvider,
): vscode.Disposable {
const configuration = new VSCodeDiagnosticConfiguration();
const manager = new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
return vscode.Disposable.from(configuration, manager);
}

View file

@ -222,7 +222,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
document: SkinnyTextDocument,
token: vscode.CancellationToken
): Promise<vscode.DocumentLink[]> {
const allLinks = await this.getAllLinks(document);
const allLinks = await this.getAllLinks(document, token);
if (token.isCancellationRequested) {
return [];
}
@ -256,8 +256,12 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
}
}
public async getAllLinks(document: SkinnyTextDocument): Promise<MdLink[]> {
public async getAllLinks(document: SkinnyTextDocument, token: vscode.CancellationToken): Promise<MdLink[]> {
const codeInDocument = await findCode(document, this.engine);
if (token.isCancellationRequested) {
return [];
}
return Array.from([
...this.getInlineLinks(document, codeInDocument),
...this.getReferenceLinks(document, codeInDocument),

View file

@ -7,6 +7,7 @@ import * as uri from 'vscode-uri';
import { MarkdownEngine } from '../markdownEngine';
import { Slugifier } from '../slugify';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { noopToken } from '../test/util';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, MdLink, MdLinkProvider } from './documentLinkProvider';
@ -70,7 +71,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
) {
super();
this._linkCache = this._register(new MdWorkspaceCache(workspaceContents, doc => linkProvider.getAllLinks(doc)));
this._linkCache = this._register(new MdWorkspaceCache(workspaceContents, doc => linkProvider.getAllLinks(doc, noopToken)));
}
async provideReferences(document: SkinnyTextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[] | undefined> {
@ -128,7 +129,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
}
private async getReferencesToLinkAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const docLinks = await this.linkProvider.getAllLinks(document);
const docLinks = await this.linkProvider.getAllLinks(document, token);
for (const link of docLinks) {
if (link.kind === 'definition') {

View file

@ -169,7 +169,7 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
}
// First rename the file
if (await this.workspaceContents.fileExists(targetUri)) {
if (await this.workspaceContents.pathExists(targetUri)) {
fileRenames.push({ from: targetUri, to: resolvedNewFilePath });
edit.renameFile(targetUri, resolvedNewFilePath);
}

View file

@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as vscode from 'vscode';
import 'mocha';
import { DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions } from '../languageFeatures/diagnostics';
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
import { createNewMarkdownEngine } from './engine';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { assertRangeEqual, joinLines, noopToken, workspacePath } from './util';
function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents: MdWorkspaceContents) {
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkProvider(engine);
const computer = new DiagnosticComputer(engine, workspaceContents, linkProvider);
return computer.getDiagnostics(doc, {
enabled: true,
validateFilePaths: DiagnosticLevel.warning,
validateOwnHeaders: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
}, noopToken);
}
function createDiagnosticsManager(workspaceContents: MdWorkspaceContents, configuration = new MemoryDiagnosticConfiguration()) {
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkProvider(engine);
return new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
}
class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
private readonly _onDidChange = new vscode.EventEmitter<void>();
public readonly onDidChange = this._onDidChange.event;
constructor(
private readonly enabled: boolean = true,
) { }
getOptions(_resource: vscode.Uri): DiagnosticOptions {
if (!this.enabled) {
return {
enabled: false,
validateFilePaths: DiagnosticLevel.ignore,
validateOwnHeaders: DiagnosticLevel.ignore,
validateReferences: DiagnosticLevel.ignore,
};
}
return {
enabled: true,
validateFilePaths: DiagnosticLevel.warning,
validateOwnHeaders: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
};
}
}
suite('markdown: Diagnostics', () => {
test('Should not return any diagnostics for empty document', async () => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`text`,
));
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
assert.deepStrictEqual(diagnostics, []);
});
test('Should generate diagnostic for link to file that does not exist', async () => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[bad](/no/such/file.md)`,
`[good](/doc.md)`,
`[good-ref]: /doc.md`,
`[bad-ref]: /no/such/file.md`,
));
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
assert.deepStrictEqual(diagnostics.length, 2);
assertRangeEqual(new vscode.Range(0, 6, 0, 22), diagnostics[0].range);
assertRangeEqual(new vscode.Range(3, 11, 3, 27), diagnostics[1].range);
});
test('Should generate diagnostics for links to header that does not exist in current file', async () => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[good](#good-header)`,
`# Good Header`,
`[bad](#no-such-header)`,
`[good](#good-header)`,
`[good-ref]: #good-header`,
`[bad-ref]: #no-such-header`,
));
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
assert.deepStrictEqual(diagnostics.length, 2);
assertRangeEqual(new vscode.Range(2, 6, 2, 21), diagnostics[0].range);
assertRangeEqual(new vscode.Range(5, 11, 5, 26), diagnostics[1].range);
});
test('Should generate diagnostics for links to non-existent headers in other files', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`# My header`,
`[good](#my-header)`,
`[good](/doc1.md#my-header)`,
`[good](doc1.md#my-header)`,
`[good](/doc2.md#other-header)`,
`[bad](/doc2.md#no-such-other-header)`,
));
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(
`# Other header`,
));
const diagnostics = await getComputedDiagnostics(doc1, new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]));
assert.deepStrictEqual(diagnostics.length, 1);
assertRangeEqual(new vscode.Range(5, 6, 5, 35), diagnostics[0].range);
});
test('Should support links both with and without .md file extension', async () => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`# My header`,
`[good](#my-header)`,
`[good](/doc.md#my-header)`,
`[good](doc.md#my-header)`,
`[good](/doc#my-header)`,
`[good](doc#my-header)`,
));
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
assert.deepStrictEqual(diagnostics.length, 0);
});
test('Should generate diagnostics for non-existent link reference', async () => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[good link][good]`,
`[bad link][no-such]`,
``,
`[good]: http://example.com`,
));
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
assert.deepStrictEqual(diagnostics.length, 1);
assertRangeEqual(new vscode.Range(1, 11, 1, 18), diagnostics[0].range);
});
test('Should not generate diagnostics when validate is disabled', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](#no-such-header)`,
`[text][no-such-ref]`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(false));
const diagnostics = await manager.getDiagnostics(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
});

View file

@ -25,7 +25,7 @@ export class InMemoryWorkspaceMarkdownDocuments implements MdWorkspaceContents {
return this._documents.get(this.getKey(resource));
}
public async fileExists(resource: vscode.Uri): Promise<boolean> {
public async pathExists(resource: vscode.Uri): Promise<boolean> {
return this._documents.has(this.getKey(resource));
}

View file

@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
export interface ITask<T> {
(): T;
}
export class Delayer<T> {
public defaultDelay: number;
private timeout: any; // Timer
private completionPromise: Promise<T | null> | null;
private onSuccess: ((value: T | PromiseLike<T> | undefined) => void) | null;
private task: ITask<T> | null;
constructor(defaultDelay: number) {
this.defaultDelay = defaultDelay;
this.timeout = null;
this.completionPromise = null;
this.onSuccess = null;
this.task = null;
}
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T | null> {
this.task = task;
if (delay >= 0) {
this.cancelTimeout();
}
if (!this.completionPromise) {
this.completionPromise = new Promise<T | undefined>((resolve) => {
this.onSuccess = resolve;
}).then(() => {
this.completionPromise = null;
this.onSuccess = null;
const result = this.task && this.task();
this.task = null;
return result;
});
}
if (delay >= 0 || this.timeout === null) {
this.timeout = setTimeout(() => {
this.timeout = null;
this.onSuccess?.(undefined);
}, delay >= 0 ? delay : this.defaultDelay);
}
return this.completionPromise;
}
private cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
export function setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable {
if (global.setImmediate) {
const handle = global.setImmediate(callback, ...args);
return { dispose: () => global.clearImmediate(handle) };
} else {
const handle = setTimeout(callback, 0, ...args);
return { dispose: () => clearTimeout(handle) };
}
}

View file

@ -42,7 +42,7 @@ export interface MdWorkspaceContents {
getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined>;
fileExists(resource: vscode.Uri): Promise<boolean>;
pathExists(resource: vscode.Uri): Promise<boolean>;
readonly onDidChangeMarkdownDocument: vscode.Event<SkinnyTextDocument>;
readonly onDidCreateMarkdownDocument: vscode.Event<SkinnyTextDocument>;
@ -159,13 +159,13 @@ export class VsCodeMdWorkspaceContents extends Disposable implements MdWorkspace
}
}
public async fileExists(target: vscode.Uri): Promise<boolean> {
public async pathExists(target: vscode.Uri): Promise<boolean> {
let targetResourceStat: vscode.FileStat | undefined;
try {
targetResourceStat = await vscode.workspace.fs.stat(target);
} catch {
return false;
}
return targetResourceStat.type === vscode.FileType.File;
return targetResourceStat.type === vscode.FileType.File || targetResourceStat.type === vscode.FileType.Directory;
}
}