Skip to content

Commit b5ea283

Browse files
authored
Watch changes to workspace in language server (#770)
Signed-off-by: Mark Sujew <mark.sujew@typefox.io>
1 parent 2338557 commit b5ea283

19 files changed

Lines changed: 210 additions & 166 deletions

packages/language/src/config/lib-expander.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
} from "../workspace/file-system-provider";
1818
import { resolveLibUri } from "./path-resolver";
1919
import {
20-
isLibsDir,
2120
JsonItem,
2221
LibsDDEntry,
2322
LibsDirEntry,
@@ -103,8 +102,6 @@ export interface ExpandedLib {
103102
*/
104103
export interface ExpandedGroup {
105104
libs: LibsEntry[];
106-
/** Lower-noise lookup for `getProcessGroupConfigFromLib`. */
107-
libsSet: Set<string>;
108105
/** Lib items that didn't resolve to anything on disk. */
109106
unresolved: JsonItem<string>[];
110107
}
@@ -140,10 +137,7 @@ export async function expandGroup(
140137
}
141138

142139
const libs = Array.from(libsByKey.values()).sort(compareByDepthThenName);
143-
const libsSet = new Set<string>(
144-
libs.filter(isLibsDir).map((e) => e.path.replace(/\\/g, "/")),
145-
);
146-
return { libs, libsSet, unresolved };
140+
return { libs, unresolved };
147141
}
148142

149143
/**
@@ -211,6 +205,7 @@ async function expandDirectoryTree(
211205
rootUri: URI,
212206
fs: FileSystemProvider,
213207
): Promise<LibsDirEntry[]> {
208+
rootLib = UriUtils.normalizePath(rootLib);
214209
const entries: LibsDirEntry[] = [];
215210
const queue: { lib: string; uri: URI }[] = [{ lib: rootLib, uri: rootUri }];
216211

packages/language/src/config/path-resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function resolveLibUri(
2727
workspace: URI,
2828
scheme?: string,
2929
): URI {
30-
lib = lib.replace(/\\/g, "/");
30+
lib = UriUtils.normalizePath(lib);
3131
const pathType = UriUtils.computePathType(lib);
3232
if (pathType === UriUtils.PathType.URI) {
3333
return UriUtils.parse(lib);

packages/language/src/config/schema.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,6 @@ export interface GroupRecord extends ProcessGroup {
103103
* directory. Sorted shallow-first.
104104
*/
105105
computedLibs: LibsEntry[];
106-
107-
/**
108-
* Lower-noise lookup for `getProcessGroupConfigFromLib` — only
109-
* directory libs, normalized to forward slashes.
110-
*/
111-
computedLibsSet: Set<string>;
112106
}
113107

