Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"prettier-plugin-svelte": "1.1.0",
"source-map": "^0.7.3",
"svelte": "3.19.2",
"svelte2tsx": "~0.1.4",
"svelte-preprocess": "~3.7.4",
"svelte2tsx": "~0.1.4",
"typescript": "*",
"vscode-css-languageservice": "4.1.0",
"vscode-emmet-helper": "1.2.17",
Expand Down
20 changes: 15 additions & 5 deletions packages/language-server/src/lib/documents/DocumentManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { EventEmitter } from 'events';
import {
TextDocumentContentChangeEvent,
TextDocumentIdentifier,
TextDocumentItem,
VersionedTextDocumentIdentifier,
} from 'vscode-languageserver';
Expand All @@ -16,6 +15,7 @@ export class DocumentManager {
private emitter = new EventEmitter();
public documents: Map<string, Document> = new Map();
public locked = new Set<string>();
public deleteCandidates = new Set<string>();

constructor(private createDocument: (textDocument: TextDocumentItem) => Document) {}

Expand All @@ -39,17 +39,27 @@ export class DocumentManager {
this.locked.add(uri);
}

closeDocument(textDocument: TextDocumentIdentifier) {
const document = this.documents.get(textDocument.uri);
releaseDocument(uri: string): void {
this.locked.delete(uri);
if (this.deleteCandidates.has(uri)) {
this.deleteCandidates.delete(uri);
this.closeDocument(uri);
}
}

closeDocument(uri: string) {
const document = this.documents.get(uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}

this.notify('documentClose', document);

// Some plugin may prevent a document from actually being closed.
if (!this.locked.has(textDocument.uri)) {
this.documents.delete(textDocument.uri);
if (!this.locked.has(uri)) {
this.documents.delete(uri);
} else {
this.deleteCandidates.add(uri);
}
}

Expand Down
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,13 +1,25 @@
import { DocumentManager, Document } from '../../lib/documents';
import { pathToUrl } from '../../utils';
import { getLanguageServiceForDocument } from './service';
import ts from 'typescript';
import { Document, DocumentManager } from '../../lib/documents';
import { debounceSameArg, pathToUrl } from '../../utils';
import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot';
import { findTsConfigPath } from './utils';
import { getLanguageServiceForDocument, getLanguageServiceForPath, getService } from './service';
import { SnapshotManager } from './SnapshotManager';
import ts from 'typescript';
import { findTsConfigPath } from './utils';

export class LSAndTSDocResolver {
constructor(private readonly docManager: DocumentManager) {}
constructor(private readonly docManager: DocumentManager) {
docManager.on(
'documentChange',
debounceSameArg(
async (document: Document) => {
// This refreshes the document in the ts language service
this.getLSAndTSDoc(document);
},
(newDoc, prevDoc) => newDoc.uri === prevDoc?.uri,
1000,
),
);
}

/**
* Create a svelte document -> should only be invoked with svelte files.
Expand All @@ -24,6 +36,10 @@ export class LSAndTSDocResolver {
return document;
};

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

getLSAndTSDoc(
document: Document,
): {
Expand Down Expand Up @@ -53,6 +69,16 @@ export class LSAndTSDocResolver {
return tsDoc;
}

updateSnapshotPath(oldPath: string, newPath: string): DocumentSnapshot {
this.deleteSnapshot(oldPath);
return this.getSnapshot(newPath);
}

deleteSnapshot(filePath: string) {
getService(filePath, this.createDocument).deleteDocument(filePath);
this.docManager.releaseDocument(pathToUrl(filePath));
}

getSnapshotManager(fileName: string): SnapshotManager {
const tsconfigPath = findTsConfigPath(fileName);
const snapshotManager = SnapshotManager.getFromTsConfigPath(tsconfigPath);
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,72 @@
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, {}, {});

this.lsAndTsDocResolver.updateSnapshotPath(oldPath, newPath);
const updateImportsChanges = fileChanges
// Assumption: Updating imports will not create new files, and to make sure just filter those out
// who - for whatever reason - might be new ones.
.filter((change) => !change.isNewFile || change.fileName === oldPath)
// The language service might want to do edits to the old path, not the new path -> rewire it.
// If there is a better solution for this, please file a PR :)
.map((change) => {
change.fileName = change.fileName.replace(oldPath, newPath);
return change;
});

const docs = new Map<string, SnapshotFragment>();
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) => {
const range = mapRangeToOriginal(
fragment!,
convertRange(fragment!, edit.span),
);
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: 14 additions & 1 deletion packages/language-server/src/plugins/typescript/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ class ModuleResolutionCache {
this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
}

/**
* Deletes module from cache. Call this if a file was deleted.
* @param resolvedModuleName full path of the module
*/
delete(resolvedModuleName: string): void {
this.cache.forEach((val, key) => {
if (val.resolvedFileName === resolvedModuleName) {
this.cache.delete(key);
}
});
}

private getKey(moduleName: string, containingFile: string) {
return containingFile + ':::' + ensureRealSvelteFilePath(moduleName);
}
Expand Down Expand Up @@ -59,14 +71,15 @@ export function createSvelteModuleLoader(
return {
fileExists: svelteSys.fileExists,
readFile: svelteSys.readFile,
deleteFromModuleCache: (path: string) => moduleCache.delete(path),
resolveModuleNames,
};

function resolveModuleNames(
moduleNames: string[],
containingFile: string,
): (ts.ResolvedModule | undefined)[] {
return moduleNames.map(moduleName => {
return moduleNames.map((moduleName) => {
const cachedModule = moduleCache.get(moduleName, containingFile);
if (cachedModule) {
return cachedModule;
Expand Down
Loading