Skip to content

Commit

Permalink
(feat) update imports for renames/moves files
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Holthausen committed May 26, 2020
1 parent acc1ca9 commit 2cd9351
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 6 deletions.
4 changes: 4 additions & 0 deletions packages/language-server/src/ls-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const defaultLSConfig: LSConfig = {
definitions: { enable: true },
documentSymbols: { enable: true },
codeActions: { enable: true },
rename: { enable: true },
},
css: {
enable: true,
Expand Down Expand Up @@ -69,6 +70,9 @@ export interface LSTypescriptConfig {
codeActions: {
enable: boolean;
};
rename: {
enable: boolean;
};
}

export interface LSCSSConfig {
Expand Down
17 changes: 16 additions & 1 deletion packages/language-server/src/plugins/PluginHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ import {
FileChangeType,
CompletionItem,
CompletionContext,
WorkspaceEdit,
} from 'vscode-languageserver';
import { LSConfig, LSConfigManager } from '../ls-config';
import { DocumentManager } from '../lib/documents';
import { LSProvider, Plugin, OnWatchFileChanges, AppCompletionItem } from './interfaces';
import {
LSProvider,
Plugin,
OnWatchFileChanges,
AppCompletionItem,
FileRename,
} from './interfaces';
import { Logger } from '../logger';

enum ExecuteMode {
Expand Down Expand Up @@ -222,6 +229,14 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
);
}

async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
return await this.execute<WorkspaceEdit>(
'updateImports',
[fileRename],
ExecuteMode.FirstNonNull,
);
}

onWatchFileChanges(fileName: string, changeType: FileChangeType): void {
for (const support of this.plugins) {
support.onWatchFileChanges?.(fileName, changeType);
Expand Down
11 changes: 11 additions & 0 deletions packages/language-server/src/plugins/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
SymbolInformation,
TextDocumentIdentifier,
TextEdit,
WorkspaceEdit,
} from 'vscode-languageserver-types';
import { Document } from '../lib/documents';

Expand Down Expand Up @@ -85,6 +86,15 @@ export interface CodeActionsProvider {
): Resolvable<CodeAction[]>;
}

export interface FileRename {
oldUri: string;
newUri: string;
}

export interface UpdateImportsProvider {
updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>;
}

export interface OnWatchFileChanges {
onWatchFileChanges(fileName: string, changeType: FileChangeType): void;
}
Expand All @@ -98,6 +108,7 @@ export type LSProvider = DiagnosticsProvider &
ColorPresentationsProvider &
DocumentSymbolsProvider &
DefinitionsProvider &
UpdateImportsProvider &
CodeActionsProvider;

export type Plugin = Partial<LSProvider & OnWatchFileChanges>;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DocumentManager, Document } from '../../lib/documents';
import { pathToUrl } from '../../utils';
import { getLanguageServiceForDocument } from './service';
import { getLanguageServiceForDocument, getLanguageServiceForPath } from './service';
import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot';
import { findTsConfigPath } from './utils';
import { SnapshotManager } from './SnapshotManager';
Expand All @@ -24,6 +24,10 @@ export class LSAndTSDocResolver {
return document;
};

getLSForPath(path: string) {
return getLanguageServiceForPath(path, this.createDocument);
}

