diff --git a/compiler/ast/ast_parsed_types.nim b/compiler/ast/ast_parsed_types.nim
index 5660d1dd341..82135ee1c3f 100644
--- a/compiler/ast/ast_parsed_types.nim
+++ b/compiler/ast/ast_parsed_types.nim
@@ -118,7 +118,6 @@ type
pnkExprEqExpr
pnkExprColonExpr
pnkIdentDefs
- pnkConstDef
pnkVarTuple
pnkPar
pnkSqrBracket
@@ -175,6 +174,7 @@ type
pnkExportStmt
pnkExportExceptStmt
pnkConstSection
+ pnkTypeSection
pnkLetSection
pnkVarSection
pnkProcDef
@@ -184,7 +184,7 @@ type
pnkIteratorDef
pnkMacroDef
pnkTemplateDef
- pnkTypeSection
+ pnkConstDef
pnkTypeDef
pnkEnumTy
pnkEnumFieldDef
diff --git a/compiler/ast/astalgo.nim b/compiler/ast/astalgo.nim
index ed20ab768da..dd7a91bd4d7 100644
--- a/compiler/ast/astalgo.nim
+++ b/compiler/ast/astalgo.nim
@@ -144,7 +144,7 @@ proc getSymFromList*(list: PNode, ident: PIdent, start: int = 0): PSym =
else: return nil
result = nil
-proc sameIgnoreBacktickGensymInfo(a, b: string): bool =
+proc sameIgnoreBacktickGensymInfo*(a, b: string): bool =
if a[0] != b[0]: return false
var alen = a.len - 1
while alen > 0 and a[alen] != '`': dec(alen)
@@ -588,6 +588,6 @@ proc listSymbolNames*(symbols: openArray[PSym]): string =
result.add sym.name.s
proc isDiscriminantField*(n: PNode): bool =
- if n.kind == nkCheckedFieldExpr: sfDiscriminant in n[0][1].sym.flags
- elif n.kind == nkDotExpr: sfDiscriminant in n[1].sym.flags
+ if n.kind == nkCheckedFieldExpr: n[0][1].kind == nkSym and sfDiscriminant in n[0][1].sym.flags
+ elif n.kind == nkDotExpr: n[1].kind == nkSym and sfDiscriminant in n[1].sym.flags
else: false
diff --git a/compiler/ast/parser.nim b/compiler/ast/parser.nim
index 91cf06356af..ebbaa2dd63a 100644
--- a/compiler/ast/parser.nim
+++ b/compiler/ast/parser.nim
@@ -2321,7 +2321,7 @@ proc parseAll(p: var Parser): ParsedNode =
if p.tok.indent != 0:
p.invalidIndentation()
-proc parseTopLevelStmt(p: var Parser): ParsedNode =
+proc parseTopLevelStmt*(p: var Parser): ParsedNode =
## Implements an iterator which, when called repeatedly, returns the next
## top-level statement or emptyNode if end of stream.
result = p.emptyNode
diff --git a/compiler/front/msgs.nim b/compiler/front/msgs.nim
index 74d75240d74..498a1608808 100644
--- a/compiler/front/msgs.nim
+++ b/compiler/front/msgs.nim
@@ -315,11 +315,7 @@ proc errorActions(
elif eh == doRaise:
result = (doRaise, false)
-
proc `==`*(a, b: TLineInfo): bool =
- result = a.line == b.line and a.fileIndex == b.fileIndex
-
-proc exactEquals*(a, b: TLineInfo): bool =
result = a.fileIndex == b.fileIndex and a.line == b.line and a.col == b.col
proc getContext*(conf: ConfigRef; lastinfo: TLineInfo): seq[ReportContext] =
@@ -342,9 +338,6 @@ proc getContext*(conf: ConfigRef; lastinfo: TLineInfo): seq[ReportContext] =
info = context.info
-proc addSourceLine(conf: ConfigRef; fileIdx: FileIndex, line: string) =
- conf[fileIdx].lines.add line
-
proc numLines*(conf: ConfigRef, fileIdx: FileIndex): int =
## xxx there's an off by 1 error that should be fixed; if a file ends with "foo" or "foo\n"
## it will return same number of lines (ie, a trailing empty line is discounted)
@@ -352,7 +345,7 @@ proc numLines*(conf: ConfigRef, fileIdx: FileIndex): int =
if result == 0:
try:
for line in lines(toFullPathConsiderDirty(conf, fileIdx).string):
- addSourceLine conf, fileIdx, line
+ conf[fileIdx].lines.add line
except IOError:
discard
result = conf[fileIdx].lines.len
diff --git a/compiler/modules/modules.nim b/compiler/modules/modules.nim
index 3286cff44fb..17bd6c3676e 100644
--- a/compiler/modules/modules.nim
+++ b/compiler/modules/modules.nim
@@ -151,7 +151,6 @@ proc compileModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymFlags, fr
initStrTables(graph, result)
result.ast = nil
processModuleAux("import(dirty)")
- graph.markClientsDirty(fileIdx)
proc importModule*(graph: ModuleGraph; s: PSym, fileIdx: FileIndex): PSym =
diff --git a/compiler/sem/semexprs.nim b/compiler/sem/semexprs.nim
index 06c979829b3..83c2c734af2 100644
--- a/compiler/sem/semexprs.nim
+++ b/compiler/sem/semexprs.nim
@@ -1775,7 +1775,7 @@ proc builtinFieldAccess(c: PContext, n: PNode, flags: TExprFlags): PNode =
when defined(nimsuggest):
if c.config.cmd == cmdIdeTools:
suggestExpr(c, n)
- if exactEquals(c.config.m.trackPos, n[1].info): suggestExprNoCheck(c, n)
+ if c.config.m.trackPos == n[1].info: suggestExprNoCheck(c, n)
let s = qualifiedLookUp(c, n, {checkAmbiguity, checkUndeclared, checkModule})
if s.isError:
@@ -2891,7 +2891,7 @@ proc semMagic(c: PContext, n: PNode, s: PSym, flags: TExprFlags): PNode =
result = semOverloadedCallAnalyseEffects(c, n, flags)
if result == nil:
result = errorNode(c, n)
- else:
+ elif result.safeLen != 0:
let callee = result[0].sym
if callee.magic == mNone:
semFinishOperands(c, result)
@@ -2900,6 +2900,8 @@ proc semMagic(c: PContext, n: PNode, s: PSym, flags: TExprFlags): PNode =
result = fixVarArgumentsAndAnalyse(c, result)
if callee.magic != mNone:
result = magicsAfterOverloadResolution(c, result, flags)
+ else:
+ unreachable()
of mRunnableExamples:
markUsed(c, n.info, s)
if c.config.cmd in cmdDocLike and n.len >= 2 and n.lastSon.kind == nkStmtList:
diff --git a/compiler/sem/sempass2.nim b/compiler/sem/sempass2.nim
index 4634ce831b4..f84a9707d5a 100644
--- a/compiler/sem/sempass2.nim
+++ b/compiler/sem/sempass2.nim
@@ -1185,7 +1185,7 @@ proc track(tracked: PEffects, n: PNode) =
of nkVarSection, nkLetSection:
for child in n:
let last = lastSon(child)
- if child.kind == nkIdentDefs and sfCompileTime in child[0].sym.flags:
+ if child.kind == nkIdentDefs and child[0].kind == nkSym and sfCompileTime in child[0].sym.flags:
# don't analyse the definition of ``.compileTime`` globals. They
# don't "exist" in the context (i.e., run time) we're analysing
# in
diff --git a/compiler/sem/semstmts.nim b/compiler/sem/semstmts.nim
index c0042483a3b..33898c4fc9a 100644
--- a/compiler/sem/semstmts.nim
+++ b/compiler/sem/semstmts.nim
@@ -1691,7 +1691,7 @@ proc semCase(c: PContext, n: PNode; flags: TExprFlags): PNode =
setCaseContextIdx(c, i)
var x = n[i]
when defined(nimsuggest):
- if c.config.ideCmd == ideSug and exactEquals(c.config.m.trackPos, x.info) and caseTyp.kind == tyEnum:
+ if c.config.ideCmd == ideSug and c.config.m.trackPos == x.info and caseTyp.kind == tyEnum:
suggestEnum(c, x, caseTyp)
case x.kind
of nkOfBranch:
diff --git a/compiler/tools/suggest.nim b/compiler/tools/suggest.nim
index 36302f928ea..ce099a96bc0 100644
--- a/compiler/tools/suggest.nim
+++ b/compiler/tools/suggest.nim
@@ -82,7 +82,7 @@ const
template origModuleName(m: PSym): string = m.name.s
proc findDocComment(n: PNode): PNode =
- if n == nil: return nil
+ if n == nil or n.kind == nkError: return nil
if n.comment.len > 0: return n
if n.kind in {nkStmtList, nkStmtListExpr, nkObjectTy, nkRecList} and n.len > 0:
result = findDocComment(n[0])
@@ -389,7 +389,7 @@ proc suggestFieldAccess(c: PContext, n, field: PNode, outputs: var Suggestions)
var typ = n.typ
var pm: PrefixMatch
when defined(nimsuggest):
- if n.kind == nkSym and n.sym.kind == skError and c.config.suggestVersion == 0:
+ if n.kind == nkSym and n.sym.kind == skError:
# consider 'foo.|' where 'foo' is some not imported module.
let fullPath = findModule(c.config, n.sym.name.s, toFullPath(c.config, n.info))
if fullPath.isEmpty:
@@ -463,40 +463,24 @@ proc inCheckpoint*(current, trackPos: TLineInfo): TCheckPointResult =
return cpFuzzy
proc isTracked*(current, trackPos: TLineInfo, tokenLen: int): bool =
- if current.fileIndex==trackPos.fileIndex and current.line==trackPos.line:
+ if current.fileIndex == trackPos.fileIndex and
+ current.line == trackPos.line:
let col = trackPos.col
- if col >= current.col and col <= current.col+tokenLen-1:
+ if col >= current.col and col <= current.col + tokenLen - 1:
return true
when defined(nimsuggest):
- # Since TLineInfo defined a == operator that doesn't include the column,
- # we map TLineInfo to a unique int here for this lookup table:
- proc infoToInt(info: TLineInfo): int64 =
- info.fileIndex.int64 + info.line.int64 shl 32 + info.col.int64 shl 48
proc addNoDup(s: PSym; info: TLineInfo) =
# ensure nothing gets too slow:
- if s.allUsages.len > 500: return
- let infoAsInt = info.infoToInt
for infoB in s.allUsages:
- if infoB.infoToInt == infoAsInt: return
+ if infoB == info: return
s.allUsages.add(info)
-proc findUsages(g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym) =
- if g.config.suggestVersion == 1:
- if usageSym == nil and isTracked(info, g.config.m.trackPos, s.name.s.len):
- usageSym = s
- suggestResult(g.config, symToSuggest(g, s, isLocal=false, ideUse, info, 100, PrefixMatch.None, false, 0))
- elif s == usageSym:
- if g.config.lastLineInfo != info:
- suggestResult(g.config, symToSuggest(g, s, isLocal=false, ideUse, info, 100, PrefixMatch.None, false, 0))
- g.config.lastLineInfo = info
-
when defined(nimsuggest):
proc listUsages*(g: ModuleGraph; s: PSym) =
- #echo "usages ", s.allUsages.len
for info in s.allUsages:
- let x = if info == s.info and info.col == s.info.col: ideDef else: ideUse
+ let x = if info == s.info: ideDef else: ideUse
suggestResult(g.config, symToSuggest(g, s, isLocal=false, x, info, 100, PrefixMatch.None, false, 0))
proc findDefinition(g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym) =
@@ -518,35 +502,18 @@ proc suggestSym*(g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym; i
## misnamed: should be 'symDeclared'
let conf = g.config
when defined(nimsuggest):
- if conf.suggestVersion == 0:
- if s.allUsages.len == 0:
- s.allUsages = @[info]
- else:
- s.addNoDup(info)
+ if s.allUsages.len == 0:
+ s.allUsages = @[info]
+ else:
+ s.addNoDup(info)
- if conf.ideCmd == ideUse:
- findUsages(g, info, s, usageSym)
- elif conf.ideCmd == ideDef:
+ if conf.ideCmd == ideDef:
findDefinition(g, info, s, usageSym)
elif conf.ideCmd == ideDus and s != nil:
if isTracked(info, conf.m.trackPos, s.name.s.len):
suggestResult(conf, symToSuggest(g, s, isLocal=false, ideDef, info, 100, PrefixMatch.None, false, 0))
- findUsages(g, info, s, usageSym)
elif conf.ideCmd == ideHighlight and info.fileIndex == conf.m.trackPos.fileIndex:
suggestResult(conf, symToSuggest(g, s, isLocal=false, ideHighlight, info, 100, PrefixMatch.None, false, 0))
- elif conf.ideCmd == ideOutline and isDecl:
- # if a module is included then the info we have is inside the include and
- # we need to walk up the owners until we find the outer most module,
- # which will be the last skModule prior to an skPackage.
- var
- parentFileIndex = info.fileIndex # assume we're in the correct module
- parentModule = s.owner
- while parentModule != nil and parentModule.kind == skModule:
- parentFileIndex = parentModule.info.fileIndex
- parentModule = parentModule.owner
-
- if parentFileIndex == conf.m.trackPos.fileIndex:
- suggestResult(conf, symToSuggest(g, s, isLocal=false, ideOutline, info, 100, PrefixMatch.None, false, 0))
proc safeSemExpr*(c: PContext, n: PNode): PNode =
# use only for idetools support!
@@ -608,7 +575,7 @@ proc suggestExprNoCheck*(c: PContext, n: PNode) =
suggestQuit()
proc suggestExpr*(c: PContext, n: PNode) =
- if exactEquals(c.config.m.trackPos, n.info): suggestExprNoCheck(c, n)
+ if c.config.m.trackPos == n.info: suggestExprNoCheck(c, n)
proc suggestDecl*(c: PContext, n: PNode; s: PSym) =
let attached = c.config.m.trackPosAttached
diff --git a/compiler/vm/vm.nim b/compiler/vm/vm.nim
index ff5c200bb2c..13f126eca95 100644
--- a/compiler/vm/vm.nim
+++ b/compiler/vm/vm.nim
@@ -31,6 +31,7 @@ import
idents,
typesrenderer,
types,
+ astalgo,
],
compiler/modules/[
modulegraphs
@@ -2714,11 +2715,11 @@ proc rawExecute(c: var TCtx, pc: var int): YieldReason =
of opcEqIdent:
decodeBC(rkInt)
- func asCString(a: TFullReg): cstring =
+ func getName(a: TFullReg): string =
case a.kind
of rkLocation, rkHandle:
if a.handle.typ.kind == akString:
- result = deref(a.handle).strVal.asCString()
+ result = $ deref(a.handle).strVal
of rkNimNode:
var aNode = a.nimNode
@@ -2735,11 +2736,11 @@ proc rawExecute(c: var TCtx, pc: var int): YieldReason =
case aNode.kind
of nkIdent:
- result = aNode.ident.s.cstring
+ result = aNode.ident.s
of nkSym:
- result = aNode.sym.name.s.cstring
+ result = aNode.sym.name.s
of nkOpenSymChoice, nkClosedSymChoice:
- result = aNode[0].sym.name.s.cstring
+ result = aNode[0].sym.name.s
else:
discard
else:
@@ -2750,12 +2751,12 @@ proc rawExecute(c: var TCtx, pc: var int): YieldReason =
# These vars are of type `cstring` to prevent unnecessary string copy.
let
- aStrVal = asCString(regs[rb])
- bStrVal = asCString(regs[rc])
+ aStrVal = getName(regs[rb])
+ bStrVal = getName(regs[rc])
- regs[ra].intVal =
- if aStrVal != nil and bStrVal != nil:
- ord(idents.cmpIgnoreStyle(aStrVal, bStrVal, high(int)) == 0)
+ regs[ra].intVal =
+ if aStrVal != "" and bStrVal != "":
+ ord(sameIgnoreBacktickGensymInfo(aStrVal, $bStrVal))
else:
0
of opcStrToIdent:
diff --git a/compiler/vm/vmjit.nim b/compiler/vm/vmjit.nim
index b884ba88d1a..53ddc127b0c 100644
--- a/compiler/vm/vmjit.nim
+++ b/compiler/vm/vmjit.nim
@@ -282,13 +282,15 @@ proc genProc(jit: var JitState, c: var TCtx, s: PSym): VmGenResult =
c.removeLastEof()
let body =
- if s.kind == skMacro:
+ if isCompileTimeProc(s) and not defined(nimsuggest):
+ # no need to go through the transformation cache
transformBody(c.graph, c.idgen, s, s.ast[bodyPos])
else:
- # watch out! While compile-time only procedures don't need to be cached
- # here, we still need to retrieve their already cached body (if one
- # exists). Lifted inner procedures would otherwise not work.
- transformBody(c.graph, c.idgen, s, cache = not isCompileTimeProc(s))
+ # watch out! Since transforming a procedure body permanently alters
+ # the state of inner procedures, we need to both cache and later
+ # retrieve the transformed body for non-compile-only routines or
+ # when in suggest mode
+ transformBody(c.graph, c.idgen, s, cache = true)
echoInput(c.config, s, body)
var (tree, sourceMap) = generateCode(c.graph, s, selectOptions(c), body)
diff --git a/tests/lang_syntax/ast_pattern_matching.nim b/lib/experimental/ast_pattern_matching.nim
similarity index 100%
rename from tests/lang_syntax/ast_pattern_matching.nim
rename to lib/experimental/ast_pattern_matching.nim
diff --git a/nimlsp/README.rst b/nimlsp/README.rst
new file mode 100644
index 00000000000..b9f2aa71800
--- /dev/null
+++ b/nimlsp/README.rst
@@ -0,0 +1,244 @@
+==========
+Nim Language Server Protocol
+==========
+
+This is a `Language Server Protocol
+`_ implementation in
+Nim, for Nim.
+It is based on nimsuggest, which means that every editor that
+supports LSP will now have the same quality of suggestions that has previously
+only been available in supported editors.
+
+Installing ``nimlsp``
+=======
+If you have installed Nim through ``choosenim`` (recommended) the easiest way to
+install ``nimlsp`` is to use ``nimble`` with:
+
+.. code:: bash
+
+ nimble install nimlsp
+
+This will compile and install it in the ``nimble`` binary directory, which if
+you have set up ``nimble`` correctly it should be in your path. When compiling
+and using ``nimlsp`` it needs to have Nim's sources available in order to work.
+With Nim installed through ``choosenim`` these should already be on your system
+and ``nimlsp`` should be able to find and use them automatically. However if you
+have installed ``nimlsp`` in a different way you might run into issues where it
+can't find certain files during compilation/running. To fix this you need to
+grab a copy of Nim sources and then point ``nimlsp`` at them on compile-time by
+using ``-d:explicitSourcePath=PATH``, where ``PATH`` is where you have your Nim
+sources. You can also pass them at run-time (if for example you're working with
+a custom copy of the stdlib by passing it as an argument to ``nimlsp``. How
+exectly to do that will depend on the LSP client.
+
+Compile ``nimlsp``
+=======
+If you want more control over the compilation feel free to clone the
+repository. ``nimlsp`` depends on the ``nimsuggest`` sources which are in the main
+Nim repository, so make sure you have a copy of that somewhere. Manually having a
+copy of Nim this way means the default source path will not work so you need to
+set it explicitly on compilation with ``-d:explicitSourcePath=PATH`` and point to
+it at runtime (technically the runtime should only need the stdlib, so omitting
+it will make ``nimlsp`` try to find it from your Nim install).
+
+To do the standard build run:
+
+.. code:: bash
+
+ nimble build
+
+Or if you want debug output when ``nimlsp`` is running:
+
+.. code:: bash
+
+ nimble debug
+
+Or if you want even more debug output from the LSP format:
+
+.. code:: bash
+
+ nimble debug -d:debugLogging
+
+Supported Protocol features
+=======
+
+====== ================================
+Status LSP Command
+====== ================================
+☑ DONE textDocument/didChange
+☑ DONE textDocument/didClose
+☑ DONE textDocument/didOpen
+☑ DONE textDocument/didSave
+☐ TODO textDocument/codeAction
+☑ DONE textDocument/completion
+☑ DONE textDocument/definition
+☐ TODO textDocument/documentHighlight
+☑ DONE textDocument/documentSymbol
+☐ TODO textDocument/executeCommand
+☐ TODO textDocument/format
+☑ DONE textDocument/hover
+☑ DONE textDocument/rename
+☑ DONE textDocument/references
+☑ DONE textDocument/signatureHelp
+☑ DONE textDocument/publishDiagnostics
+☐ TODO workspace/symbol
+====== ================================
+
+
+Setting up ``nimlsp``
+=======
+Sublime Text
+-------
+Install the `LSP plugin `_.
+Install the `NimLime plugin `_ for syntax highlighting.
+
+Apart from syntax highlighting, NimLime can perform many of the features that ``nimlsp`` provides.
+It is recommended to disable those for optimal experience.
+For this, navigate to ``Preferences > Package Settings > NimLime > Settings`` and set ``*.enabled`` settings to ``false``:
+
+.. code:: js
+
+ {
+ "error_handler.enabled": false,
+ "check.on_save.enabled": false,
+ "check.current_file.enabled": false,
+ "check.external_file.enabled": false,
+ "check.clear_errors.enabled": false,
+ }
+
+To set up LSP, run ``Preferences: LSP settings`` from the command palette and add the following:
+
+.. code:: js
+
+ {
+ "clients": {
+ "nimlsp": {
+ "command": ["nimlsp"],
+ "enabled": true,
+
+ // ST4 only
+ "selector": "source.nim",
+
+ // ST3 only
+ "languageId": "nim",
+ "scopes": ["source.nim"],
+ "syntaxes": ["Packages/NimLime/Syntaxes/Nim.tmLanguage"]
+ }
+ }
+ }
+
+*Note: Make sure ``/.nimble/bin`` is added to your ``PATH``.*
+
+Vim
+-------
+To use ``nimlsp`` in Vim install the ``prabirshrestha/vim-lsp`` plugin and
+dependencies:
+
+.. code:: vim
+
+ Plugin 'prabirshrestha/asyncomplete.vim'
+ Plugin 'prabirshrestha/async.vim'
+ Plugin 'prabirshrestha/vim-lsp'
+ Plugin 'prabirshrestha/asyncomplete-lsp.vim'
+
+Then set it up to use ``nimlsp`` for Nim files:
+
+.. code:: vim
+
+ let s:nimlspexecutable = "nimlsp"
+ let g:lsp_log_verbose = 1
+ let g:lsp_log_file = expand('/tmp/vim-lsp.log')
+ " for asyncomplete.vim log
+ let g:asyncomplete_log_file = expand('/tmp/asyncomplete.log')
+
+ let g:asyncomplete_auto_popup = 0
+
+ if has('win32')
+ let s:nimlspexecutable = "nimlsp.cmd"
+ " Windows has no /tmp directory, but has $TEMP environment variable
+ let g:lsp_log_file = expand('$TEMP/vim-lsp.log')
+ let g:asyncomplete_log_file = expand('$TEMP/asyncomplete.log')
+ endif
+ if executable(s:nimlspexecutable)
+ au User lsp_setup call lsp#register_server({
+ \ 'name': 'nimlsp',
+ \ 'cmd': {server_info->[s:nimlspexecutable]},
+ \ 'whitelist': ['nim'],
+ \ })
+ endif
+
+ function! s:check_back_space() abort
+ let col = col('.') - 1
+ return !col || getline('.')[col - 1] =~ '\s'
+ endfunction
+
+ inoremap
+ \ pumvisible() ? "\" :
+ \ check_back_space() ? "\" :
+ \ asyncomplete#force_refresh()
+ inoremap pumvisible() ? "\" : "\"
+
+This configuration allows you to hit Tab to get auto-complete, and to call
+various functions to rename and get definitions. Of course you are free to
+configure this any way you'd like.
+
+Emacs
+-------
+
+With lsp-mode and use-package:
+
+.. code:: emacs-lisp
+
+ (use-package nim-mode
+ :ensure t
+ :hook
+ (nim-mode . lsp))
+
+Intellij
+-------
+You will need to install the `LSP support plugin `_.
+For syntax highlighting i would recommend the "official" `nim plugin `_
+(its not exactly official, but its developed by an intellij dev), the plugin will eventually use nimsuggest and have support for
+all this things and probably more, but since its still very new most of the features are still not implemented, so the LSP is a
+decent solution (and the only one really).
+
+To use it:
+
+1. Install the LSP and the nim plugin.
+
+2. Go into ``settings > Language & Frameworks > Language Server Protocol > Server Definitions``.
+
+3. Set the LSP mode to ``executable``, the extension to ``nim`` and in the Path, the path to your nimlsp executable.
+
+4. Hit apply and everything should be working now.
+
+Kate
+-------
+The LSP plugin has to be enabled in the Kate (version >= 19.12.0) plugins menu:
+
+1. In ``Settings > Configure Kate > Application > Plugins``, check box next to ``LSP Client`` to enable LSP functionality.
+
+2. Go to the now-available LSP Client menu (``Settings > Configure Kate > Application``) and enter the following in the User Server Settings tab:
+
+.. code:: json
+
+ {
+ "servers": {
+ "nim": {
+ "command": [".nimble/bin/nimlsp"],
+ "url": "https://github.com/PMunch/nimlsp",
+ "highlightingModeRegex": "^Nim$"
+ }
+ }
+ }
+
+This assumes that nimlsp was installed through nimble.
+*Note: Server initialization may fail without full path specified, from home directory, under the ``"command"`` entry, even if nimlsp is in system's ``PATH``.*
+
+Run Tests
+=========
+Not too many at the moment unfortunately, but they can be run with:
+
+.. code:: bash
+
+ nimble test
diff --git a/nimlsp/config.nims b/nimlsp/config.nims
new file mode 100644
index 00000000000..2885677a493
--- /dev/null
+++ b/nimlsp/config.nims
@@ -0,0 +1,4 @@
+import std/os
+
+const explicitSourcePath {.strdefine.} = getCurrentCompilerExe().parentDir.parentDir
+switch "path", explicitSourcePath
diff --git a/nimlsp/nimlsp.nim b/nimlsp/nimlsp.nim
new file mode 100644
index 00000000000..2f3c9bc4837
--- /dev/null
+++ b/nimlsp/nimlsp.nim
@@ -0,0 +1,670 @@
+import std/[algorithm, hashes, os, osproc, sets,
+ streams, strformat, strutils, tables, uri]
+
+import nimlsppkg/[baseprotocol, logger, suggestlib, utfmapping]
+include nimlsppkg/[messages, messageenums]
+
+
+const
+ version = block:
+ var version = "0.0.0"
+ let nimbleFile = staticRead(currentSourcePath().parentDir / "nimlsp.nimble")
+ for line in nimbleFile.splitLines:
+ let keyval = line.split('=')
+ if keyval.len == 2:
+ if keyval[0].strip == "version":
+ version = keyval[1].strip(chars = Whitespace + {'"'})
+ break
+ version
+ # This is used to explicitly set the default source path
+ explicitSourcePath {.strdefine.} = getCurrentCompilerExe().parentDir.parentDir
+
+type
+ UriParseError* = object of Defect
+ uri: string
+
+var nimpath = explicitSourcePath
+
+infoLog("Version: ", version)
+infoLog("explicitSourcePath: ", explicitSourcePath)
+for i in 1..paramCount():
+ infoLog("Argument ", i, ": ", paramStr(i))
+
+var
+ gotShutdown = false
+ initialized = false
+ projectFiles = initTable[string, tuple[nimsuggest: NimSuggest, openFiles: OrderedSet[string]]]()
+ openFiles = initTable[string, tuple[projectFile: string, fingerTable: seq[seq[tuple[u16pos, offset: int]]]]]()
+
+template fileuri(p: untyped): string =
+ p["textDocument"]["uri"].getStr
+
+template filePath(p: untyped): string =
+ p.fileuri[7..^1]
+
+template filestash(p: untyped): string =
+ storage / (hash(p.fileuri).toHex & ".nim" )
+
+template rawLine(p: untyped): int =
+ p["position"]["line"].getInt
+
+template rawChar(p: untyped): int =
+ p["position"]["character"].getInt
+
+template col(openFiles: typeof openFiles; p: untyped): int =
+ openFiles[p.fileuri].fingerTable[p.rawLine].utf16to8(p.rawChar)
+
+template textDocumentRequest(message: typed; kind: typed; name, body: untyped): untyped =
+ if message.hasKey("params"):
+ let p = message["params"]
+ var name = kind(p)
+ if p.isValid(kind, allowExtra = false):
+ body
+ else:
+ debugLog("Unable to parse data as ", kind)
+
+template textDocumentNotification(message: typed; kind: typed; name, body: untyped): untyped =
+ if message.hasKey("params"):
+ var p = message["params"]
+ var name = kind(p)
+ if p.isValid(kind, allowExtra = false):
+ if "languageId" notin name["textDocument"] or name["textDocument"]["languageId"].getStr == "nim":
+ body
+ else:
+ debugLog("Unable to parse data as ", kind)
+
+proc pathToUri(path: string): string =
+ # This is a modified copy of encodeUrl in the uri module. This doesn't encode
+ # the / character, meaning a full file path can be passed in without breaking
+ # it.
+ result = newStringOfCap(path.len + path.len shr 2) # assume 12% non-alnum-chars
+ when defined(windows):
+ result.add '/'
+ for c in path:
+ case c
+ # https://tools.ietf.org/html/rfc3986#section-2.3
+ of 'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~', '/': result.add c
+ of '\\':
+ when defined(windows):
+ result.add '/'
+ else:
+ result.add '%'
+ result.add toHex(ord(c), 2)
+ else:
+ result.add '%'
+ result.add toHex(ord(c), 2)
+
+proc uriToPath(uri: string): string =
+ ## Convert an RFC 8089 file URI to a native, platform-specific, absolute path.
+ #let startIdx = when defined(windows): 8 else: 7
+ #normalizedPath(uri[startIdx..^1])
+ let parsed = uri.parseUri
+ if parsed.scheme != "file":
+ var e = newException(UriParseError, &"Invalid scheme: {parsed.scheme}, only \"file\" is supported")
+ e.uri = uri
+ raise e
+ if parsed.hostname != "":
+ var e = newException(UriParseError, &"Invalid hostname: {parsed.hostname}, only empty hostname is supported")
+ e.uri = uri
+ raise e
+ return normalizedPath(
+ when defined(windows):
+ parsed.path[1..^1]
+ else:
+ parsed.path).decodeUrl
+
+proc parseId(node: JsonNode): string =
+ if node == nil: return
+ if node.kind == JString:
+ node.getStr
+ elif node.kind == JInt:
+ $node.getInt
+ else:
+ ""
+
+proc respond(outs: Stream, request: JsonNode, data: JsonNode) =
+ let resp = create(ResponseMessage, "2.0", parseId(request["id"]), some(data), none(ResponseError)).JsonNode
+ outs.sendJson resp
+
+proc error(outs: Stream, request: JsonNode, errorCode: ErrorCode, message: string, data: JsonNode) =
+ let err = some(create(ResponseError, ord(errorCode), message, data))
+ let resp = create(ResponseMessage, "2.0", parseId(request{"id"}), none(JsonNode), err).JsonNode
+ outs.sendJson resp
+
+proc notify(outs: Stream, notification: string, data: JsonNode) =
+ let resp = create(NotificationMessage, "2.0", notification, some(data)).JsonNode
+ outs.sendJson resp
+
+type Certainty = enum
+ None,
+ Folder,
+ Cfg,
+ Nimble
+
+proc getProjectFile(file: string): string =
+ result = file
+ let (dir, _, _) = result.splitFile()
+ var
+ path = dir
+ certainty = None
+ while not path.isRootDir:
+ let
+ (dir, fname, ext) = path.splitFile()
+ current = fname & ext
+ if fileExists(path / current.addFileExt(".nim")) and certainty <= Folder:
+ result = path / current.addFileExt(".nim")
+ certainty = Folder
+ if fileExists(path / current.addFileExt(".nim")) and
+ (fileExists(path / current.addFileExt(".nim.cfg")) or
+ fileExists(path / current.addFileExt(".nims"))) and certainty <= Cfg:
+ result = path / current.addFileExt(".nim")
+ certainty = Cfg
+ if certainty <= Nimble:
+ for nimble in walkFiles(path / "*.nimble"):
+ let info = execProcess("nimble dump " & nimble)
+ var sourceDir, name: string
+ for line in info.splitLines:
+ if line.startsWith("srcDir"):
+ sourceDir = path / line[(1 + line.find '"')..^2]
+ if line.startsWith("name"):
+ name = line[(1 + line.find '"')..^2]
+ let projectFile = sourceDir / (name & ".nim")
+ if sourceDir.len != 0 and name.len != 0 and
+ file.isRelativeTo(sourceDir) and fileExists(projectFile):
+ result = projectFile
+ certainty = Nimble
+ path = dir
+
+template getNimsuggest(fileuri: string): Nimsuggest =
+ projectFiles[openFiles[fileuri].projectFile].nimsuggest
+
+if paramCount() == 1:
+ case paramStr(1):
+ of "--help":
+ echo "Usage: nimlsp [OPTION | PATH]\n"
+ echo "--help, shows this message"
+ echo "--version, shows only the version"
+ echo "PATH, path to the Nim source directory, defaults to \"", nimpath, "\""
+ quit 0
+ of "--version":
+ echo "nimlsp v", version
+ when defined(debugLogging): echo "Compiled with debug logging"
+ when defined(debugCommunication): echo "Compiled with communication logging"
+ quit 0
+ else: nimpath = expandFilename(paramStr(1))
+if not fileExists(nimpath / "config/nim.cfg"):
+ stderr.write &"""Unable to find "config/nim.cfg" in "{nimpath
+ }". Supply the Nim project folder by adding it as an argument.
+"""
+ quit 1
+
+proc checkVersion(outs: Stream) =
+ let
+ nimoutputTuple =
+ execCmdEx("nim --version", options = {osproc.poEvalCommand, osproc.poUsePath})
+ if nimoutputTuple.exitcode == 0:
+ let
+ nimoutput = nimoutputTuple.output
+ versionStart = "Nim Compiler Version ".len
+ version = nimoutput[versionStart.. 10: &" and {suggestions.len-10} more" else: ""
+ var
+ completionItems = newJarray()
+ seenLabels: CountTable[string]
+ addedSuggestions: HashSet[string]
+ for suggestion in suggestions:
+ seenLabels.inc suggestion.collapseByIdentifier
+ for i in 0..suggestions.high:
+ let
+ suggestion = suggestions[i]
+ collapsed = suggestion.collapseByIdentifier
+ if not addedSuggestions.contains collapsed:
+ addedSuggestions.incl collapsed
+ let
+ seenTimes = seenLabels[collapsed]
+ detail =
+ if seenTimes == 1: some(nimSymDetails(suggestion))
+ else: some(&"[{seenTimes} overloads]")
+ completionItems.add create(CompletionItem,
+ label = suggestion.qualifiedPath[^1].strip(chars = {'`'}),
+ kind = some(nimSymToLSPKind(suggestion).int),
+ detail = detail,
+ documentation = some(suggestion.doc),
+ deprecated = none(bool),
+ preselect = none(bool),
+ sortText = some(fmt"{i:04}"),
+ filterText = none(string),
+ insertText = none(string),
+ insertTextFormat = none(int),
+ textEdit = none(TextEdit),
+ additionalTextEdits = none(seq[TextEdit]),
+ commitCharacters = none(seq[string]),
+ command = none(Command),
+ data = none(JsonNode)
+ ).JsonNode
+ outs.respond(message, completionItems)
+ of "textDocument/hover":
+ textDocumentRequest(message, TextDocumentPositionParams, req):
+ debugLog "Running equivalent of: def ", req.filePath, " ", req.filestash, "(",
+ req.rawLine + 1, ":",
+ openFiles.col(req), ")"
+ let suggestions = getNimsuggest(req.fileuri).def(req.filePath, dirtyfile = req.filestash,
+ req.rawLine + 1,
+ openFiles.col(req)
+ )
+ debugLog "Found suggestions: ",
+ suggestions[0 ..< min(suggestions.len, 10)],
+ if suggestions.len > 10: &" and {suggestions.len-10} more" else: ""
+ var resp: JsonNode
+ if suggestions.len == 0:
+ resp = newJNull()
+ else:
+ var label = suggestions[0].qualifiedPath.join(".")
+ if suggestions[0].forth != "":
+ label &= ": "
+ label &= suggestions[0].forth
+ let
+ rangeopt =
+ some(create(Range,
+ create(Position, req.rawLine, req.rawChar),
+ create(Position, req.rawLine, req.rawChar + suggestions[0].qualifiedPath[^1].len)
+ ))
+ markedString = create(MarkedStringOption, "nim", label)
+ if suggestions[0].doc != "":
+ resp = create(Hover,
+ @[
+ markedString,
+ create(MarkedStringOption, "", suggestions[0].doc),
+ ],
+ rangeopt
+ ).JsonNode
+ else:
+ resp = create(Hover, markedString, rangeopt).JsonNode;
+ outs.respond(message, resp)
+ of "textDocument/references":
+ textDocumentRequest(message, ReferenceParams, req):
+ debugLog "Running equivalent of: use ", req.fileuri, " ", req.filestash, "(",
+ req.rawLine + 1, ":",
+ openFiles.col(req), ")"
+ let suggestions = getNimsuggest(req.fileuri).use(req.filePath, dirtyfile = req.filestash,
+ req.rawLine + 1,
+ openFiles.col(req)
+ )
+ debugLog "Found suggestions: ",
+ suggestions[0 ..< min(suggestions.len, 10)],
+ if suggestions.len > 10: &" and {suggestions.len-10} more" else: ""
+ var response = newJarray()
+ for suggestion in suggestions:
+ if suggestion.section == ideUse or req["context"]["includeDeclaration"].getBool:
+ response.add create(Location,
+ "file://" & pathToUri(suggestion.filepath),
+ create(Range,
+ create(Position, suggestion.line-1, suggestion.column),
+ create(Position, suggestion.line-1, suggestion.column + suggestion.qualifiedPath[^1].len)
+ )
+ ).JsonNode
+ if response.len == 0:
+ outs.respond(message, newJNull())
+ else:
+ outs.respond(message, response)
+ of "textDocument/rename":
+ textDocumentRequest(message, RenameParams, req):
+ debugLog "Running equivalent of: use ", req.fileuri, " ", req.filestash, "(",
+ req.rawLine + 1, ":",
+ openFiles.col(req), ")"
+ let suggestions = getNimsuggest(req.fileuri).use(req.filePath, dirtyfile = req.filestash,
+ req.rawLine + 1,
+ openFiles.col(req)
+ )
+ debugLog "Found suggestions: ",
+ suggestions[0.. 10: &" and {suggestions.len-10} more" else: ""
+ var resp: JsonNode
+ if suggestions.len == 0:
+ resp = newJNull()
+ else:
+ var textEdits = newJObject()
+ for suggestion in suggestions:
+ let uri = "file://" & pathToUri(suggestion.filepath)
+ if uri notin textEdits:
+ textEdits[uri] = newJArray()
+ textEdits[uri].add create(TextEdit, create(Range,
+ create(Position, suggestion.line-1, suggestion.column),
+ create(Position, suggestion.line-1, suggestion.column + suggestion.qualifiedPath[^1].len)
+ ),
+ req["newName"].getStr
+ ).JsonNode
+ resp = create(WorkspaceEdit,
+ some(textEdits),
+ none(seq[TextDocumentEdit])
+ ).JsonNode
+ outs.respond(message, resp)
+ of "textDocument/definition":
+ textDocumentRequest(message, TextDocumentPositionParams, req):
+ debugLog "Running equivalent of: def ", req.fileuri, " ", req.filestash, "(",
+ req.rawLine + 1, ":",
+ openFiles.col(req), ")"
+ let declarations = getNimsuggest(req.fileuri).def(req.filePath, dirtyfile = req.filestash,
+ req.rawLine + 1,
+ openFiles.col(req)
+ )
+ debugLog "Found suggestions: ",
+ declarations[0.. 10: &" and {declarations.len-10} more" else: ""
+ var resp: JsonNode
+ if declarations.len == 0:
+ resp = newJNull()
+ else:
+ resp = newJarray()
+ for declaration in declarations:
+ resp.add create(Location,
+ "file://" & pathToUri(declaration.filepath),
+ create(Range,
+ create(Position, declaration.line-1, declaration.column),
+ create(Position, declaration.line-1, declaration.column + declaration.qualifiedPath[^1].len)
+ )
+ ).JsonNode
+ outs.respond(message, resp)
+ of "textDocument/documentSymbol":
+ textDocumentRequest(message, DocumentSymbolParams, req):
+ debugLog "Running equivalent of: outline ", req.fileuri,
+ " ", req.filestash
+ let projectFile = openFiles[req.fileuri].projectFile
+ let syms = getNimsuggest(req.fileuri).outline(
+ req.filePath,
+ dirtyfile = req.filestash
+ )
+ debugLog "Found outlines: ", syms[0.. 10: &" and {syms.len-10} more" else: ""
+ var resp: JsonNode
+ if syms.len == 0:
+ resp = newJNull()
+ else:
+ resp = newJarray()
+ for sym in syms.sortedByIt((it.line,it.column,it.quality)):
+ if sym.qualifiedPath.len != 2:
+ continue
+ resp.add create(
+ SymbolInformation,
+ sym.qualifiedPath[^1],
+ nimSymToLSPKind(sym.symKind).int,
+ some(false),
+ create(Location,
+ "file://" & pathToUri(sym.filepath),
+ create(Range,
+ create(Position, sym.line-1, sym.column),
+ create(Position, sym.line-1, sym.column + sym.qualifiedPath[^1].len)
+ )
+ ),
+ none(string)
+ ).JsonNode
+ outs.respond(message, resp)
+ of "textDocument/signatureHelp":
+ textDocumentRequest(message, TextDocumentPositionParams, req):
+ debugLog "Running equivalent of: con ", req.filePath, " ", req.filestash, "(",
+ req.rawLine + 1, ":",
+ openFiles.col(req), ")"
+ let suggestions = getNimsuggest(req.fileuri).con(req.filePath, dirtyfile = req.filestash, req.rawLine + 1, req.rawChar)
+ var signatures = newSeq[SignatureInformation]()
+ for suggestion in suggestions:
+ var label = suggestion.qualifiedPath.join(".")
+ if suggestion.forth != "":
+ label &= ": "
+ label &= suggestion.forth
+ signatures.add create(SignatureInformation,
+ label = label,
+ documentation = some(suggestion.doc),
+ parameters = none(seq[ParameterInformation])
+ )
+ let resp = create(SignatureHelp,
+ signatures = signatures,
+ activeSignature = some(0),
+ activeParameter = some(0)
+ ).JsonNode
+ outs.respond(message, resp)
+ else:
+ let msg = "Unknown request method: " & message["method"].getStr
+ debugLog msg
+ outs.error(message, MethodNotFound, msg, newJObject())
+ continue
+ elif isValid(message, NotificationMessage):
+ debugLog "Got valid Notification message of type ", message["method"].getStr
+ if not initialized and message["method"].getStr != "exit":
+ continue
+ case message["method"].getStr:
+ of "exit":
+ debugLog "Exiting"
+ if gotShutdown:
+ quit 0
+ else:
+ quit 1
+ of "initialized":
+ discard
+ of "textDocument/didOpen":
+ textDocumentNotification(message, DidOpenTextDocumentParams, req):
+ let
+ file = open(req.filestash, fmWrite)
+ projectFile = getProjectFile(uriToPath(req.fileuri))
+ debugLog "New document: ", req.fileuri, " stash: ", req.filestash
+ openFiles[req.fileuri] = (
+ projectFile: projectFile,
+ fingerTable: @[]
+ )
+
+ if projectFile notin projectFiles:
+ debugLog "Initialising with project file: ", projectFile
+ projectFiles[projectFile] = (nimsuggest: initNimsuggest(projectFile, nimpath), openFiles: initOrderedSet[string]())
+ projectFiles[projectFile].openFiles.incl(req.fileuri)
+
+ for line in req["textDocument"]["text"].getStr.splitLines:
+ openFiles[req.fileuri].fingerTable.add line.createUTFMapping()
+ file.writeLine line
+ file.close()
+ of "textDocument/didChange":
+ textDocumentNotification(message, DidChangeTextDocumentParams, req):
+ let file = open(req.filestash, fmWrite)
+ debugLog "Got document change for URI: ", req.fileuri, " saving to ", req.filestash
+ openFiles[req.fileuri].fingerTable = @[]
+ for line in req["contentChanges"][0]["text"].getStr.splitLines:
+ openFiles[req.fileuri].fingerTable.add line.createUTFMapping()
+ file.writeLine line
+ file.close()
+
+ # Notify nimsuggest about a file modification.
+ discard getNimsuggest(req.fileuri).mod(req.filePath, dirtyfile = req.filestash)
+ of "textDocument/didClose":
+ textDocumentNotification(message, DidCloseTextDocumentParams, req):
+ let projectFile = getProjectFile(uriToPath(req.fileuri))
+ debugLog "Got document close for URI: ", req.fileuri, " copied to ", req.filestash
+ removeFile(req.filestash)
+ projectFiles[projectFile].openFiles.excl(req.fileuri)
+ if projectFiles[projectFile].openFiles.len == 0:
+ debugLog "Trying to stop nimsuggest"
+ debugLog "Stopped nimsuggest with code: ",
+ getNimsuggest(req.fileuri).stopNimsuggest()
+ openFiles.del(req.fileuri)
+ of "textDocument/didSave":
+ textDocumentNotification(message, DidSaveTextDocumentParams, req):
+ if req["text"].isSome:
+ let file = open(req.filestash, fmWrite)
+ debugLog "Got document save for URI: ", req.fileuri, " saving to ", req.filestash
+ openFiles[req.fileuri].fingerTable = @[]
+ for line in req["text"].unsafeGet.getStr.splitLines:
+ openFiles[req.fileuri].fingerTable.add line.createUTFMapping()
+ file.writeLine line
+ file.close()
+ debugLog "fileuri: ", req.fileuri, ", project file: ", openFiles[req.fileuri].projectFile, ", dirtyfile: ", req.filestash
+ let diagnostics = getNimsuggest(req.fileuri).chk(req.filePath, dirtyfile = req.filestash)
+ debugLog "Got diagnostics: ",
+ diagnostics[0.. 10: &" and {diagnostics.len-10} more" else: ""
+ var response: seq[Diagnostic]
+ for diagnostic in diagnostics:
+ if diagnostic.line == 0:
+ continue
+
+ if diagnostic.filePath != req.filePath:
+ continue
+ # Try to guess the size of the identifier
+ let
+ message = diagnostic.doc
+ endcolumn = diagnostic.column + message.rfind('\'') - message.find('\'') - 1
+ response.add create(Diagnostic,
+ create(Range,
+ create(Position, diagnostic.line-1, diagnostic.column),
+ create(Position, diagnostic.line-1, max(diagnostic.column, endcolumn))
+ ),
+ some(case diagnostic.forth:
+ of "Error": DiagnosticSeverity.Error.int
+ of "Hint": DiagnosticSeverity.Hint.int
+ of "Warning": DiagnosticSeverity.Warning.int
+ else: DiagnosticSeverity.Error.int),
+ none(int),
+ some("nimsuggest chk"),
+ message,
+ none(seq[DiagnosticRelatedInformation])
+ )
+
+ # Invoke chk on all open files.
+ let projectFile = openFiles[req.fileuri].projectFile
+ for f in projectFiles[projectFile].openFiles.items:
+ let diagnostics = getNimsuggest(f).chk(req.filePath, dirtyfile = req.filestash)
+ debugLog "Got diagnostics: ",
+ diagnostics[0 ..< min(diagnostics.len, 10)],
+ if diagnostics.len > 10: &" and {diagnostics.len-10} more" else: ""
+
+ var response: seq[Diagnostic]
+ for diagnostic in diagnostics:
+ if diagnostic.line == 0:
+ continue
+
+ if diagnostic.filePath != uriToPath(f):
+ continue
+ # Try to guess the size of the identifier
+ let
+ message = diagnostic.doc
+ endcolumn = diagnostic.column + message.rfind('\'') - message.find('\'') - 1
+
+ response.add create(
+ Diagnostic,
+ create(Range,
+ create(Position, diagnostic.line-1, diagnostic.column),
+ create(Position, diagnostic.line-1, max(diagnostic.column, endcolumn))
+ ),
+ some(case diagnostic.forth:
+ of "Error": DiagnosticSeverity.Error.int
+ of "Hint": DiagnosticSeverity.Hint.int
+ of "Warning": DiagnosticSeverity.Warning.int
+ else: DiagnosticSeverity.Error.int),
+ none(int),
+ some("nimsuggest chk"),
+ message,
+ none(seq[DiagnosticRelatedInformation])
+ )
+ let resp = create(PublishDiagnosticsParams, f, response).JsonNode
+ outs.notify("textDocument/publishDiagnostics", resp)
+ let resp = create(PublishDiagnosticsParams,
+ req.fileuri,
+ response).JsonNode
+ outs.notify("textDocument/publishDiagnostics", resp)
+ else:
+ let msg = "Unknown notification method: " & message["method"].getStr
+ warnLog msg
+ outs.error(message, MethodNotFound, msg, newJObject())
+ continue
+ else:
+ let msg = "Invalid message: " & frame
+ debugLog msg
+ outs.error(message, InvalidRequest, msg, newJObject())
+ except MalformedFrame as e:
+ warnLog "Got Invalid message id: ", e.msg
+ continue
+ except UriParseError as e:
+ warnLog "Got exception parsing URI: ", e.msg
+ continue
+ except IOError as e:
+ errorLog "Got IOError: ", e.msg
+ break
+ except CatchableError as e:
+ warnLog "Got exception: ", e.msg
+ continue
+
+var
+ ins = newFileStream(stdin)
+ outs = newFileStream(stdout)
+main(ins, outs)
diff --git a/nimlsp/nimlsp.nim.cfg b/nimlsp/nimlsp.nim.cfg
new file mode 100644
index 00000000000..5092829b85e
--- /dev/null
+++ b/nimlsp/nimlsp.nim.cfg
@@ -0,0 +1,20 @@
+hint[XDeclaredButNotUsed]:off
+
+path:"$lib/packages/docutils"
+path:"$config/.."
+
+define:useStdoutAsStdmsg
+define:nimsuggest
+define:nimcore
+
+# die when nimsuggest uses more than 4GB:
+@if cpu32:
+ define:"nimMaxHeap=2000"
+@else:
+ define:"nimMaxHeap=4000"
+@end
+
+--threads:off
+--warning[Spacing]:off # The JSON schema macro uses a syntax similar to TypeScript
+# --warning[CaseTransition]:off
+-d:nimOldCaseObjects
diff --git a/nimlsp/nimlsp.nimble b/nimlsp/nimlsp.nimble
new file mode 100644
index 00000000000..77414e31cd2
--- /dev/null
+++ b/nimlsp/nimlsp.nimble
@@ -0,0 +1,25 @@
+# Package
+
+version = "0.4.4"
+author = "PMunch"
+description = "Nim Language Server Protocol - nimlsp implements the Language Server Protocol"
+license = "MIT"
+srcDir = "src"
+bin = @["nimlsp", "nimlsp_debug"]
+
+# Dependencies
+
+# nimble test does not work for me out of the box
+#task test, "Runs the test suite":
+ #exec "nim c -r tests/test_messages.nim"
+# exec "nim c -d:debugLogging -d:jsonSchemaDebug -r tests/test_messages2.nim"
+
+task debug, "Builds the language server":
+ exec "nim c --threads:on -d:nimcore -d:nimsuggest -d:debugCommunication -d:debugLogging -o:nimlsp src/nimlsp"
+
+before test:
+ exec "nimble build"
+
+task findNim, "Tries to find the current Nim installation":
+ echo NimVersion
+ echo currentSourcePath
diff --git a/nimlsp/nimlsp_debug.nim b/nimlsp/nimlsp_debug.nim
new file mode 100644
index 00000000000..f4533775873
--- /dev/null
+++ b/nimlsp/nimlsp_debug.nim
@@ -0,0 +1 @@
+include nimlsp
diff --git a/nimlsp/nimlsp_debug.nim.cfg b/nimlsp/nimlsp_debug.nim.cfg
new file mode 100644
index 00000000000..739903ae492
--- /dev/null
+++ b/nimlsp/nimlsp_debug.nim.cfg
@@ -0,0 +1,22 @@
+hint[XDeclaredButNotUsed]:off
+
+path:"$lib/packages/docutils"
+path:"$config/.."
+
+define:useStdoutAsStdmsg
+define:nimsuggest
+define:nimcore
+define:debugCommunication
+define:debugLogging
+
+# die when nimsuggest uses more than 4GB:
+@if cpu32:
+ define:"nimMaxHeap=2000"
+@else:
+ define:"nimMaxHeap=4000"
+@end
+
+--threads:off
+--warning[Spacing]:off # The JSON schema macro uses a syntax similar to TypeScript
+# --warning[CaseTransition]:off
+-d:nimOldCaseObjects
diff --git a/nimlsp/nimlsppkg/baseprotocol.nim b/nimlsp/nimlsppkg/baseprotocol.nim
new file mode 100644
index 00000000000..7fe61aed293
--- /dev/null
+++ b/nimlsp/nimlsppkg/baseprotocol.nim
@@ -0,0 +1,85 @@
+import std/[json, parseutils, streams, strformat,
+ strutils]
+when defined(debugCommunication):
+ import logger
+
+type
+ BaseProtocolError* = object of CatchableError
+
+ MalformedFrame* = object of BaseProtocolError
+ UnsupportedEncoding* = object of BaseProtocolError
+
+proc skipWhitespace(x: string, pos: int): int =
+ result = pos
+ while result < x.len and x[result] in Whitespace:
+ inc result
+
+proc sendFrame*(s: Stream, frame: string) =
+ when defined(debugCommunication):
+ frameLog(Out, frame)
+ when s is Stream:
+ s.write frame
+ s.flush
+ else:
+ s.write frame
+
+proc formFrame*(data: JsonNode): string =
+ var frame = newStringOfCap(1024)
+ toUgly(frame, data)
+ result = &"Content-Length: {frame.len}\r\n\r\n{frame}"
+
+proc sendJson*(s: Stream, data: JsonNode) =
+ let frame = formFrame(data)
+ s.sendFrame(frame)
+
+proc readFrame*(s: Stream): string =
+ var contentLen = -1
+ var headerStarted = false
+ var ln: string
+ while true:
+ ln = s.readLine()
+ if ln.len != 0:
+ headerStarted = true
+ let sep = ln.find(':')
+ if sep == -1:
+ raise newException(MalformedFrame, "invalid header line: " & ln)
+
+ let valueStart = ln.skipWhitespace(sep + 1)
+
+ case ln[0 ..< sep]
+ of "Content-Type":
+ if ln.find("utf-8", valueStart) == -1 and ln.find("utf8", valueStart) == -1:
+ raise newException(UnsupportedEncoding, "only utf-8 is supported")
+ of "Content-Length":
+ if parseInt(ln, contentLen, valueStart) == 0:
+ raise newException(MalformedFrame, "invalid Content-Length: " &
+ ln.substr(valueStart))
+ else:
+ # Unrecognized headers are ignored
+ discard
+ when defined(debugCommunication):
+ frameLog(In, ln)
+ elif not headerStarted:
+ continue
+ else:
+ when defined(debugCommunication):
+ frameLog(In, ln)
+ if contentLen != -1:
+ when s is Stream:
+ var buf = s.readStr(contentLen)
+ else:
+ var
+ buf = newString(contentLen)
+ head = 0
+ while contentLen > 0:
+ let bytesRead = s.readBuffer(buf[head].addr, contentLen)
+ if bytesRead == 0:
+ raise newException(MalformedFrame, "Unexpected EOF")
+ contentLen -= bytesRead
+ head += bytesRead
+ when defined(debugCommunication):
+ frameLog(In, buf)
+ return buf
+ else:
+ raise newException(MalformedFrame, "missing Content-Length header")
+
diff --git a/nimlsp/nimlsppkg/jsonschema.nim b/nimlsp/nimlsppkg/jsonschema.nim
new file mode 100644
index 00000000000..6a22ca73136
--- /dev/null
+++ b/nimlsp/nimlsppkg/jsonschema.nim
@@ -0,0 +1,479 @@
+import std/[macros, json, sequtils, options, strutils, tables]
+import experimental/ast_pattern_matching
+
+const ManglePrefix {.strdefine.}: string = "the"
+
+type NilType* = enum Nil
+
+proc extractKinds(node: NimNode): seq[tuple[name: string, isArray: bool]] =
+ if node.kind == nnkIdent:
+ return @[(name: $node, isArray: false)]
+ elif node.kind == nnkInfix and node[0].kind == nnkIdent and $node[0] == "or":
+ result = node[2].extractKinds
+ result.insert(node[1].extractKinds)
+ elif node.kind == nnkBracketExpr and node[0].kind == nnkIdent:
+ return @[(name: $node[0], isArray: true)]
+ elif node.kind == nnkNilLit:
+ return @[(name: "nil", isArray: false)]
+ elif node.kind == nnkBracketExpr and node[0].kind == nnkNilLit:
+ raise newException(AssertionError, "Array of nils not allowed")
+ else:
+ raise newException(AssertionError, "Unknown node kind: " & $node.kind)
+
+proc matchDefinition(pattern: NimNode):
+ tuple[
+ name: string,
+ kinds: seq[tuple[name: string, isArray: bool]],
+ optional: bool,
+ mangle: bool
+ ] {.compileTime.} =
+ matchAst(pattern):
+ of nnkCall(
+ `name` @ nnkIdent,
+ nnkStmtList(
+ `kind`
+ )
+ ):
+ return (
+ name: $name,
+ kinds: kind.extractKinds,
+ optional: false,
+ mangle: false
+ )
+ of nnkInfix(
+ ident"?:",
+ `name` @ nnkIdent,
+ `kind`
+ ):
+ return (
+ name: $name,
+ kinds: kind.extractKinds,
+ optional: true,
+ mangle: false
+ )
+ of nnkCall(
+ `name` @ nnkStrLit,
+ nnkStmtList(
+ `kind`
+ )
+ ):
+ return (
+ name: $name,
+ kinds: kind.extractKinds,
+ optional: false,
+ mangle: true
+ )
+ of nnkInfix(
+ ident"?:",
+ `name` @ nnkStrLit,
+ `kind`
+ ):
+ return (
+ name: $name,
+ kinds: kind.extractKinds,
+ optional: true,
+ mangle: true
+ )
+
+proc matchDefinitions(definitions: NimNode):
+ seq[
+ tuple[
+ name: string,
+ kinds: seq[
+ tuple[
+ name: string,
+ isArray: bool
+ ]
+ ],
+ optional: bool,
+ mangle: bool
+ ]
+ ] {.compileTime.} =
+ result = @[]
+ for definition in definitions:
+ result.add matchDefinition(definition)
+
+macro jsonSchema*(pattern: untyped): untyped =
+ var types: seq[
+ tuple[
+ name: string,
+ extends: string,
+ definitions:seq[
+ tuple[
+ name: string,
+ kinds: seq[
+ tuple[
+ name: string,
+ isArray: bool
+ ]
+ ],
+ optional: bool,
+ mangle: bool
+ ]
+ ]
+ ]
+ ] = @[]
+ for part in pattern:
+ matchAst(part):
+ of nnkCall(
+ `objectName` @ nnkIdent,
+ `definitions` @ nnkStmtList
+ ):
+ let defs = definitions.matchDefinitions
+ types.add (name: $objectName, extends: "", definitions: defs)
+ of nnkCommand(
+ `objectName` @ nnkIdent,
+ nnkCommand(
+ ident"extends",
+ `extends` @ nnkIdent
+ ),
+ `definitions` @ nnkStmtList
+ ):
+ let defs = definitions.matchDefinitions
+ types.add (name: $objectName, extends: $extends, definitions: defs)
+
+ var
+ typeDefinitions = newStmtList()
+ validationBodies = initOrderedTable[string, NimNode]()
+ validFields = initOrderedTable[string, NimNode]()
+ optionalFields = initOrderedTable[string, NimNode]()
+ creatorBodies = initOrderedTable[string, NimNode]()
+ createArgs = initOrderedTable[string, NimNode]()
+ let
+ data = newIdentNode("data")
+ fields = newIdentNode("fields")
+ traverse = newIdentNode("traverse")
+ allowExtra = newIdentNode("allowExtra")
+ ret = newIdentNode("ret")
+ for t in types:
+ let
+ name = newIdentNode(t.name)
+ objname = newIdentNode(t.name & "Obj")
+ creatorBodies[t.name] = newStmtList()
+ typeDefinitions.add quote do:
+ type
+ `objname` = distinct JsonNodeObj
+ `name` = ref `objname`
+ #converter toJsonNode(input: `name`): JsonNode {.used.} = input.JsonNode
+
+ var
+ requiredFields = 0
+ validations = newStmtList()
+ validFields[t.name] = nnkBracket.newTree()
+ optionalFields[t.name] = nnkBracket.newTree()
+ createArgs[t.name] = nnkFormalParams.newTree(name)
+ for field in t.definitions:
+ let
+ fname = field.name
+ aname = if field.mangle: newIdentNode(ManglePrefix & field.name) else: newIdentNode(field.name)
+ cname = quote do:
+ `data`[`fname`]
+ if field.optional:
+ optionalFields[t.name].add newLit(field.name)
+ else:
+ validFields[t.name].add newLit(field.name)
+ var
+ checks: seq[NimNode] = @[]
+ argumentChoices: seq[NimNode] = @[]
+ for kind in field.kinds:
+ let
+ tKind = if kind.name == "any":
+ if kind.isArray:
+ nnkBracketExpr.newTree(
+ newIdentNode("seq"),
+ newIdentNode("JsonNode")
+ )
+ else:
+ newIdentNode("JsonNode")
+ elif kind.isArray:
+ nnkBracketExpr.newTree(
+ newIdentNode("seq"),
+ newIdentNode(kind.name)
+ )
+ else:
+ newIdentNode(kind.name)
+ isBaseType = kind.name.toLowerASCII in
+ ["int", "string", "float", "bool"]
+ if kind.name != "nil":
+ if kind.isArray:
+ argumentChoices.add tkind
+ else:
+ argumentChoices.add tkind
+ else:
+ argumentChoices.add newIdentNode("NilType")
+ if isBaseType:
+ let
+ jkind = newIdentNode("J" & kind.name)
+ if kind.isArray:
+ checks.add quote do:
+ `cname`.kind != JArray or `cname`.anyIt(it.kind != `jkind`)
+ else:
+ checks.add quote do:
+ `cname`.kind != `jkind`
+ elif kind.name == "any":
+ if kind.isArray:
+ checks.add quote do:
+ `cname`.kind != JArray
+ else:
+ checks.add newLit(false)
+ elif kind.name == "nil":
+ checks.add quote do:
+ `cname`.kind != JNull
+ else:
+ let kindNode = newIdentNode(kind.name)
+ if kind.isArray:
+ checks.add quote do:
+ `cname`.kind != JArray or
+ (`traverse` and not `cname`.allIt(it.isValid(`kindNode`, allowExtra = `allowExtra`)))
+ else:
+ checks.add quote do:
+ (`traverse` and not `cname`.isValid(`kindNode`, allowExtra = `allowExtra`))
+ if kind.name == "nil":
+ if field.optional:
+ creatorBodies[t.name].add quote do:
+ when `aname` is Option[NilType]:
+ if `aname`.isSome:
+ `ret`[`fname`] = newJNull()
+ else:
+ creatorBodies[t.name].add quote do:
+ when `aname` is NilType:
+ `ret`[`fname`] = newJNull()
+ elif kind.isArray:
+ let
+ i = newIdentNode("i")
+ accs = if isBaseType:
+ quote do:
+ %`i`
+ else:
+ quote do:
+ `i`.JsonNode
+ if field.optional:
+ creatorBodies[t.name].add quote do:
+ when `aname` is Option[`tkind`]:
+ if `aname`.isSome:
+ `ret`[`fname`] = newJArray()
+ for `i` in `aname`.unsafeGet:
+ `ret`[`fname`].add `accs`
+ else:
+ creatorBodies[t.name].add quote do:
+ when `aname` is `tkind`:
+ `ret`[`fname`] = newJArray()
+ for `i` in `aname`:
+ `ret`[`fname`].add `accs`
+ else:
+ if field.optional:
+ let accs = if isBaseType:
+ quote do:
+ %`aname`.unsafeGet
+ else:
+ quote do:
+ `aname`.unsafeGet.JsonNode
+ creatorBodies[t.name].add quote do:
+ when `aname` is Option[`tkind`]:
+ if `aname`.isSome:
+ `ret`[`fname`] = `accs`
+ else:
+ let accs = if isBaseType:
+ quote do:
+ %`aname`
+ else:
+ quote do:
+ `aname`.JsonNode
+ creatorBodies[t.name].add quote do:
+ when `aname` is `tkind`:
+ `ret`[`fname`] = `accs`
+ while checks.len != 1:
+ let newFirst = nnkInfix.newTree(
+ newIdentNode("and"),
+ checks[0],
+ checks[1]
+ )
+ checks = checks[2..^1]
+ checks.insert(newFirst)
+ if field.optional:
+ argumentChoices[0] = nnkBracketExpr.newTree(
+ newIdentNode("Option"),
+ argumentChoices[0]
+ )
+ while argumentChoices.len != 1:
+ let newFirst = nnkInfix.newTree(
+ newIdentNode("or"),
+ argumentChoices[0],
+ if not field.optional: argumentChoices[1]
+ else: nnkBracketExpr.newTree(
+ newIdentNode("Option"),
+ argumentChoices[1]
+ )
+ )
+ argumentChoices = argumentChoices[2..^1]
+ argumentChoices.insert(newFirst)
+ createArgs[t.name].add nnkIdentDefs.newTree(
+ aname,
+ argumentChoices[0],
+ newEmptyNode()
+ )
+ let check = checks[0]
+ if field.optional:
+ validations.add quote do:
+ if `data`.hasKey(`fname`):
+ `fields` += 1
+ if `check`: return false
+ else:
+ requiredFields += 1
+ validations.add quote do:
+ if not `data`.hasKey(`fname`): return false
+ if `check`: return false
+
+ if t.extends.len == 0:
+ validationBodies[t.name] = quote do:
+ var `fields` = `requiredFields`
+ `validations`
+ else:
+ let extends = validationBodies[t.extends]
+ validationBodies[t.name] = quote do:
+ `extends`
+ `fields` += `requiredFields`
+ `validations`
+ for i in countdown(createArgs[t.extends].len - 1, 1):
+ createArgs[t.name].insert(1, createArgs[t.extends][i])
+ creatorBodies[t.name].insert(0, creatorBodies[t.extends])
+ for field in validFields[t.extends]:
+ validFields[t.name].add field
+ for field in optionalFields[t.extends]:
+ optionalFields[t.name].add field
+
+ var forwardDecls = newStmtList()
+ var validators = newStmtList()
+ let schemaType = newIdentNode("schemaType")
+ for kind, body in validationBodies.pairs:
+ let kindIdent = newIdentNode(kind)
+ validators.add quote do:
+ proc isValid(`data`: JsonNode, `schemaType`: typedesc[`kindIdent`],
+ `traverse` = true, `allowExtra` = false): bool {.used.} =
+ if `data`.kind != JObject: return false
+ `body`
+ if not `allowExtra` and `fields` != `data`.len: return false
+ return true
+ forwardDecls.add quote do:
+ proc isValid(`data`: JsonNode, `schemaType`: typedesc[`kindIdent`],
+ `traverse` = true, `allowExtra` = false): bool {.used.}
+ var accessors = newStmtList()
+ var creators = newStmtList()
+ for t in types:
+ let
+ creatorBody = creatorBodies[t.name]
+ kindIdent = newIdentNode(t.name)
+ kindName = t.name
+ var creatorArgs = createArgs[t.name]
+ creatorArgs.insert(1, nnkIdentDefs.newTree(
+ schemaType,
+ nnkBracketExpr.newTree(
+ newIdentNode("typedesc"),
+ kindIdent
+ ),
+ newEmptyNode()
+ ))
+ var createProc = quote do:
+ proc create() {.used.} =
+ var `ret` = newJObject()
+ `creatorBody`
+ return `ret`.`kindIdent`
+ createProc[3] = creatorArgs
+ creators.add createProc
+ var forwardCreateProc = quote do:
+ proc create() {.used.}
+ forwardCreateProc[3] = creatorArgs
+ forwardDecls.add forwardCreateProc
+
+ let macroName = nnkAccQuoted.newTree(
+ newIdentNode("[]")
+ )
+ let
+ validFieldsList = validFields[t.name]
+ optionalFieldsList = optionalFields[t.name]
+ data = newIdentNode("data")
+ field = newIdentNode("field")
+ var accessorbody = nnkIfExpr.newTree()
+ if validFields[t.name].len != 0:
+ accessorbody.add nnkElifBranch.newTree(nnkInfix.newTree(newIdentNode("in"), field, validFieldsList), quote do:
+ return nnkStmtList.newTree(
+ nnkCall.newTree(
+ newIdentNode("unsafeAccess"),
+ `data`,
+ newLit(`field`)
+ )
+ )
+ )
+ if optionalFields[t.name].len != 0:
+ accessorbody.add nnkElifBranch.newTree(nnkInfix.newTree(newIdentNode("in"), field, optionalFieldsList), quote do:
+ return nnkStmtList.newTree(
+ nnkCall.newTree(
+ newIdentNode("unsafeOptAccess"),
+ `data`,
+ newLit(`field`)
+ )
+ )
+ )
+ accessorbody.add nnkElse.newTree(quote do:
+ raise newException(KeyError, "unable to access field \"" & `field` & "\" in data with schema " & `kindName`)
+ )
+ accessors.add quote do:
+ proc unsafeAccess(data: `kindIdent`, field: static[string]): JsonNode {.used.} =
+ JsonNode(data)[field]
+ proc unsafeOptAccess(data: `kindIdent`, field: static[string]): Option[JsonNode] {.used.} =
+ if JsonNode(data).hasKey(field):
+ some(JsonNode(data)[field])
+ else:
+ none(JsonNode)
+
+ macro `macroName`(`data`: `kindIdent`, `field`: static[string]): untyped {.used.} =
+ `accessorbody`
+
+ result = quote do:
+ import macros
+ `typeDefinitions`
+ `forwardDecls`
+ `validators`
+ `creators`
+ `accessors`
+
+ when defined(jsonSchemaDebug):
+ echo result.repr
+
+when isMainModule:
+ jsonSchema:
+ CancelParams:
+ id?: int or string or float
+ something?: float
+
+ WrapsCancelParams:
+ cp: CancelParams
+ name: string
+
+ ExtendsCancelParams extends CancelParams:
+ name: string
+
+ WithArrayAndAny:
+ test?: CancelParams[]
+ ralph: int[] or float
+ bob: any
+ john?: int or nil
+
+ NameTest:
+ "method": string
+ "result": int
+ "if": bool
+ "type": float
+
+ var wcp = create(WrapsCancelParams,
+ create(CancelParams, some(10), none(float)), "Hello"
+ )
+ echo wcp.JsonNode.isValid(WrapsCancelParams) == true
+ echo wcp.JsonNode.isValid(WrapsCancelParams, false) == true
+ var ecp = create(ExtendsCancelParams, some(10), some(5.3), "Hello")
+ echo ecp.JsonNode.isValid(ExtendsCancelParams) == true
+ var war = create(WithArrayAndAny, some(@[
+ create(CancelParams, some(10), some(1.0)),
+ create(CancelParams, some("hello"), none(float))
+ ]), 2.0, %*{"hello": "world"}, none(NilType))
+ echo war.JsonNode.isValid(WithArrayAndAny) == true
\ No newline at end of file
diff --git a/nimlsp/nimlsppkg/logger.nim b/nimlsp/nimlsppkg/logger.nim
new file mode 100644
index 00000000000..6fdfb2f511a
--- /dev/null
+++ b/nimlsp/nimlsppkg/logger.nim
@@ -0,0 +1,38 @@
+import std/[logging, os]
+
+
+let storage* = getTempDir() / "nimlsp-" & $getCurrentProcessId()
+discard existsOrCreateDir(storage)
+let rollingLog = newRollingFileLogger(storage / "nimlsp.log")
+addHandler(rollingLog)
+
+template debugLog*(args: varargs[string, `$`]) =
+ when defined(debugLogging):
+ debug join(args)
+ flushFile rollingLog.file
+
+template infoLog*(args: varargs[string, `$`]) =
+ when defined(debugLogging):
+ info join(args)
+ flushFile rollingLog.file
+
+template errorLog*(args: varargs[string, `$`]) =
+ when defined(debugLogging):
+ error join(args)
+
+template warnLog*(args: varargs[string, `$`]) =
+ when defined(debugLogging):
+ warn join(args)
+
+type FrameDirection* = enum In, Out
+
+template frameLog*(direction: FrameDirection, args: varargs[string, `$`]) =
+ let oldFmtStr = rollingLog.fmtStr
+ case direction:
+ of Out: rollingLog.fmtStr = "<< "
+ of In: rollingLog.fmtStr = ">> "
+ let msg = join(args)
+ for line in msg.splitLines:
+ info line
+ flushFile rollingLog.file
+ rollingLog.fmtStr = oldFmtStr
\ No newline at end of file
diff --git a/nimlsp/nimlsppkg/messageenums.nim b/nimlsp/nimlsppkg/messageenums.nim
new file mode 100644
index 00000000000..63b0e34867f
--- /dev/null
+++ b/nimlsp/nimlsppkg/messageenums.nim
@@ -0,0 +1,114 @@
+type
+ ErrorCode* = enum
+ RequestCancelled = -32800 # All the other error codes are from JSON-RPC
+ ParseError = -32700,
+ InternalError = -32603,
+ InvalidParams = -32602,
+ MethodNotFound = -32601,
+ InvalidRequest = -32600,
+ ServerErrorStart = -32099,
+ ServerNotInitialized = -32002,
+ ServerErrorEnd = -32000
+
+# Anything below here comes from the LSP specification
+type
+ DiagnosticSeverity* {.pure.} = enum
+ Error = 1,
+ Warning = 2,
+ Information = 3,
+ Hint = 4
+
+ SymbolKind* {.pure.} = enum
+ File = 1,
+ Module = 2,
+ Namespace = 3,
+ Package = 4,
+ Class = 5,
+ Method = 6,
+ Property = 7,
+ Field = 8,
+ Constructor = 9,
+ Enum = 10,
+ Interface = 11,
+ Function = 12,
+ Variable = 13,
+ Constant = 14,
+ String = 15,
+ Number = 16,
+ Boolean = 17,
+ Array = 18,
+ Object = 19,
+ Key = 20,
+ Null = 21,
+ EnumMember = 22,
+ Struct = 23,
+ Event = 24,
+ Operator = 25,
+ TypeParameter = 26
+
+ CompletionItemKind* {.pure.} = enum
+ Text = 1,
+ Method = 2,
+ Function = 3,
+ Constructor = 4,
+ Field = 5,
+ Variable = 6,
+ Class = 7,
+ Interface = 8,
+ Module = 9,
+ Property = 10,
+ Unit = 11,
+ Value = 12,
+ Enum = 13,
+ Keyword = 14,
+ Snippet = 15,
+ Color = 16,
+ File = 17,
+ Reference = 18,
+ Folder = 19,
+ EnumMember = 20,
+ Constant = 21,
+ Struct = 22,
+ Event = 23,
+ Operator = 24,
+ TypeParameter = 25
+
+ TextDocumentSyncKind* {.pure.} = enum
+ None = 0,
+ Full = 1,
+ Incremental = 2
+
+ MessageType* {.pure.} = enum
+ Error = 1,
+ Warning = 2,
+ Info = 3,
+ Log = 4
+
+ FileChangeType* {.pure.} = enum
+ Created = 1,
+ Changed = 2,
+ Deleted = 3
+
+ WatchKind* {.pure.} = enum
+ Create = 1,
+ Change = 2,
+ Delete = 4
+
+ TextDocumentSaveReason* {.pure.} = enum
+ Manual = 1,
+ AfterDelay = 2,
+ FocusOut = 3
+
+ CompletionTriggerKind* {.pure.} = enum
+ Invoked = 1,
+ TriggerCharacter = 2,
+ TriggerForIncompleteCompletions = 3
+
+ InsertTextFormat* {.pure.} = enum
+ PlainText = 1,
+ Snippet = 2
+
+ DocumentHighlightKind* {.pure.} = enum
+ Text = 1,
+ Read = 2,
+ Write = 3
diff --git a/nimlsp/nimlsppkg/messages.nim b/nimlsp/nimlsppkg/messages.nim
new file mode 100644
index 00000000000..e6ed66f3456
--- /dev/null
+++ b/nimlsp/nimlsppkg/messages.nim
@@ -0,0 +1,596 @@
+import std/[json, options, sequtils]
+# Anything below here comes from the LSP specification
+import ./jsonschema
+
+
+jsonSchema:
+ Message:
+ jsonrpc: string
+
+ RequestMessage extends Message:
+ id: int or float or string
+ "method": string
+ params ?: any[] or any
+
+ ResponseMessage extends Message:
+ id: int or float or string or nil
+ "result" ?: any
+ error ?: ResponseError
+
+ ResponseError:
+ code: int or float
+ message: string
+ data: any
+
+ NotificationMessage extends Message:
+ "method": string
+ params ?: any[] or any
+
+ CancelParams:
+ id: int or float or string
+
+ Position:
+ line: int or float
+ character: int or float
+
+ Range:
+ start: Position
+ "end": Position
+
+ Location:
+ uri: string # Note that this is not checked
+ "range": Range
+
+ Diagnostic:
+ "range": Range
+ severity ?: int or float
+ code ?: int or float or string
+ source ?: string
+ message: string
+ relatedInformation ?: DiagnosticRelatedInformation[]
+
+ DiagnosticRelatedInformation:
+ location: Location
+ message: string
+
+ Command:
+ title: string
+ command: string
+ arguments ?: any[]
+
+ TextEdit:
+ "range": Range
+ newText: string
+
+ TextDocumentEdit:
+ textDocument: VersionedTextDocumentIdentifier
+ edits: TextEdit[]
+
+ WorkspaceEdit:
+ changes ?: any # This is a uri(string) to TextEdit[] mapping
+ documentChanges ?: TextDocumentEdit[]
+
+ TextDocumentIdentifier:
+ uri: string # Note that this is not checked
+
+ TextDocumentItem:
+ uri: string
+ languageId: string
+ version: int or float
+ text: string
+
+ VersionedTextDocumentIdentifier extends TextDocumentIdentifier:
+ version: int or float or nil
+ languageId ?: string # SublimeLSP adds this field erroneously
+
+ TextDocumentPositionParams:
+ textDocument: TextDocumentIdentifier
+ position: Position
+
+ DocumentFilter:
+ language ?: string
+ scheme ?: string
+ pattern ?: string
+
+ MarkupContent:
+ kind: string # "plaintext" or "markdown"
+ value: string
+
+ InitializeParams:
+ processId: int or float or nil
+ rootPath ?: string or nil
+ rootUri: string or nil # String is DocumentUri
+ initializationOptions ?: any
+ capabilities: ClientCapabilities
+ trace ?: string # 'off' or 'messages' or 'verbose'
+ workspaceFolders ?: WorkspaceFolder[] or nil
+
+ WorkspaceEditCapability:
+ documentChanges ?: bool
+
+ DidChangeConfigurationCapability:
+ dynamicRegistration ?: bool
+
+ DidChangeWatchedFilesCapability:
+ dynamicRegistration ?: bool
+
+ SymbolKindCapability:
+ valueSet ?: int # SymbolKind enum
+
+ SymbolCapability:
+ dynamicRegistration ?: bool
+ symbolKind ?: SymbolKindCapability
+
+ ExecuteCommandCapability:
+ dynamicRegistration ?: bool
+
+ WorkspaceClientCapabilities:
+ applyEdit ?: bool
+ workspaceEdit ?: WorkspaceEditCapability
+ didChangeConfiguration ?: DidChangeConfigurationCapability
+ didChangeWatchedFiles ?: DidChangeWatchedFilesCapability
+ symbol ?: SymbolCapability
+ executeCommand ?: ExecuteCommandCapability
+ workspaceFolders ?: bool
+ configuration ?: bool
+
+ SynchronizationCapability:
+ dynamicRegistration ?: bool
+ willSave ?: bool
+ willSaveWaitUntil ?: bool
+ didSave ?: bool
+
+ CompletionItemCapability:
+ snippetSupport ?: bool
+ commitCharactersSupport ?: bool
+ documentFormat ?: string[] # MarkupKind
+ deprecatedSupport ?: bool
+
+ CompletionItemKindCapability:
+ valueSet ?: int[] # CompletionItemKind enum
+
+ CompletionCapability:
+ dynamicRegistration ?: bool
+ completionItem ?: CompletionItemCapability
+ completionItemKind ?: CompletionItemKindCapability
+ contextSupport ?: bool
+
+ HoverCapability:
+ dynamicRegistration ?: bool
+ contentFormat ?: string[] # MarkupKind
+
+ SignatureInformationCapability:
+ documentationFormat ?: string[] # MarkupKind
+
+ SignatureHelpCapability:
+ dynamicRegistration ?: bool
+ signatureInformation ?: SignatureInformationCapability
+
+ ReferencesCapability:
+ dynamicRegistration ?: bool
+
+ DocumentHighlightCapability:
+ dynamicRegistration ?: bool
+
+ DocumentSymbolCapability:
+ dynamicRegistration ?: bool
+ symbolKind ?: SymbolKindCapability
+
+ FormattingCapability:
+ dynamicRegistration ?: bool
+
+ RangeFormattingCapability:
+ dynamicRegistration ?: bool
+
+ OnTypeFormattingCapability:
+ dynamicRegistration ?: bool
+
+ DefinitionCapability:
+ dynamicRegistration ?: bool
+
+ TypeDefinitionCapability:
+ dynamicRegistration ?: bool
+
+ ImplementationCapability:
+ dynamicRegistration ?: bool
+
+ CodeActionCapability:
+ dynamicRegistration ?: bool
+
+ CodeLensCapability:
+ dynamicRegistration ?: bool
+
+ DocumentLinkCapability:
+ dynamicRegistration ?: bool
+
+ ColorProviderCapability:
+ dynamicRegistration ?: bool
+
+ RenameCapability:
+ dynamicRegistration ?: bool
+
+ PublishDiagnosticsCapability:
+ dynamicRegistration ?: bool
+
+ TextDocumentClientCapabilities:
+ synchronization ?: SynchronizationCapability
+ completion ?: CompletionCapability
+ hover ?: HoverCapability
+ signatureHelp ?: SignatureHelpCapability
+ references ?: ReferencesCapability
+ documentHighlight ?: DocumentHighlightCapability
+ documentSymbol ?: DocumentSymbolCapability
+ formatting ?: FormattingCapability
+ rangeFormatting ?: RangeFormattingCapability
+ onTypeFormatting ?: OnTypeFormattingCapability
+ definition ?: DefinitionCapability
+ typeDefinition ?: TypeDefinitionCapability
+ implementation ?: ImplementationCapability
+ codeAction ?: CodeActionCapability
+ codeLens ?: CodeLensCapability
+ documentLink ?: DocumentLinkCapability
+ colorProvider ?: ColorProviderCapability
+ rename ?: RenameCapability
+ publishDiagnostics ?: PublishDiagnosticsCapability
+
+ ClientCapabilities:
+ workspace ?: WorkspaceClientCapabilities
+ textDocument ?: TextDocumentClientCapabilities
+ experimental ?: any
+
+ WorkspaceFolder:
+ uri: string
+ name: string
+
+ InitializeResult:
+ capabilities: ServerCapabilities
+
+ InitializeError:
+ retry: bool
+
+ CompletionOptions:
+ resolveProvider ?: bool
+ triggerCharacters ?: string[]
+
+ SignatureHelpOptions:
+ triggerCharacters ?: string[]
+
+ CodeLensOptions:
+ resolveProvider ?: bool
+
+ DocumentOnTypeFormattingOptions:
+ firstTriggerCharacter: string
+ moreTriggerCharacter ?: string[]
+
+ DocumentLinkOptions:
+ resolveProvider ?: bool
+
+ ExecuteCommandOptions:
+ commands: string[]
+
+ SaveOptions:
+ includeText ?: bool
+
+ ColorProviderOptions:
+ DUMMY ?: nil # This is actually an empty object
+
+ TextDocumentSyncOptions:
+ openClose ?: bool
+ change ?: int or float
+ willSave ?: bool
+ willSaveWaitUntil ?: bool
+ save ?: SaveOptions
+
+ StaticRegistrationOptions:
+ id ?: string
+
+ WorkspaceFolderCapability:
+ supported ?: bool
+ changeNotifications ?: string or bool
+
+ WorkspaceCapability:
+ workspaceFolders ?: WorkspaceFolderCapability
+
+ TextDocumentRegistrationOptions:
+ documentSelector: DocumentFilter[] or nil
+
+ TextDocumentAndStaticRegistrationOptions extends TextDocumentRegistrationOptions:
+ id ?: string
+
+ ServerCapabilities:
+ textDocumentSync ?: TextDocumentSyncOptions or int or float
+ hoverProvider ?: bool
+ completionProvider ?: CompletionOptions
+ signatureHelpProvider ?: SignatureHelpOptions
+ definitionProvider ?: bool
+ typeDefinitionProvider ?: bool or TextDocumentAndStaticRegistrationOptions
+ implementationProvider ?: bool or TextDocumentAndStaticRegistrationOptions
+ referencesProvider ?: bool
+ documentHighlightProvider ?: bool
+ documentSymbolProvider ?: bool
+ workspaceSymbolProvider ?: bool
+ codeActionProvider ?: bool
+ codeLensProvider ?: CodeLensOptions
+ documentFormattingProvider ?: bool
+ documentRangeFormattingProvider ?: bool
+ documentOnTypeFormattingProvider ?: DocumentOnTypeFormattingOptions
+ renameProvider ?: bool
+ documentLinkProvider ?: DocumentLinkOptions
+ colorProvider ?: bool or ColorProviderOptions or TextDocumentAndStaticRegistrationOptions
+ executeCommandProvider ?: ExecuteCommandOptions
+ workspace ?: WorkspaceCapability
+ experimental ?: any
+
+ InitializedParams:
+ DUMMY ?: nil # This is actually an empty object
+
+ ShowMessageParams:
+ "type": int # MessageType
+ message: string
+
+ MessageActionItem:
+ title: string
+
+ ShowMessageRequestParams:
+ "type": int # MessageType
+ message: string
+ actions ?: MessageActionItem[]
+
+ LogMessageParams:
+ "type": int # MessageType
+ message: string
+
+ Registration:
+ id: string
+ "method": string
+ registrationOptions ?: any
+
+ RegistrationParams:
+ registrations: Registration[]
+
+ Unregistration:
+ id: string
+ "method": string
+
+ UnregistrationParams:
+ unregistrations: Unregistration[]
+
+ WorkspaceFoldersChangeEvent:
+ added: WorkspaceFolder[]
+ removed: WorkspaceFolder[]
+
+ DidChangeWorkspaceFoldersParams:
+ event: WorkspaceFoldersChangeEvent
+
+ DidChangeConfigurationParams:
+ settings: any
+
+ ConfigurationParams:
+ "items": ConfigurationItem[]
+
+ ConfigurationItem:
+ scopeUri ?: string
+ section ?: string
+
+ FileEvent:
+ uri: string # DocumentUri
+ "type": int # FileChangeType
+
+ DidChangeWatchedFilesParams:
+ changes: FileEvent[]
+
+ DidChangeWatchedFilesRegistrationOptions:
+ watchers: FileSystemWatcher[]
+
+ FileSystemWatcher:
+ globPattern: string
+ kind ?: int # WatchKindCreate (bitmap)
+
+ WorkspaceSymbolParams:
+ query: string
+
+ ExecuteCommandParams:
+ command: string
+ arguments ?: any[]
+
+ ExecuteCommandRegistrationOptions:
+ commands: string[]
+
+ ApplyWorkspaceEditParams:
+ label ?: string
+ edit: WorkspaceEdit
+
+ ApplyWorkspaceEditResponse:
+ applied: bool
+
+ DidOpenTextDocumentParams:
+ textDocument: TextDocumentItem
+
+ DidChangeTextDocumentParams:
+ textDocument: VersionedTextDocumentIdentifier
+ contentChanges: TextDocumentContentChangeEvent[]
+
+ TextDocumentContentChangeEvent:
+ range ?: Range
+ rangeLength ?: int or float
+ text: string
+
+ TextDocumentChangeRegistrationOptions extends TextDocumentRegistrationOptions:
+ syncKind: int or float
+
+ WillSaveTextDocumentParams:
+ textDocument: TextDocumentIdentifier
+ reason: int # TextDocumentSaveReason
+
+ DidSaveTextDocumentParams:
+ textDocument: TextDocumentIdentifier
+ text ?: string
+
+ TextDocumentSaveRegistrationOptions extends TextDocumentRegistrationOptions:
+ includeText ?: bool
+
+ DidCloseTextDocumentParams:
+ textDocument: TextDocumentIdentifier
+
+ PublishDiagnosticsParams:
+ uri: string # DocumentUri
+ diagnostics: Diagnostic[]
+
+ CompletionParams extends TextDocumentPositionParams:
+ context ?: CompletionContext
+
+ CompletionContext:
+ triggerKind: int # CompletionTriggerKind
+ triggerCharacter ?: string
+
+ CompletionList:
+ isIncomplete: bool
+ "items": CompletionItem[]
+
+ CompletionItem:
+ label: string
+ kind ?: int # CompletionItemKind
+ detail ?: string
+ documentation ?: string or MarkupContent
+ deprecated ?: bool
+ preselect ?: bool
+ sortText ?: string
+ filterText ?: string
+ insertText ?: string
+ insertTextFormat ?: int #InsertTextFormat
+ textEdit ?: TextEdit
+ additionalTextEdits ?: TextEdit[]
+ commitCharacters ?: string[]
+ command ?: Command
+ data ?: any
+
+ CompletionRegistrationOptions extends TextDocumentRegistrationOptions:
+ triggerCharacters ?: string[]
+ resolveProvider ?: bool
+
+ MarkedStringOption:
+ language: string
+ value: string
+
+ Hover:
+ contents: string or MarkedStringOption or string[] or MarkedStringOption[] or MarkupContent
+ range ?: Range
+
+ SignatureHelp:
+ signatures: SignatureInformation[]
+ activeSignature ?: int or float
+ activeParameter ?: int or float
+
+ SignatureInformation:
+ label: string
+ documentation ?: string or MarkupContent
+ parameters ?: ParameterInformation[]
+
+ ParameterInformation:
+ label: string
+ documentation ?: string or MarkupContent
+
+ SignatureHelpRegistrationOptions extends TextDocumentRegistrationOptions:
+ triggerCharacters ?: string[]
+
+ ReferenceParams extends TextDocumentPositionParams:
+ context: ReferenceContext
+
+ ReferenceContext:
+ includeDeclaration: bool
+
+ DocumentHighlight:
+ "range": Range
+ kind ?: int # DocumentHighlightKind
+
+ DocumentSymbolParams:
+ textDocument: TextDocumentIdentifier
+
+ SymbolInformation:
+ name: string
+ kind: int # SymbolKind
+ deprecated ?: bool
+ location: Location
+ containerName ?: string
+
+ CodeActionParams:
+ textDocument: TextDocumentIdentifier
+ "range": Range
+ context: CodeActionContext
+
+ CodeActionContext:
+ diagnostics: Diagnostic[]
+
+ CodeLensParams:
+ textDocument: TextDocumentIdentifier
+
+ CodeLens:
+ "range": Range
+ command ?: Command
+ data ?: any
+
+ CodeLensRegistrationOptions extends TextDocumentRegistrationOptions:
+ resolveProvider ?: bool
+
+ DocumentLinkParams:
+ textDocument: TextDocumentIdentifier
+
+ DocumentLink:
+ "range": Range
+ target ?: string # DocumentUri
+ data ?: any
+
+ DocumentLinkRegistrationOptions extends TextDocumentRegistrationOptions:
+ resolveProvider ?: bool
+
+ DocumentColorParams:
+ textDocument: TextDocumentIdentifier
+
+ ColorInformation:
+ "range": Range
+ color: Color
+
+ Color:
+ red: int or float
+ green: int or float
+ blue: int or float
+ alpha: int or float
+
+ ColorPresentationParams:
+ textDocument: TextDocumentIdentifier
+ color: Color
+ "range": Range
+
+ ColorPresentation:
+ label: string
+ textEdit ?: TextEdit
+ additionalTextEdits ?: TextEdit[]
+
+ DocumentFormattingParams:
+ textDocument: TextDocumentIdentifier
+ options: any # FormattingOptions
+
+ #FormattingOptions:
+ # tabSize: int or float
+ # insertSpaces: bool
+ # [key: string]: boolean | int or float | string (jsonschema doesn't support variable key objects)
+
+ DocumentRangeFormattingParams:
+ textDocument: TextDocumentIdentifier
+ "range": Range
+ options: any # FormattingOptions
+
+ DocumentOnTypeFormattingParams:
+ textDocument: TextDocumentIdentifier
+ position: Position
+ ch: string
+ options: any # FormattingOptions
+
+ DocumentOnTypeFormattingRegistrationOptions extends TextDocumentRegistrationOptions:
+ firstTriggerCharacter: string
+ moreTriggerCharacter ?: string[]
+
+ RenameParams:
+ textDocument: TextDocumentIdentifier
+ position: Position
+ newName: string
diff --git a/nimlsp/nimlsppkg/nimsuggest.nim b/nimlsp/nimlsppkg/nimsuggest.nim
new file mode 100644
index 00000000000..e211c33c27b
--- /dev/null
+++ b/nimlsp/nimlsppkg/nimsuggest.nim
@@ -0,0 +1,293 @@
+when not defined(nimcore):
+ {.error: "nimcore MUST be defined for Nim's core tooling".}
+
+import std/[os, net]
+import std/options as stdOptions
+import
+ compiler/ast/[
+ idents,
+ lineinfos,
+ ast,
+ syntaxes,
+ parser,
+ ast_parsed_types,
+ ast_types
+ ],
+ compiler/modules/[
+ modules,
+ modulegraphs
+ ],
+ compiler/front/[
+ options,
+ optionsprocessor,
+ # commands,
+ msgs,
+ cmdlinehelper,
+ cli_reporter
+ ],
+ compiler/utils/[
+ # prefixmatches,
+ pathutils
+ ],
+ compiler/sem/[
+ sem,
+ passes,
+ passaux,
+ ]
+
+from compiler/ast/reports import Report,
+ category,
+ kind,
+ location
+
+from compiler/front/main import customizeForBackend
+
+from compiler/tools/suggest import isTracked, listUsages, suggestSym, `$`
+
+export Suggest
+export IdeCmd
+export AbsoluteFile
+type
+ CachedMsgs = seq[Report]
+ NimSuggest* = ref object
+ graph: ModuleGraph
+ idle: int
+ cachedMsgs: CachedMsgs
+
+proc defaultStructuredReportHook(conf: ConfigRef, report: Report): TErrorHandling =
+ discard
+
+proc initNimSuggest*(project: string, nimPath: string = ""): NimSuggest =
+ var retval: ModuleGraph
+ proc mockCommand(graph: ModuleGraph) =
+ retval = graph
+ let conf = graph.config
+ clearPasses(graph)
+ registerPass graph, verbosePass
+ registerPass graph, semPass
+ conf.setCmd cmdIdeTools
+
+ add(conf.searchPaths, conf.libpath)
+
+ conf.setErrorMaxHighMaybe
+ # do not print errors, but log them
+ conf.writelnHook = proc(conf: ConfigRef, msg: string, flags: MsgFlags) =
+ discard
+ conf.structuredReportHook = defaultStructuredReportHook
+
+ # compile the project before showing any input so that we already
+ # can answer questions right away:
+ compileProject(graph)
+
+
+ proc mockCmdLine(pass: TCmdLinePass, argv: openArray[string];
+ conf: ConfigRef) =
+ conf.writeHook = proc(conf: ConfigRef, s: string, flags: MsgFlags) = discard
+
+ let a = unixToNativePath(project)
+ if dirExists(a) and not fileExists(a.addFileExt("nim")):
+ conf.projectName = findProjectNimFile(conf, a)
+ # don't make it worse, report the error the old way:
+ if conf.projectName.len == 0: conf.projectName = a
+ else:
+ conf.projectName = a
+ let
+ cache = newIdentCache()
+ conf = newConfigRef(cli_reporter.reportHook)
+ self = NimProg(
+ suggestMode: true,
+ processCmdLine: mockCmdLine
+ )
+ conf.astDiagToLegacyReport = cli_reporter.legacyReportBridge
+ self.initDefinesProg(conf, "nimsuggest")
+
+ self.processCmdLineAndProjectPath(conf, [])
+
+ # Find Nim's prefix dir.
+ if nimPath == "":
+ let binaryPath = findExe("nim")
+ if binaryPath == "":
+ raise newException(IOError,
+ "Cannot find Nim standard library: Nim compiler not in PATH")
+ conf.prefixDir = AbsoluteDir binaryPath.splitPath().head.parentDir()
+ if not dirExists(conf.prefixDir / RelativeDir"lib"):
+ conf.prefixDir = AbsoluteDir""
+ else:
+ conf.prefixDir = AbsoluteDir nimPath
+
+ var graph = newModuleGraph(cache, conf)
+ graph.onMarkUsed = proc (g: ModuleGraph; info: TLineInfo; s: PSym; usageSym: var PSym; isDecl: bool) =
+ suggestSym(g, info, s, usageSym, isDecl)
+ graph.onSymImport = graph.onMarkUsed # same callback
+ if self.loadConfigsAndProcessCmdLine(cache, conf, graph, []):
+ customizeForBackend(graph, conf, backendC)
+ mockCommand(graph)
+
+ retval.doStopCompile = proc (): bool = false
+ return NimSuggest(graph: retval, idle: 0, cachedMsgs: @[])
+
+proc findNode(n: PNode; trackPos: TLineInfo): PSym =
+ if n.kind == nkSym:
+ if isTracked(n.info, trackPos, n.sym.name.s.len): return n.sym
+ else:
+ for i in 0 ..< safeLen(n):
+ let res = findNode(n[i], trackPos)
+ if res != nil: return res
+
+proc symFromInfo(graph: ModuleGraph; trackPos: TLineInfo; moduleIdx: FileIndex): PSym =
+ let m = graph.getModule(moduleIdx)
+ if m != nil and m.ast != nil:
+ result = findNode(m.ast, trackPos)
+
+proc getSymNode(node: ParsedNode): ParsedNode =
+ result = node
+ if result.kind == pnkPostfix:
+ result = result[^1]
+ elif result.kind == pnkPragmaExpr:
+ result = getSymNode(result[0])
+
+proc pnkToSymKind(kind: ParsedNodeKind): TSymKind =
+ result = skUnknown
+ case kind
+ of pnkConstSection, pnkConstDef: result = skConst
+ of pnkLetSection: result = skLet
+ of pnkVarSection: result = skVar
+ of pnkProcDef: result = skProc
+ of pnkFuncDef: result = skFunc
+ of pnkMethodDef: result = skMethod
+ of pnkConverterDef: result = skConverter
+ of pnkIteratorDef: result = skIterator
+ of pnkMacroDef: result = skMacro
+ of pnkTemplateDef: result = skTemplate
+ of pnkTypeDef, pnkTypeSection: result = skType
+ else: discard
+
+proc getName(node: ParsedNode): string =
+ if node.kind == pnkIdent:
+ result = node.startToken.ident.s
+ elif node.kind == pnkAccQuoted:
+ result = "`"
+ for t in node.idents:
+ result.add t.ident.s
+ result.add "`"
+
+proc parsedNodeToSugget(n: ParsedNode; moduleName: string): Suggest =
+ if n.kind in {pnkError, pnkEmpty}: return
+ if n.kind notin pnkConstSection..pnkTypeDef: return
+ new(result)
+ let token = getToken(n)
+ var name = ""
+
+ if n.kind in pnkProcDef..pnkTypeDef:
+ var node: ParsedNode = getSymNode(n[0])
+ if node.kind != pnkError:
+ name = getName(node)
+
+ if name != "":
+ result.qualifiedPath = @[moduleName, name]
+ result.line = token.line.int
+ result.column = token.col.int
+ result.symkind = byte pnkToSymKind(n.kind)
+
+proc outline(graph: ModuleGraph; file: AbsoluteFile; fileIdx: FileIndex) =
+ let conf = graph.config
+ var parser: Parser
+ var sug: Suggest
+ var parsedNode: ParsedNode
+ var s: ParsedNode
+ let m = splitFile(file.string)
+ const Sections = {pnkTypeSection, pnkConstSection, pnkLetSection, pnkVarSection}
+ template suggestIt(parsedNode: ParsedNode) =
+ sug = parsedNodeToSugget(parsedNode, m.name)
+ if sug != nil:
+ sug.filepath = file.string
+ conf.suggestionResultHook(sug)
+ if setupParser(parser, fileIdx, graph.cache, conf):
+ while true:
+ parsedNode = parser.parseTopLevelStmt()
+ if parsedNode.kind == pnkEmpty:
+ break
+
+ if parsedNode.kind in Sections:
+ for node in parsedNode.sons:
+ suggestIt(node)
+ else:
+ suggestIt(parsedNode)
+ closeParser(parser)
+
+proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int,
+ graph: ModuleGraph) =
+ let conf = graph.config
+ conf.ideCmd = cmd
+
+ var isKnownFile = true
+ let dirtyIdx = fileInfoIdx(conf, file, isKnownFile)
+
+ if not dirtyfile.isEmpty: msgs.setDirtyFile(conf, dirtyIdx, dirtyfile)
+ else: msgs.setDirtyFile(conf, dirtyIdx, AbsoluteFile"")
+
+ conf.m.trackPos = newLineInfo(dirtyIdx, line, col)
+ conf.m.trackPosAttached = false
+ conf.errorCounter = 0
+ var moduleIdx: FileIndex
+ var needCompile = true
+ if conf.ideCmd in {ideUse, ideDus} and
+ dirtyfile.isEmpty:
+ needCompile = false
+ if conf.ideCmd == ideOutline:
+ needCompile = false
+ outline(graph, file, dirtyIdx)
+
+ if needCompile:
+ if not isKnownFile:
+ moduleIdx = dirtyIdx
+ # stderr.writeLine "Compile unknown module: " & toFullPath(conf, moduleIdx)
+ discard graph.compileModule(moduleIdx, {})
+ else:
+ moduleIdx = graph.parentModule(dirtyIdx)
+ # stderr.writeLine "Compile known module: " & toFullPath(conf, moduleIdx)
+ graph.markDirty dirtyIdx
+ graph.markClientsDirty dirtyIdx
+ # partially recompiling the project means that that VM and JIT state
+ # would become stale, which we prevent by discarding all of it:
+ graph.vm = nil
+ if conf.ideCmd != ideMod:
+ discard graph.compileModule(moduleIdx, {})
+ if conf.ideCmd in {ideUse, ideDus}:
+ let u = graph.symFromInfo(conf.m.trackPos, moduleIdx)
+ if u != nil:
+ listUsages(graph, u)
+ else:
+ stderr.writeLine "found no symbol at position: " & (conf $ conf.m.trackPos)
+
+proc runCmd*(nimsuggest: NimSuggest, cmd: IdeCmd, file,
+ dirtyfile: AbsoluteFile, line, col: int): seq[Suggest] =
+ var retval: seq[Suggest] = @[]
+ let conf = nimsuggest.graph.config
+ conf.ideCmd = cmd
+ conf.suggestionResultHook = proc (s: Suggest) =
+ retval.add(s)
+
+ if conf.ideCmd == ideKnown:
+ retval.add(Suggest(section: ideKnown, quality: ord(fileInfoKnown(conf, file))))
+ elif conf.ideCmd == ideProject:
+ retval.add(Suggest(section: ideProject,
+ filePath: string conf.projectFull))
+ else:
+ # if conf.ideCmd == ideChk:
+ # for cm in nimsuggest.cachedMsgs: errorHook(conf, cm.info, cm.msg, cm.sev)
+ if conf.ideCmd == ideChk:
+ conf.structuredReportHook = proc (conf: ConfigRef, report: Report): TErrorHandling =
+ let loc = report.location()
+ if stdOptions.isSome(loc):
+ let info = loc.get()
+ retval.add(Suggest(section: ideChk, filePath: toFullPath(conf,
+ info),
+ line: toLinenumber(info), column: toColumn(info),
+ forth: $severity(conf, report)))
+ return doNothing
+ else:
+ conf.structuredReportHook = defaultStructuredReportHook
+ executeNoHooks(conf.ideCmd, file, dirtyfile, line, col,
+ nimsuggest.graph)
+ return retval
diff --git a/nimlsp/nimlsppkg/suggestlib.nim b/nimlsp/nimlsppkg/suggestlib.nim
new file mode 100644
index 00000000000..77cd8e6d491
--- /dev/null
+++ b/nimlsp/nimlsppkg/suggestlib.nim
@@ -0,0 +1,113 @@
+import std/[strformat, strutils]
+import messageenums
+import nimsuggest
+export Suggest
+export IdeCmd
+export NimSuggest
+export initNimSuggest
+import
+ compiler/ast/[
+ ast
+ ]
+
+proc stopNimSuggest*(nimsuggest: NimSuggest): int = 42
+
+proc `$`*(suggest: Suggest): string =
+ &"""(section: {suggest.section}, symKind: {suggest.symkind.TSymKind
+ }, qualifiedPath: {suggest.qualifiedPath.join(".")}, forth: {suggest.forth
+ }, filePath: {suggest.filePath}, line: {suggest.line}, column: {suggest.column
+ }, doc: {suggest.doc}, quality: {suggest.quality}, prefix: {suggest.prefix})"""
+
+func collapseByIdentifier*(suggest: Suggest): string =
+ ## Function to create an identifier that can be used to remove duplicates in a list
+ fmt"{suggest.qualifiedPath[^1]}__{suggest.symKind.TSymKind}"
+
+func nimSymToLSPKind*(suggest: Suggest): CompletionItemKind =
+ case suggest.symKind.TSymKind:
+ of skConst: CompletionItemKind.Value
+ of skEnumField: CompletionItemKind.Enum
+ of skForVar: CompletionItemKind.Variable
+ of skIterator: CompletionItemKind.Keyword
+ of skLabel: CompletionItemKind.Keyword
+ of skLet: CompletionItemKind.Value
+ of skMacro: CompletionItemKind.Snippet
+ of skMethod: CompletionItemKind.Method
+ of skParam: CompletionItemKind.Variable
+ of skProc: CompletionItemKind.Function
+ of skResult: CompletionItemKind.Value
+ of skTemplate: CompletionItemKind.Snippet
+ of skType: CompletionItemKind.Class
+ of skVar: CompletionItemKind.Field
+ of skFunc: CompletionItemKind.Function
+ else: CompletionItemKind.Property
+
+func nimSymToLSPKind*(suggest: byte): SymbolKind =
+ case TSymKind(suggest):
+ of skConst: SymbolKind.Constant
+ of skEnumField: SymbolKind.EnumMember
+ of skIterator: SymbolKind.Function
+ of skConverter: SymbolKind.Function
+ of skLet: SymbolKind.Variable
+ of skMacro: SymbolKind.Function
+ of skMethod: SymbolKind.Method
+ of skProc: SymbolKind.Function
+ of skTemplate: SymbolKind.Function
+ of skType: SymbolKind.Class
+ of skVar: SymbolKind.Variable
+ of skFunc: SymbolKind.Function
+ else: SymbolKind.Function
+
+func nimSymDetails*(suggest: Suggest): string =
+ case suggest.symKind.TSymKind:
+ of skConst: fmt"""const {suggest.qualifiedPath.join(".")}: {suggest.forth}"""
+ of skEnumField: "enum " & suggest.forth
+ of skForVar: "for var of " & suggest.forth
+ of skIterator: suggest.forth
+ of skLabel: "label"
+ of skLet: "let of " & suggest.forth
+ of skMacro: "macro"
+ of skMethod: suggest.forth
+ of skParam: "param"
+ of skProc: suggest.forth
+ of skResult: "result"
+ of skTemplate: suggest.forth
+ of skType: "type " & suggest.qualifiedPath.join(".")
+ of skVar: "var of " & suggest.forth
+ else: suggest.forth
+
+
+template createFullCommand(command: untyped) =
+ proc command*(nimsuggest: NimSuggest, file: string, dirtyfile = "",
+ line: int, col: int): seq[Suggest] =
+ nimsuggest.runCmd(`ide command`, AbsoluteFile file, AbsoluteFile dirtyfile, line, col)
+
+template createFileOnlyCommand(command: untyped) =
+ proc command*(nimsuggest: NimSuggest, file: string, dirtyfile = ""): seq[Suggest] =
+ nimsuggest.runCmd(`ide command`, AbsoluteFile file, AbsoluteFile dirtyfile, 0, 0)
+
+createFullCommand(sug)
+createFullCommand(con)
+createFullCommand(def)
+createFullCommand(use)
+createFullCommand(dus)
+createFileOnlyCommand(chk)
+createFileOnlyCommand(highlight)
+createFileOnlyCommand(outline)
+createFileOnlyCommand(known)
+
+proc `mod`*(nimsuggest: NimSuggest, file: string, dirtyfile = ""): seq[Suggest] =
+ nimsuggest.runCmd(ideMod, AbsoluteFile file, AbsoluteFile dirtyfile, 0, 0)
+
+when isMainModule:
+ import os, sequtils, algorithm
+ var graph = initNimSuggest(currentSourcePath.parentDir.parentDir / "nimlsp.nim", nimPath = currentSourcePath.parentDir.parentDir.parentDir)
+ var files = toSeq(walkDirRec(currentSourcePath.parentDir.parentDir)).filterIt(it.endsWith(".nim")).sorted()
+ for f in files:
+ echo "outline:" & f
+ let syms = graph.outline(f, f)
+ echo "outline: symbols(" & $syms.len & ")"
+
+ var suggestions = graph.sug(currentSourcePath, currentSourcePath, 86, 16)
+ echo "Got ", suggestions.len, " suggestions"
+ for suggestion in suggestions:
+ echo suggestion
\ No newline at end of file
diff --git a/nimlsp/nimlsppkg/suggestlib.nim.cfg b/nimlsp/nimlsppkg/suggestlib.nim.cfg
new file mode 100644
index 00000000000..89f43620c13
--- /dev/null
+++ b/nimlsp/nimlsppkg/suggestlib.nim.cfg
@@ -0,0 +1,20 @@
+hint[XDeclaredButNotUsed]:off
+
+path:"$lib/packages/docutils"
+path:"$config/.."
+path:"../.."
+define:useStdoutAsStdmsg
+define:nimsuggest
+define:nimcore
+
+# die when nimsuggest uses more than 4GB:
+@if cpu32:
+ define:"nimMaxHeap=2000"
+@else:
+ define:"nimMaxHeap=4000"
+@end
+
+--threads:off
+--warning[Spacing]:off # The JSON schema macro uses a syntax similar to TypeScript
+# --warning[CaseTransition]:off
+-d:nimOldCaseObjects
diff --git a/nimlsp/nimlsppkg/utfmapping.nim b/nimlsp/nimlsppkg/utfmapping.nim
new file mode 100644
index 00000000000..818a060aa8d
--- /dev/null
+++ b/nimlsp/nimlsppkg/utfmapping.nim
@@ -0,0 +1,73 @@
+import std/unicode
+
+
+type FingerTable = seq[tuple[u16pos, offset: int]]
+
+proc createUTFMapping*(line: string): FingerTable =
+ var pos = 0
+ for rune in line.runes:
+ #echo pos
+ #echo rune.int32
+ case rune.int32:
+ of 0x0000..0x007F:
+ # One UTF-16 unit, one UTF-8 unit
+ pos += 1
+ of 0x0080..0x07FF:
+ # One UTF-16 unit, two UTF-8 units
+ result.add (u16pos: pos, offset: 1)
+ pos += 1
+ of 0x0800..0xFFFF:
+ # One UTF-16 unit, three UTF-8 units
+ result.add (u16pos: pos, offset: 2)
+ pos += 1
+ of 0x10000..0x10FFFF:
+ # Two UTF-16 units, four UTF-8 units
+ result.add (u16pos: pos, offset: 2)
+ pos += 2
+ else: discard
+
+ #echo fingerTable
+
+proc utf16to8*(fingerTable: FingerTable, utf16pos: int): int =
+ result = utf16pos
+ for finger in fingerTable:
+ if finger.u16pos < utf16pos:
+ result += finger.offset
+ else:
+ break
+
+when isMainModule:
+ import termstyle
+ var x = "heållo☀☀wor𐐀𐐀☀ld heållo☀wor𐐀ld heållo☀wor𐐀ld"
+ var fingerTable = populateUTFMapping(x)
+
+ var corrected = utf16to8(fingerTable, 5)
+ for y in x:
+ if corrected == 0:
+ echo "-"
+ if ord(y) > 125:
+ echo ord(y).red
+ else:
+ echo ord(y)
+ corrected -= 1
+
+ echo "utf16\tchar\tutf8\tchar\tchk"
+ var pos = 0
+ for c in x.runes:
+ stdout.write pos
+ stdout.write '\t'
+ stdout.write c
+ stdout.write '\t'
+ var corrected = utf16to8(fingerTable, pos)
+ stdout.write corrected
+ stdout.write '\t'
+ stdout.write x.runeAt(corrected)
+ if c.int32 == x.runeAt(corrected).int32:
+ stdout.write "\tOK".green
+ else:
+ stdout.write "\tERR".red
+ stdout.write '\n'
+ if c.int >= 0x10000:
+ pos += 2
+ else:
+ pos += 1
diff --git a/nimlsp/tests/nim.cfg b/nimlsp/tests/nim.cfg
new file mode 100644
index 00000000000..28cc8e2ac15
--- /dev/null
+++ b/nimlsp/tests/nim.cfg
@@ -0,0 +1 @@
+--path:"../"
\ No newline at end of file
diff --git a/nimlsp/tests/test_messages2.nim b/nimlsp/tests/test_messages2.nim
new file mode 100644
index 00000000000..ec84a643837
--- /dev/null
+++ b/nimlsp/tests/test_messages2.nim
@@ -0,0 +1,38 @@
+import std / [unittest]
+include nimlsppkg / [messages, messageenums]
+
+let message = "Hello World"
+
+suite "Create ResponseError":
+ test "Generate a parse error message":
+ let error = JsonNode(create(ResponseError, ParseError.ord, message,
+ newJNull()))
+ check(getInt(error["code"]) == ord(ParseError))
+ check(getStr(error["message"]) == message)
+
+suite "Create ResponseMessage":
+ test "Generate a response":
+ let res = create(ResponseMessage, "2.0", 100, some(%*{"result": "Success"}),
+ none(ResponseError))
+ check(getStr(res["result"].unsafeGet()["result"]) == "Success")
+ test "Generate an error response":
+ let response = create(ResponseMessage, "2.0", 101, none(JsonNode), some(
+ create(ResponseError, ParseError.ord, message, newJNull())))
+ check(getInt(response["id"]) == 101)
+ check(getInt(response["error"].unsafeGet()["code"]) == ord(ParseError))
+
+suite "Read RequestMessage":
+ const requestMessage = """{
+ "jsonrpc": "2.0",
+ "id": 100,
+ "method": "something",
+ }"""
+ const notificationMessage = """{
+ "jsonrpc": "2.0",
+ "method": "something",
+ }"""
+ test "Verify RequestMessage":
+ check(parseJson(requestMessage).isValid(RequestMessage))
+ test "Verify NotificationMessage":
+ check(parseJson(notificationMessage).isValid(NotificationMessage))
+
diff --git a/nimsuggest/nimsuggest.nim b/nimsuggest/nimsuggest.nim
index fa10084c31f..1a93e3d9368 100644
--- a/nimsuggest/nimsuggest.nim
+++ b/nimsuggest/nimsuggest.nim
@@ -230,6 +230,7 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int;
conf.ideCmd = cmd
if cmd == ideUse and conf.suggestVersion != 0:
+ graph.vm = nil # discard the VM and JIT state
graph.resetAllModules()
var isKnownFile = true
let dirtyIdx = fileInfoIdx(conf, file, isKnownFile)
@@ -251,6 +252,9 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int;
let modIdx = graph.parentModule(dirtyIdx)
graph.markDirty dirtyIdx
graph.markClientsDirty dirtyIdx
+ # partially recompiling the project means that that VM and JIT state
+ # would become stale, which we prevent by discarding all of it:
+ graph.vm = nil
if conf.ideCmd != ideMod:
if isKnownFile:
graph.compileProject(modIdx)
diff --git a/tests/lang_syntax/astspec/tastspec.nim b/tests/lang_syntax/astspec/tastspec.nim
index 49db7decf87..30f49ecee9a 100644
--- a/tests/lang_syntax/astspec/tastspec.nim
+++ b/tests/lang_syntax/astspec/tastspec.nim
@@ -5,7 +5,7 @@ joinable: false
# this test should ensure that the AST doesn't change slightly without it getting noticed.
-import ../ast_pattern_matching
+import experimental/ast_pattern_matching
template expectNimNode(arg: untyped): NimNode = arg
## This template here is just to be injected by `myquote`, so that