Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1ddab8b
Bump dependency versions.
moigagoo Jul 15, 2026
770331d
CI: Update Nimble to 0.22.3.
moigagoo Jul 15, 2026
911d077
Add config prop to toggle `nim track`-based goto def and find refs.
moigagoo Jul 16, 2026
b6e5590
LSP: Add `nim track` paths to definition and usages.
moigagoo Jul 16, 2026
9c4346a
Add `nim track` handling module.
moigagoo Jul 16, 2026
344988b
MCP: Add `nim track` path for find usages.
moigagoo Jul 16, 2026
643d3eb
Format witn nph 0.6.2.
moigagoo Jul 16, 2026
7d72b30
Tests: MCP: Cover `nim track` path.
moigagoo Jul 16, 2026
1f38107
Tests: LSP: Cover `nim track` path.
moigagoo Jul 16, 2026
a4844ee
Require Nim 2.3.1 for `nim track`.
moigagoo Jul 17, 2026
728cdc9
Require Nim#head for `nim track`.
moigagoo Jul 17, 2026
35fe171
CI: Update Nimble version.
moigagoo Jul 17, 2026
f931845
Remove lock file.
moigagoo Jul 17, 2026
4b38124
Hide `nim track` behind feature.
moigagoo Jul 17, 2026
85c153f
Format witn nph 0.6.2.
moigagoo Jul 17, 2026
0668507
Fix typo in define name.
moigagoo Jul 17, 2026
b1a4ff7
Remove feature usage.
moigagoo Jul 17, 2026
7f24905
Use nim binary from the project when possible.
moigagoo Jul 20, 2026
41958bc
Tests: Move track tests to a separate module.
moigagoo Jul 20, 2026
67f1735
Tests: Move track tests to a separate module.
moigagoo Jul 20, 2026
576cbe5
Tests: Add a track test project.
moigagoo Jul 20, 2026
68900b3
Use getNimPath to get the project Nim instead of using the global one.
moigagoo Jul 20, 2026
dd7fde1
CI: Install Nimble fork with nim#head fix.
moigagoo Jul 21, 2026
cec97cd
Format with nph.
moigagoo Jul 21, 2026
7235bb6
Remove workaround for `nimble dump` not handling nim#head.
moigagoo Jul 21, 2026
4609067
Tests: trackproject: Do no require unittest2.
moigagoo Jul 21, 2026
fea5411
ls: getWorkingDir: Return project dir by default.
moigagoo Jul 21, 2026
2267a4f
ls: getNimPath: Look for Nim binary in the project.
moigagoo Jul 21, 2026
4a77e45
LSP: Make getNimPath async.
moigagoo Jul 21, 2026
56b8283
ls: getNimSuggestPathAndVersion: Call `nimble dump` in the project di…
moigagoo Jul 21, 2026
e4956e1
ls: getWorkingDir: Export proc.
moigagoo Jul 21, 2026
4855a57
LSP: Make getNimPath async.
moigagoo Jul 21, 2026
0d5f64a
track: Use nim binary from the project when possible.
moigagoo Jul 21, 2026
2bdedce
Handle cancellation: kill child processes and re-reise CancellationEr…
moigagoo Jul 28, 2026
98e62df
Read from stdout and stderr concurrently with the `nim track` process.
moigagoo Jul 28, 2026
85c18f3
Revert "Bump dependency versions."
moigagoo Aug 10, 2026
20f24e2
Track API: Catch CancelledError as e.
moigagoo Aug 10, 2026
90e9292
Run `nimble dump` in the project dir instead of the cwd.
moigagoo Aug 10, 2026
3d9769d
LSP: Fall back to nimsuggest on find refs and goto def for unsaved fi…
moigagoo Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Setup Nimble
uses: nim-lang/setup-nimble-action@v1
with:
nimble-version: "0.20.1"
nimble-version: "latest"
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Restore nimble dependencies from cache
Expand All @@ -35,6 +35,10 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ env.cache_nonce }}-

