Skip to content

Commit

Permalink
Merge pull request #114 from microsoft/dev/mjbvz/preferredMdPathExten…
Browse files Browse the repository at this point in the history
…sionStyle

Add preferredMdPathExtensionStyle setting
  • Loading branch information
mjbvz authored Feb 1, 2023
2 parents 2743974 + 7e61dcb commit 7190860
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 53 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Changelog

## 0.3.0-alpha.4 UNRELEASED
## 0.3.0-alpha.4 February 1, 2023
- Add support for cross workspace header completions when triggered on `##`.
- Add `preferredMdPathExtensionStyle` configuration option to control if generated paths to Markdown files should include or drop the file extension.

## 0.3.0-alpha.3 November 30, 2022
- Republish with missing types files.
Expand Down
13 changes: 13 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ export interface LsConfiguration {
* List of path globs that should be excluded from cross-file operations.
*/
readonly excludePaths: readonly string[];

/**
* Preferred style for file paths to {@link markdownFileExtensions markdown files}.
*
* This is used for paths added by the language service, such as for path completions and on file renames.
*
* Valid values:
*
* - `auto` — Try to maintain the existing of the path.
* - `includeExtension` — Include the file extension when possible.
* - `removeExtension` — Drop the file extension when possible.
*/
readonly preferredMdPathExtensionStyle?: 'auto' | 'includeExtension' | 'removeExtension';
}

export const defaultMarkdownFileExtension = 'md';
Expand Down
2 changes: 1 addition & 1 deletion src/languageFeatures/codeActions/extractLinkDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class MdExtractLinkDefinitionCodeActionProvider {
return {
title: MdExtractLinkDefinitionCodeActionProvider.genericTitle,
kind: MdExtractLinkDefinitionCodeActionProvider.#kind,
edit: builder.renameFragment(),
edit: builder.getEdit(),
command: {
command: 'vscodeMarkdownLanguageservice.rename',
title: 'Rename',
Expand Down
2 changes: 1 addition & 1 deletion src/languageFeatures/codeActions/removeLinkDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ export class MdRemoveLinkDefinitionCodeActionProvider {
const range = definition.source.range;
builder.replace(getDocUri(doc), makeRange(range.start.line, 0, range.start.line + 1, 0), '');

return { title, kind: lsp.CodeActionKind.QuickFix, edit: builder.renameFragment() };
return { title, kind: lsp.CodeActionKind.QuickFix, edit: builder.getEdit() };
}
}
4 changes: 2 additions & 2 deletions src/languageFeatures/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { translatePosition } from '../types/position';
import { modifyRange } from '../types/range';
import { getDocUri, ITextDocument } from '../types/textDocument';
import { Disposable, IDisposable } from '../util/dispose';
import { looksLikeMarkdownPath } from '../util/file';
import { looksLikeMarkdownUri } from '../util/file';
import { Limiter } from '../util/limiter';
import { ResourceMap } from '../util/resourceMap';
import { FileStat, IWorkspace, IWorkspaceWithWatching, statLinkToMarkdownFile } from '../workspace';
Expand Down Expand Up @@ -420,7 +420,7 @@ export class DiagnosticComputer {
}

#isMarkdownPath(resolvedHrefPath: URI) {
return this.#workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownPath(this.#configuration, resolvedHrefPath);
return this.#workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownUri(this.#configuration, resolvedHrefPath);
}

#isIgnoredLink(options: DiagnosticOptions, link: string): boolean {
Expand Down
32 changes: 21 additions & 11 deletions src/languageFeatures/fileRename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { isExcludedPath, LsConfiguration } from '../config';
import { getDocUri, ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { WorkspaceEditBuilder } from '../util/editBuilder';
import { looksLikeMarkdownPath } from '../util/file';
import { looksLikeMarkdownUri } from '../util/file';
import { isParentDir } from '../util/path';
import { IWorkspace } from '../workspace';
import { MdWorkspaceInfoCache } from '../workspaceCache';
import { HrefKind, MdLink, resolveInternalDocumentLink } from './documentLinks';
import { HrefKind, InternalHref, MdLink, resolveInternalDocumentLink } from './documentLinks';
import { MdReferenceKind, MdReferencesProvider } from './references';
import { getFilePathRange, getLinkRenameText } from './rename';

Expand Down Expand Up @@ -69,7 +69,7 @@ export class MdFileRenameProvider extends Disposable {
}
}

return { participatingRenames, edit: builder.renameFragment() };
return { participatingRenames, edit: builder.getEdit() };
}

