diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 729824b7dbe..32eef08f82e 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -51,7 +51,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(workspace, parser, tocProvider); const contentProvider = new MdDocumentRenderer(engine, context, cspArbiter, contributions, logger); - const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, tocProvider); + const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider); context.subscriptions.push(previewManager); context.subscriptions.push(registerMarkdownLanguageFeatures(parser, workspace, commandManager, tocProvider, logger)); @@ -88,7 +88,7 @@ function registerMarkdownLanguageFeatures( registerFindFileReferenceSupport(commandManager, referencesProvider), registerFoldingSupport(selector, parser, tocProvider), registerPasteSupport(selector), - registerPathCompletionSupport(selector, parser, linkProvider), + registerPathCompletionSupport(selector, workspace, parser, linkProvider), registerReferencesSupport(selector, referencesProvider), registerRenameSupport(selector, workspace, referencesProvider, parser.slugifier), registerSmartSelectSupport(selector, parser, tocProvider), diff --git a/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts b/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts index 8e1c4ebd6a9..448da308a25 100644 --- a/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts +++ b/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts @@ -9,6 +9,8 @@ import { IMdParser } from '../markdownEngine'; import { TableOfContents } from '../tableOfContents'; import { ITextDocument } from '../types/textDocument'; import { resolveUriToMarkdownFile } from '../util/openDocumentLink'; +import { Schemes } from '../util/schemes'; +import { IMdWorkspace } from '../workspace'; import { MdLinkProvider } from './documentLinks'; enum CompletionContextKind { @@ -87,6 +89,7 @@ function tryDecodeUriComponent(str: string): string { export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProvider { constructor( + private readonly workspace: IMdWorkspace, private readonly parser: IMdParser, private readonly linkProvider: MdLinkProvider, ) { } @@ -128,7 +131,7 @@ export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProv if (context.anchorInfo) { // Anchor to a different document const rawUri = this.resolveReference(document, context.anchorInfo.beforeAnchor); if (rawUri) { - const otherDoc = await resolveUriToMarkdownFile(rawUri); + const otherDoc = await resolveUriToMarkdownFile(this.workspace, rawUri); if (otherDoc) { const anchorStartPosition = position.translate({ characterDelta: -(context.anchorInfo.anchorPrefix.length + 1) }); const range = new vscode.Range(anchorStartPosition, position); @@ -284,13 +287,7 @@ export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProv const pathSegmentEnd = position.translate({ characterDelta: context.linkSuffix.length }); const replacementRange = new vscode.Range(pathSegmentStart, pathSegmentEnd); - let dirInfo: Array<[string, vscode.FileType]>; - try { - dirInfo = await vscode.workspace.fs.readDirectory(parentDir); - } catch { - return; - } - + const dirInfo = await this.workspace.readDirectory(parentDir); for (const [name, type] of dirInfo) { // Exclude paths that start with `.` if (name.startsWith('.')) { @@ -328,7 +325,7 @@ export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProv private resolvePath(root: vscode.Uri, ref: string): vscode.Uri | undefined { try { - if (root.scheme === 'file') { + if (root.scheme === Schemes.file) { return vscode.Uri.file(resolve(dirname(root.fsPath), ref)); } else { return root.with({ @@ -356,8 +353,9 @@ export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProv export function registerPathCompletionSupport( selector: vscode.DocumentSelector, + workspace: IMdWorkspace, parser: IMdParser, linkProvider: MdLinkProvider, ): vscode.Disposable { - return vscode.languages.registerCompletionItemProvider(selector, new MdVsCodePathCompletionProvider(parser, linkProvider), '.', '/', '#'); + return vscode.languages.registerCompletionItemProvider(selector, new MdVsCodePathCompletionProvider(workspace, parser, linkProvider), '.', '/', '#'); } diff --git a/extensions/markdown-language-features/src/preview/preview.ts b/extensions/markdown-language-features/src/preview/preview.ts index 0a9eccb12ed..6e792d36edc 100644 --- a/extensions/markdown-language-features/src/preview/preview.ts +++ b/extensions/markdown-language-features/src/preview/preview.ts @@ -14,6 +14,7 @@ import { isMarkdownFile } from '../util/file'; import { openDocumentLink, resolveDocumentLink, resolveUriToMarkdownFile } from '../util/openDocumentLink'; import { WebviewResourceProvider } from '../util/resources'; import { urlToUri } from '../util/url'; +import { IMdWorkspace } from '../workspace'; import { MdDocumentRenderer } from './documentRenderer'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling'; @@ -118,6 +119,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private readonly delegate: MarkdownPreviewDelegate, private readonly _contentProvider: MdDocumentRenderer, private readonly _previewConfigurations: MarkdownPreviewConfigurationManager, + private readonly _workspace: IMdWorkspace, private readonly _logger: ILogger, private readonly _contributionProvider: MarkdownContributionProvider, private readonly _tocProvider: MdTableOfContentsProvider, @@ -449,7 +451,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { const config = vscode.workspace.getConfiguration('markdown', this.resource); const openLinks = config.get('preview.openMarkdownLinks', 'inPreview'); if (openLinks === 'inPreview') { - const linkedDoc = await resolveUriToMarkdownFile(targetResource); + const linkedDoc = await resolveUriToMarkdownFile(this._workspace, targetResource); if (linkedDoc) { this.delegate.openPreviewLinkToMarkdownFile(linkedDoc.uri, targetResource.fragment); return; @@ -502,12 +504,13 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow contentProvider: MdDocumentRenderer, previewConfigurations: MarkdownPreviewConfigurationManager, topmostLineMonitor: TopmostLineMonitor, + workspace: IMdWorkspace, logger: ILogger, contributionProvider: MarkdownContributionProvider, tocProvider: MdTableOfContentsProvider, scrollLine?: number, ): StaticMarkdownPreview { - return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, tocProvider, scrollLine); + return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, workspace, logger, contributionProvider, tocProvider, scrollLine); } private readonly preview: MarkdownPreview; @@ -518,6 +521,7 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow contentProvider: MdDocumentRenderer, private readonly _previewConfigurations: MarkdownPreviewConfigurationManager, topmostLineMonitor: TopmostLineMonitor, + workspace: IMdWorkspace, logger: ILogger, contributionProvider: MarkdownContributionProvider, tocProvider: MdTableOfContentsProvider, @@ -532,7 +536,7 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow fragment }), StaticMarkdownPreview.customEditorViewType, this._webviewPanel.viewColumn); } - }, contentProvider, _previewConfigurations, logger, contributionProvider, tocProvider)); + }, contentProvider, _previewConfigurations, workspace, logger, contributionProvider, tocProvider)); this._register(this._webviewPanel.onDidDispose(() => { this.dispose(); @@ -613,6 +617,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo webview: vscode.WebviewPanel, contentProvider: MdDocumentRenderer, previewConfigurations: MarkdownPreviewConfigurationManager, + workspace: IMdWorkspace, logger: ILogger, topmostLineMonitor: TopmostLineMonitor, contributionProvider: MarkdownContributionProvider, @@ -621,7 +626,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo webview.iconPath = contentProvider.iconPath; return new DynamicMarkdownPreview(webview, input, - contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, tocProvider); + contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider); } public static create( @@ -629,6 +634,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo previewColumn: vscode.ViewColumn, contentProvider: MdDocumentRenderer, previewConfigurations: MarkdownPreviewConfigurationManager, + workspace: IMdWorkspace, logger: ILogger, topmostLineMonitor: TopmostLineMonitor, contributionProvider: MarkdownContributionProvider, @@ -642,7 +648,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo webview.iconPath = contentProvider.iconPath; return new DynamicMarkdownPreview(webview, input, - contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, tocProvider); + contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider); } private constructor( @@ -650,6 +656,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo input: DynamicPreviewInput, private readonly _contentProvider: MdDocumentRenderer, private readonly _previewConfigurations: MarkdownPreviewConfigurationManager, + private readonly _workspace: IMdWorkspace, private readonly _logger: ILogger, private readonly _topmostLineMonitor: TopmostLineMonitor, private readonly _contributionProvider: MarkdownContributionProvider, @@ -807,6 +814,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo }, this._contentProvider, this._previewConfigurations, + this._workspace, this._logger, this._contributionProvider, this._tocProvider); diff --git a/extensions/markdown-language-features/src/preview/previewManager.ts b/extensions/markdown-language-features/src/preview/previewManager.ts index b1e85165d64..134989ce181 100644 --- a/extensions/markdown-language-features/src/preview/previewManager.ts +++ b/extensions/markdown-language-features/src/preview/previewManager.ts @@ -9,6 +9,7 @@ import { MarkdownContributionProvider } from '../markdownExtensions'; import { MdTableOfContentsProvider } from '../tableOfContents'; import { Disposable, disposeAll } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; +import { IMdWorkspace } from '../workspace'; import { MdDocumentRenderer } from './documentRenderer'; import { DynamicMarkdownPreview, IManagedMarkdownPreview, StaticMarkdownPreview } from './preview'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; @@ -69,6 +70,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview public constructor( private readonly _contentProvider: MdDocumentRenderer, + private readonly _workspace: IMdWorkspace, private readonly _logger: ILogger, private readonly _contributions: MarkdownContributionProvider, private readonly _tocProvider: MdTableOfContentsProvider, @@ -163,6 +165,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview webview, this._contentProvider, this._previewConfigurations, + this._workspace, this._logger, this._topmostLineMonitor, this._contributions, @@ -182,6 +185,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this._contentProvider, this._previewConfigurations, this._topmostLineMonitor, + this._workspace, this._logger, this._contributions, this._tocProvider, @@ -206,6 +210,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview previewSettings.previewColumn, this._contentProvider, this._previewConfigurations, + this._workspace, this._logger, this._topmostLineMonitor, this._contributions, diff --git a/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts b/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts index 9cd2b03bbe5..81f1101e5fa 100644 --- a/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts +++ b/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts @@ -35,6 +35,19 @@ export class InMemoryMdWorkspace implements IMdWorkspace { return this._documents.has(resource); } + public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> { + const files = new Map(); + const pathPrefix = resource.fsPath + (resource.fsPath.endsWith('/') ? '' : '/'); + for (const doc of this._documents.values()) { + const path = doc.uri.fsPath; + if (path.startsWith(pathPrefix)) { + const parts = path.slice(pathPrefix.length).split('/'); + files.set(parts[0], parts.length > 1 ? vscode.FileType.Directory : vscode.FileType.File); + } + } + return Array.from(files.entries()); + } + private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter(); public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event; diff --git a/extensions/markdown-language-features/src/test/pathCompletion.test.ts b/extensions/markdown-language-features/src/test/pathCompletion.test.ts index 55927a098b8..d124461f102 100644 --- a/extensions/markdown-language-features/src/test/pathCompletion.test.ts +++ b/extensions/markdown-language-features/src/test/pathCompletion.test.ts @@ -10,36 +10,45 @@ import { MdLinkProvider } from '../languageFeatures/documentLinks'; import { MdVsCodePathCompletionProvider } from '../languageFeatures/pathCompletions'; import { noopToken } from '../util/cancellation'; import { InMemoryDocument } from '../util/inMemoryDocument'; +import { IMdWorkspace } from '../workspace'; import { createNewMarkdownEngine } from './engine'; import { InMemoryMdWorkspace } from './inMemoryWorkspace'; import { nulLogger } from './nulLogging'; import { CURSOR, getCursorPositions, joinLines, workspacePath } from './util'; -function getCompletionsAtCursor(resource: vscode.Uri, fileContents: string) { +async function getCompletionsAtCursor(resource: vscode.Uri, fileContents: string, workspace?: IMdWorkspace) { const doc = new InMemoryDocument(resource, fileContents); - const workspace = new InMemoryMdWorkspace([doc]); const engine = createNewMarkdownEngine(); - const linkProvider = new MdLinkProvider(engine, workspace, nulLogger); - const provider = new MdVsCodePathCompletionProvider(engine, linkProvider); + const ws = workspace ?? new InMemoryMdWorkspace([doc]); + const linkProvider = new MdLinkProvider(engine, ws, nulLogger); + const provider = new MdVsCodePathCompletionProvider(ws, engine, linkProvider); const cursorPositions = getCursorPositions(fileContents, doc); - return provider.provideCompletionItems(doc, cursorPositions[0], noopToken, { + const completions = await provider.provideCompletionItems(doc, cursorPositions[0], noopToken, { triggerCharacter: undefined, triggerKind: vscode.CompletionTriggerKind.Invoke, }); + + return completions.sort((a, b) => (a.label as string).localeCompare(b.label as string)); } -suite('Markdown path completion provider', () => { +function assertCompletionsEqual(actual: readonly vscode.CompletionItem[], expected: readonly { label: string; insertText?: string }[]) { + assert.strictEqual(actual.length, expected.length, 'Completion counts should be equal'); - setup(async () => { - // These tests assume that the markdown completion provider is already registered - await vscode.extensions.getExtension('vscode.markdown-language-features')!.activate(); - }); + for (let i = 0; i < actual.length; ++i) { + assert.strictEqual(actual[i].label, expected[i].label, `Completion labels ${i} should be equal`); + if (typeof expected[i].insertText !== 'undefined') { + assert.strictEqual(actual[i].insertText, expected[i].insertText, `Completion insert texts ${i} should be equal`); + } + } +} + +suite('Markdown: Path completions', () => { test('Should not return anything when triggered in empty doc', async () => { const completions = await getCompletionsAtCursor(workspacePath('new.md'), `${CURSOR}`); - assert.strictEqual(completions.length, 0); + assertCompletionsEqual(completions, []); }); test('Should return anchor completions', async () => { @@ -50,9 +59,10 @@ suite('Markdown path completion provider', () => { `# x y Z`, )); - assert.strictEqual(completions.length, 2); - assert.ok(completions.some(x => x.label === '#a-b-c'), 'Has a-b-c anchor completion'); - assert.ok(completions.some(x => x.label === '#x-y-z'), 'Has x-y-z anchor completion'); + assertCompletionsEqual(completions, [ + { label: '#a-b-c' }, + { label: '#x-y-z' }, + ]); }); test('Should not return suggestions for http links', async () => { @@ -64,53 +74,87 @@ suite('Markdown path completion provider', () => { `# https:`, )); - assert.strictEqual(completions.length, 0); + assertCompletionsEqual(completions, []); }); test('Should return relative path suggestions', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + new InMemoryDocument(workspacePath('sub/foo.md'), ''), + ]); const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[](${CURSOR}`, ``, `# A b C`, - )); + ), workspace); - assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion'); - assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion'); - assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion'); + assertCompletionsEqual(completions, [ + { label: '#a-b-c' }, + { label: 'a.md' }, + { label: 'b.md' }, + { label: 'sub/' }, + ]); }); test('Should return relative path suggestions using ./', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + new InMemoryDocument(workspacePath('sub/foo.md'), ''), + ]); + const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[](./${CURSOR}`, ``, `# A b C`, - )); + ), workspace); - assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion'); - assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion'); - assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion'); + assertCompletionsEqual(completions, [ + { label: 'a.md' }, + { label: 'b.md' }, + { label: 'sub/' }, + ]); }); test('Should return absolute path suggestions using /', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + new InMemoryDocument(workspacePath('sub/c.md'), ''), + ]); + const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines( `[](/${CURSOR}`, ``, `# A b C`, - )); + ), workspace); - assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion'); - assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion'); - assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion'); - assert.ok(!completions.some(x => x.label === 'c.md'), 'Should not have c.md from sub folder'); + assertCompletionsEqual(completions, [ + { label: 'a.md' }, + { label: 'b.md' }, + { label: 'sub/' }, + ]); }); test('Should return anchor suggestions in other file', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('b.md'), joinLines( + `# b`, + ``, + `[./a](./a)`, + ``, + `# header1`, + )), + ]); const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines( `[](/b.md#${CURSOR}`, - )); + ), workspace); - assert.ok(completions.some(x => x.label === '#b'), 'Has #b header completion'); - assert.ok(completions.some(x => x.label === '#header1'), 'Has #header1 header completion'); + assertCompletionsEqual(completions, [ + { label: '#b' }, + { label: '#header1' }, + ]); }); test('Should reference links for current file', async () => { @@ -121,9 +165,10 @@ suite('Markdown path completion provider', () => { `[ref-2]: bla`, )); - assert.strictEqual(completions.length, 2); - assert.ok(completions.some(x => x.label === 'ref-1'), 'Has ref-1 reference completion'); - assert.ok(completions.some(x => x.label === 'ref-2'), 'Has ref-2 reference completion'); + assertCompletionsEqual(completions, [ + { label: 'ref-1' }, + { label: 'ref-2' }, + ]); }); test('Should complete headers in link definitions', async () => { @@ -133,67 +178,118 @@ suite('Markdown path completion provider', () => { `[ref-1]: ${CURSOR}`, )); - assert.ok(completions.some(x => x.label === '#a-b-c'), 'Has #a-b-c header completion'); - assert.ok(completions.some(x => x.label === '#x-y-z'), 'Has #x-y-z header completion'); + assertCompletionsEqual(completions, [ + { label: '#a-b-c' }, + { label: '#x-y-z' }, + { label: 'new.md' }, + ]); }); test('Should complete relative paths in link definitions', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + new InMemoryDocument(workspacePath('sub/c.md'), ''), + ]); + const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `# a B c`, `[ref-1]: ${CURSOR}`, - )); + ), workspace); - assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion'); - assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion'); - assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion'); + assertCompletionsEqual(completions, [ + { label: '#a-b-c' }, + { label: 'a.md' }, + { label: 'b.md' }, + { label: 'sub/' }, + ]); }); test('Should escape spaces in path names', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + new InMemoryDocument(workspacePath('sub/file with space.md'), ''), + ]); + const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[](./sub/${CURSOR})` - )); + ), workspace); - assert.ok(completions.some(x => x.insertText === 'file%20with%20space.md'), 'Has encoded path completion'); + assertCompletionsEqual(completions, [ + { label: 'file with space.md', insertText: 'file%20with%20space.md' }, + ]); }); test('Should support completions on angle bracket path with spaces', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('sub with space/a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + ]); + const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[]( x.insertText === 'file.md'), 'Has path completion'); + assertCompletionsEqual(completions, [ + { label: 'a.md', insertText: 'a.md' }, + ]); }); test('Should not escape spaces in path names that use angle brackets', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('sub/file with space.md'), ''), + ]); + { const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[](<./sub/${CURSOR}` - )); + ), workspace); - assert.ok(completions.some(x => x.insertText === 'file with space.md'), 'Has encoded path completion'); + assertCompletionsEqual(completions, [ + { label: 'file with space.md', insertText: 'file with space.md' }, + ]); } { const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[](<./sub/${CURSOR}>` - )); + ), workspace); - assert.ok(completions.some(x => x.insertText === 'file with space.md'), 'Has encoded path completion'); + assertCompletionsEqual(completions, [ + { label: 'file with space.md', insertText: 'file with space.md' }, + ]); } }); test('Should complete paths for path with encoded spaces', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + new InMemoryDocument(workspacePath('sub with space/file.md'), ''), + ]); + const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[](./sub%20with%20space/${CURSOR})` - )); + ), workspace); - assert.ok(completions.some(x => x.insertText === 'file.md'), 'Has file from space'); + assertCompletionsEqual(completions, [ + { label: 'file.md', insertText: 'file.md' }, + ]); }); test('Should complete definition path for path with encoded spaces', async () => { + const workspace = new InMemoryMdWorkspace([ + new InMemoryDocument(workspacePath('a.md'), ''), + new InMemoryDocument(workspacePath('b.md'), ''), + new InMemoryDocument(workspacePath('sub with space/file.md'), ''), + ]); + const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines( `[def]: ./sub%20with%20space/${CURSOR}` - )); + ), workspace); - assert.ok(completions.some(x => x.insertText === 'file.md'), 'Has file from space'); + assertCompletionsEqual(completions, [ + { label: 'file.md', insertText: 'file.md' }, + ]); }); }); diff --git a/extensions/markdown-language-features/src/util/openDocumentLink.ts b/extensions/markdown-language-features/src/util/openDocumentLink.ts index b27bfe62173..d117aa15c3e 100644 --- a/extensions/markdown-language-features/src/util/openDocumentLink.ts +++ b/extensions/markdown-language-features/src/util/openDocumentLink.ts @@ -7,6 +7,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as uri from 'vscode-uri'; import { MdTableOfContentsProvider } from '../tableOfContents'; +import { ITextDocument } from '../types/textDocument'; +import { IMdWorkspace } from '../workspace'; import { isMarkdownFile } from './file'; export interface OpenDocumentLinkArgs { @@ -154,9 +156,9 @@ function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: str return false; } -export async function resolveUriToMarkdownFile(resource: vscode.Uri): Promise { +export async function resolveUriToMarkdownFile(workspace: IMdWorkspace, resource: vscode.Uri): Promise { try { - const doc = await tryResolveUriToMarkdownFile(resource); + const doc = await workspace.getOrLoadMarkdownDocument(resource); if (doc) { return doc; } @@ -166,21 +168,8 @@ export async function resolveUriToMarkdownFile(resource: vscode.Uri): Promise { - let document: vscode.TextDocument; - try { - document = await vscode.workspace.openTextDocument(resource); - } catch { - return undefined; - } - if (isMarkdownFile(document)) { - return document; - } - return undefined; -} diff --git a/extensions/markdown-language-features/src/workspace.ts b/extensions/markdown-language-features/src/workspace.ts index 9fdef64076d..6d89b79e35d 100644 --- a/extensions/markdown-language-features/src/workspace.ts +++ b/extensions/markdown-language-features/src/workspace.ts @@ -30,6 +30,8 @@ export interface IMdWorkspace { pathExists(resource: vscode.Uri): Promise; + readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]>; + readonly onDidChangeMarkdownDocument: vscode.Event; readonly onDidCreateMarkdownDocument: vscode.Event; readonly onDidDeleteMarkdownDocument: vscode.Event; @@ -189,4 +191,8 @@ export class VsCodeMdWorkspace extends Disposable implements IMdWorkspace { } return targetResourceStat.type === vscode.FileType.File || targetResourceStat.type === vscode.FileType.Directory; } + + public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> { + return vscode.workspace.fs.readDirectory(resource); + } }