Skip to content

Commit 6f6b5e0

Browse files
committed
fix incremental syncing of compilation units
1 parent 08056ba commit 6f6b5e0

6 files changed

Lines changed: 447 additions & 10 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/DebouncingTimer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ public synchronized void start(Duration d) {
4141
}, d.toMillis(), TimeUnit.MILLISECONDS);
4242
}
4343

44+
/** marks timer as ready immediately and triggers action */
45+
public synchronized void triggerNow() {
46+
stop();
47+
isReady = true;
48+
action.run();
49+
}
50+
4451

4552

4653

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageWorker.java

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public void setRootPath(WFile rootPath) {
5252

5353
private final Object lock = new Object();
5454
private ModelManager.Changes changesToReconcile = ModelManager.Changes.empty();
55+
private boolean reconcileNowRequested = false;
5556
private final DebouncingTimer packagesToReconcileTimer = new DebouncingTimer(() -> {
5657
synchronized (lock) {
5758
lock.notify();
@@ -112,6 +113,17 @@ public String toString() {
112113
}
113114
}
114115

116+
class FileSystemUpdated extends PendingChange {
117+
public FileSystemUpdated(WFile filename) {
118+
super(filename);
119+
}
120+
121+
@Override
122+
public String toString() {
123+
return "FileSystemUpdated(" + getFilename() + ")";
124+
}
125+
}
126+
115127
class FileDeleted extends PendingChange {
116128

117129
public FileDeleted(WFile filename) {
@@ -182,6 +194,12 @@ private Workitem getNextWorkItem() {
182194
// cannot do anything useful at the moment
183195
WLogger.info("LanguageWorker is waiting for init ... ");
184196
}
197+
} else if (reconcileNowRequested && !changesToReconcile.isEmpty() && changes.isEmpty()) {
198+
packagesToReconcileTimer.stop();
199+
ModelManager.Changes changes = changesToReconcile;
200+
changesToReconcile = ModelManager.Changes.empty();
201+
reconcileNowRequested = false;
202+
return new Workitem("reconcile files (save)", () -> modelManager.reconcile(changes));
185203
} else if (!userRequests.isEmpty()) {
186204
UserRequest<?> req = userRequests.remove();
187205
return new Workitem(req.toString(), () -> req.run(modelManager));
@@ -191,11 +209,25 @@ private Workitem getNextWorkItem() {
191209
return new Workitem(change.toString(), () -> {
192210
ModelManager.Changes affected = null;
193211
if (isWurstDependencyFile(change)) {
194-
if (!(change instanceof FileReconcile)) {
195-
modelManager.clean();
212+
if (change instanceof FileReconcile) {
213+
FileReconcile fr = (FileReconcile) change;
214+
affected = modelManager.syncCompilationUnitContent(fr.getFilename(), fr.getContents());
215+
} else if (change instanceof FileSystemUpdated || change instanceof FileDeleted) {
216+
// Dependency roots may have changed (e.g. grill install), refresh and sync incrementally.
217+
modelManager.refreshDependencies();
218+
if (change instanceof FileDeleted) {
219+
affected = modelManager.removeCompilationUnit(change.getFilename());
220+
} else {
221+
affected = modelManager.syncCompilationUnit(change.getFilename());
222+
}
223+
} else {
224+
// Editor-triggered updates (save/close) use the normal incremental path.
225+
affected = modelManager.syncCompilationUnit(change.getFilename());
196226
}
197227
} else if (change instanceof FileDeleted) {
198228
affected = modelManager.removeCompilationUnit(change.getFilename());
229+
} else if (change instanceof FileSystemUpdated) {
230+
affected = modelManager.syncCompilationUnit(change.getFilename());
199231
} else if (change instanceof FileUpdated) {
200232
affected = modelManager.syncCompilationUnit(change.getFilename());
201233
} else if (change instanceof FileReconcile) {
@@ -214,6 +246,7 @@ private Workitem getNextWorkItem() {
214246
packagesToReconcileTimer.stop();
215247
ModelManager.Changes changes = changesToReconcile;
216248
changesToReconcile = ModelManager.Changes.empty();
249+
reconcileNowRequested = false;
217250

218251
return new Workitem("reconcile files", () -> {
219252
modelManager.reconcile(changes);
@@ -223,7 +256,11 @@ private Workitem getNextWorkItem() {
223256
}
224257

225258
private boolean isWurstDependencyFile(PendingChange change) {
226-
String uri = change.getFilename().getUriString().replace('\\', '/');
259+
return isWurstDependencyFile(change.getFilename());
260+
}
261+
262+
private boolean isWurstDependencyFile(WFile file) {
263+
String uri = file.getUriString().replace('\\', '/');
227264
return uri.contains("/_build/dependencies/");
228265
}
229266

@@ -262,13 +299,18 @@ private void log(String s) {
262299
public void handleFileChanged(DidChangeWatchedFilesParams params) {
263300
synchronized (lock) {
264301
for (FileEvent fileEvent : params.getChanges()) {
265-
bufferManager.handleFileChange(fileEvent);
266-
267302
WFile file = WFile.create(fileEvent.getUri());
303+
boolean isOpenInEditor = bufferManager.getTextDocumentVersion(file) >= 0;
304+
// For open documents incremental didChange is authoritative.
305+
// Ignore watcher changed/created events to avoid clobbering in-memory state.
306+
if (isOpenInEditor && fileEvent.getType() != FileChangeType.Deleted) {
307+
continue;
308+
}
309+
bufferManager.handleFileChange(fileEvent);
268310
if (fileEvent.getType() == FileChangeType.Deleted) {
269311
changes.put(file, new FileDeleted(file));
270312
} else {
271-
changes.put(file, new FileUpdated(file));
313+
changes.put(file, new FileSystemUpdated(file));
272314
}
273315
}
274316
lock.notifyAll();
@@ -306,7 +348,10 @@ public void handleClose(DidCloseTextDocumentParams params) {
306348
public void handleSave(DidSaveTextDocumentParams params) {
307349
synchronized (lock) {
308350
WFile file = WFile.create(params.getTextDocument().getUri());
351+
reconcileNowRequested = true;
309352
changes.put(file, new FileUpdated(file));
353+
// Save should flush diagnostics quickly instead of waiting for debounce.
354+
packagesToReconcileTimer.triggerNow();
310355
lock.notifyAll();
311356
}
312357
}

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public interface ModelManager {
2929

3030
void buildProject();
3131

32+
/**
33+
* refresh discovered dependency roots (e.g. _build/dependencies after grill install)
34+
*/
35+
void refreshDependencies();
36+
3237
Changes syncCompilationUnit(WFile changedFilePath);
3338

3439
Changes syncCompilationUnitContent(WFile filename, String contents);

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ private void readDependencies() {
179179
WurstCompilerJassImpl.addDependenciesFromFolder(projectPath, dependencies);
180180
}
181181

182+
@Override
183+
public void refreshDependencies() {
184+
readDependencies();
185+
}
186+
182187
private String getCanonicalPath(File f) {
183188
try {
184189
return f.getCanonicalPath();
@@ -477,6 +482,13 @@ private void replaceCompilationUnit(WFile filename) {
477482
@Override
478483
public Changes syncCompilationUnitContent(WFile filename, String contents) {
479484
WLogger.debug("sync contents for " + filename);
485+
int newHash = contentHash(contents);
486+
Integer oldHash = fileHashcodes.get(filename);
487+
CompilationUnit existing = getCompilationUnit(filename);
488+
if (oldHash != null && oldHash == newHash && existing != null) {
489+
// No content change and CU still present -> skip expensive reconcile/typecheck work.
490+
return Changes.empty();
491+
}
480492
Set<String> oldPackages = declaredPackages(filename);
481493
replaceCompilationUnit(filename, contents, false);
482494
return new Changes(io.vavr.collection.HashSet.of(filename), oldPackages);
@@ -509,8 +521,16 @@ public CompilationUnit replaceCompilationUnitContent(WFile filename, String cont
509521
@Override
510522
public Changes syncCompilationUnit(WFile f) {
511523
WLogger.debug("syncCompilationUnit File " + f);
524+
String contents = bufferManager.getBuffer(f);
525+
int newHash = contentHash(contents);
526+
Integer oldHash = fileHashcodes.get(f);
527+
CompilationUnit existing = getCompilationUnit(f);
528+
if (oldHash != null && oldHash == newHash && existing != null) {
529+
WLogger.trace(() -> "syncCompilationUnit no-op for " + f);
530+
return Changes.empty();
531+
}
512532
Set<String> oldPackages = declaredPackages(f);
513-
replaceCompilationUnit(f);
533+
replaceCompilationUnit(f, contents, true);
514534
WLogger.debug("replaced file " + f);
515535
WurstGui gui = new WurstGuiLogger();
516536
doTypeCheckPartial(gui, ImmutableList.of(f), oldPackages);
@@ -521,9 +541,10 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents,
521541
if (!isInWurstFolder(filename) && !isAlreadyLoaded(filename)) {
522542
return null;
523543
}
544+
int newHash = contentHash(contents);
524545
if (fileHashcodes.containsKey(filename)) {
525546
int oldHash = fileHashcodes.get(filename);
526-
if (oldHash == contents.hashCode()) {
547+
if (oldHash == newHash) {
527548
CompilationUnit existing = getCompilationUnit(filename);
528549
if (existing != null) {
529550
// no change
@@ -533,7 +554,7 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents,
533554
// Stale hash cache after remove/move; CU is gone, so reparse.
534555
WLogger.debug("CU hash unchanged but model entry missing for " + filename + ", reparsing.");
535556
} else {
536-
WLogger.debug("CU changed. oldHash = " + oldHash + " == " + contents.hashCode());
557+
WLogger.debug("CU changed. oldHash = " + oldHash + " == " + newHash);
537558
}
538559
}
539560

@@ -543,7 +564,7 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents,
543564
CompilationUnit cu = c.parse(filename.toString(), new StringReader(contents));
544565
cu.getCuInfo().setFile(filename.toString());
545566
updateModel(cu, gui);
546-
fileHashcodes.put(filename, contents.hashCode());
567+
fileHashcodes.put(filename, newHash);
547568
if (reportErrors) {
548569
if (gui.getErrorCount() > 0) {
549570
WLogger.debug("found " + gui.getErrorCount() + " errors in file " + filename);
@@ -832,4 +853,10 @@ private boolean isAlreadyLoaded(WFile file) {
832853
public File getProjectPath() {
833854
return projectPath;
834855
}
856+
857+
private int contentHash(String s) {
858+
// Normalize line endings to avoid false "changed" signals between editor buffers and disk text.
859+
String normalized = s.replace("\r\n", "\n").replace('\r', '\n');
860+
return normalized.hashCode();
861+
}
835862
}

0 commit comments

Comments
 (0)