Skip to content

Commit db8eb76

Browse files
authored
Watch js/ts file add (#639)
* read dir again to update project file when adding * fix fileSpec undefined * cleanup * short when empty include * do a full directory search for added files it's slower but easier to implement * test
1 parent 829cf87 commit db8eb76

File tree

7 files changed

+156
-43
lines changed

7 files changed

+156
-43
lines changed

packages/language-server/src/plugins/PluginHost.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
SymbolInformation,
1515
TextDocumentIdentifier,
1616
TextEdit,
17-
FileChangeType,
1817
CompletionItem,
1918
CompletionContext,
2019
WorkspaceEdit,
@@ -31,7 +30,8 @@ import {
3130
OnWatchFileChanges,
3231
AppCompletionItem,
3332
FileRename,
34-
LSPProviderConfig
33+
LSPProviderConfig,
34+
OnWatchFileChangesPara
3535
} from './interfaces';
3636
import { Logger } from '../logger';
3737
import { regexLastIndexOf } from '../utils';
@@ -384,9 +384,9 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
384384
}
385385
}
386386

387-
onWatchFileChanges(fileName: string, changeType: FileChangeType): void {
387+
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void {
388388
for (const support of this.plugins) {
389-
support.onWatchFileChanges?.(fileName, changeType);
389+
support.onWatchFileChanges?.(onWatchFileChangesParas);
390390
}
391391
}
392392

packages/language-server/src/plugins/interfaces.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,13 @@ export interface SelectionRangeProvider {
132132
getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
133133
}
134134

135+
export interface OnWatchFileChangesPara {
136+
fileName: string;
137+
changeType: FileChangeType;
138+
}
139+
135140
export interface OnWatchFileChanges {
136-
onWatchFileChanges(fileName: string, changeType: FileChangeType): void;
141+
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void;
137142
}
138143

139144
type ProviderBase = DiagnosticsProvider &

packages/language-server/src/plugins/typescript/SnapshotManager.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,49 @@
1+
import ts from 'typescript';
12
import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot';
23
import { Logger } from '../../logger';
34

5+
export interface TsFilesSpec {
6+
include?: readonly string[];
7+
exclude?: readonly string[];
8+
}
9+
410
export class SnapshotManager {
511
private documents: Map<string, DocumentSnapshot> = new Map();
612
private lastLogged = new Date(new Date().getTime() - 60_001);
713

8-
constructor(private projectFiles: string[]) {}
14+
private readonly watchExtensions = [
15+
ts.Extension.Dts,
16+
ts.Extension.Js,
17+
ts.Extension.Jsx,
18+
ts.Extension.Ts,
19+
ts.Extension.Tsx,
20+
ts.Extension.Json
21+
];
22+
23+
constructor(
24+
private projectFiles: string[],
25+
private fileSpec: TsFilesSpec,
26+
private workspaceRoot: string
27+
) {}
28+
29+
updateProjectFiles() {
30+
const { include, exclude } = this.fileSpec;
31+
32+
// Since we default to not include anything,
33+
// just don't waste time on this
34+
if (include?.length === 0) {
35+
return;
36+
}
37+
38+
const projectFiles = ts.sys.readDirectory(
39+
this.workspaceRoot,
40+
this.watchExtensions,
41+
exclude,
42+
include
43+
);
44+
45+
this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
46+
}
947

1048
updateByFileName(fileName: string, options: SvelteSnapshotOptions) {
1149
if (!this.has(fileName)) {

packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ import {
3939
OnWatchFileChanges,
4040
RenameProvider,
4141
SelectionRangeProvider,
42-
UpdateImportsProvider
42+
UpdateImportsProvider,
43+
OnWatchFileChangesPara
4344
} from '../interfaces';
4445
import { SnapshotFragment } from './DocumentSnapshot';
4546
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
@@ -56,6 +57,7 @@ import { convertToLocationRange, getScriptKindFromFileName, symbolKindFromString
5657
import { getDirectiveCommentCompletions } from './features/getDirectiveCommentCompletions';
5758
import { FindReferencesProviderImpl } from './features/FindReferencesProvider';
5859
import { SelectionRangeProviderImpl } from './features/SelectionRangeProvider';
60+
import { SnapshotManager } from './SnapshotManager';
5961

6062
export class TypeScriptPlugin
6163
implements
@@ -334,24 +336,32 @@ export class TypeScriptPlugin
334336
return this.findReferencesProvider.findReferences(document, position, context);
335337
}
336338

337-
onWatchFileChanges(fileName: string, changeType: FileChangeType) {
338-
const scriptKind = getScriptKindFromFileName(fileName);
339+
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]) {
340+
const doneUpdateProjectFiles = new Set<SnapshotManager>();
339341

340-
if (scriptKind === ts.ScriptKind.Unknown) {
341-
// We don't deal with svelte files here
342-
return;
343-
}
342+
for (const { fileName, changeType } of onWatchFileChangesParas) {
343+
const scriptKind = getScriptKindFromFileName(fileName);
344344

345-
const snapshotManager = this.getSnapshotManager(fileName);
345+
if (scriptKind === ts.ScriptKind.Unknown) {
346+
// We don't deal with svelte files here
347+
continue;
348+
}
346349

347-
if (changeType === FileChangeType.Deleted) {
348-
snapshotManager.delete(fileName);
349-
return;
350-
}
350+
const snapshotManager = this.getSnapshotManager(fileName);
351+
if (changeType === FileChangeType.Created) {
352+
if (!doneUpdateProjectFiles.has(snapshotManager)) {
353+
snapshotManager.updateProjectFiles();
354+
doneUpdateProjectFiles.add(snapshotManager);
355+
}
356+
} else if (changeType === FileChangeType.Deleted) {
357+
snapshotManager.delete(fileName);
358+
return;
359+
}
351360

352-
// Since the options parameter only applies to svelte snapshots, and this is not
353-
// a svelte file, we can just set it to false without having any effect.
354-
snapshotManager.updateByFileName(fileName, { strictMode: false });
361+
// Since the options parameter only applies to svelte snapshots, and this is not
362+
// a svelte file, we can just set it to false without having any effect.
363+
snapshotManager.updateByFileName(fileName, { strictMode: false });
364+
}
355365
}
356366

357367
async getSelectionRange(

packages/language-server/src/plugins/typescript/service.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ export function createLanguageService(
6060
): LanguageServiceContainer {
6161
const workspacePath = tsconfigPath ? dirname(tsconfigPath) : '';
6262

63-
const { compilerOptions, files } = getCompilerOptionsAndProjectFiles();
64-
const snapshotManager = new SnapshotManager(files);
63+
const { options: compilerOptions, fileNames: files, raw } = getParsedConfig();
64+
// raw is the tsconfig merged with extending config
65+
// see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537
66+
const snapshotManager = new SnapshotManager(files, raw, workspacePath || process.cwd());
6567

6668
const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions);
6769

@@ -160,7 +162,7 @@ export function createLanguageService(
160162
return doc;
161163
}
162164

163-
function getCompilerOptionsAndProjectFiles() {
165+
function getParsedConfig() {
164166
const forcedCompilerOptions: ts.CompilerOptions = {
165167
allowNonTsExtensions: true,
166168
target: ts.ScriptTarget.Latest,
@@ -199,7 +201,6 @@ export function createLanguageService(
199201
[{ extension: 'svelte', isMixedContent: false, scriptKind: ts.ScriptKind.TSX }]
200202
);
201203

202-
const files = parsedConfig.fileNames;
203204
const compilerOptions: ts.CompilerOptions = {
204205
...parsedConfig.options,
205206
...forcedCompilerOptions
@@ -223,7 +224,10 @@ export function createLanguageService(
223224
}
224225
}
225226

226-
return { compilerOptions, files };
227+
return {
228+
...parsedConfig,
229+
options: compilerOptions
230+
};
227231
}
228232

229233
/**

packages/language-server/src/server.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
HTMLPlugin,
2828
PluginHost,
2929
SveltePlugin,
30-
TypeScriptPlugin
30+
TypeScriptPlugin,
31+
OnWatchFileChangesPara
3132
} from './plugins';
3233
import { urlToPath } from './utils';
3334

@@ -273,12 +274,12 @@ export function startServer(options?: LSOptions) {
273274
);
274275

275276
connection.onDidChangeWatchedFiles((para) => {
276-
for (const change of para.changes) {
277-
const filename = urlToPath(change.uri);
278-
if (filename) {
279-
pluginHost.onWatchFileChanges(filename, change.type);
280-
}
281-
}
277+
const onWatchFileChangesParas = para.changes.map((change) => ({
278+
fileName: urlToPath(change.uri),
279+
changeType: change.type
280+
})).filter((change): change is OnWatchFileChangesPara => !!change.fileName);
281+
282+
pluginHost.onWatchFileChanges(onWatchFileChangesParas);
282283

283284
diagnosticsManager.updateAll();
284285
});

packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as assert from 'assert';
22
import * as path from 'path';
33
import ts from 'typescript';
4+
import fs from 'fs';
45
import { FileChangeType, Position, Range } from 'vscode-languageserver';
56
import { Document, DocumentManager } from '../../../src/lib/documents';
67
import { LSConfigManager } from '../../../src/ls-config';
@@ -348,14 +349,36 @@ describe('TypescriptPlugin', () => {
348349

349350
const setupForOnWatchedFileChanges = () => {
350351
const { plugin, document } = setup('empty.svelte');
351-
const filePath = document.getFilePath()!;
352-
const snapshotManager = plugin.getSnapshotManager(filePath);
352+
const targetSvelteFile = document.getFilePath()!;
353+
const snapshotManager = plugin.getSnapshotManager(targetSvelteFile);
353354

354-
// make it the same style of path delimiter as vscode's request
355-
const projectJsFile =
356-
urlToPath(pathToUrl(path.join(path.dirname(filePath), 'documentation.ts'))) ?? '';
355+
return {
356+
snapshotManager,
357+
plugin,
358+
targetSvelteFile
359+
};
360+
};
361+
362+
/**
363+
* make it the same style of path delimiter as vscode's request
364+
*/
365+
const normalizeWatchFilePath = (path: string) => {
366+
return urlToPath(pathToUrl(path)) ?? '';
367+
};
357368

358-
plugin.onWatchFileChanges(projectJsFile, FileChangeType.Changed);
369+
const setupForOnWatchedFileUpdateOrDelete = () => {
370+
const { plugin, snapshotManager, targetSvelteFile } = setupForOnWatchedFileChanges();
371+
372+
const projectJsFile = normalizeWatchFilePath(
373+
path.join(path.dirname(targetSvelteFile), 'documentation.ts')
374+
);
375+
376+
plugin.onWatchFileChanges([
377+
{
378+
fileName: projectJsFile,
379+
changeType: FileChangeType.Changed
380+
}
381+
]);
359382

360383
return {
361384
snapshotManager,
@@ -365,28 +388,60 @@ describe('TypescriptPlugin', () => {
365388
};
366389

367390
it('bumps snapshot version when watched file changes', () => {
368-
const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileChanges();
391+
const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileUpdateOrDelete();
369392

370393
const firstSnapshot = snapshotManager.get(projectJsFile);
371394
const firstVersion = firstSnapshot?.version;
372395

373396
assert.notEqual(firstVersion, INITIAL_VERSION);
374397

375-
plugin.onWatchFileChanges(projectJsFile, FileChangeType.Changed);
398+
plugin.onWatchFileChanges([
399+
{
400+
fileName: projectJsFile,
401+
changeType: FileChangeType.Changed
402+
}
403+
]);
376404
const secondSnapshot = snapshotManager.get(projectJsFile);
377405

378406
assert.notEqual(secondSnapshot?.version, firstVersion);
379407
});
380408

381409
it('should delete snapshot cache when file delete', () => {
382-
const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileChanges();
410+
const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileUpdateOrDelete();
383411

384412
const firstSnapshot = snapshotManager.get(projectJsFile);
385413
assert.notEqual(firstSnapshot, undefined);
386414

387-
plugin.onWatchFileChanges(projectJsFile, FileChangeType.Deleted);
415+
plugin.onWatchFileChanges([
416+
{
417+
fileName: projectJsFile,
418+
changeType: FileChangeType.Deleted
419+
}
420+
]);
388421
const secondSnapshot = snapshotManager.get(projectJsFile);
389422

390423
assert.equal(secondSnapshot, undefined);
391424
});
425+
426+
it('should add snapshot when project file added', () => {
427+
const { snapshotManager, plugin, targetSvelteFile } = setupForOnWatchedFileChanges();
428+
const addFile = path.join(path.dirname(targetSvelteFile), 'foo.ts');
429+
const normalizedAddFilePath = normalizeWatchFilePath(addFile);
430+
431+
try {
432+
fs.writeFileSync(addFile, 'export function abc() {}');
433+
assert.equal(snapshotManager.has(normalizedAddFilePath), false);
434+
435+
plugin.onWatchFileChanges([
436+
{
437+
fileName: normalizedAddFilePath,
438+
changeType: FileChangeType.Created
439+
}
440+
]);
441+
442+
assert.equal(snapshotManager.has(normalizedAddFilePath), true);
443+
} finally {
444+
fs.unlinkSync(addFile);
445+
}
446+
});
392447
});

0 commit comments

Comments
 (0)