Skip to content

Commit

Permalink
fix(language-service): transform markdown links in completion items
Browse files Browse the repository at this point in the history
This resolves completion item descriptions the same way as hover
descriptions are resolved.

The `transformDocumentLinkTarget` and `transformMarkdown` functions were
both moved into `utils/transform`, but their implementation remains the
same.

The fix was verified to work in MDX analyzer.

Refs mdx-js/mdx-analyzer#394
  • Loading branch information
remcohaszing committed Feb 10, 2024
1 parent d8a6dc0 commit 7537aab
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 66 deletions.
Expand Up @@ -91,6 +91,7 @@ export function register(context: ServiceContext) {
cacheData.list,
range => map.getSourceRange(range),
map.virtualFileDocument,
context,
);
}
}
Expand Down Expand Up @@ -208,6 +209,7 @@ export function register(context: ServiceContext) {
completionList,
range => map.getSourceRange(range, isCompletionEnabled),
document,
context,
);
}

Expand Down
Expand Up @@ -3,7 +3,7 @@ import type { ServiceContext } from '../types';
import { NoneCancellationToken } from '../utils/cancellation';
import { notEmpty } from '../utils/common';
import { documentFeatureWorker } from '../utils/featureWorkers';
import { transformDocumentLinkTarget } from './resolveDocumentLink';
import { transformDocumentLinkTarget } from '../utils/transform';
import { isDocumentLinkEnabled } from '@volar/language-core';

export interface DocumentLinkData {
Expand Down
19 changes: 6 additions & 13 deletions packages/language-service/lib/features/provideHover.ts
Expand Up @@ -4,8 +4,8 @@ import { languageFeatureWorker } from '../utils/featureWorkers';
import { isInsideRange } from '../utils/common';
import { errorMarkups } from './provideDiagnostics';
import { NoneCancellationToken } from '../utils/cancellation';
import { transformMarkdown } from '../utils/transform';
import { isHoverEnabled } from '@volar/language-core';
import { transformDocumentLinkTarget } from './resolveDocumentLink';

export function register(context: ServiceContext) {

Expand Down Expand Up @@ -68,15 +68,15 @@ export function register(context: ServiceContext) {

function getHoverTexts(hover: vscode.Hover): string[] {
if (typeof hover.contents === 'string') {
return [transformMarkdown(hover.contents)];
return [transformMarkdown(hover.contents, context)];
}
if (Array.isArray(hover.contents)) {
return hover.contents.map(content => {
if (typeof content === 'string') {
return transformMarkdown(content);
return transformMarkdown(content, context);
}
if (content.language === 'md') {
return `\`\`\`${content.language}\n${transformMarkdown(content.value)}\n\`\`\``;
return `\`\`\`${content.language}\n${transformMarkdown(content.value, context)}\n\`\`\``;
}
else {
return `\`\`\`${content.language}\n${content.value}\n\`\`\``;
Expand All @@ -85,24 +85,17 @@ export function register(context: ServiceContext) {
}
if ('kind' in hover.contents) {
if (hover.contents.kind === 'markdown') {
return [transformMarkdown(hover.contents.value)];
return [transformMarkdown(hover.contents.value, context)];
}
else {
return [hover.contents.value];
}
}
if (hover.contents.language === 'md') {
return [`\`\`\`${hover.contents.language}\n${transformMarkdown(hover.contents.value)}\n\`\`\``];
return [`\`\`\`${hover.contents.language}\n${transformMarkdown(hover.contents.value, context)}\n\`\`\``];
}
else {
return [`\`\`\`${hover.contents.language}\n${hover.contents.value}\n\`\`\``];
}
}

function transformMarkdown(content: string) {
return content.replace(/(\[[^\]]+\])(\([^)]+\))/g, s => {
const match = s.match(/(\[[^\]]+\])(\([^)]+\))/)!;
return `${match[1]}(${transformDocumentLinkTarget(match[2].slice(1, -1), context)})`;
});
}
}
Expand Up @@ -33,6 +33,7 @@ export function register(context: ServiceContext) {
item,
embeddedRange => map.getSourceRange(embeddedRange),
map.virtualFileDocument,
context,
);
}
}
Expand Down
51 changes: 1 addition & 50 deletions packages/language-service/lib/features/resolveDocumentLink.ts
@@ -1,9 +1,8 @@
import type * as vscode from 'vscode-languageserver-protocol';
import { URI } from 'vscode-uri';
import type { ServiceContext } from '../types';
import { NoneCancellationToken } from '../utils/cancellation';
import { transformDocumentLinkTarget } from '../utils/transform';
import type { DocumentLinkData } from './provideDocumentLinks';
import { isDocumentLinkEnabled } from '@volar/language-core';

export function register(context: ServiceContext) {

Expand All @@ -27,51 +26,3 @@ export function register(context: ServiceContext) {
return item;
};
}

