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