Skip to content

Commit

Permalink
got lsp connection running in browser
Browse files Browse the repository at this point in the history
  • Loading branch information
Nimaoth committed Mar 9, 2024
1 parent 51feac9 commit ad4c3e3
Show file tree
Hide file tree
Showing 19 changed files with 272 additions and 153 deletions.
2 changes: 1 addition & 1 deletion config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ switch("path", "scripting")
switch("path", "src")

switch("d", "mingw")
switch("mm", "refc")
switch("mm", "orc")
switch("tlsEmulation", "off")
switch("d", "enableGui=true")
switch("d", "enableTerminal=true")
Expand Down
4 changes: 2 additions & 2 deletions src/misc/custom_logger.nim
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ proc newCustomLogger*(levelThreshold = logging.lvlAll, fmtStr = logging.defaultF
result.levelThreshold = levelThreshold
logging.addHandler(result)

proc enableFileLogger*(self: CustomLogger) =
proc enableFileLogger*(self: CustomLogger, filename = "messages.log") =
when not defined(js):
var file = open("messages.log", fmWrite)
var file = open(filename, fmWrite)
self.fileLogger = logging.newFileLogger(file, self.levelThreshold, self.fmtStr, flushThreshold=logging.lvlAll).some

proc enableConsoleLogger*(self: CustomLogger) =
Expand Down
17 changes: 16 additions & 1 deletion src/misc/util.nim
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,19 @@ type CatchableAssertion* = object of CatchableError
template softAssert*(condition: bool, message: string): untyped =
if not condition:
echo message
raise newException(CatchableAssertion, message)
raise newException(CatchableAssertion, message)

when defined(js):
# on js the normal string.add function can cause stack overflows when the
# argument is too long, because it uses x.apply(push, y)
template append*(x: var string, y: string): untyped =
## Concatenates `x` and `y` in place.
##
## See also `system.add`.
x = x & y
else:
template append*(x: var string, y: string) =
## Concatenates `x` and `y` in place.
##
## See also `system.add`.
x.add(y)
13 changes: 10 additions & 3 deletions src/servers/languages_server.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[os, osproc, asynchttpserver, strutils, strformat, uri, asyncfile, json]
import std/[os, osproc, asynchttpserver, strutils, strformat, uri, asyncfile, json, sequtils]
import misc/[custom_async, util, myjsonutils]
import router, server_utils

Expand All @@ -9,6 +9,10 @@ proc callback(req: Request): Future[void] {.async.} =

let headers = newHttpHeaders([("Access-Control-Allow-Origin", "*")])

let (workspaceName, hostedFolders) = block:
{.gcsafe.}:
(workspaceName, hostedFolders)

withRequest req:
options "/":
let headers = newHttpHeaders([
Expand All @@ -26,14 +30,17 @@ proc callback(req: Request): Future[void] {.async.} =
let executablePath = reqBody["path"].str
let additionalArgs = reqBody["args"].jsonTo seq[string]
let proxyPath = getCurrentDir() / "tools/lsp-ws.exe"
let args = @[fmt"--port:{port}", fmt"--exe:{executablePath}", "--"] & additionalArgs

let directories = hostedFolders.mapIt(fmt"{it.path}").join(";")
let args = @[fmt"--port:{port}", fmt"--exe:{executablePath}", fmt"--log:lsp-ws-{port}.log", fmt"--workspace:{workspaceName}", fmt"--directories:{directories}", "--"] & additionalArgs
let process = startProcess(proxyPath, args=args, options={poUsePath, poDaemon})

{.gcsafe.}:
processes.add process

let response = %*{
"port": port.int
"port": port.int,
"processId": os.getCurrentProcessId()
}

await req.respond(Http200, response.pretty, headers)
Expand Down
1 change: 1 addition & 0 deletions src/servers/server_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import misc/[custom_async]
proc `$`*(p: Port): string {.borrow.}

var hostedFolders*: seq[tuple[path: string, name: Option[string]]]
var workspaceName*: string

proc getFreePort*(): Port =
var server = newAsyncHttpServer()
Expand Down
13 changes: 11 additions & 2 deletions src/servers/workspace_server.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ type DirInfo = object
files: seq[string]
folders: seq[string]

var workspaceName: string
var ignoredPatterns: seq[Glob]

proc shouldIgnore(path: string): bool =
Expand Down Expand Up @@ -49,7 +48,15 @@ proc callback(req: Request): Future[void] {.async.} =
get "/info/name":
await req.respond(Http200, workspaceName, headers)

get "/info/workspace-folders":
let message = %hostedFolders.mapIt(%*{
"path": it.path,
"name": it.name,
})
await req.respond(Http200, $message, headers)

get "/relative-path/":
##
var relativePath = ""

let (name, actualPath) = path.splitWorkspacePath
Expand Down Expand Up @@ -83,7 +90,9 @@ proc callback(req: Request): Future[void] {.async.} =
else:
var folders: seq[string]
for i, f in hostedFolders:
folders.add "@" & f.name.get($i)
# todo
# folders.add "@" & f.name.get($i)
folders.add f.path
DirInfo(folders: folders)

let response = result.toJson
Expand Down
5 changes: 3 additions & 2 deletions src/text/language/language_server.nim
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import std/[options]
import misc/[custom_async, util]
import language_server_base
import workspaces/workspace

import language_server_lsp

{.used.}

proc getOrCreateLanguageServerImpl*(languageId: string, filename: string, workspaces: seq[string], languagesServer: Option[(string, int)] = (string, int).none): Future[Option[LanguageServer]] {.async.} =
let lsp = await getOrCreateLanguageServerLSP(languageId, workspaces, languagesServer)
proc getOrCreateLanguageServerImpl*(languageId: string, filename: string, workspaces: seq[string], languagesServer: Option[(string, int)] = (string, int).none, workspace = WorkspaceFolder.none): Future[Option[LanguageServer]] {.async.} =
let lsp = await getOrCreateLanguageServerLSP(languageId, workspaces, languagesServer, workspace)
if lsp.getSome(server):
return server.LanguageServer.some

Expand Down
3 changes: 2 additions & 1 deletion src/text/language/language_server_base.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import std/[options, tables]
import misc/[custom_async, custom_logger]
import scripting_api except DocumentEditor, TextDocumentEditor, AstDocumentEditor
import workspaces/workspace

type OnRequestSaveHandle* = distinct int

Expand Down Expand Up @@ -31,7 +32,7 @@ type TextCompletion* = object
typ*: string
doc*: string

var getOrCreateLanguageServer*: proc(languageId: string, filename: string, workspaces: seq[string], languagesServer: Option[(string, int)] = (string, int).none): Future[Option[LanguageServer]] = nil
var getOrCreateLanguageServer*: proc(languageId: string, filename: string, workspaces: seq[string], languagesServer: Option[(string, int)] = (string, int).none, workspace = WorkspaceFolder.none): Future[Option[LanguageServer]] = nil

method start*(self: LanguageServer): Future[void] {.base.} = discard
method stop*(self: LanguageServer) {.base.} = discard
Expand Down
58 changes: 40 additions & 18 deletions src/text/language/language_server_lsp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import scripting_api except DocumentEditor, TextDocumentEditor, AstDocumentEdito
import misc/[event, util, custom_logger, custom_async, myjsonutils]
import text/text_editor
import language_server_base, app, app_interface, config_provider, lsp_client
import workspaces/workspace as ws

logCategory "lsp"

Expand Down Expand Up @@ -32,7 +33,7 @@ method disconnect*(self: LanguageServerLSP) =
self.stop()
# todo: remove from languageServers

proc getOrCreateLanguageServerLSP*(languageId: string, workspaces: seq[string], languagesServer: Option[(string, int)] = (string, int).none): Future[Option[LanguageServerLSP]] {.async.} =
proc getOrCreateLanguageServerLSP*(languageId: string, workspaces: seq[string], languagesServer: Option[(string, int)] = (string, int).none, workspace = ws.WorkspaceFolder.none): Future[Option[LanguageServerLSP]] {.async.} =
if languageServers.contains(languageId):
return languageServers[languageId].some

Expand All @@ -52,8 +53,7 @@ proc getOrCreateLanguageServerLSP*(languageId: string, workspaces: seq[string],
else:
@[]


var client = LSPClient()
var client = LSPClient(workspace: workspace)
languageServers[languageId] = LanguageServerLSP(client: client)
await client.connect(exePath, workspaces, args, languagesServer)
client.run()
Expand All @@ -68,16 +68,28 @@ proc getOrCreateLanguageServerLSP*(languageId: string, workspaces: seq[string],
if textDocumentEditor.document.languageId != languageId:
return

asyncCheck client.notifyOpenedTextDocument(languageId, textDocumentEditor.document.fullPath, textDocumentEditor.document.contentString)
if textDocumentEditor.document.isLoadingAsync:
discard textDocumentEditor.document.onLoaded.subscribe proc(document: TextDocument) =
asyncCheck client.notifyOpenedTextDocument(languageId, document.fullPath, document.contentString)
else:
asyncCheck client.notifyOpenedTextDocument(languageId, textDocumentEditor.document.fullPath, textDocumentEditor.document.contentString)

discard textDocumentEditor.document.textInserted.subscribe proc(args: auto) =
# debugf"TEXT INSERTED {args.document.fullPath}:{args.location}: {args.text}"
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toSelection.toRange, text: args.text)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toSelection.toRange, text: args.text)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

discard textDocumentEditor.document.textDeleted.subscribe proc(args: auto) =
# debugf"TEXT DELETED {args.document.fullPath}: {args.selection}"
let changes = @[TextDocumentContentChangeEvent(`range`: args.selection.toRange)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)
if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.selection.toRange)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

discard gEditor.onEditorDeregistered.subscribe proc(editor: auto) =
if not (editor of TextDocumentEditor):
Expand All @@ -99,17 +111,27 @@ proc getOrCreateLanguageServerLSP*(languageId: string, workspaces: seq[string],
continue

# debugf"Register events for {textDocumentEditor.document.fullPath}"
asyncCheck client.notifyOpenedTextDocument(languageId, textDocumentEditor.document.fullPath, textDocumentEditor.document.contentString)
if textDocumentEditor.document.isLoadingAsync:
discard textDocumentEditor.document.onLoaded.subscribe proc(document: TextDocument) =
asyncCheck client.notifyOpenedTextDocument(languageId, document.fullPath, document.contentString)
else:
asyncCheck client.notifyOpenedTextDocument(languageId, textDocumentEditor.document.fullPath, textDocumentEditor.document.contentString)

discard textDocumentEditor.document.textInserted.subscribe proc(args: auto) =
# debugf"TEXT INSERTED {args.document.fullPath}:{args.location}: {args.text}"
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toSelection.toRange, text: args.text)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)
if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.location.toSelection.toRange, text: args.text)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