export function transformDocumentLinkTarget(target: string, context: ServiceContext) {

const targetUri = URI.parse(target);
const clearUri = targetUri.with({ fragment: '' }).toString(true);
const [virtualCode] = context.documents.getVirtualCodeByUri(clearUri);

if (virtualCode) {
for (const map of context.documents.getMaps(virtualCode)) {

if (!map.map.mappings.some(mapping => isDocumentLinkEnabled(mapping.data))) {
continue;
}

target = map.sourceFileDocument.uri;

const hash = targetUri.fragment;
const range = hash.match(/^L(\d+)(,(\d+))?(-L(\d+)(,(\d+))?)?$/);

if (range) {
const startLine = Number(range[1]) - 1;
const startCharacter = Number(range[3] ?? 1) - 1;
if (range[5] !== undefined) {
const endLine = Number(range[5]) - 1;
const endCharacter = Number(range[7] ?? 1) - 1;
const sourceRange = map.getSourceRange({
start: { line: startLine, character: startCharacter },
end: { line: endLine, character: endCharacter },
});
if (sourceRange) {
target += '#L' + (sourceRange.start.line + 1) + ',' + (sourceRange.start.character + 1);
target += '-L' + (sourceRange.end.line + 1) + ',' + (sourceRange.end.character + 1);
break;
}
}
else {
const sourcePos = map.getSourcePosition({ line: startLine, character: startCharacter });
if (sourcePos) {
target += '#L' + (sourcePos.line + 1) + ',' + (sourcePos.character + 1);
break;
}
}
}
}
}

return target;
}
68 changes: 66 additions & 2 deletions packages/language-service/lib/utils/transform.ts
@@ -1,13 +1,69 @@
import { isRenameEnabled, resolveRenameEditText, type CodeInformation } from '@volar/language-core';
import { isDocumentLinkEnabled, isRenameEnabled, resolveRenameEditText, type CodeInformation } from '@volar/language-core';
import type * as vscode from 'vscode-languageserver-protocol';
import { URI } from 'vscode-uri';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import type { ServiceContext } from '../types';
import { notEmpty } from './common';

export function transformDocumentLinkTarget(target: string, context: ServiceContext) {
const targetUri = URI.parse(target);
const clearUri = targetUri.with({ fragment: '' }).toString(true);
const [virtualCode] = context.documents.getVirtualCodeByUri(clearUri);

if (virtualCode) {
for (const map of context.documents.getMaps(virtualCode)) {

if (!map.map.mappings.some(mapping => isDocumentLinkEnabled(mapping.data))) {
continue;
}

target = map.sourceFileDocument.uri;

const hash = targetUri.fragment;
const range = hash.match(/^L(\d+)(,(\d+))?(-L(\d+)(,(\d+))?)?$/);

if (range) {
const startLine = Number(range[1]) - 1;
const startCharacter = Number(range[3] ?? 1) - 1;
if (range[5] !== undefined) {
const endLine = Number(range[5]) - 1;
const endCharacter = Number(range[7] ?? 1) - 1;
const sourceRange = map.getSourceRange({
start: { line: startLine, character: startCharacter },
end: { line: endLine, character: endCharacter },
});
if (sourceRange) {
target += '#L' + (sourceRange.start.line + 1) + ',' + (sourceRange.start.character + 1);
target += '-L' + (sourceRange.end.line + 1) + ',' + (sourceRange.end.character + 1);
break;
}
}
else {
const sourcePos = map.getSourcePosition({ line: startLine, character: startCharacter });
if (sourcePos) {
target += '#L' + (sourcePos.line + 1) + ',' + (sourcePos.character + 1);
break;
}
}
}
}
}

return target;
}

export function transformMarkdown(content: string, context: ServiceContext) {
return content.replace(/(\[[^\]]+\])(\([^)]+\))/g, s => {
const match = s.match(/(\[[^\]]+\])(\([^)]+\))/)!;
return `${match[1]}(${transformDocumentLinkTarget(match[2].slice(1, -1), context)})`;
});
}

export function transformCompletionItem<T extends vscode.CompletionItem>(
item: T,
getOtherRange: (range: vscode.Range) => vscode.Range | undefined,
document: vscode.TextDocument,
context: ServiceContext
): T {
return {
...item,
Expand All @@ -17,13 +73,21 @@ export function transformCompletionItem<T extends vscode.CompletionItem>(
textEdit: item.textEdit
? transformTextEdit(item.textEdit, getOtherRange, document)
: undefined,
documentation:
item.documentation ?
typeof item.documentation === 'string' ? transformMarkdown(item.documentation, context) :
item.documentation.kind === 'markdown' ?
{ kind: 'markdown', value: transformMarkdown(item.documentation.value, context) }
: item.documentation
: undefined
};
}

export function transformCompletionList<T extends vscode.CompletionList>(
completionList: T,
getOtherRange: (range: vscode.Range) => vscode.Range | undefined,
document: TextDocument,
context: ServiceContext,
): T {
return {
isIncomplete: completionList.isIncomplete,
Expand All @@ -38,7 +102,7 @@ export function transformCompletionList<T extends vscode.CompletionList>(
: getOtherRange(completionList.itemDefaults.editRange)
: undefined,
} : undefined,
items: completionList.items.map(item => transformCompletionItem(item, getOtherRange, document)),
items: completionList.items.map(item => transformCompletionItem(item, getOtherRange, document, context)),
} as T;
}

Expand Down

0 comments on commit 7537aab

Please sign in to comment.