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

Add preferredMdPathExtensionStyle setting #114

Merged
merged 1 commit into from
Feb 1, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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