Skip to content

Commit 4a9ef5e

Browse files
authored
fix: map refactor/quick-fix of svelte files in typescript plugin (#2439)
#2435
1 parent 527c2ad commit 4a9ef5e

File tree

3 files changed

+182
-1
lines changed

3 files changed

+182
-1
lines changed

packages/typescript-plugin/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
146146
// don't clear semantic cache here
147147
// typescript now expected the program updates to be completely in their control
148148
// doing so will result in a crash
149-
info.project.markAsDirty();
149+
// @ts-expect-error internal API since TS 5.5
150+
info.project.markAsDirty?.();
150151

151152
// updateGraph checks for new root files
152153
// if there's no tsconfig there isn't root files to check
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import type ts from 'typescript';
2+
import { SvelteSnapshot, SvelteSnapshotManager } from '../svelte-snapshots';
3+
import { isNotNullOrUndefined, isSvelteFilePath } from '../utils';
4+
5+
type _ts = typeof ts;
6+
7+
export function decorateQuickFixAndRefactor(
8+
ls: ts.LanguageService,
9+
ts: _ts,
10+
snapshotManager: SvelteSnapshotManager
11+
) {
12+
const getEditsForRefactor = ls.getEditsForRefactor;
13+
const getCodeFixesAtPosition = ls.getCodeFixesAtPosition;
14+
15+
ls.getEditsForRefactor = (...args) => {
16+
const result = getEditsForRefactor(...args);
17+
18+
if (!result) {
19+
return;
20+
}
21+
22+
const edits = result.edits.map(mapFileTextChanges).filter(isNotNullOrUndefined);
23+
if (edits.length === 0) {
24+
return;
25+
}
26+
27+
return {
28+
...result,
29+
edits
30+
};
31+
};
32+
33+
ls.getCodeFixesAtPosition = (...args) => {
34+
const result = getCodeFixesAtPosition(...args);
35+
36+
return result
37+
.map((fix) => {
38+
return {
39+
...fix,
40+
changes: fix.changes.map(mapFileTextChanges).filter(isNotNullOrUndefined)
41+
};
42+
})
43+
.filter((fix) => fix.changes.length > 0);
44+
};
45+
46+
function mapFileTextChanges(change: ts.FileTextChanges) {
47+
const snapshot = snapshotManager.get(change.fileName);
48+
if (!isSvelteFilePath(change.fileName) || !snapshot) {
49+
return change;
50+
}
51+
52+
let baseIndent: string | undefined;
53+
const getBaseIndent = () => {
54+
if (baseIndent !== undefined) {
55+
return baseIndent;
56+
}
57+
58+
baseIndent = getIndentOfFirstStatement(ts, ls, change.fileName, snapshot);
59+
60+
return baseIndent;
61+
};
62+
63+
const textChanges = change.textChanges
64+
.map((textChange) => mapEdit(textChange, snapshot, getBaseIndent))
65+
.filter(isNotNullOrUndefined);
66+
67+
// If part of the text changes are invalid, filter out the whole change
68+
if (textChanges.length === 0 || textChanges.length !== change.textChanges.length) {
69+
return null;
70+
}
71+
72+
return {
73+
...change,
74+
textChanges
75+
};
76+
}
77+
}
78+
79+
function mapEdit(change: ts.TextChange, snapshot: SvelteSnapshot, getBaseIndent: () => string) {
80+
const isNewImportStatement = change.newText.trimStart().startsWith('import');
81+
if (isNewImportStatement) {
82+
return mapNewImport(change, snapshot, getBaseIndent);
83+
}
84+
85+
const span = snapshot.getOriginalTextSpan(change.span);
86+
87+
if (!span) {
88+
return null;
89+
}
90+
91+
return {
92+
span,
93+
newText: change.newText
94+
};
95+
}
96+
97+
function mapNewImport(
98+
change: ts.TextChange,
99+
snapshot: SvelteSnapshot,
100+
getBaseIndent: () => string
101+
): ts.TextChange | null {
102+
const previousLineEnds = getPreviousLineEnds(snapshot.getText(), change.span.start);
103+
104+
if (previousLineEnds === -1) {
105+
return null;
106+
}
107+
const mappable = snapshot.getOriginalTextSpan({
108+
start: previousLineEnds,
109+
length: 0
110+
});
111+
112+
if (!mappable) {
113+
// There might not be any import at all but this is rare enough so ignore for now
114+
return null;
115+
}
116+
117+
const originalText = snapshot.getOriginalText();
118+
const span = {
119+
start: originalText.indexOf('\n', mappable.start) + 1,
120+
length: change.span.length
121+
};
122+
123+
const baseIndent = getBaseIndent();
124+
let newText = baseIndent
125+
? change.newText
126+
.split('\n')
127+
.map((line) => (line ? baseIndent + line : line))
128+
.join('\n')
129+
: change.newText;
130+
131+
return { span, newText };
132+
}
133+
134+
function getPreviousLineEnds(text: string, start: number) {
135+
const index = text.lastIndexOf('\n', start);
136+
if (index === -1) {
137+
return index;
138+
}
139+
140+
if (text[index - 1] === '\r') {
141+
return index - 1;
142+
}
143+
144+
return index;
145+
}
146+
147+
function getIndentOfFirstStatement(
148+
ts: _ts,
149+
ls: ts.LanguageService,
150+
fileName: string,
151+
snapshot: SvelteSnapshot
152+
) {
153+
const firstExportOrImport = ls
154+
.getProgram()
155+
?.getSourceFile(fileName)
156+
?.statements.find((node) => ts.isExportDeclaration(node) || ts.isImportDeclaration(node));
157+
158+
const originalPosition = firstExportOrImport
159+
? snapshot.getOriginalOffset(firstExportOrImport.getStart())
160+
: -1;
161+
if (originalPosition === -1) {
162+
return '';
163+
}
164+
165+
const source = snapshot.getOriginalText();
166+
const start = source.lastIndexOf('\n', originalPosition) + 1;
167+
let index = start;
168+
while (index < originalPosition) {
169+
const char = source[index];
170+
if (char.trim()) {
171+
break;
172+
}
173+
174+
index++;
175+
}
176+
177+
return source.substring(start, index);
178+
}

packages/typescript-plugin/src/language-service/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { decorateLanguageServiceHost } from './host';
1717
import { decorateNavigateToItems } from './navigate-to-items';
1818
import { decorateFileReferences } from './file-references';
1919
import { decorateMoveToRefactoringFileSuggestions } from './move-to-file';
20+
import { decorateQuickFixAndRefactor } from './code-action';
2021

2122
const patchedProject = new Set<string>();
2223

@@ -66,6 +67,7 @@ function decorateLanguageServiceInner(
6667
decorateNavigateToItems(ls, snapshotManager);
6768
decorateFileReferences(ls, snapshotManager);
6869
decorateMoveToRefactoringFileSuggestions(ls);
70+
decorateQuickFixAndRefactor(ls, typescript, snapshotManager);
6971
decorateDispose(ls, info.project, onDispose);
7072
return ls;
7173
}

0 commit comments

Comments
 (0)