Skip to content

Commit

Permalink
feat(typescript): add option to prevent offset in plugin mode (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk committed Jun 3, 2024
1 parent 2069027 commit f5a6efb
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 69 deletions.
2 changes: 2 additions & 0 deletions packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export interface TypeScriptServiceScript {
code: VirtualCode;
extension: '.ts' | '.js' | '.mts' | '.mjs' | '.cjs' | '.cts' | '.d.ts' | string;
scriptKind: ts.ScriptKind;
/** See #188 */
preventLeadingOffset?: boolean;
}

export interface TypeScriptExtraServiceScript extends TypeScriptServiceScript {
Expand Down
94 changes: 55 additions & 39 deletions packages/typescript/lib/node/decorateLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,20 @@ import {
} from '@volar/language-core';
import type * as ts from 'typescript';
import { dedupeDocumentSpans } from './dedupe';
import {
getMappingOffset,
toGeneratedOffset,
toGeneratedOffsets,
toSourceOffset,
transformCallHierarchyItem,
transformDiagnostic,
transformDocumentSpan,
transformFileTextChanges,
transformSpan,
transformTextChange,
transformTextSpan,
} from './transform';
import { getServiceScript, notEmpty } from './utils';
import { toGeneratedOffsets, toGeneratedOffset, toSourceOffset, transformCallHierarchyItem, transformDiagnostic, transformDocumentSpan, transformFileTextChanges, transformSpan, transformTextChange, transformTextSpan } from './transform';

const windowsPathReg = /\\/g;

Expand Down Expand Up @@ -104,7 +116,7 @@ export function decorateLanguageService(
}
const edits = getFormattingEditsForDocument(fileName, options);
return edits
.map(edit => transformTextChange(sourceScript, map, edit, isFormattingEnabled))
.map(edit => transformTextChange(serviceScript, sourceScript, map, edit, isFormattingEnabled))
.filter(notEmpty);
}
else {
Expand All @@ -115,12 +127,12 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generateStart = toGeneratedOffset(sourceScript, map, start, isFormattingEnabled);
const generateEnd = toGeneratedOffset(sourceScript, map, end, isFormattingEnabled);
const generateStart = toGeneratedOffset(serviceScript, sourceScript, map, start, isFormattingEnabled);
const generateEnd = toGeneratedOffset(serviceScript, sourceScript, map, end, isFormattingEnabled);
if (generateStart !== undefined && generateEnd !== undefined) {
const edits = getFormattingEditsForRange(fileName, generateStart, generateEnd, options);
return edits
.map(edit => transformTextChange(sourceScript, map, edit, isFormattingEnabled))
.map(edit => transformTextChange(serviceScript, sourceScript, map, edit, isFormattingEnabled))
.filter(notEmpty);
}
return [];
Expand All @@ -133,11 +145,11 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, position, isFormattingEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, position, isFormattingEnabled);
if (generatePosition !== undefined) {
const edits = getFormattingEditsAfterKeystroke(fileName, generatePosition, key, options);
return edits
.map(edit => transformTextChange(sourceScript, map, edit, isFormattingEnabled))
.map(edit => transformTextChange(serviceScript, sourceScript, map, edit, isFormattingEnabled))
.filter(notEmpty);
}
return [];
Expand All @@ -156,13 +168,13 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, position, isLinkedEditingEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, position, isLinkedEditingEnabled);
if (generatePosition !== undefined) {
const info = getLinkedEditingRangeAtPosition(fileName, generatePosition);
if (info) {
return {
ranges: info.ranges
.map(span => transformTextSpan(sourceScript, map, span, isLinkedEditingEnabled))
.map(span => transformTextSpan(serviceScript, sourceScript, map, span, isLinkedEditingEnabled))
.filter(notEmpty),
wordPattern: info.wordPattern,
};
Expand All @@ -177,7 +189,7 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, position, isCallHierarchyEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, position, isCallHierarchyEnabled);
if (generatePosition !== undefined) {
const item = prepareCallHierarchy(fileName, generatePosition);
if (Array.isArray(item)) {
Expand All @@ -197,7 +209,7 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, position, isCallHierarchyEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, position, isCallHierarchyEnabled);
if (generatePosition !== undefined) {
calls = provideCallHierarchyIncomingCalls(fileName, generatePosition);
}
Expand All @@ -222,7 +234,7 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, position, isCallHierarchyEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, position, isCallHierarchyEnabled);
if (generatePosition !== undefined) {
calls = provideCallHierarchyOutgoingCalls(fileName, generatePosition);
}
Expand All @@ -235,7 +247,7 @@ export function decorateLanguageService(
const to = transformCallHierarchyItem(language, call.to, isCallHierarchyEnabled);
const fromSpans = call.fromSpans
.map(span => sourceScript
? transformTextSpan(sourceScript, map, span, isCallHierarchyEnabled)
? transformTextSpan(serviceScript, sourceScript, map, span, isCallHierarchyEnabled)
: span
)
.filter(notEmpty);
Expand All @@ -257,13 +269,13 @@ export function decorateLanguageService(
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const infos: ts.QuickInfo[] = [];
for (const [generatePosition, mapping] of toGeneratedOffsets(sourceScript, map, position)) {
for (const [generatePosition, mapping] of toGeneratedOffsets(serviceScript, sourceScript, map, position)) {
if (!isHoverEnabled(mapping.data)) {
continue;
}
const info = getQuickInfoAtPosition(fileName, generatePosition);
if (info) {
const textSpan = transformTextSpan(sourceScript, map, info.textSpan, isHoverEnabled);
const textSpan = transformTextSpan(serviceScript, sourceScript, map, info.textSpan, isHoverEnabled);
if (textSpan) {
infos.push({
...info,
Expand Down Expand Up @@ -319,11 +331,11 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, position, isSignatureHelpEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, position, isSignatureHelpEnabled);
if (generatePosition !== undefined) {
const result = getSignatureHelpItems(fileName, generatePosition, options);
if (result) {
const applicableSpan = transformTextSpan(sourceScript, map, result.applicableSpan, isSignatureHelpEnabled);
const applicableSpan = transformTextSpan(serviceScript, sourceScript, map, result.applicableSpan, isSignatureHelpEnabled);
if (applicableSpan) {
return {
...result,
Expand Down Expand Up @@ -377,7 +389,7 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos, isCodeActionsEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos, isCodeActionsEnabled);
if (generatePosition !== undefined) {
const por = typeof positionOrRange === 'number'
? generatePosition
Expand All @@ -399,6 +411,7 @@ export function decorateLanguageService(
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(
serviceScript,
sourceScript,
map,
typeof positionOrRange === 'number'
Expand Down Expand Up @@ -431,13 +444,13 @@ export function decorateLanguageService(
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
let failed: ts.RenameInfoFailure | undefined;
for (const [generateOffset, mapping] of toGeneratedOffsets(sourceScript, map, position)) {
for (const [generateOffset, mapping] of toGeneratedOffsets(serviceScript, sourceScript, map, position)) {
if (!isRenameEnabled(mapping.data)) {
continue;
}
const info = getRenameInfo(fileName, generateOffset, options);
if (info.canRename) {
const span = transformTextSpan(sourceScript, map, info.triggerSpan, isRenameEnabled);
const span = transformTextSpan(serviceScript, sourceScript, map, info.triggerSpan, isRenameEnabled);
if (span) {
info.triggerSpan = span;
return info;
Expand All @@ -464,8 +477,8 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generateStart = toGeneratedOffset(sourceScript, map, start, isCodeActionsEnabled);
const generateEnd = toGeneratedOffset(sourceScript, map, end, isCodeActionsEnabled);
const generateStart = toGeneratedOffset(serviceScript, sourceScript, map, start, isCodeActionsEnabled);
const generateEnd = toGeneratedOffset(serviceScript, sourceScript, map, end, isCodeActionsEnabled);
if (generateStart !== undefined && generateEnd !== undefined) {
fixes = getCodeFixesAtPosition(
fileName,
Expand Down Expand Up @@ -503,13 +516,14 @@ export function decorateLanguageService(
}
start ??= 0;
end ??= sourceScript.snapshot.getLength();
start += sourceScript.snapshot.getLength();
end += sourceScript.snapshot.getLength();
const mappingOffset = getMappingOffset(serviceScript, sourceScript);
start += mappingOffset;
end += mappingOffset;
const result = getEncodedSemanticClassifications(fileName, { start, length: end - start }, format);
const spans: number[] = [];
for (let i = 0; i < result.spans.length; i += 3) {
const sourceStart = toSourceOffset(sourceScript, map, result.spans[i], isSemanticTokensEnabled);
const sourceEnd = toSourceOffset(sourceScript, map, result.spans[i] + result.spans[i + 1], isSemanticTokensEnabled);
const sourceStart = toSourceOffset(serviceScript, sourceScript, map, result.spans[i], isSemanticTokensEnabled);
const sourceEnd = toSourceOffset(serviceScript, sourceScript, map, result.spans[i] + result.spans[i + 1], isSemanticTokensEnabled);
if (sourceStart !== undefined && sourceEnd !== undefined) {
spans.push(
sourceStart,
Expand Down Expand Up @@ -702,7 +716,7 @@ export function decorateLanguageService(
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const results: ts.CompletionInfo[] = [];
for (const [generatedOffset, mapping] of toGeneratedOffsets(sourceScript, map, position)) {
for (const [generatedOffset, mapping] of toGeneratedOffsets(serviceScript, sourceScript, map, position)) {
if (!isCompletionEnabled(mapping.data)) {
continue;
}
Expand All @@ -714,10 +728,10 @@ export function decorateLanguageService(
result.entries = result.entries.filter(entry => !!entry.sourceDisplay);
}
for (const entry of result.entries) {
entry.replacementSpan = entry.replacementSpan && transformTextSpan(sourceScript, map, entry.replacementSpan, isCompletionEnabled);
entry.replacementSpan = entry.replacementSpan && transformTextSpan(serviceScript, sourceScript, map, entry.replacementSpan, isCompletionEnabled);
}
result.optionalReplacementSpan = result.optionalReplacementSpan
&& transformTextSpan(sourceScript, map, result.optionalReplacementSpan, isCompletionEnabled);
&& transformTextSpan(serviceScript, sourceScript, map, result.optionalReplacementSpan, isCompletionEnabled);
const isAdditional = typeof mapping.data.completion === 'object' && mapping.data.completion.isAdditional;
if (isAdditional) {
results.push(result);
Expand Down Expand Up @@ -746,7 +760,7 @@ export function decorateLanguageService(
const fileName = filePath.replace(windowsPathReg, '/');
const [serviceScript, sourceScript, map] = getServiceScript(language, fileName);
if (serviceScript) {
const generatePosition = toGeneratedOffset(sourceScript, map, position, isCompletionEnabled);
const generatePosition = toGeneratedOffset(serviceScript, sourceScript, map, position, isCompletionEnabled);
if (generatePosition !== undefined) {
details = getCompletionEntryDetails(fileName, generatePosition, entryName, formatOptions, source, preferences, data);
}
Expand Down Expand Up @@ -781,12 +795,13 @@ export function decorateLanguageService(
start = 0;
end = 0;
}
start += sourceScript.snapshot.getLength();
end += sourceScript.snapshot.getLength();
const mappingOffset = getMappingOffset(serviceScript, sourceScript);
start += mappingOffset;
end += mappingOffset;
const result = provideInlayHints(fileName, { start, length: end - start }, preferences);
const hints: ts.InlayHint[] = [];
for (const hint of result) {
const sourcePosition = toSourceOffset(sourceScript, map, hint.position, isInlayHintsEnabled);
const sourcePosition = toSourceOffset(serviceScript, sourceScript, map, hint.position, isInlayHintsEnabled);
if (sourcePosition !== undefined) {
hints.push({
...hint,
Expand Down Expand Up @@ -822,7 +837,7 @@ export function decorateLanguageService(
if (serviceScript) {
for (const [generatedOffset, mapping] of map.getGeneratedOffsets(position)) {
if (filter(mapping.data)) {
process(fileName, generatedOffset + sourceScript.snapshot.getLength());
process(fileName, generatedOffset + getMappingOffset(serviceScript, sourceScript));
}
}
}
Expand All @@ -846,18 +861,19 @@ export function decorateLanguageService(

processedFilePositions.add(ref[0] + ':' + ref[1]);

const [virtualFile, sourceScript] = getServiceScript(language, ref[0]);
if (!virtualFile) {
const [serviceScript, sourceScript] = getServiceScript(language, ref[0]);
if (!serviceScript) {
continue;
}

const linkedCodeMap = language.linkedCodeMaps.get(virtualFile.code);
const linkedCodeMap = language.linkedCodeMaps.get(serviceScript.code);
if (!linkedCodeMap) {
continue;
}

for (const linkedCodeOffset of linkedCodeMap.getLinkedOffsets(ref[1] - sourceScript.snapshot.getLength())) {
process(ref[0], linkedCodeOffset + sourceScript.snapshot.getLength());
const mappingOffset = getMappingOffset(serviceScript, sourceScript);
for (const linkedCodeOffset of linkedCodeMap.getLinkedOffsets(ref[1] - mappingOffset)) {
process(ref[0], linkedCodeOffset + mappingOffset);
}
}
}
Expand Down
29 changes: 19 additions & 10 deletions packages/typescript/lib/node/decorateLanguageServiceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function decorateLanguageServiceHost(
version: string,
virtualScript?: {
snapshot: ts.IScriptSnapshot;
kind: ts.ScriptKind;
scriptKind: ts.ScriptKind;
extension: string;
},
]>();
Expand Down Expand Up @@ -94,7 +94,7 @@ export function decorateLanguageServiceHost(
languageServiceHost.getScriptKind = fileName => {
const virtualScript = updateVirtualScript(fileName);
if (virtualScript) {
return virtualScript.kind;
return virtualScript.scriptKind;
}
return getScriptKind(fileName);
};
Expand Down Expand Up @@ -123,14 +123,23 @@ export function decorateLanguageServiceHost(
if (sourceScript?.generated) {
const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root);
if (serviceScript) {
const sourceContents = sourceScript.snapshot.getText(0, sourceScript.snapshot.getLength());
let virtualContents = sourceContents.split('\n').map(line => ' '.repeat(line.length)).join('\n');
virtualContents += serviceScript.code.snapshot.getText(0, serviceScript.code.snapshot.getLength());
script[1] = {
extension: serviceScript.extension,
kind: serviceScript.scriptKind,
snapshot: ts.ScriptSnapshot.fromString(virtualContents),
};
if (serviceScript.preventLeadingOffset) {
script[1] = {
extension: serviceScript.extension,
scriptKind: serviceScript.scriptKind,
snapshot: serviceScript.code.snapshot,
};
}
else {
const sourceContents = sourceScript.snapshot.getText(0, sourceScript.snapshot.getLength());
const virtualContents = sourceContents.split('\n').map(line => ' '.repeat(line.length)).join('\n')
+ serviceScript.code.snapshot.getText(0, serviceScript.code.snapshot.getLength());
script[1] = {
extension: serviceScript.extension,
scriptKind: serviceScript.scriptKind,
snapshot: ts.ScriptSnapshot.fromString(virtualContents),
};
}
}
if (sourceScript.generated.languagePlugin.typescript?.getExtraServiceScripts) {
console.warn('getExtraServiceScripts() is not available in TS plugin.');
Expand Down
8 changes: 4 additions & 4 deletions packages/typescript/lib/node/proxyCreateProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function proxyCreateProgram(
getLanguagePlugins: (ts: typeof import('typescript'), options: ts.CreateProgramOptions) => LanguagePlugin<string>[]
) {
const sourceFileSnapshots = new FileMap<[ts.SourceFile | undefined, ts.IScriptSnapshot | undefined]>(ts.sys.useCaseSensitiveFileNames);
const parsedSourceFiles = new WeakMap<ts.SourceFile, ts.SourceFile>();
const parsedSourceFiles = new WeakMap<ts.SourceFile, ts.SourceFile | undefined>();

let lastOptions: ts.CreateProgramOptions | undefined;
let languagePlugins: LanguagePlugin<string>[] | undefined;
Expand Down Expand Up @@ -135,11 +135,11 @@ export function proxyCreateProgram(
if (!parsedSourceFiles.has(originalSourceFile)) {
const sourceScript = language!.scripts.get(fileName);
assert(!!sourceScript, '!!sourceScript');
parsedSourceFiles.set(originalSourceFile, originalSourceFile);
parsedSourceFiles.set(originalSourceFile, undefined);
if (sourceScript.generated?.languagePlugin.typescript) {
const { getServiceScript, getExtraServiceScripts } = sourceScript.generated.languagePlugin.typescript;
const serviceScript = getServiceScript(sourceScript.generated.root);
if (serviceScript) {
if (serviceScript && !serviceScript.preventLeadingOffset) {
let patchedText = originalSourceFile.text.split('\n').map(line => ' '.repeat(line.length)).join('\n');
let scriptKind = ts.ScriptKind.TS;
scriptKind = serviceScript.scriptKind;
Expand All @@ -160,7 +160,7 @@ export function proxyCreateProgram(
}
}
}
return parsedSourceFiles.get(originalSourceFile);
return parsedSourceFiles.get(originalSourceFile) ?? originalSourceFile;
};

if (extensions.length) {
Expand Down
Loading

0 comments on commit f5a6efb

Please sign in to comment.