Skip to content

Commit 37291dc

Browse files
authored
(feat) listen to TS/JS file changes (#766)
#636 Listen to document text changes and manually update TS/JS files in the language server through a dedicated notification message
1 parent 8906eaf commit 37291dc

File tree

8 files changed

+134
-25
lines changed

8 files changed

+134
-25
lines changed

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

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
SignatureHelp,
2222
SignatureHelpContext,
2323
SymbolInformation,
24+
TextDocumentContentChangeEvent,
2425
TextDocumentIdentifier,
2526
TextEdit,
2627
WorkspaceEdit
@@ -424,6 +425,12 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
424425
}
425426
}
426427

428+
updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void {
429+
for (const support of this.plugins) {
430+
support.updateTsOrJsFile?.(fileName, changes);
431+
}
432+
}
433+
427434
private getDocument(uri: string) {
428435
return this.documentsManager.get(uri);
429436
}

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

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { CompletionContext, FileChangeType, SemanticTokens, SignatureHelpContext } from 'vscode-languageserver';
1+
import {
2+
CompletionContext,
3+
FileChangeType,
4+
SemanticTokens,
5+
SignatureHelpContext,
6+
TextDocumentContentChangeEvent
7+
} from 'vscode-languageserver';
28
import {
39
CodeAction,
410
CodeActionContext,
@@ -134,18 +140,15 @@ export interface SignatureHelpProvider {
134140
document: Document,
135141
position: Position,
136142
context: SignatureHelpContext | undefined
137-
): Resolvable<SignatureHelp | null>
143+
): Resolvable<SignatureHelp | null>;
138144
}
139145

140146
export interface SelectionRangeProvider {
141147
getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
142148
}
143149

144150
export interface SemanticTokensProvider {
145-
getSemanticTokens(
146-
textDocument: Document,
147-
range?: Range
148-
): Resolvable<SemanticTokens>
151+
getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens>;
149152
}
150153