114108
/**

packages/language/src/language-server/code-actions/apply-quick-fixes.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import {
1616
Diagnostic,
1717
TextEdit,
1818
} from "vscode-languageserver-types";
19-
import { PluginConfigUnresolvedLibData } from "../../workspace/plugin-configuration-provider";
19+
import {
20+
isLibsDir,
21+
PluginConfigUnresolvedLibData,
22+
} from "../../workspace/plugin-configuration-provider";
2023
import { WorkspaceContext } from "../../workspace/workspace-context";
2124

2225
import { Commands, PluginConfiguration } from "../constants";
@@ -72,7 +75,10 @@ export async function quickFixResolveInclude(
7275
unresolvedFilePath,
7376
workspaceFolderUri,
7477
);
75-
if (!parentFolder || procGrpsConfig.computedLibsSet.has(parentFolder)) {
78+
const isExistingLib = procGrpsConfig.computedLibs.some(
79+
(lib) => isLibsDir(lib) && lib.path === parentFolder,
80+
);
81+
if (!parentFolder || isExistingLib) {
7682
return undefined;
7783
}
7884

packages/language/src/language-server/connection-handler.ts

Lines changed: 124 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import {
1616
import {
1717
CancellationToken,
1818
Connection,
19+
DidChangeWatchedFilesNotification,
1920
DocumentHighlight,
21+
FileChangeType,
22+
FileEvent,
2023
TextDocumentSyncKind,
2124
} from "vscode-languageserver";
2225
import { URI, UriUtils } from "../utils/uri";
@@ -148,6 +151,15 @@ export function startLanguageServer(
148151
};
149152
});
150153
connection.onInitialized(async () => {
154+
connection.client.register(DidChangeWatchedFilesNotification.type, {
155+
watchers: [
156+
{
157+
// Watch all changes
158+
// Includes changes to any directory or file in the workspace
159+
globPattern: "**/*",
160+
},
161+
],
162+
});
151163
const promises: Promise<void>[] = [];
152164
for (const folder of folders) {
153165
promises.push(
@@ -408,21 +420,123 @@ export function startLanguageServer(
408420
);
409421
});
410422
});
423+
424+
async function pluginConfigChanged() {
425+
// handle changes to the .pliplugin config folder's contents
426+
const diagnosticsByUri = await workspace.config.reloadConfigurations();
427+
publishPluginConfigDiagnostics(diagnosticsByUri);
428+
429+
// reindex reachable compilation units
430+
await compilationUnitHandler.reindex(connection, CancellationToken.None);
431+
432+
// refresh semantic tokens so syntax coloring updates immediately
433+
connection.languages.semanticTokens.refresh();
434+
}
435+
436+
// A structural change to a lib folder (a file/dir created or deleted
437+
// inside a lib, or a directory removed that is or contains a lib)
438+
// invalidates the computed lib index, so the libs must be re-expanded.
439+
// Note: Simple file edits are not considered structural changes,
440+
// so they don't trigger a reindex.
441+
function changeAffectsLibs(changes: readonly FileEvent[]): boolean {
442+
const libDirs = workspace.config.getLibDirectoryUris();
443+
if (libDirs.length === 0) {
444+
return false;
445+
}
446+
for (const change of changes) {
447+
if (change.type === FileChangeType.Changed) {
448+
continue;
449+
}
450+
const changeUri = UriUtils.toUri(change.uri);
451+
for (const libDir of libDirs) {
452+
// Change inside a lib (create/delete of a member) OR the change
453+
// is/contains the lib dir itself (whole lib folder gone).
454+
if (
455+
UriUtils.contains(libDir, changeUri) ||
456+
UriUtils.contains(changeUri, libDir)
457+
) {
458+
return true;
459+
}
460+
}
461+
}
462+
return false;
463+
}
411464
onNotification(
412465
connection,
413-
Messages.WorkspaceDidChangePluginConfigNotification,
466+
Messages.OnDidChangePluginConfigSettingsNotification,
414467
async () => {
415-
// handle changes to the .pliplugin config folder's contents
416-
const diagnosticsByUri = await workspace.config.reloadConfigurations();
417-
publishPluginConfigDiagnostics(diagnosticsByUri);
418-
419-
// reindex reachable compilation units
420-
await compilationUnitHandler.reindex(connection, CancellationToken.None);
421-
422-
// refresh semantic tokens so syntax coloring updates immediately
423-
connection.languages.semanticTokens.refresh();
468+
// Handle changes to the pli.pgm_conf and pli.proc_grps settings in vscode
469+
await pluginConfigChanged();
424470
},
425471
);
472+
connection.onDidChangeWatchedFiles(async (params) => {
473+
// First thing: Figure out whether any of the changed files are plugin config files
474+
// If they are, we need to reindex the workspace anyway - no need to check for other changes.
475+
function isPluginConfigFile(uri: string): boolean {
476+
return (
477+
uri.endsWith("/.pliplugin") ||
478+
uri.endsWith("/.pliplugin/pgm_conf.json") ||
479+
uri.endsWith("/.pliplugin/proc_grps.json")
480+
);
481+
}
482+
// Since we cannot know whether a created directory is a lib in advance
483+
// We always have to reindex if a directory is created, since it may contain a lib.
484+
let directoryCreated = false;
485+
for (const change of params.changes) {
486+
if (change.type === FileChangeType.Created) {
487+
// Try to stat the changed file to see if it's a directory.
488+
const uri = UriUtils.toUri(change.uri);
489+
try {
490+
const stats = await workspace.fs.stat(uri);
491+
if (stats.isDirectory) {
492+
directoryCreated = true;
493+
break;
494+
}
495+
} catch {
496+
// Ignore errors, assume it's not a directory.
497+
}
498+
}
499+
}
500+
const pluginConfigHasChanged = params.changes.some((change) =>
501+
isPluginConfigFile(change.uri),
502+
);
503+
if (
504+
directoryCreated ||
505+
pluginConfigHasChanged ||
506+
changeAffectsLibs(params.changes)
507+
) {
508+
await pluginConfigChanged();
509+
} else {
510+
const compilationUnits = new Set<CompilationUnit>();
511+
// Not a plugin config change, meaning that individual folders/files have changed.
512+
for (const compilationUnit of compilationUnitHandler.getAllCompilationUnits()) {
513+
if (compilationUnit.includeError) {
514+
// If the compilation unit has an unresolved include, we need to re-run the lifecycle
515+
// to see if the include can now be resolved.
516+
compilationUnits.add(compilationUnit);
517+
// No need to change the change contents for this
518+
continue;
519+
}
520+
changeLoop: for (const change of params.changes) {
521+
for (const file of compilationUnit.services.files.keys()) {
522+
// Either equal (i.e. file has changed) or the changed dir is a parent of the file
523+
if (UriUtils.contains(change.uri, file)) {
524+
compilationUnits.add(compilationUnit);
525+
break changeLoop;
526+
}
527+
}
528+
}
529+
}
530+
// Finally, update the compilation units themselves that might be affected by the change
531+
for (const compilationUnit of compilationUnits) {
532+
await compilationUnitHandler.updateUri(compilationUnit.uri);
533+
}
534+
if (compilationUnits.size > 0) {
535+
// refresh semantic tokens so syntax coloring updates immediately
536+
connection.languages.semanticTokens.refresh();
537+
}
538+
}
539+
});
426540
onRequest(connection, Messages.ExistingFile, (uriString: string): boolean => {
427541
const uri = UriUtils.toUri(uriString);
428542
const compilationUnit = compilationUnitHandler.getCompilationUnit(uri);

packages/language/src/preprocessor/instruction-interpreter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,6 +2418,9 @@ async function runInclude(
24182418
entryUri: context.entryUri.toString(),
24192419
};
24202420
}
2421+
// Failed to resolve an include file, mark the unit
2422+
// This way, the language server can react to file system changes and re-run the lifecycle
2423+
context.unit.includeError = true;
24212424
context.diagnostics.push(diagnostic);
24222425
}
24232426

