Skip to content

Commit bb58b0c

Browse files
authored
(fix) prevent duplicate import updates (#1466)
The TS plugin and the Svelte extension want to update the same locations after a file move/rename in some cases; prevent that #1461
1 parent 59b6bbf commit bb58b0c

File tree

5 files changed

+76
-8
lines changed

5 files changed

+76
-8
lines changed

packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider {
1515
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
1616

1717
async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
18+
// TODO does this handle folder moves/renames correctly? old/new path isn't a file then
1819
const oldPath = urlToPath(fileRename.oldUri);
1920
const newPath = urlToPath(fileRename.newUri);
2021
if (!oldPath || !newPath) {

packages/svelte-vscode/src/extension.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,47 @@ function addRenameFileListener(getLS: () => LanguageClient) {
352352
newUri: evt.files[0].newUri.toString(true)
353353
}
354354
);
355-
if (!editsForFileRename) {
355+
const edits = editsForFileRename?.documentChanges?.filter(TextDocumentEdit.is);
356+
if (!edits) {
356357
return;
357358
}
358359

359360
const workspaceEdit = new WorkspaceEdit();
360-
// Renaming a file should only result in edits of existing files
361-
editsForFileRename.documentChanges?.filter(TextDocumentEdit.is).forEach((change) =>
361+
// We need to take into account multiple cases:
362+
// - A Svelte file is moved/renamed
363+
// -> all updates will be related to that Svelte file, do that here. The TS LS won't even notice the update
364+
// - A TS/JS file is moved/renamed
365+
// -> all updates will be related to that TS/JS file
366+
// -> let the TS LS take care of these updates in TS/JS files, do Svelte file updates here
367+
// - A folder with TS/JS AND Svelte files is moved/renamed
368+
// -> all Svelte file updates are handled here
369+
// -> all TS/JS file updates that consist of only TS/JS import updates are handled by the TS LS
370+
// -> all TS/JS file updates that consist of only Svelte import updates are handled here
371+
// -> all TS/JS file updates that are mixed are handled here, but also possibly by the TS LS
372+
// if the TS plugin doesn't prevent it. This trades risk of broken updates with certainty of missed updates
373+
edits.forEach((change) => {
374+
const isTsOrJsFile =
375+
change.textDocument.uri.endsWith('.ts') ||
376+
change.textDocument.uri.endsWith('.js');
377+
const containsSvelteImportUpdate = change.edits.some((edit) =>
378+
edit.newText.endsWith('.svelte')
379+
);
380+
if (isTsOrJsFile && !containsSvelteImportUpdate) {
381+
return;
382+
}
383+
362384
change.edits.forEach((edit) => {
385+
if (
386+
isTsOrJsFile &&
387+
!TsPlugin.isEnabled() &&
388+
!edit.newText.endsWith('.svelte')
389+
) {
390+
// TS plugin enabled -> all mixed imports are handled here
391+
// TS plugin disabled -> let TS/JS path updates be handled by the TS LS, Svelte here
392+
return;
393+
}
394+
395+
// Renaming a file should only result in edits of existing files
363396
workspaceEdit.replace(
364397
Uri.parse(change.textDocument.uri),
365398
new Range(
@@ -368,8 +401,8 @@ function addRenameFileListener(getLS: () => LanguageClient) {
368401
),
369402
edit.newText
370403
);
371-
})
372-
);
404+
});
405+
});
373406
workspace.applyEdit(workspaceEdit);
374407
}
375408
);

packages/svelte-vscode/src/tsplugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ export class TsPlugin {
44
private enabled: boolean;
55

66
constructor(context: ExtensionContext) {
7-
this.enabled = this.getEnabledState();
7+
this.enabled = TsPlugin.isEnabled();
88
this.toggleTsPlugin(this.enabled);
99

1010
context.subscriptions.push(
1111
workspace.onDidChangeConfiguration(() => {
12-
const enabled = this.getEnabledState();
12+
const enabled = TsPlugin.isEnabled();
1313
if (enabled !== this.enabled) {
1414
this.enabled = enabled;
1515
this.toggleTsPlugin(this.enabled);
@@ -18,7 +18,7 @@ export class TsPlugin {
1818
);
1919
}
2020

21-
private getEnabledState(): boolean {
21+
static isEnabled(): boolean {
2222
return workspace.getConfiguration('svelte').get<boolean>('enable-ts-plugin') ?? false;
2323
}
2424

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { decorateDiagnostics } from './diagnostics';
99
import { decorateFindReferences } from './find-references';
1010
import { decorateGetImplementation } from './implementation';
1111
import { decorateRename } from './rename';
12+
import { decorateUpdateImports } from './update-imports';
1213

1314
const sveltePluginPatchSymbol = Symbol('sveltePluginPatchSymbol');
1415

@@ -41,6 +42,7 @@ function decorateLanguageServiceInner(
4142
decorateCompletions(ls, logger);
4243
decorateGetDefinition(ls, snapshotManager, logger);
4344
decorateGetImplementation(ls, snapshotManager, logger);
45+
decorateUpdateImports(ls, snapshotManager, logger);
4446
return ls;
4547
}
4648

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type ts from 'typescript/lib/tsserverlibrary';
2+
import { Logger } from '../logger';
3+
import { SvelteSnapshotManager } from '../svelte-snapshots';
4+
import { isSvelteFilePath } from '../utils';
5+
6+
export function decorateUpdateImports(
7+
ls: ts.LanguageService,
8+
snapshotManager: SvelteSnapshotManager,
9+
logger: Logger
10+
): void {
11+
const getEditsForFileRename = ls.getEditsForFileRename;
12+
ls.getEditsForFileRename = (oldFilePath, newFilePath, formatOptions, preferences) => {
13+
const renameLocations = getEditsForFileRename(
14+
oldFilePath,
15+
newFilePath,
16+
formatOptions,
17+
preferences
18+
);
19+
// If a file move/rename of a TS/JS file results a Svelte file change,
20+
// the Svelte extension will notice that, too, and adjusts the same imports.
21+
// This results in duplicate adjustments or race conditions with conflicting text spans
22+
// which can break imports in some cases.
23+
// Therefore don't do any updates of Svelte files and and also no updates of mixed TS files
24+
// and let the Svelte extension handle that.
25+
return renameLocations?.filter((renameLocation) => {
26+
return (
27+
!isSvelteFilePath(renameLocation.fileName) &&
28+
!renameLocation.textChanges.some((change) => change.newText.endsWith('.svelte'))
29+
);
30+
});
31+
};
32+
}

0 commit comments

Comments
 (0)