getLSAndTSDoc(
document: Document,
): {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Position,
Range,
SymbolInformation,
WorkspaceEdit,
} from 'vscode-languageserver';
import {
Document,
Expand All @@ -30,15 +31,18 @@ import {
DefinitionsProvider,
DiagnosticsProvider,
DocumentSymbolsProvider,
FileRename,
HoverProvider,
OnWatchFileChanges,
UpdateImportsProvider,
} from '../interfaces';
import { DocumentSnapshot, SnapshotFragment } from './DocumentSnapshot';
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
import {
CompletionEntryWithIdentifer,
CompletionsProviderImpl,
} from './features/CompletionProvider';
import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider';
import { LSAndTSDocResolver } from './LSAndTSDocResolver';
import {
convertRange,
Expand All @@ -55,18 +59,21 @@ export class TypeScriptPlugin
DocumentSymbolsProvider,
DefinitionsProvider,
CodeActionsProvider,
UpdateImportsProvider,
OnWatchFileChanges,
CompletionsProvider<CompletionEntryWithIdentifer> {
private configManager: LSConfigManager;
private readonly lsAndTsDocResolver: LSAndTSDocResolver;
private readonly completionProvider: CompletionsProviderImpl;
private readonly codeActionsProvider: CodeActionsProviderImpl;
private readonly updateImportsProvider: UpdateImportsProviderImpl;

constructor(docManager: DocumentManager, configManager: LSConfigManager) {
this.configManager = configManager;
this.lsAndTsDocResolver = new LSAndTSDocResolver(docManager);
this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver);
this.codeActionsProvider = new CodeActionsProviderImpl(this.lsAndTsDocResolver);
this.updateImportsProvider = new UpdateImportsProviderImpl(this.lsAndTsDocResolver);
}

async getDiagnostics(document: Document): Promise<Diagnostic[]> {
Expand Down Expand Up @@ -257,6 +264,14 @@ export class TypeScriptPlugin
return this.codeActionsProvider.getCodeActions(document, range, context);
}

async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
if (!this.featureEnabled('rename')) {
return null;
}

return this.updateImportsProvider.updateImports(fileRename);
}

onWatchFileChanges(fileName: string, changeType: FileChangeType) {
const scriptKind = getScriptKindFromFileName(fileName);

Expand Down Expand Up @@ -290,6 +305,10 @@ export class TypeScriptPlugin
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}

private getLSForPath(path: string) {
return this.lsAndTsDocResolver.getLSForPath(path);
}

private getSnapshot(filePath: string, document?: Document) {
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
TextDocumentEdit,
TextEdit,
VersionedTextDocumentIdentifier,
WorkspaceEdit,
} from 'vscode-languageserver';
import { Document, mapRangeToOriginal } from '../../../lib/documents';
import { urlToPath } from '../../../utils';
import { FileRename, UpdateImportsProvider } from '../../interfaces';
import { SnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange } from '../utils';

export class UpdateImportsProviderImpl implements UpdateImportsProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}

async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
const oldPath = urlToPath(fileRename.oldUri);
const newPath = urlToPath(fileRename.newUri);
if (!oldPath || !newPath) {
return null;
}

const ls = this.getLSForPath(newPath);
// `getEditsForFileRename` might take a while
const fileChanges = ls.getEditsForFileRename(oldPath, newPath, {}, {});

const docs = new Map<string, SnapshotFragment>();
// Assumption: Updating imports will not create new files, and to make sure just filter those out
// who - for whatever reason - might be new ones.
const updateImportsChanges = fileChanges.filter((change) => !change.isNewFile);

const documentChanges = await Promise.all(
updateImportsChanges.map(async (change) => {
let fragment = docs.get(change.fileName);
if (!fragment) {
fragment = await this.getSnapshot(change.fileName).getFragment();
docs.set(change.fileName, fragment);
}

return TextDocumentEdit.create(
VersionedTextDocumentIdentifier.create(fragment.getURL(), null),
change.textChanges.map((edit) => {
let range = mapRangeToOriginal(
fragment!,
convertRange(fragment!, edit.span),
);
// Handle svelte2tsx wrong import mapping:
// The character after the last import maps to the start of the script
// TODO find a way to fix this in svelte2tsx and then remove this
if (range.end.line === 0 && range.end.character === 1) {
edit.span.length -= 1;
range = mapRangeToOriginal(
fragment!,
convertRange(fragment!, edit.span),
);
range.end.character += 1;
}
return TextEdit.replace(range, edit.newText);
}),
);
}),
);

return { documentChanges };
}

private getLSForPath(path: string) {
return this.lsAndTsDocResolver.getLSForPath(path);
}

private getSnapshot(filePath: string, document?: Document) {
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
}
}
15 changes: 13 additions & 2 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,22 @@ const services = new Map<string, LanguageServiceContainer>();

export type CreateDocument = (fileName: string, content: string) => Document;

export function getLanguageServiceForPath(
path: string,
createDocument: CreateDocument,
): ts.LanguageService {
return getService(path, createDocument).getService();
}

export function getLanguageServiceForDocument(
document: Document,
createDocument: CreateDocument,
): ts.LanguageService {
const tsconfigPath = findTsConfigPath(document.getFilePath()!);
return getService(document.getFilePath() || '', createDocument).updateDocument(document);
}

