Skip to content

Commit

Permalink
fix: map refactor/quick-fix of svelte files in typescript plugin (#2439)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonlyu123 authored Jul 30, 2024
1 parent 527c2ad commit 4a9ef5e
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/typescript-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
// don't clear semantic cache here
// typescript now expected the program updates to be completely in their control
// doing so will result in a crash
info.project.markAsDirty();
// @ts-expect-error internal API since TS 5.5
info.project.markAsDirty?.();

// updateGraph checks for new root files
// if there's no tsconfig there isn't root files to check
Expand Down
178 changes: 178 additions & 0 deletions packages/typescript-plugin/src/language-service/code-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type ts from 'typescript';
import { SvelteSnapshot, SvelteSnapshotManager } from '../svelte-snapshots';
import { isNotNullOrUndefined, isSvelteFilePath } from '../utils';

type _ts = typeof ts;

export function decorateQuickFixAndRefactor(
ls: ts.LanguageService,
ts: _ts,
snapshotManager: SvelteSnapshotManager
) {
const getEditsForRefactor = ls.getEditsForRefactor;
const getCodeFixesAtPosition = ls.getCodeFixesAtPosition;

ls.getEditsForRefactor = (...args) => {
const result = getEditsForRefactor(...args);

if (!result) {
return;
}

const edits = result.edits.map(mapFileTextChanges).filter(isNotNullOrUndefined);
if (edits.length === 0) {
return;
}

return {
...result,
edits
};
};

ls.getCodeFixesAtPosition = (...args) => {
const result = getCodeFixesAtPosition(...args);

return result
.map((fix) => {
return {
...fix,
changes: fix.changes.map(mapFileTextChanges).filter(isNotNullOrUndefined)
};
})
.filter((fix) => fix.changes.length > 0);
};

function mapFileTextChanges(change: ts.FileTextChanges) {
const snapshot = snapshotManager.get(change.fileName);
if (!isSvelteFilePath(change.fileName) || !snapshot) {
return change;
}

let baseIndent: string | undefined;
const getBaseIndent = () => {
if (baseIndent !== undefined) {
return baseIndent;
}

baseIndent = getIndentOfFirstStatement(ts, ls, change.fileName, snapshot);

return baseIndent;
};

const textChanges = change.textChanges
.map((textChange) => mapEdit(textChange, snapshot, getBaseIndent))
.filter(isNotNullOrUndefined);

// If part of the text changes are invalid, filter out the whole change
if (textChanges.length === 0 || textChanges.length !== change.textChanges.length) {
return null;
}

return {
...change,
textChanges
};
}
}

function mapEdit(change: ts.TextChange, snapshot: SvelteSnapshot, getBaseIndent: () => string) {
const isNewImportStatement = change.newText.trimStart().startsWith('import');
if (isNewImportStatement) {
return mapNewImport(change, snapshot, getBaseIndent);
}

const span = snapshot.getOriginalTextSpan(change.span);

if (!span) {
return null;
}

return {
span,
newText: change.newText
};
}

function mapNewImport(
change: ts.TextChange,
snapshot: SvelteSnapshot,
getBaseIndent: () => string
): ts.TextChange | null {
const previousLineEnds = getPreviousLineEnds(snapshot.getText(), change.span.start);

if (previousLineEnds === -1) {
return null;
}
const mappable = snapshot.getOriginalTextSpan({
start: previousLineEnds,
length: 0
});

if (!mappable) {
// There might not be any import at all but this is rare enough so ignore for now
return null;
}

const originalText = snapshot.getOriginalText();
const span = {
start: originalText.indexOf('\n', mappable.start) + 1,
length: change.span.length
};

const baseIndent = getBaseIndent();
let newText = baseIndent
? change.newText
.split('\n')
.map((line) => (line ? baseIndent + line : line))
.join('\n')
: change.newText;

return { span, newText };
}

function getPreviousLineEnds(text: string, start: number) {
const index = text.lastIndexOf('\n', start);
if (index === -1) {
return index;
}

if (text[index - 1] === '\r') {
return index - 1;
}

return index;
}

function getIndentOfFirstStatement(
ts: _ts,
ls: ts.LanguageService,
fileName: string,
snapshot: SvelteSnapshot
) {
const firstExportOrImport = ls
.getProgram()
?.getSourceFile(fileName)
?.statements.find((node) => ts.isExportDeclaration(node) || ts.isImportDeclaration(node));

const originalPosition = firstExportOrImport
? snapshot.getOriginalOffset(firstExportOrImport.getStart())
: -1;
if (originalPosition === -1) {
return '';
}

const source = snapshot.getOriginalText();
const start = source.lastIndexOf('\n', originalPosition) + 1;
let index = start;
while (index < originalPosition) {
const char = source[index];
if (char.trim()) {
break;
}

index++;
}

return source.substring(start, index);
}
2 changes: 2 additions & 0 deletions packages/typescript-plugin/src/language-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { decorateLanguageServiceHost } from './host';
import { decorateNavigateToItems } from './navigate-to-items';
import { decorateFileReferences } from './file-references';
import { decorateMoveToRefactoringFileSuggestions } from './move-to-file';
import { decorateQuickFixAndRefactor } from './code-action';

const patchedProject = new Set<string>();

Expand Down Expand Up @@ -66,6 +67,7 @@ function decorateLanguageServiceInner(
decorateNavigateToItems(ls, snapshotManager);
decorateFileReferences(ls, snapshotManager);
decorateMoveToRefactoringFileSuggestions(ls);
decorateQuickFixAndRefactor(ls, typescript, snapshotManager);
decorateDispose(ls, info.project, onDispose);
return ls;
}
Expand Down

0 comments on commit 4a9ef5e

Please sign in to comment.