async #addSingleFileRenameEdits(edit: FileRename, allEdits: readonly FileRename[], builder: WorkspaceEditBuilder, token: CancellationToken): Promise<boolean> {
Expand Down Expand Up @@ -154,7 +154,7 @@ export class MdFileRenameProvider extends Disposable {
* In this case we also need to update links within the file.
*/
async #tryAddEditsInSelf(edit: FileRename, allEdits: readonly FileRename[], builder: WorkspaceEditBuilder): Promise<boolean> {
if (!looksLikeMarkdownPath(this.#config, edit.newUri)) {
if (!looksLikeMarkdownUri(this.#config, edit.newUri)) {
return false;
}

Expand Down Expand Up @@ -241,14 +241,11 @@ export class MdFileRenameProvider extends Disposable {

let newFilePath = newUri;

// If the original markdown link did not use a file extension, remove ours too
if (!Utils.extname(link.href.path)) {
if (this.#shouldRemoveFileExtensionForRename(link.href, newUri)) {
const editExt = Utils.extname(newUri);
if (this.#config.markdownFileExtensions.includes(editExt.replace('.', ''))) {
newFilePath = newUri.with({
path: newUri.path.slice(0, newUri.path.length - editExt.length)
});
}
newFilePath = newUri.with({
path: newUri.path.slice(0, newUri.path.length - editExt.length)
});
}

const newLinkText = getLinkRenameText(this.#workspace, link.source, newFilePath, link.source.pathText.startsWith('.'));
Expand All @@ -258,4 +255,17 @@ export class MdFileRenameProvider extends Disposable {
}
return false;
}

#shouldRemoveFileExtensionForRename(originalHref: InternalHref, newUri: URI): boolean {
if (!looksLikeMarkdownUri(this.#config, newUri)) {
return false;
}

if (this.#config.preferredMdPathExtensionStyle === 'removeExtension') {
return true;
}

// If the original markdown link did not use a file extension, remove ours too
return !Utils.extname(originalHref.path);
}
}
25 changes: 21 additions & 4 deletions src/languageFeatures/pathCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as l10n from '@vscode/l10n';
import { dirname, resolve } from 'path';
import { dirname, extname, resolve } from 'path';
import type { CancellationToken, CompletionContext } from 'vscode-languageserver-protocol';
import * as lsp from 'vscode-languageserver-types';
import { URI, Utils } from 'vscode-uri';
Expand All @@ -14,12 +14,13 @@ import { MdTableOfContentsProvider, TableOfContents, TocEntry } from '../tableOf
import { translatePosition } from '../types/position';
import { makeRange } from '../types/range';
import { getDocUri, getLine, ITextDocument } from '../types/textDocument';
import { looksLikeMarkdownFilePath } from '../util/file';
import { computeRelativePath } from '../util/path';
import { Schemes } from '../util/schemes';
import { r } from '../util/string';
import { FileStat, getWorkspaceFolder, IWorkspace, openLinkToMarkdownFile } from '../workspace';
import { MdWorkspaceInfoCache } from '../workspaceCache';
import { MdLinkProvider } from './documentLinks';
import { computeRelativePath } from './rename';

enum CompletionContextKind {
/** `[...](|)` */
Expand Down Expand Up @@ -359,7 +360,8 @@ export class MdPathCompletionProvider {
continue;
}

const path = context.skipEncoding ? rawPath : encodeURI(rawPath);
const normalizedPath = this.#normalizeFileNameCompletion(rawPath);
const path = context.skipEncoding ? normalizedPath : encodeURI(normalizedPath);
for (const entry of toc.entries) {
const completionItem = this.#createHeaderCompletion(entry, insertionRange, replacementRange, path);
completionItem.filterText = '#' + completionItem.label;
Expand Down Expand Up @@ -397,12 +399,17 @@ export class MdPathCompletionProvider {
return;
}

for (const [name, type] of dirInfo) {
// eslint-disable-next-line prefer-const
for (let [name, type] of dirInfo) {
const uri = Utils.joinPath(parentDir, name);
if (isExcludedPath(this.#configuration, uri)) {
continue;
}

if (!type.isDirectory) {
name = this.#normalizeFileNameCompletion(name);
}

const isDir = type.isDirectory;
const newText = (context.skipEncoding ? name : encodeURIComponent(name)) + (isDir ? '/' : '');
yield {
Expand All @@ -418,6 +425,16 @@ export class MdPathCompletionProvider {
}
}

#normalizeFileNameCompletion(name: string): string {
if (this.#configuration.preferredMdPathExtensionStyle === 'removeExtension') {
if (looksLikeMarkdownFilePath(this.#configuration, name)) {
const ext = extname(name);
name = name.slice(0, -ext.length);
}
}
return name;
}

#resolveReference(document: ITextDocument, ref: string): URI | undefined {
const docUri = this.#getFileUriOfTextDocument(document);

Expand Down
4 changes: 2 additions & 2 deletions src/languageFeatures/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { translatePosition } from '../types/position';
import { areRangesEqual, modifyRange, rangeContains } from '../types/range';
import { getDocUri, ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { looksLikeMarkdownPath } from '../util/file';
import { looksLikeMarkdownUri } from '../util/file';
import { IWorkspace, statLinkToMarkdownFile } from '../workspace';
import { MdWorkspaceInfoCache } from '../workspaceCache';
import { HrefKind, looksLikeLinkToResource, MdLink, MdLinkKind } from './documentLinks';
Expand Down Expand Up @@ -269,7 +269,7 @@ export class MdReferencesProvider extends Disposable {
}

#isMarkdownPath(resolvedHrefPath: URI) {
return this.#workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownPath(this.#configuration, resolvedHrefPath);
return this.#workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownUri(this.#configuration, resolvedHrefPath);
}

*#findLinksToFile(resource: URI, links: readonly MdLink[], sourceLink: MdLink | undefined): Iterable<MdReference> {
Expand Down
32 changes: 11 additions & 21 deletions src/languageFeatures/rename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { modifyRange, rangeContains } from '../types/range';
import { getDocUri, ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { WorkspaceEditBuilder } from '../util/editBuilder';
import { Schemes } from '../util/schemes';
import { computeRelativePath } from '../util/path';
import { tryDecodeUri } from '../util/uri';
import { IWorkspace, statLinkToMarkdownFile } from '../workspace';
import { HrefKind, InternalHref, MdLink, MdLinkKind, MdLinkSource, resolveInternalDocumentLink } from './documentLinks';
Expand Down Expand Up @@ -138,20 +138,23 @@ export class MdRenameProvider extends Disposable {
} else if (triggerRef.kind === MdReferenceKind.Header || (triggerRef.kind === MdReferenceKind.Link && triggerRef.link.source.fragmentRange && rangeContains(triggerRef.link.source.fragmentRange, position) && (triggerRef.link.kind === MdLinkKind.Definition || triggerRef.link.kind === MdLinkKind.Link && triggerRef.link.href.kind === HrefKind.Internal))) {
return this.#renameFragment(allRefsInfo, newName);
} else if (triggerRef.kind === MdReferenceKind.Link && !(triggerRef.link.source.fragmentRange && rangeContains(triggerRef.link.source.fragmentRange, position)) && (triggerRef.link.kind === MdLinkKind.Link || triggerRef.link.kind === MdLinkKind.Definition) && triggerRef.link.href.kind === HrefKind.Internal) {
return this.#renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName);
return this.#renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName, token);
}

return undefined;
}

async #renameFilePath(triggerDocument: URI, triggerHref: InternalHref, allRefsInfo: MdReferencesResponse, newName: string): Promise<lsp.WorkspaceEdit> {
async #renameFilePath(triggerDocument: URI, triggerHref: InternalHref, allRefsInfo: MdReferencesResponse, newName: string, token: CancellationToken): Promise<lsp.WorkspaceEdit> {
const builder = new WorkspaceEditBuilder();

const targetUri = await statLinkToMarkdownFile(this.#configuration, this.#workspace, triggerHref.path) ?? triggerHref.path;
if (token.isCancellationRequested) {
return builder.getEdit();
}

const rawNewFilePath = resolveInternalDocumentLink(triggerDocument, newName, this.#workspace);
if (!rawNewFilePath) {
return builder.renameFragment();
return builder.getEdit();
}

let resolvedNewFilePath = rawNewFilePath.resource;
Expand Down Expand Up @@ -179,7 +182,7 @@ export class MdRenameProvider extends Disposable {
}
}

return builder.renameFragment();
return builder.getEdit();
}

#renameFragment(allRefsInfo: MdReferencesResponse, newName: string): lsp.WorkspaceEdit {
Expand All @@ -197,7 +200,7 @@ export class MdRenameProvider extends Disposable {
break;
}
}
return builder.renameFragment();
return builder.getEdit();
}

#renameExternalLink(allRefsInfo: MdReferencesResponse, newName: string): lsp.WorkspaceEdit {
Expand All @@ -207,7 +210,7 @@ export class MdRenameProvider extends Disposable {
builder.replace(ref.link.source.resource, ref.location.range, newName);
}
}
return builder.renameFragment();
return builder.getEdit();
}

#renameReferenceLinks(allRefsInfo: MdReferencesResponse, newName: string): lsp.WorkspaceEdit {
Expand All @@ -223,7 +226,7 @@ export class MdRenameProvider extends Disposable {
}
}

return builder.renameFragment();
return builder.getEdit();
}

async #getAllReferences(document: ITextDocument, position: lsp.Position, token: CancellationToken): Promise<MdReferencesResponse | undefined> {
Expand Down Expand Up @@ -271,19 +274,6 @@ export function getLinkRenameText(workspace: IWorkspace, source: MdLinkSource, n
return computeRelativePath(source.resource, newPath, preferDotSlash);
}

export function computeRelativePath(fromDoc: URI, toDoc: URI, preferDotSlash = false): string | undefined {
if (fromDoc.scheme === toDoc.scheme && fromDoc.scheme !== Schemes.untitled) {
const rootDir = Utils.dirname(fromDoc);
let newLink = path.posix.relative(rootDir.path, toDoc.path);
if (preferDotSlash && !(newLink.startsWith('../') || newLink.startsWith('..\\'))) {
newLink = './' + newLink;
}
return newLink;
}

return undefined;
}

export function getFilePathRange(link: MdLink): lsp.Range {
if (link.source.fragmentRange) {
return modifyRange(link.source.hrefRange, undefined, translatePosition(link.source.fragmentRange.start, { characterDelta: -1 }));
Expand Down
34 changes: 29 additions & 5 deletions src/test/fileRename.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as assert from 'assert';
import * as lsp from 'vscode-languageserver-types';
import { URI } from 'vscode-uri';
import { getLsConfiguration } from '../config';
import { getLsConfiguration, LsConfiguration } from '../config';
import { createWorkspaceLinkCache } from '../languageFeatures/documentLinks';
import { FileRenameResponse, MdFileRenameProvider } from '../languageFeatures/fileRename';
import { MdReferencesProvider } from '../languageFeatures/references';
Expand All @@ -24,13 +24,13 @@ import { assertRangeEqual, joinLines, withStore, workspacePath } from './util';
/**
* Get all the edits for a file rename.
*/
function getFileRenameEdits(store: DisposableStore, edits: ReadonlyArray<{ oldUri: URI, newUri: URI }>, workspace: IWorkspace): Promise<FileRenameResponse | undefined> {
const config = getLsConfiguration({});
function getFileRenameEdits(store: DisposableStore, edits: ReadonlyArray<{ oldUri: URI, newUri: URI }>, workspace: IWorkspace, configOverrides: Partial<LsConfiguration> = {}): Promise<FileRenameResponse | undefined> {
const config = getLsConfiguration(configOverrides);
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const linkCache = store.add(createWorkspaceLinkCache(engine, workspace));
const referencesProvider = store.add(new MdReferencesProvider(config, engine, workspace, tocProvider, linkCache, nulLogger));
const renameProvider = store.add(new MdFileRenameProvider(getLsConfiguration({}), workspace, linkCache, referencesProvider));
const renameProvider = store.add(new MdFileRenameProvider(config, workspace, linkCache, referencesProvider));
return renameProvider.getRenameFilesInWorkspaceEdit(edits, noopToken);
}

Expand Down Expand Up @@ -114,7 +114,31 @@ suite('File Rename', () => {
});
}));

test('Rename file should preserve usage of file extensions', withStore(async (store) => {
test('Rename file should drop file extensions if it has been configured to', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[abc](/old.md#frag)`,
`[abc](old.md#frag)`,
`[abc](./old.md#frag)`,
`[xyz]: ./old.md#frag`,
));
const workspace = store.add(new InMemoryWorkspace([doc]));

const oldUri = workspacePath('old.md');
const newUri = workspacePath('new.md');

const response = await getFileRenameEdits(store, [{ oldUri, newUri }], workspace, { preferredMdPathExtensionStyle: 'removeExtension' });
assertEditsEqual(response!.edit, {
uri: docUri, edits: [
lsp.TextEdit.replace(makeRange(0, 6, 0, 13), '/new'),
lsp.TextEdit.replace(makeRange(1, 6, 1, 12), 'new'),
lsp.TextEdit.replace(makeRange(2, 6, 2, 14), './new'),
lsp.TextEdit.replace(makeRange(3, 7, 3, 15), './new'),
]
});
}));

test('Rename file should preserve usage of file extensions by default', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[abc](/old#frag)`,
Expand Down
Loading

0 comments on commit 7190860

Please sign in to comment.