function getService(path: string, createDocument: CreateDocument) {
const tsconfigPath = findTsConfigPath(path);

let service: LanguageServiceContainer;
if (services.has(tsconfigPath)) {
Expand All @@ -35,7 +46,7 @@ export function getLanguageServiceForDocument(
services.set(tsconfigPath, service);
}

return service.updateDocument(document);
return service;
}

export function createLanguageService(
Expand Down
7 changes: 7 additions & 0 deletions packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TextDocumentIdentifier,
IConnection,
CodeActionKind,
RenameFile,
} from 'vscode-languageserver';
import { DocumentManager, Document } from './lib/documents';
import {
Expand Down Expand Up @@ -182,6 +183,12 @@ export function startServer(options?: LSOptions) {
}, 500),
);

// The language server protocol does not have a specific "did rename/move files" event,
// so we create our own in the extension client and handle it here
connection.onRequest('$/getEditsForFileRename', async (fileRename: RenameFile) =>
pluginHost.updateImports(fileRename),
);

// This event is triggered by Svelte-Check:
connection.onRequest('$/getDiagnostics', async (params) => {
return await pluginHost.getDiagnostics({ uri: params.uri });
Expand Down
6 changes: 5 additions & 1 deletion packages/svelte-vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Provides syntax highlighting and rich intellisense for Svelte components in VS C

Do you want to use Typescript/SCSS/Less/..? See [Using with preprocessors](using-with-preprocessors).

If you added `"files.associations": {"*.svelte": "html" }` to your VSCode settings, remove it.
If you added `"files.associations": {"*.svelte": "html" }` to your VSCode settings, remove it.

## Features

Expand Down Expand Up @@ -183,3 +183,7 @@ Enable hover info for Svelte (for tags like #if/#each). _Default_: `true`
##### `svelte.plugin.svelte.completions.enable`

Enable autocompletion for Svelte (for tags like #if/#each). _Default_: `true`

##### `svelte.plugin.svelte.rename.enable`

Enable rename functionality (rename svelte files or variables inside svelte files). _Default_: `true`
48 changes: 47 additions & 1 deletion packages/svelte-vscode/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { workspace, ExtensionContext, TextDocument, Position, commands, window } from 'vscode';
import {
workspace,
ExtensionContext,
TextDocument,
Position,
Range,
commands,
window,
WorkspaceEdit,
Uri,
} from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
Expand All @@ -7,6 +17,8 @@ import {
TextDocumentPositionParams,
RequestType,
RevealOutputChannelOn,
WorkspaceEdit as LSWorkspaceEdit,
TextDocumentEdit,
} from 'vscode-languageclient';
import { activateTagClosing } from './html/autoClose';

Expand Down Expand Up @@ -84,6 +96,40 @@ export function activate(context: ExtensionContext) {
window.showInformationMessage('Svelte language server restarted.');
}),
);

workspace.onDidRenameFiles(async (evt) => {
const editsForFileRename = await ls.sendRequest<LSWorkspaceEdit | null>(
'$/getEditsForFileRename',
// Right now files is always an array with a single entry.
// The signature was only designed that way to - maybe, in the future -
// have the possibility to change that. If that ever does, update this.
// In the meantime, just assume it's a single entry and simplify the
// rest of the logic that way.
{
oldUri: evt.files[0].oldUri.toString(true),
newUri: evt.files[0].newUri.toString(true),
},
);
if (!editsForFileRename) {
return;
}

const workspaceEdit = new WorkspaceEdit();
// Renaming a file should only result in edits of existing files
editsForFileRename.documentChanges?.filter(TextDocumentEdit.is).forEach((change) =>
change.edits.forEach((edit) => {
workspaceEdit.replace(
Uri.parse(change.textDocument.uri),
new Range(
new Position(edit.range.start.line, edit.range.start.character),
new Position(edit.range.end.line, edit.range.end.character),
),
edit.newText,
);
}),
);
workspace.applyEdit(workspaceEdit);
});
}

function createLanguageServer(serverOptions: ServerOptions, clientOptions: LanguageClientOptions) {
Expand Down

0 comments on commit 2cd9351

Please sign in to comment.