discard textDocumentEditor.document.textDeleted.subscribe proc(args: auto) =
# debugf"TEXT DELETED {args.document.fullPath}: {args.selection}"
let changes = @[TextDocumentContentChangeEvent(`range`: args.selection.toRange)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)
if client.fullDocumentSync:
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, args.document.contentString)
else:
let changes = @[TextDocumentContentChangeEvent(`range`: args.selection.toRange)]
asyncCheck client.notifyTextDocumentChanged(args.document.fullPath, args.document.version, changes)

log lvlInfo, fmt"Started language server for {languageId}"
return languageServers[languageId].some
Expand All @@ -131,16 +153,16 @@ method getDefinition*(self: LanguageServerLSP, filename: string, location: Curso
let parsedResponse = response.result

if parsedResponse.asLocation().getSome(location):
return Definition(filename: location.uri.parseUri.path.myNormalizedPath, location: (line: location.`range`.start.line, column: location.`range`.start.character)).some
return Definition(filename: location.uri.decodeUrl.parseUri.path.myNormalizedPath, location: (line: location.`range`.start.line, column: location.`range`.start.character)).some

if parsedResponse.asLocationSeq().getSome(locations) and locations.len > 0:
let location = locations[0]
return Definition(filename: location.uri.parseUri.path.myNormalizedPath, location: (line: location.`range`.start.line, column: location.`range`.start.character)).some
return Definition(filename: location.uri.decodeUrl.parseUri.path.myNormalizedPath, location: (line: location.`range`.start.line, column: location.`range`.start.character)).some

if parsedResponse.asLocationLinkSeq().getSome(locations) and locations.len > 0:
let location = locations[0]
return Definition(
filename: location.targetUri.parseUri.path.myNormalizedPath,
filename: location.targetUri.decodeUrl.parseUri.path.myNormalizedPath,
location: (line: location.targetSelectionRange.start.line, column: location.targetSelectionRange.start.character)).some

log(lvlError, "No definition found")
Expand Down Expand Up @@ -195,7 +217,7 @@ method getSymbols*(self: LanguageServerLSP, filename: string): Future[seq[Symbol
location: (line: r.location.range.start.line, column: r.location.range.start.character),
name: r.name,
symbolType: symbolKind,
filename: r.location.uri.parseUri.path.myNormalizedPath,
filename: r.location.uri.decodeUrl.parseUri.path.myNormalizedPath,
)

return completions
Expand All @@ -206,7 +228,7 @@ method getCompletions*(self: LanguageServerLSP, languageId: string, filename: st

let response = await self.client.getCompletions(filename, location.line, location.column)
if response.isError:
log(lvlError, fmt"Error: {response.error}")
log(lvlError, &"Error: {response.error.code}\n{response.error.message}")
return completionsResult

let completions = response.result
Expand Down
Loading

0 comments on commit ad4c3e3

Please sign in to comment.