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

feat(typescript): add option to prevent offset in plugin mode and tsc #191

Merged
merged 1 commit into from
Jun 3, 2024
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
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
Loading