Summary
When removeIdleNimsuggests runs against an idle project whose nimsuggest's
openFiles still references a URI that has already been evicted from
ls.openFiles, the unchecked indexing ls.openFiles[uri] raises KeyError.
The exception is caught by tick's outer try/except, but the cleanup
sequence (project.stop() and ls.projectFiles.del(project.file)) never runs,
so the same stale project is re-selected for idle removal on every subsequent
tick — producing infinite log spam at the configured tick interval.
Versions
- nimlangserver 1.14.0
- Nim 2.2.8 (MacOSX amd64, via Rosetta on Apple Silicon)
- VS Code / Cursor 1.105.1 with nimlang.nimlang 1.8.1
Reproduction
- Open any
.nim file outside the workspace (e.g. /tmp/scratch.nim) that
does not belong to a project. The extension/server registers a per-file
nimsuggest project for it.
- Either delete the file from disk, or close the editor tab so its URI is
removed from ls.openFiles while the nimsuggest itself still tracks it.
- Wait
nimsuggestIdleTimeout ms (default 120000) for the idle-cleanup tick.
Observed output (repeats indefinitely, once per tick):
DBG Removing idle nimsuggest project=/tmp/scratch.nim
DBG Removing idle nimsuggest open file uri=file:///tmp/scratch.nim
ERR Error in tick msg="key not found: file:///tmp/scratch.nim"
An exception occured
key not found: file:///tmp/scratch.nim
tables.nim(235) raiseKeyError
ls.nim(1323) tick
Root cause
ls.nim removeIdleNimsuggests:
for uri in ns.openFiles:
debug "Removing idle nimsuggest open file", uri = uri
await ls.makeIdleFile(ls.openFiles[uri]) # <-- KeyError when uri ∉ ls.openFiles
project.stop()
ls.projectFiles.del(project.file)
KeyError aborts the for loop on the first orphan URI, so project.stop()
and ls.projectFiles.del(project.file) are never reached. The outer try in
tick swallows the exception, the project stays in ls.projectFiles, and the
next tick repeats the whole sequence.
Suggested fix
Skip URIs that no longer have a corresponding NlsFileInfo rather than
indexing blindly, e.g.:
for uri in ns.openFiles:
debug "Removing idle nimsuggest open file", uri = uri
ls.openFiles.withValue(uri, info):
await ls.makeIdleFile(info[])
project.stop()
ls.projectFiles.del(project.file)
…or getOrDefault + nil guard. The important property is that
ls.projectFiles.del(project.file) is always reached so the same idle project
is not reconsidered on the next tick.
2026-05-16_langserver_openfiles.patch
Status
Patch built and compiled against 1.14.0 (Nim 2.0.8 / 2.2.8); rebased onto and
verified to apply cleanly on current upstream master (8f0786b). Non-fatal bug:
tick's outer try/except already prevents a crash, so the symptom is log spam
plus a leaked idle project (its nimsuggest is never stopped, ls.projectFiles
keeps the stale entry). After the fix the orphan URI is skipped, project.stop()
/ ls.projectFiles.del run, and the project is removed once with the normal
"Nimsuggest for … was stopped because it was idle for too long" message — no
repeat on the next tick.
How to reproduce / verify quickly
Lower the idle timeout so a tick fires in seconds instead of 2 min — global Zed
lsp.nim.settings (or VS Code nim.nimsuggestIdleTimeout):
"nimsuggestIdleTimeout": 3000. Then open a project-less scratch file
(/tmp/scratch.nim), delete it / close the tab so its URI leaves ls.openFiles
while the nimsuggest still tracks it, and watch the LSP log: pre-patch =
ERR Error in tick msg="key not found: …" every tick; post-patch = a single
clean stop, then quiet. (Must be a non-entry-point nimsuggest — removeIdleNimsuggests
skips ls.entryPoints.)
Summary
When
removeIdleNimsuggestsruns against an idle project whose nimsuggest'sopenFilesstill references a URI that has already been evicted fromls.openFiles, the unchecked indexingls.openFiles[uri]raisesKeyError.The exception is caught by
tick's outertry/except, but the cleanupsequence (
project.stop()andls.projectFiles.del(project.file)) never runs,so the same stale project is re-selected for idle removal on every subsequent
tick — producing infinite log spam at the configured tick interval.
Versions
Reproduction
.nimfile outside the workspace (e.g./tmp/scratch.nim) thatdoes not belong to a project. The extension/server registers a per-file
nimsuggest project for it.
removed from
ls.openFileswhile the nimsuggest itself still tracks it.nimsuggestIdleTimeoutms (default 120000) for the idle-cleanup tick.Observed output (repeats indefinitely, once per tick):
Root cause
ls.nimremoveIdleNimsuggests:KeyErroraborts theforloop on the first orphan URI, soproject.stop()and
ls.projectFiles.del(project.file)are never reached. The outertryintickswallows the exception, the project stays inls.projectFiles, and thenext tick repeats the whole sequence.
Suggested fix
Skip URIs that no longer have a corresponding
NlsFileInforather thanindexing blindly, e.g.:
…or
getOrDefault+nilguard. The important property is thatls.projectFiles.del(project.file)is always reached so the same idle projectis not reconsidered on the next tick.
2026-05-16_langserver_openfiles.patch
Status
Patch built and compiled against 1.14.0 (Nim 2.0.8 / 2.2.8); rebased onto and
verified to apply cleanly on current upstream
master(8f0786b). Non-fatal bug:tick's outertry/exceptalready prevents a crash, so the symptom is log spamplus a leaked idle project (its nimsuggest is never stopped,
ls.projectFileskeeps the stale entry). After the fix the orphan URI is skipped,
project.stop()/
ls.projectFiles.delrun, and the project is removed once with the normal"Nimsuggest for … was stopped because it was idle for too long" message — no
repeat on the next tick.
How to reproduce / verify quickly
Lower the idle timeout so a tick fires in seconds instead of 2 min — global Zed
lsp.nim.settings(or VS Codenim.nimsuggestIdleTimeout):"nimsuggestIdleTimeout": 3000. Then open a project-less scratch file(
/tmp/scratch.nim), delete it / close the tab so its URI leavesls.openFileswhile the nimsuggest still tracks it, and watch the LSP log: pre-patch =
ERR Error in tick msg="key not found: …"every tick; post-patch = a singleclean stop, then quiet. (Must be a non-entry-point nimsuggest —
removeIdleNimsuggestsskips
ls.entryPoints.)