151154
export interface OnWatchFileChangesPara {
@@ -157,6 +160,10 @@ export interface OnWatchFileChanges {
157160
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void;
158161
}
159162

163+
export interface UpdateTsOrJsFile {
164+
updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void;
165+
}
166+
160167
type ProviderBase = DiagnosticsProvider &
161168
HoverProvider &
162169
CompletionsProvider &
@@ -187,5 +194,9 @@ export interface LSPProviderConfig {
187194
}
188195

189196
export type Plugin = Partial<
190-
ProviderBase & DefinitionsProvider & OnWatchFileChanges & SelectionRangeProvider
197+
ProviderBase &
198+
DefinitionsProvider &
199+
OnWatchFileChanges &
200+
SelectionRangeProvider &
201+
UpdateTsOrJsFile
191202
>;

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

+19-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { RawSourceMap, SourceMapConsumer } from 'source-map';
22
import svelte2tsx, { IExportedNames, ComponentEvents } from 'svelte2tsx';
33
import ts from 'typescript';
4-
import { Position, Range } from 'vscode-languageserver';
4+
import { Position, Range, TextDocumentContentChangeEvent } from 'vscode-languageserver';
55
import {
66
Document,
77
DocumentMapper,
@@ -282,11 +282,7 @@ export class JSOrTSDocumentSnapshot
282282
scriptKind = getScriptKindFromFileName(this.filePath);
283283
scriptInfo = null;
284284

285-
constructor(
286-
public version: number,
287-
public readonly filePath: string,
288-
private readonly text: string
289-
) {
285+
constructor(public version: number, public readonly filePath: string, private text: string) {
290286
super(pathToUrl(filePath));
291287
}
292288

@@ -317,6 +313,23 @@ export class JSOrTSDocumentSnapshot
317313
destroyFragment() {
318314
// nothing to clean up
319315
}
316+
317+
update(changes: TextDocumentContentChangeEvent[]): void {
318+
for (const change of changes) {
319+
let start = 0;
320+
let end = 0;
321+
if ('range' in change) {
322+
start = this.offsetAt(change.range.start);
323+
end = this.offsetAt(change.range.end);
324+
} else {
325+
end = this.getLength();
326+
}
327+
328+
this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
329+
}
330+
331+
this.version++;
332+
}
320333
}
321334

322335
/**

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import ts from 'typescript';
2-
import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot';
2+
import {
3+
DocumentSnapshot,
4+
JSOrTSDocumentSnapshot,
5+
SvelteSnapshotOptions
6+
} from './DocumentSnapshot';
37
import { Logger } from '../../logger';
8+
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
49

510
export interface TsFilesSpec {
611
include?: readonly string[];
@@ -64,6 +69,19 @@ export class SnapshotManager {
6469
this.set(fileName, newSnapshot);
6570
}
6671

72+
updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void {
73+
if (!this.has(fileName)) {
74+
return;
75+
}
76+
77+
const previousSnapshot = this.get(fileName);
78+
if (!(previousSnapshot instanceof JSOrTSDocumentSnapshot)) {
79+
return;
80+
}
81+
82+
previousSnapshot.update(changes);
83+
}
84+
6785
has(fileName: string) {
6886
return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName);
6987
}

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

+15-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
SelectionRange,
1919
SignatureHelp,
2020
SignatureHelpContext,
21-
SemanticTokens
21+
SemanticTokens,
22+
TextDocumentContentChangeEvent
2223
} from 'vscode-languageserver';
2324
import {
2425
Document,
@@ -45,7 +46,8 @@ import {
4546
SignatureHelpProvider,
4647
UpdateImportsProvider,
4748
OnWatchFileChangesPara,
48-
SemanticTokensProvider
49+
SemanticTokensProvider,
50+
UpdateTsOrJsFile
4951
} from '../interfaces';
5052
import { SnapshotFragment } from './DocumentSnapshot';
5153
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
@@ -80,7 +82,8 @@ export class TypeScriptPlugin
8082
SignatureHelpProvider,
8183
SemanticTokensProvider,
8284
OnWatchFileChanges,
83-
CompletionsProvider<CompletionEntryWithIdentifer> {
85+
CompletionsProvider<CompletionEntryWithIdentifer>,
86+
UpdateTsOrJsFile {
8487
private readonly configManager: LSConfigManager;
8588
private readonly lsAndTsDocResolver: LSAndTSDocResolver;
8689
private readonly completionProvider: CompletionsProviderImpl;
@@ -100,11 +103,7 @@ export class TypeScriptPlugin
100103
workspaceUris: string[]
101104
) {
102105
this.configManager = configManager;
103-
this.lsAndTsDocResolver = new LSAndTSDocResolver(
104-
docManager,
105-
workspaceUris,
106-
configManager
107-
);
106+
this.lsAndTsDocResolver = new LSAndTSDocResolver(docManager, workspaceUris, configManager);
108107
this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver);
109108
this.codeActionsProvider = new CodeActionsProviderImpl(
110109
this.lsAndTsDocResolver,
@@ -386,6 +385,11 @@ export class TypeScriptPlugin
386385
}
387386
}
388387

388+
updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void {
389+
const snapshotManager = this.getSnapshotManager(fileName);
390+
snapshotManager.updateTsOrJsFile(fileName, changes);
391+
}
392+
389393
async getSelectionRange(
390394
document: Document,
391395
position: Position
@@ -398,7 +402,9 @@ export class TypeScriptPlugin
398402
}
399403

400404
async getSignatureHelp(
401-
document: Document, position: Position, context: SignatureHelpContext | undefined
405+
document: Document,
406+
position: Position,
407+
context: SignatureHelpContext | undefined
402408
): Promise<SignatureHelp | null> {
403409
if (!this.featureEnabled('signatureHelp')) {
404410
return null;

packages/language-server/src/server.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export function startServer(options?: LSOptions) {
282282
pluginHost.getDiagnostics.bind(pluginHost)
283283
);
284284

285+
const updateAllDiagnostics = _.debounce(() => diagnosticsManager.updateAll(), 1000);
285286
connection.onDidChangeWatchedFiles((para) => {
286287
const onWatchFileChangesParas = para.changes
287288
.map((change) => ({
@@ -292,9 +293,16 @@ export function startServer(options?: LSOptions) {
292293

293294
pluginHost.onWatchFileChanges(onWatchFileChangesParas);
294295

295-
diagnosticsManager.updateAll();
296+
updateAllDiagnostics();
297+
});
298+
connection.onDidSaveTextDocument(updateAllDiagnostics);
299+
connection.onNotification('$/onDidChangeTsOrJsFile', async (e: any) => {
300+
const path = urlToPath(e.uri);
301+
if (path) {
302+
pluginHost.updateTsOrJsFile(path, e.changes);
303+
}
304+
updateAllDiagnostics();
296305
});
297-
connection.onDidSaveTextDocument(() => diagnosticsManager.updateAll());
298306

299307
connection.languages.semanticTokens.on((evt) => pluginHost.getSemanticTokens(evt.textDocument));
300308
connection.languages.semanticTokens.onRange((evt) =>

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

+24
Original file line numberDiff line numberDiff line change
@@ -444,4 +444,28 @@ describe('TypescriptPlugin', () => {
444444
fs.unlinkSync(addFile);
445445
}
446446
});
447+
448+
it('should update ts/js file after document change', () => {
449+
const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileUpdateOrDelete();
450+
451+
const firstSnapshot = snapshotManager.get(projectJsFile);
452+
const firstVersion = firstSnapshot?.version;
453+
const firstText = firstSnapshot?.getText(0, firstSnapshot?.getLength());
454+
455+
assert.notEqual(firstVersion, INITIAL_VERSION);
456+
457+
plugin.updateTsOrJsFile(projectJsFile, [
458+
{
459+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
460+
text: 'const = "hello world";'
461+
}
462+
]);
463+
const secondSnapshot = snapshotManager.get(projectJsFile);
464+
465+
assert.notEqual(secondSnapshot?.version, firstVersion);
466+
assert.equal(
467+
secondSnapshot?.getText(0, secondSnapshot?.getLength()),
468+
'const = "hello world";' + firstText
469+
);
470+
});
447471
});

packages/svelte-vscode/src/extension.ts

+22
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ export function activate(context: ExtensionContext) {
147147
return ls;
148148
}
149149

150+
addDidChangeTextDocumentListener(getLS);
151+
150152
addRenameFileListener(getLS);
151153

152154
addCompilePreviewCommand(getLS, context);
@@ -231,6 +233,26 @@ export function activate(context: ExtensionContext) {
231233
};
232234
}
233235

236+
function addDidChangeTextDocumentListener(getLS: () => LanguageClient) {
237+
// Only Svelte file changes are automatically notified through the inbuilt LSP
238+
// because the extension says it's only responsible for Svelte files.
239+
// Therefore we need to set this up for TS/JS files manually.
240+
workspace.onDidChangeTextDocument((evt) => {
241+
if (evt.document.languageId === 'typescript' || evt.document.languageId === 'javascript') {
242+
getLS().sendNotification('$/onDidChangeTsOrJsFile', {
243+
uri: evt.document.uri.toString(true),
244+
changes: evt.contentChanges.map((c) => ({
245+
range: {
246+
start: { line: c.range.start.line, character: c.range.start.character },
247+
end: { line: c.range.end.line, character: c.range.end.character }
248+
},
249+
text: c.text
250+
}))
251+
});
252+
}
253+
});
254+
}
255+
234256
function addRenameFileListener(getLS: () => LanguageClient) {
235257
workspace.onDidRenameFiles(async (evt) => {
236258
const oldUri = evt.files[0].oldUri.toString(true);

0 commit comments

Comments
 (0)