packages/language/src/utils/messages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export namespace Messages {
3838
/**
3939
* Notification sent to the LS when the workspace's plugin configuration changes.
4040
*/
41-
export const WorkspaceDidChangePluginConfigNotification =
41+
export const OnDidChangePluginConfigSettingsNotification =
4242
createNotificationType<void>("workspace/didChangePluginConfig");
4343

4444
/**

packages/language/src/utils/uri.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export namespace UriUtils {
8989
return URI.file(input);
9090
}
9191

92+
export function normalizePath(path: string): string {
93+
return path.replace(/\\/g, "/");
94+
}
95+
9296
export function parse(uri: string): URI {
9397
const safeInput = FRAGMENTLESS_SCHEME_REGEX.test(uri)
9498
? uri.replace(/#/g, "%23")
@@ -97,7 +101,7 @@ export namespace UriUtils {
97101
}
98102

99103
export function file(path: string): URI {
100-
return URI.file(path.replace(/\\/g, "/"));
104+
return URI.file(normalizePath(path));
101105
}
102106

103107
export function equals(a?: URI | string, b?: URI | string): boolean {
@@ -122,7 +126,7 @@ export namespace UriUtils {
122126
const convert = lowerCase
123127
? (s: string) => s.toLowerCase()
124128
: (s: string) => s;
125-
const path = convert(uri.path.replace(/\\/g, "/"));
129+
const path = convert(normalizePath(uri.path));
126130
const scheme = uri.scheme ? convert(uri.scheme) + "://" : "";
127131
const authority = uri.authority ? convert(uri.authority) : "";
128132
return `${scheme}${authority}${path}`;
@@ -136,7 +140,7 @@ export namespace UriUtils {
136140
*/
137141
export function toFilePath(input: URI | string): string {
138142
const uri = typeof input === "string" ? toUri(input) : input;
139-
let path = uri.path.replace(/\\/g, "/");
143+
let path = normalizePath(uri.path);
140144
if (/^\/[a-zA-Z]:\//.test(path)) {
141145
path = path[1].toUpperCase() + path.slice(2);
142146
} else if (/^[a-zA-Z]:\//.test(path)) {

packages/language/src/workspace/compilation-unit.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ export interface CompilationUnit {
8383
instructionCache: InstructionCache;
8484
rootScope: Scope;
8585
rootPreprocessorScope: Scope;
86+
/**
87+
* Indicates whether an include file could not be resolved during the last lifecycle.
88+
* This is used to trigger a re-run of the lifecycle when the file system changes.
89+
* Maybe we should have a more general mechanism for this, but for now this is sufficient.
90+
*/
91+
includeError: boolean;
8692
readonly services: CompilationServices;
8793
readonly programConfig: ProgramRecord | undefined;
8894
readonly processGroup: GroupRecord | undefined;
@@ -173,6 +179,7 @@ export async function createCompilationUnit(
173179
}),
174180
rootScope: Scope.createRoot(),
175181
rootPreprocessorScope: Scope.createRoot(),
182+
includeError: false,
176183
get programConfig() {
177184
if (cachedProgramConfig !== null) {
178185
return cachedProgramConfig;
@@ -200,6 +207,7 @@ export async function createCompilationUnit(
200207
unit.statementOrderCache.clear();
201208
unit.referencesCache.clear();
202209
unit.scopeCaches.clear();
210+
unit.includeError = false;
203211
unit.diagnostics.clear();
204212
cachedProcessGroup = null;
205213
cachedProgramConfig = null;

packages/language/src/workspace/file-system-provider.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,22 +225,22 @@ export class VirtualFileSystemProvider implements FileSystemProvider {
225225
* lib path for a file the resolver couldn't find.
226226
*/
227227
async findFile(
228-
path: URI,
228+
uri: URI,
229229
extensions: readonly string[],
230230
): Promise<URI | undefined> {
231-
let targetPath = path.path.replace(/\\/g, "/");
231+
let targetPath = UriUtils.normalizePath(uri.path);
232232
if (!this.caseSensitive) {
233233
targetPath = targetPath.toLowerCase();
234234
}
235235
const sortedFiles = this.getSortedFiles();
236236
for (const filePath of sortedFiles) {
237237
if (filePath.endsWith(targetPath)) {
238-
return UriUtils.toUri(filePath).with({ scheme: path.scheme });
238+
return UriUtils.toUri(filePath).with({ scheme: uri.scheme });
239239
}
240240
for (const ext of extensions) {
241241
const fullPath = targetPath + (ext.startsWith(".") ? ext : `.${ext}`);
242242
if (filePath.endsWith(fullPath)) {
243-
return UriUtils.toUri(filePath).with({ scheme: path.scheme });
243+
return UriUtils.toUri(filePath).with({ scheme: uri.scheme });
244244
}
245245
}
246246
}

0 commit comments

Comments
 (0)