- name: Install Nimble fork with the nim#head fix
shell: bash
run: nimble install https://github.com/moigagoo/nimble@#bugfix/nimble_dump_with_nim_head

- name: Install dependencies
shell: bash
run: nimble -y install -l
Expand Down
48 changes: 38 additions & 10 deletions ls.nim
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type
nimExpandMacro*: Option[bool]
maxNimsuggestProcesses*: Option[int]
#max number of nimsuggest processes to keep alive. zero means unlimited
useNimTrack*: Option[bool]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when it is enabled in a nim version that doesnt support track?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll get an error:

"nim track not supported (requires nim >= 2.3.1)"

The reasoning behind this decision is, this is a very experimental feature, it requires development version of Nim and some dedication. You can't accidentally turn it on, if it's on, you know what you did. So I thought I shouldn't invest too much into handling this, the simplest logic will do. If invalid command: track is in the stderr, that means you're not using Nim#devel.


NlsFileInfo* = ref object of RootObj
projectFile*: Future[string]
Expand Down Expand Up @@ -271,14 +272,28 @@ proc supportSignatureHelp*(cc: LspClientCapabilities): bool =
caps.isSome and caps.get.signatureHelp.isSome

proc getNimbleDumpInfo*(
ls: LanguageServer, nimbleFile: string
ls: LanguageServer, nimbleFile: string, workingDir = ""
): Future[NimbleDumpInfo] {.async.} =
if nimbleFile in ls.nimDumpCache:
return ls.nimDumpCache.getOrDefault(nimbleFile)
# `nimble dump` resolves the project's Nim (`nimDir`) relative to the process
# working directory. When a project pins a local Nim (e.g. `nim#head` in
# `nimbledeps`), running `nimble dump` from the wrong directory reports the
# global Nim instead. Run it in the project directory so the project-local Nim
# is picked up. Fall back to the nimble file's directory when no working dir
# is supplied.
let dumpDir =
if workingDir != "":
workingDir
elif nimbleFile != "":
nimbleFile.parentDir
else:
getCurrentDir()
var process: AsyncProcessRef
try:
process = await startProcess(
"nimble",
workingDir = dumpDir,
arguments = @["dump", nimbleFile],
options = {UsePath},
stderrHandle = AsyncProcess.Pipe,
Expand Down Expand Up @@ -508,10 +523,15 @@ proc getNimVersion(nimDir: string): string =
proc getNimSuggestPathAndVersion(
ls: LanguageServer, conf: NlsConfig, workingDir: string
): Future[(string, string)] {.async.} =
#Attempting to see if the project is using a custom Nim version, if it's the case this will be slower than usual
let nimbleDumpInfo = await ls.getNimbleDumpInfo("")
let nimDir = nimbleDumpInfo.nimDir.get ""
let nimbleFiles = walkFiles(workingDir / "*.nimble").toSeq

let nimbleDumpInfo =
if nimbleFiles.len > 0:
await ls.getNimbleDumpInfo(nimbleFiles[0], workingDir)
else:
await ls.getNimbleDumpInfo("", workingDir)

let nimDir = nimbleDumpInfo.nimDir.get ""
var nimsuggestPath = expandTilde(conf.nimsuggestPath.get(""))
var nimVersion = ""
if nimsuggestPath == "":
Expand All @@ -527,11 +547,19 @@ proc getNimSuggestPathAndVersion(
debug "Using {nimVersion}", nimVersion = nimVersion
(nimsuggestPath, nimVersion)

proc getNimPath*(conf: NlsConfig): Option[string] =
proc getNimPath*(
ls: LanguageServer, conf: NlsConfig, workingDir = ""
): Future[Option[string]] {.async.} =
if conf.nimSuggestPath.isSome and conf.nimsuggestPath.get().fileExists():
some(conf.nimSuggestPath.get.parentDir / "nim")
else:
let path = findExe "nim"
let (nimsuggestPath, _) = await ls.getNimSuggestPathAndVersion(conf, workingDir)
let path =
if nimsuggestPath.fileExists():
nimsuggestPath.parentDir / "nim"
else:
findExe "nim"

if path != "":
some(path)
else:
Expand Down Expand Up @@ -597,7 +625,7 @@ proc getRootPath*(ip: LspInitializeParams): string =
proc getRootPath*(ip: McpInitializeParams): string =
getCurrentDir().pathToUri.uriToPath

proc getWorkingDir(ls: LanguageServer, path: string): Future[string] {.async.} =
proc getWorkingDir*(ls: LanguageServer, path: string): Future[string] {.async.} =
let rootPath =
case ls.serverMode
of lsp: ls.lspInitializeParams.getRootPath
Expand All @@ -607,7 +635,7 @@ proc getWorkingDir(ls: LanguageServer, path: string): Future[string] {.async.} =
pathRelativeToRoot = path.tryRelativeTo(rootPath)
mapping = ls.getWorkspaceConfiguration.await().workingDirectoryMapping.get(@[])

result = getCurrentDir()
result = rootPath

for m in mapping:
if pathRelativeToRoot.isSome and m.projectFile == pathRelativeToRoot.get():
Expand Down Expand Up @@ -945,7 +973,7 @@ proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async.} =
let conf = await ls.getAndWaitForWorkspaceConfiguration()
let useNimCheck = conf.useNimCheck.get(USE_NIM_CHECK_BY_DEFAULT)

let nimPath = getNimPath(conf)
let nimPath = await ls.getNimPath(conf)

if useNimCheck and nimPath.isSome:
proc getFilePath(c: CheckResult): string =
Expand Down Expand Up @@ -1258,7 +1286,7 @@ proc getProjectFile*(fileUri: string, ls: LanguageServer): Future[string] {.asyn
proc checkFile*(ls: LanguageServer, uri: string): Future[void] {.async.} =
let conf = await ls.getAndWaitForWorkspaceConfiguration()
let useNimCheck = conf.useNimCheck.get(USE_NIM_CHECK_BY_DEFAULT)
let nimPath = conf.getNimPath()
let nimPath = await ls.getNimPath(conf)
let token = fmt "Checking file {uri}"
ls.workDoneProgressCreate(token)
ls.progress(token, "begin", fmt "Checking {uri.uriToPath}")
Expand Down
4 changes: 2 additions & 2 deletions nimlangserver.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ requires "nim == 2.0.8",
"."

task test, "run tests":
--silent
--run
setCommand "c", "tests/all.nim"
--silent
setCommand("c", "tests/all.nim")

task book, "Generate book":
exec "mdbook build book -d ../docs"
Expand Down
72 changes: 65 additions & 7 deletions routes/lsp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import
stew/byteutils,
with,
],
../[testrunner, nimexpand, asyncprocmonitor, suggestapi, ls, utils],
../[testrunner, nimexpand, asyncprocmonitor, suggestapi, trackapi, ls, utils],
../protocol/[enums, types]

import macros except error
Expand Down Expand Up @@ -157,6 +157,34 @@ proc definition*(
): Future[seq[Location]] {.async.} =
with (params.position, params.textDocument):
asyncSpawn ls.addProjectFileToPendingRequest(id.uint, uri)
let config = await ls.getWorkspaceConfiguration()
# `nim track` only works on files as saved on disk; it has no dirty-buffer
# support. Use it only when the file is open and has no unsaved changes,
# otherwise fall back to nimsuggest (which supports dirty buffers).
if config.useNimTrack.get(false) and uri in ls.openFiles and
not ls.openFiles[uri].changed:
let ch = ls.getCharacter(uri, line, character)
if ch.isNone:
return @[]
let projectFile = await ls.openFiles[uri].projectFile
let timeout = config.timeout.get(REQUEST_TIMEOUT)
let workingDir = await ls.getWorkingDir(projectFile)
let nimPath = await ls.getNimPath(config, workingDir)
if nimPath.isNone:
return @[]
result = (
await track(
projectFile,
uriToPath(uri),
line + 1,
ch.get,
tmDef,
nimPath = nimPath.get,
workingDir = workingDir,
timeout = timeout,
)
).map(x => x.toUtf16Pos(ls).toLocation)
return
let ns = await ls.tryGetNimsuggest(uri)
if ns.isNone:
return @[]
Expand Down Expand Up @@ -410,14 +438,14 @@ proc hover*(
content.value.add &"```nim\n{expanded[0].doc}\n```"
else:
# debug "Couldnt expand the macro. Trying with nim expand", suggest = suggest[]
let nimPath = config.getNimPath()
let nimPath = await ls.getNimPath(config)
if nimPath.isSome:
let expanded = await nimExpandMacro(nimPath.get, suggest, uriToPath(uri))
content.value.add &"```nim\n{expanded}\n```"
if suggest.section == ideDef and suggest.symkind in ["skProc"] and
config.nimExpandArc.get(NIM_EXPAND_ARC_BY_DEFAULT):
debug "#Expanding arc", suggest = suggest[]
let nimPath = config.getNimPath()
let nimPath = await ls.getNimPath(config)
if nimPath.isSome:
let expanded = await nimExpandArc(nimPath.get, suggest, uriToPath(uri))
let arcContent = "#Expanded arc \n" & expanded
Expand All @@ -432,6 +460,36 @@ proc references*(
ls: LanguageServer, params: ReferenceParams
): Future[seq[Location]] {.async.} =
with (params.position, params.textDocument, params.context):
let config = await ls.getWorkspaceConfiguration()
# `nim track` only works on files as saved on disk; it has no dirty-buffer
# support. Use it only when the file is open and has no unsaved changes,
# otherwise fall back to nimsuggest (which supports dirty buffers).
if config.useNimTrack.get(false) and uri in ls.openFiles and
not ls.openFiles[uri].changed:
let ch = ls.getCharacter(uri, line, character)
if ch.isNone:
return @[]
let projectFile = await ls.openFiles[uri].projectFile
let mode = if includeDeclaration: tmDefUsages else: tmUsages
let timeout = config.timeout.get(REQUEST_TIMEOUT)
let workingDir = await ls.getWorkingDir(projectFile)
let nimPath = await ls.getNimPath(config, workingDir)
if nimPath.isNone:
return @[]
let refs = await track(
projectFile,
uriToPath(uri),
line + 1,
ch.get,
mode,
nimPath = nimPath.get,
workingDir = workingDir,
timeout = timeout,
)
result = refs
.filter(suggest => suggest.section != ideDef or includeDeclaration)
.map(x => x.toUtf16Pos(ls).toLocation)
return
let nimsuggest = await ls.tryGetNimsuggest(uri)
if nimsuggest.isNone:
return @[]
Expand Down Expand Up @@ -854,27 +912,27 @@ proc listTests*(
ls: LanguageServer, params: ListTestsParams
): Future[ListTestsResult] {.async.} =
let config = await ls.getWorkspaceConfiguration()
let nimPath = config.getNimPath()
let workspaceRoot = ls.lspInitializeParams.getRootPath
let nimPath = await ls.getNimPath(config, workspaceRoot)
if nimPath.isNone:
error "Nim path not found when listing tests"
return ListTestsResult(
projectInfo: TestProjectInfo(
entryPoint: params.entryPoint, suites: initTable[string, TestSuiteInfo]()
)
)
let workspaceRoot = ls.lspInitializeParams.getRootPath
let testProjectInfo = await listTests(params.entryPoint, nimPath.get(), workspaceRoot)
result.projectInfo = testProjectInfo

proc runTests*(
ls: LanguageServer, params: RunTestParams
): Future[RunTestProjectResult] {.async.} =
let config = await ls.getWorkspaceConfiguration()
let nimPath = config.getNimPath()
let workspaceRoot = ls.lspInitializeParams.getRootPath
let nimPath = await ls.getNimPath(config, workspaceRoot)
if nimPath.isNone:
error "Nim path not found when running tests"
return RunTestProjectResult()
let workspaceRoot = ls.lspInitializeParams.getRootPath
await runTests(
params.entryPoint,
nimPath.get(),
Expand Down
Loading