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

(fix) prevent duplicate import updates #1466

Merged
merged 3 commits into from May 7, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -15,6 +15,7 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}

async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
// TODO does this handle folder moves/renames correctly? old/new path isn't a file then
const oldPath = urlToPath(fileRename.oldUri);
const newPath = urlToPath(fileRename.newUri);
if (!oldPath || !newPath) {
Expand Down
43 changes: 38 additions & 5 deletions packages/svelte-vscode/src/extension.ts
Expand Up @@ -352,14 +352,47 @@ function addRenameFileListener(getLS: () => LanguageClient) {
newUri: evt.files[0].newUri.toString(true)
}
);
if (!editsForFileRename) {
const edits = editsForFileRename?.documentChanges?.filter(TextDocumentEdit.is);
if (!edits) {
return;
}

const workspaceEdit = new WorkspaceEdit();
// Renaming a file should only result in edits of existing files
editsForFileRename.documentChanges?.filter(TextDocumentEdit.is).forEach((change) =>
// We need to take into account multiple cases:
// - A Svelte file is moved/renamed
// -> all updates will be related to that Svelte file, do that here. The TS LS won't even notice the update
// - A TS/JS file is moved/renamed
// -> all updates will be related to that TS/JS file
// -> let the TS LS take care of these updates in TS/JS files, do Svelte file updates here
// - A folder with TS/JS AND Svelte files is moved/renamed
// -> all Svelte file updates are handled here
// -> all TS/JS file updates that consist of only TS/JS import updates are handled by the TS LS
// -> all TS/JS file updates that consist of only Svelte import updates are handled here
// -> all TS/JS file updates that are mixed are handled here, but also possibly by the TS LS
// if the TS plugin doesn't prevent it. This trades risk of broken updates with certainty of missed updates
edits.forEach((change) => {
const isTsOrJsFile =
change.textDocument.uri.endsWith('.ts') ||
change.textDocument.uri.endsWith('.js');
const containsSvelteImportUpdate = change.edits.some((edit) =>
edit.newText.endsWith('.svelte')
);
if (isTsOrJsFile && !containsSvelteImportUpdate) {
return;
}

change.edits.forEach((edit) => {
if (
isTsOrJsFile &&
!TsPlugin.isEnabled() &&
!edit.newText.endsWith('.svelte')
) {
// TS plugin enabled -> all mixed imports are handled here
// TS plugin disabled -> let TS/JS path updates be handled by the TS LS, Svelte here
return;
}

// Renaming a file should only result in edits of existing files
workspaceEdit.replace(
Uri.parse(change.textDocument.uri),
new Range(
Expand All @@ -368,8 +401,8 @@ function addRenameFileListener(getLS: () => LanguageClient) {
),
edit.newText
);
})
);
});
});
workspace.applyEdit(workspaceEdit);
}
);
Expand Down
6 changes: 3 additions & 3 deletions packages/svelte-vscode/src/tsplugin.ts
Expand Up @@ -4,12 +4,12 @@ export class TsPlugin {
private enabled: boolean;

constructor(context: ExtensionContext) {
this.enabled = this.getEnabledState();
this.enabled = TsPlugin.isEnabled();
this.toggleTsPlugin(this.enabled);

context.subscriptions.push(
workspace.onDidChangeConfiguration(() => {
const enabled = this.getEnabledState();
const enabled = TsPlugin.isEnabled();
if (enabled !== this.enabled) {
this.enabled = enabled;
this.toggleTsPlugin(this.enabled);
Expand All @@ -18,7 +18,7 @@ export class TsPlugin {
);
}

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

Expand Down
2 changes: 2 additions & 0 deletions packages/typescript-plugin/src/language-service/index.ts
Expand Up @@ -9,6 +9,7 @@ import { decorateDiagnostics } from './diagnostics';
import { decorateFindReferences } from './find-references';
import { decorateGetImplementation } from './implementation';
import { decorateRename } from './rename';
import { decorateUpdateImports } from './update-imports';

const sveltePluginPatchSymbol = Symbol('sveltePluginPatchSymbol');

Expand Down Expand Up @@ -41,6 +42,7 @@ function decorateLanguageServiceInner(
decorateCompletions(ls, logger);
decorateGetDefinition(ls, snapshotManager, logger);
decorateGetImplementation(ls, snapshotManager, logger);
decorateUpdateImports(ls, snapshotManager, logger);
return ls;
}

Expand Down
32 changes: 32 additions & 0 deletions packages/typescript-plugin/src/language-service/update-imports.ts
@@ -0,0 +1,32 @@
import type ts from 'typescript/lib/tsserverlibrary';
import { Logger } from '../logger';
import { SvelteSnapshotManager } from '../svelte-snapshots';
import { isSvelteFilePath } from '../utils';

export function decorateUpdateImports(
ls: ts.LanguageService,
snapshotManager: SvelteSnapshotManager,
logger: Logger
): void {
const getEditsForFileRename = ls.getEditsForFileRename;
ls.getEditsForFileRename = (oldFilePath, newFilePath, formatOptions, preferences) => {
const renameLocations = getEditsForFileRename(
oldFilePath,
newFilePath,
formatOptions,
preferences
);
// If a file move/rename of a TS/JS file results a Svelte file change,
// the Svelte extension will notice that, too, and adjusts the same imports.
// This results in duplicate adjustments or race conditions with conflicting text spans
// which can break imports in some cases.
// Therefore don't do any updates of Svelte files and and also no updates of mixed TS files
// and let the Svelte extension handle that.
return renameLocations?.filter((renameLocation) => {
return (
!isSvelteFilePath(renameLocation.fileName) &&
!renameLocation.textChanges.some((change) => change.newText.endsWith('.svelte'))
);
});
};
}