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
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import ts from 'typescript';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
import { Document, DocumentManager } from '../../lib/documents';
import { LSConfigManager } from '../../ls-config';
import { debounceSameArg, pathToUrl } from '../../utils';
import { debounceSameArg, normalizePath, pathToUrl } from '../../utils';
import { DocumentSnapshot, SvelteDocumentSnapshot } from './DocumentSnapshot';
import {
getService,
getServiceForTsconfig,
hasServiceForFile,
forAllServices,
LanguageServiceContainer,
LanguageServiceDocumentContext
} from './service';
import { SnapshotManager } from './SnapshotManager';
import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager';

export class LSAndTSDocResolver {
/**
Expand Down Expand Up @@ -59,10 +60,13 @@ export class LSAndTSDocResolver {
return document;
};

private globalSnapshotsManager = new GlobalSnapshotsManager();

private get lsDocumentContext(): LanguageServiceDocumentContext {
return {
createDocument: this.createDocument,
transformOnTemplateError: !this.tsconfigPath
transformOnTemplateError: !this.tsconfigPath,
globalSnapshotsManager: this.globalSnapshotsManager
};
}

Expand All @@ -82,6 +86,11 @@ export class LSAndTSDocResolver {
return { tsDoc, lang, userPreferences };
}

/**
* Retrieves and updates the snapshot for the given document or path from
* the ts service it primarely belongs into.
* The update is mirrored in all other services, too.
*/
async getSnapshot(document: Document): Promise<SvelteDocumentSnapshot>;
async getSnapshot(pathOrDoc: string | Document): Promise<DocumentSnapshot>;
async getSnapshot(pathOrDoc: string | Document) {
Expand All @@ -90,21 +99,49 @@ export class LSAndTSDocResolver {
return tsService.updateSnapshot(pathOrDoc);
}

/**
* Updates snapshot path in all existing ts services and retrieves snapshot
*/
async updateSnapshotPath(oldPath: string, newPath: string): Promise<DocumentSnapshot> {
await this.deleteSnapshot(oldPath);
return this.getSnapshot(newPath);
}

/**
* Deletes snapshot in all existing ts services
*/
async deleteSnapshot(filePath: string) {
if (!hasServiceForFile(filePath, this.workspaceUris)) {
// Don't initialize a service for a file that should be deleted
return;
}

(await this.getTSService(filePath)).deleteSnapshot(filePath);
await forAllServices((service) => service.deleteSnapshot(filePath));
this.docManager.releaseDocument(pathToUrl(filePath));
}

/**
* Updates project files in all existing ts services
*/
async updateProjectFiles() {
await forAllServices((service) => service.updateProjectFiles());
}

/**
* Updates file in all ts services where it exists
*/
async updateExistingTsOrJsFile(
path: string,
changes?: TextDocumentContentChangeEvent[]
): Promise<void> {
path = normalizePath(path);
// Only update once because all snapshots are shared between
// services. Since we don't have a current version of TS/JS
// files, the operation wouldn't be idempotent.
let didUpdate = false;
await forAllServices((service) => {
if (service.hasFile(path) && !didUpdate) {
didUpdate = true;
service.updateTsOrJsFile(path, changes);
}
});
}

/**
* @internal Public for tests only
*/
Expand Down
155 changes: 113 additions & 42 deletions packages/language-server/src/plugins/typescript/SnapshotManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,72 @@ import ts from 'typescript';
import { DocumentSnapshot, JSOrTSDocumentSnapshot } from './DocumentSnapshot';
import { Logger } from '../../logger';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
import { normalizePath } from '../../utils';
import { EventEmitter } from 'events';

/**
* Every snapshot corresponds to a unique file on disk.
* A snapshot can be part of multiple projects, but for a given file path
* there can be only one snapshot.
*/
export class GlobalSnapshotsManager {
private emitter = new EventEmitter();
private documents = new Map<string, DocumentSnapshot>();

get(fileName: string) {
fileName = normalizePath(fileName);
return this.documents.get(fileName);
}

set(fileName: string, document: DocumentSnapshot) {
fileName = normalizePath(fileName);
const prev = this.get(fileName);
if (prev) {
prev.destroyFragment();
}

this.documents.set(fileName, document);
this.emitter.emit('change', fileName, document);
}

delete(fileName: string) {
fileName = normalizePath(fileName);
this.documents.delete(fileName);
this.emitter.emit('change', fileName, undefined);
}

updateTsOrJsFile(
fileName: string,
changes?: TextDocumentContentChangeEvent[]
): JSOrTSDocumentSnapshot | undefined {
fileName = normalizePath(fileName);
const previousSnapshot = this.get(fileName);

if (changes) {
if (!(previousSnapshot instanceof JSOrTSDocumentSnapshot)) {
return;
}
previousSnapshot.update(changes);
return previousSnapshot;
} else {
const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName);

if (previousSnapshot) {
newSnapshot.version = previousSnapshot.version + 1;
} else {
// ensure it's greater than initial version
// so that ts server picks up the change
newSnapshot.version += 1;
}
this.set(fileName, newSnapshot);
return newSnapshot;
}
}

onChange(listener: (fileName: string, newDocument: DocumentSnapshot | undefined) => void) {
this.emitter.on('change', listener);
}
}

export interface TsFilesSpec {
include?: readonly string[];
Expand All @@ -12,7 +78,7 @@ export interface TsFilesSpec {
* Should only be used by `service.ts`
*/
export class SnapshotManager {
private documents: Map<string, DocumentSnapshot> = new Map();
private documents = new Map<string, DocumentSnapshot>();
private lastLogged = new Date(new Date().getTime() - 60_001);

private readonly watchExtensions = [
Expand All @@ -25,12 +91,26 @@ export class SnapshotManager {
];

constructor(
private globalSnapshotsManager: GlobalSnapshotsManager,
private projectFiles: string[],
private fileSpec: TsFilesSpec,
private workspaceRoot: string
) {}
) {
this.globalSnapshotsManager.onChange((fileName, document) => {
// Only delete/update snapshots, don't add new ones,
// as they could be from another TS service and this
// snapshot manager can't reach this file.
// For these, instead wait on a `get` method invocation
// and set them "manually" in the set/update methods.
if (!document) {
this.documents.delete(fileName);
} else if (this.documents.has(fileName)) {
this.documents.set(fileName, document);
}
});
}

updateProjectFiles() {
updateProjectFiles(): void {
const { include, exclude } = this.fileSpec;

// Since we default to not include anything,
Expand All @@ -39,67 +119,58 @@ export class SnapshotManager {
return;
}

const projectFiles = ts.sys.readDirectory(
this.workspaceRoot,
this.watchExtensions,
exclude,
include
);
const projectFiles = ts.sys
.readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include)
.map(normalizePath);

this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
}

updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void {
const previousSnapshot = this.get(fileName);

if (changes) {
if (!(previousSnapshot instanceof JSOrTSDocumentSnapshot)) {
return;
}
previousSnapshot.update(changes);
} else {
const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName);

if (previousSnapshot) {
newSnapshot.version = previousSnapshot.version + 1;
} else {
// ensure it's greater than initial version
// so that ts server picks up the change
newSnapshot.version += 1;
}
this.set(fileName, newSnapshot);
const snapshot = this.globalSnapshotsManager.updateTsOrJsFile(fileName, changes);
// This isn't duplicated logic to the listener, because this could
// be a new snapshot which the listener wouldn't add.
if (snapshot) {
this.documents.set(normalizePath(fileName), snapshot);
}
}

has(fileName: string) {
has(fileName: string): boolean {
fileName = normalizePath(fileName);
return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName);
}

set(fileName: string, snapshot: DocumentSnapshot) {
const prev = this.get(fileName);
if (prev) {
prev.destroyFragment();
}

set(fileName: string, snapshot: DocumentSnapshot): void {
this.globalSnapshotsManager.set(fileName, snapshot);
// This isn't duplicated logic to the listener, because this could
// be a new snapshot which the listener wouldn't add.
this.documents.set(normalizePath(fileName), snapshot);
this.logStatistics();

return this.documents.set(fileName, snapshot);
}

get(fileName: string) {
return this.documents.get(fileName);
get(fileName: string): DocumentSnapshot | undefined {
fileName = normalizePath(fileName);
let snapshot = this.documents.get(fileName);
if (!snapshot) {
snapshot = this.globalSnapshotsManager.get(fileName);
if (snapshot) {
this.documents.set(fileName, snapshot);
}
}
return snapshot;
}

delete(fileName: string) {
delete(fileName: string): void {
fileName = normalizePath(fileName);
this.projectFiles = this.projectFiles.filter((s) => s !== fileName);
return this.documents.delete(fileName);
this.globalSnapshotsManager.delete(fileName);
}

getFileNames() {
getFileNames(): string[] {
return Array.from(this.documents.keys());
}

getProjectFileNames() {
getProjectFileNames(): string[] {
return [...this.projectFiles];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
import { UpdateImportsProviderImpl } from './features/UpdateImportsProvider';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
import { LSAndTSDocResolver } from './LSAndTSDocResolver';
import { LanguageServiceContainer } from './service';
import { ignoredBuildDirectories } from './SnapshotManager';
import { convertToLocationRange, getScriptKindFromFileName, symbolKindFromString } from './utils';

Expand Down Expand Up @@ -371,7 +370,7 @@ export class TypeScriptPlugin
}

async onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): Promise<void> {
const doneUpdateProjectFiles = new Set<LanguageServiceContainer>();
let doneUpdateProjectFiles = false;

for (const { fileName, changeType } of onWatchFileChangesParas) {
const pathParts = fileName.split(/\/|\\/);
Expand All @@ -386,19 +385,13 @@ export class TypeScriptPlugin
continue;
}

const tsService = await this.lsAndTsDocResolver.getTSService(fileName);
if (changeType === FileChangeType.Created) {
if (!doneUpdateProjectFiles.has(tsService)) {
tsService.updateProjectFiles();
doneUpdateProjectFiles.add(tsService);
}
if (changeType === FileChangeType.Created && !doneUpdateProjectFiles) {
doneUpdateProjectFiles = true;
await this.lsAndTsDocResolver.updateProjectFiles();
} else if (changeType === FileChangeType.Deleted) {
tsService.deleteSnapshot(fileName);
} else if (tsService.hasFile(fileName)) {
// Only allow existing files to be update
// Otherwise, new files would still get loaded
// into snapshot manager after update
tsService.updateTsOrJsFile(fileName);
await this.lsAndTsDocResolver.deleteSnapshot(fileName);
} else {
await this.lsAndTsDocResolver.updateExistingTsOrJsFile(fileName);
}
}
}
Expand All @@ -407,8 +400,7 @@ export class TypeScriptPlugin
fileName: string,
changes: TextDocumentContentChangeEvent[]
): Promise<void> {
const snapshotManager = await this.getSnapshotManager(fileName);
snapshotManager.updateTsOrJsFile(fileName, changes);
await this.lsAndTsDocResolver.updateExistingTsOrJsFile(fileName, changes);
}

async getSelectionRange(
Expand Down
Loading