Skip to content

Commit f5c52e4

Browse files
authored
Adds the concept of idleFile a file (#280)
* Adds the concept of `idleFile` a file Adds the concept of `idleFile` a file thats open but there is no nimsuggest attached to it. Improve stability around managing nimsuggests instances * adds test: "after a period of inactivity, nimsuggest should be stopped" * Fixes and makes failing tests more robust
1 parent 8a5889f commit f5c52e4

File tree

10 files changed

+265
-132
lines changed

10 files changed

+265
-132
lines changed

ls.nim

Lines changed: 185 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ type
9494
cancelFileCheck*: Future[void]
9595
checkInProgress*: bool
9696
needsChecking*: bool
97+
textDocument*: TextDocumentItem
9798

9899
CommandLineParams* = object
99100
clientProcessId*: Option[int]
@@ -134,6 +135,7 @@ type
134135
onExit*: OnExitCallback
135136
projectFiles*: Table[string, Project]
136137
openFiles*: Table[string, NlsFileInfo]
138+
idleOpenFiles*: Table[string, NlsFileInfo] #We close the file when its inactive and store it here.
137139
workspaceConfiguration*: Future[JsonNode]
138140
prevWorkspaceConfiguration*: Future[JsonNode]
139141
inlayHintsRefreshRequest*: Future[JsonNode]
@@ -193,6 +195,7 @@ proc initLs*(params: CommandLineParams, storageDir: string): LanguageServer =
193195
filesWithDiags: initHashSet[string](),
194196
transportMode: params.transport.get(),
195197
openFiles: initTable[string, NlsFileInfo](),
198+
# idleOpenFiles: initTable[string, NlsFileInfo](),
196199
responseMap: newTable[string, Future[JsonNode]](),
197200
storageDir: storageDir,
198201
cmdLineClientProcessId: params.clientProcessId,
@@ -472,7 +475,8 @@ proc getNimSuggestPathAndVersion(
472475
nimsuggestPath = findExe "nimsuggest"
473476
else:
474477
nimVersion = getNimVersion(nimsuggestPath.parentDir)
475-
ls.showMessage(fmt "Using {nimVersion}", MessageType.Info)
478+
# ls.showMessage(fmt "Using {nimVersion}", MessageType.Info)
479+
debug "Using {nimVersion}", nimVersion = nimVersion
476480
(nimsuggestPath, nimVersion)
477481

478482
proc getNimPath*(conf: NlsConfig): Option[string] =
@@ -693,9 +697,146 @@ proc sendDiagnostics*(ls: LanguageServer, diagnostics: seq[Suggest] | seq[CheckR
693697
else:
694698
ls.filesWithDiags.excl path
695699

700+
701+
proc warnIfUnknown*(
702+
ls: LanguageServer, ns: Nimsuggest, uri: string, projectFile: string
703+
): Future[void] {.async, gcsafe.} =
704+
let path = uri.uriToPath
705+
let isFileKnown = await ns.isKnown(path)
706+
if not isFileKnown and not ns.canHandleUnknown:
707+
ls.showMessage(
708+
fmt """{path} is not compiled as part of project {projectFile}.
709+
In orde to get the IDE features working you must either configure nim.projectMapping or import the module.""",
710+
MessageType.Warning,
711+
)
712+
713+
proc createOrRestartNimsuggest*(
714+
ls: LanguageServer, projectFile: string, uri = ""
715+
) {.gcsafe, raises: [].}
716+
717+
proc getNimsuggestInner(ls: LanguageServer, uri: string): Future[Nimsuggest] {.async.} =
718+
assert uri in ls.openFiles, "File not open"
719+
720+
let projectFile = await ls.openFiles[uri].projectFile
721+
if not ls.projectFiles.hasKey(projectFile):
722+
debug "Creating new nimsuggest instance", uri = uri, projectFile = projectFile
723+
ls.createOrRestartNimsuggest(projectFile, uri)
724+
# Wait a bit to allow nimsuggest to start
725+
await sleepAsync(10)
726+
727+
# Check multiple times with small delays
728+
var attempts = 0
729+
const maxAttempts = 10
730+
while attempts < maxAttempts:
731+
if projectFile in ls.projectFiles:
732+
ls.lastNimsuggest = ls.projectFiles[projectFile].ns
733+
return await ls.projectFiles[projectFile].ns
734+
735+
inc attempts
736+
if attempts < maxAttempts:
737+
await sleepAsync(100)
738+
debug "Waiting for nimsuggest to initialize",
739+
uri = uri,
740+
projectFile = projectFile,
741+
attempt = attempts
742+
743+
debug "Failed to get nimsuggest after waiting", uri = uri, projectFile = projectFile
744+
return nil
745+
746+
proc tryGetNimsuggest*(
747+
ls: LanguageServer, uri: string
748+
): Future[Option[Nimsuggest]] {.raises:[], gcsafe.}
749+
750+
proc checkFile*(ls: LanguageServer, uri: string): Future[void] {.raises:[], gcsafe.}
751+
752+
proc didCloseFile*(
753+
ls: LanguageServer, uri: string
754+
): Future[void] {.async, gcsafe.} =
755+
debug "Closed the following document:", uri = uri
756+
757+
if ls.openFiles[uri].changed:
758+
# check the file if it is closed but not saved.
759+
traceAsyncErrors ls.checkFile(uri)
760+
761+
ls.openFiles.del uri
762+
763+
proc makeIdleFile*(
764+
ls: LanguageServer, file: NlsFileInfo
765+
): Future[void] {.async, gcsafe.} =
766+
let uri = file.textDocument.uri
767+
if uri in ls.openFiles:
768+
await ls.didCloseFile(uri)
769+
ls.idleOpenFiles[uri] = file
770+
ls.openFiles.del(uri)
771+
772+
proc getProjectFile*(fileUri: string, ls: LanguageServer): Future[string] {.raises:[], gcsafe.}
773+
774+
proc didOpenFile*(
775+
ls: LanguageServer, textDocument: TextDocumentItem
776+
): Future[void] {.async, gcsafe.} =
777+
with textDocument:
778+
debug "New document opened for URI:", uri = uri
779+
let
780+
file = open(ls.uriStorageLocation(uri), fmWrite)
781+
projectFileFuture = getProjectFile(uriToPath(uri), ls)
782+
783+
ls.openFiles[uri] =
784+
NlsFileInfo(projectFile: projectFileFuture, changed: false, fingerTable: @[], textDocument: textDocument)
785+
786+
if uri in ls.idleOpenFiles:
787+
ls.idleOpenFiles.del(uri)
788+
789+
let projectFile = await projectFileFuture
790+
debug "Document associated with the following projectFile",
791+
uri = uri, projectFile = projectFile
792+
if not ls.projectFiles.hasKey(projectFile):
793+
debug "Will create nimsuggest for this file", uri = uri
794+
ls.createOrRestartNimsuggest(projectFile, uri)
795+
796+
for line in text.splitLines:
797+
if uri in ls.openFiles:
798+
ls.openFiles[uri].fingerTable.add line.createUTFMapping()
799+
file.writeLine line
800+
file.close()
801+
ls.tryGetNimSuggest(uri).addCallback do(fut: Future[Option[Nimsuggest]]) -> void:
802+
if not fut.failed and fut.read.isSome:
803+
discard ls.warnIfUnknown(fut.read.get(), uri, projectFile)
804+
805+
let projectFileUri = projectFile.pathToUri
806+
if projectFileUri notin ls.openFiles:
807+
var textDocument = textDocument
808+
textDocument.uri = projectFileUri
809+
await didOpenFile(ls, textDocument)
810+
811+
debug "Opening project file", uri = projectFile, file = uri
812+
ls.showMessage(fmt "Opening {uri}", MessageType.Info)
813+
696814
proc tryGetNimsuggest*(
697815
ls: LanguageServer, uri: string
698-
): Future[Option[Nimsuggest]] {.async.}
816+
): Future[Option[Nimsuggest]] {.async.} =
817+
818+
if uri in ls.idleOpenFiles:
819+
let idleFile = ls.idleOpenFiles[uri]
820+
await didOpenFile(ls, idleFile.textDocument)
821+
822+
if uri notin ls.openFiles:
823+
return none(NimSuggest)
824+
825+
var retryCount = 0
826+
const maxRetries = 3
827+
while retryCount < maxRetries:
828+
let ns = await getNimsuggestInner(ls, uri)
829+
if not ns.isNil:
830+
return some ns
831+
832+
# If nimsuggest is nil, wait a bit and retry
833+
inc retryCount
834+
if retryCount < maxRetries:
835+
debug "Nimsuggest not ready, retrying...", uri = uri, attempt = retryCount
836+
await sleepAsync(10000 * retryCount) # Exponential backoff
837+
838+
debug "Nimsuggest not found after retries", uri = uri
839+
return none(NimSuggest)
699840

700841
proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async, gcsafe.} =
701842
if not ls.getWorkspaceConfiguration.await().autoCheckProject.get(true):
@@ -781,9 +922,7 @@ proc checkProject*(ls: LanguageServer, uri: string): Future[void] {.async, gcsaf
781922
debug "Running delayed check project...", uri = uri
782923
traceAsyncErrors ls.checkProject(uri)
783924

784-
proc createOrRestartNimsuggest*(
785-
ls: LanguageServer, projectFile: string, uri = ""
786-
) {.gcsafe, raises: [].}
925+
787926

788927
proc onErrorCallback(args: (LanguageServer, string), project: Project) =
789928
let
@@ -816,6 +955,7 @@ proc createOrRestartNimsuggest*(
816955
ls: LanguageServer, projectFile: string, uri = ""
817956
) {.gcsafe, raises: [].} =
818957
try:
958+
debug "Starting createOrRestartNimsuggest", projectFile = projectFile, uri = uri
819959
let
820960
configuration = ls.getWorkspaceConfiguration().waitFor()
821961
workingDir = ls.getWorkingDir(projectFile).waitFor()
@@ -832,64 +972,43 @@ proc createOrRestartNimsuggest*(
832972
ls.createOrRestartNimsuggest(projectFile, uri)
833973
ls.sendStatusChanged()
834974
errorCallback = partial(onErrorCallback, (ls, uri))
835-
#TODO instead of waiting here, this whole function should be async.
836-
projectNext = waitFor createNimsuggest(
837-
projectFile,
838-
nimsuggestPath,
839-
version,
840-
timeout,
841-
restartCallback,
842-
errorCallback,
843-
workingDir,
844-
configuration.logNimsuggest.get(false),
845-
configuration.exceptionHintsEnabled,
846-
)
847-
token = fmt "Creating nimsuggest for {projectFile}"
848-
849-
ls.workDoneProgressCreate(token)
850-
851-
if ls.projectFiles.hasKey(projectFile):
975+
976+
debug "Creating new nimsuggest project", projectFile = projectFile
977+
let projectNext = waitFor createNimsuggest(
978+
projectFile,
979+
nimsuggestPath,
980+
version,
981+
timeout,
982+
restartCallback,
983+
errorCallback,
984+
workingDir,
985+
configuration.logNimsuggest.get(false),
986+
configuration.exceptionHintsEnabled,
987+
)
988+
989+
if projectFile in ls.projectFiles:
852990
var project = ls.projectFiles[projectFile]
853991
project.stop()
854-
ls.projectFiles[projectFile] = projectNext
855-
ls.progress(token, "begin", fmt "Restarting nimsuggest for {projectFile}")
856-
else:
857-
ls.progress(token, "begin", fmt "Creating nimsuggest for {projectFile}")
858-
ls.projectFiles[projectFile] = projectNext
859-
992+
ls.projectFiles[projectFile] = projectNext
993+
860994
projectNext.ns.addCallback do(fut: Future[Nimsuggest]):
861-
if fut.read.project.failed:
862-
let msg = fut.read.project.errorMessage
995+
if fut.failed:
996+
let msg = fut.error.msg
997+
error "Nimsuggest initialization failed", projectFile = projectFile, error = msg
863998
ls.showMessage(
864999
fmt "Nimsuggest initialization for {projectFile} failed with: {msg}",
8651000
MessageType.Error,
8661001
)
8671002
else:
1003+
debug "Nimsuggest initialized successfully", projectFile = projectFile
8681004
ls.showMessage(fmt "Nimsuggest initialized for {projectFile}", MessageType.Info)
8691005
traceAsyncErrors ls.checkProject(uri)
8701006
fut.read().openFiles.incl uri
871-
ls.progress(token, "end")
8721007
ls.sendStatusChanged()
873-
except CatchableError:
874-
discard
875-
876-
proc getNimsuggestInner(ls: LanguageServer, uri: string): Future[Nimsuggest] {.async.} =
877-
assert uri in ls.openFiles, "File not open"
878-
879-
let projectFile = await ls.openFiles[uri].projectFile
880-
if not ls.projectFiles.hasKey(projectFile):
881-
ls.createOrRestartNimsuggest(projectFile, uri)
882-
883-
ls.lastNimsuggest = ls.projectFiles[projectFile].ns
884-
return await ls.projectFiles[projectFile].ns
885-
886-
proc tryGetNimsuggest*(
887-
ls: LanguageServer, uri: string
888-
): Future[Option[Nimsuggest]] {.async.} =
889-
if uri notin ls.openFiles:
890-
none(NimSuggest)
891-
else:
892-
some await getNimsuggestInner(ls, uri)
1008+
except CatchableError as ex:
1009+
error "Failed to create/restart nimsuggest",
1010+
projectFile = projectFile,
1011+
error = ex.msg
8931012

8941013
proc restartAllNimsuggestInstances(ls: LanguageServer) =
8951014
debug "Restarting all nimsuggest instances"
@@ -1006,17 +1125,7 @@ proc getProjectFile*(fileUri: string, ls: LanguageServer): Future[string] {.asyn
10061125

10071126
debug "getProjectFile ", project = result, fileUri = fileUri
10081127

1009-
proc warnIfUnknown*(
1010-
ls: LanguageServer, ns: Nimsuggest, uri: string, projectFile: string
1011-
): Future[void] {.async, gcsafe.} =
1012-
let path = uri.uriToPath
1013-
let isFileKnown = await ns.isKnown(path)
1014-
if not isFileKnown and not ns.canHandleUnknown:
1015-
ls.showMessage(
1016-
fmt """{path} is not compiled as part of project {projectFile}.
1017-
In orde to get the IDE features working you must either configure nim.projectMapping or import the module.""",
1018-
MessageType.Warning,
1019-
)
1128+
10201129

10211130
proc checkFile*(ls: LanguageServer, uri: string): Future[void] {.async.} =
10221131
let conf = await ls.getAndWaitForWorkspaceConfiguration()
@@ -1072,15 +1181,26 @@ proc removeIdleNimsuggests*(ls: LanguageServer) {.async.} =
10721181
for project in toStop:
10731182
debug "Removing idle nimsuggest", project = project.file
10741183
project.errorCallback = none(ProjectCallback)
1184+
1185+
let ns = await project.ns
1186+
for uri in ns.openFiles:
1187+
debug "Removing idle nimsuggest open file", uri = uri
1188+
await ls.makeIdleFile(ls.openFiles[uri])
10751189
project.stop()
10761190
ls.projectFiles.del(project.file)
1191+
10771192
ls.showMessage(
10781193
fmt"Nimsuggest for {project.file} was stopped because it was idle for too long",
10791194
MessageType.Info,
10801195
)
10811196
10821197
proc tick*(ls: LanguageServer): Future[void] {.async.} =
10831198
# debug "Ticking at ", now = now(), prs = ls.pendingRequests.len
1084-
ls.removeCompletedPendingRequests()
1085-
await ls.removeIdleNimsuggests()
1086-
ls.sendStatusChanged
1199+
try:
1200+
ls.removeCompletedPendingRequests()
1201+
await ls.removeIdleNimsuggests()
1202+
ls.sendStatusChanged
1203+
except CatchableError as ex:
1204+
error "Error in tick", msg = ex.msg
1205+
writeStacktrace(ex)
1206+

nimlangserver.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ proc registerProcMonitor(ls: LanguageServer) =
133133

134134
hookAsyncProcMonitor(ls.cmdLineClientProcessId.get, onCmdLineClientProcessExit)
135135

136-
proc tickLs(ls: LanguageServer, time = 1.seconds) {.async.} =
136+
proc tickLs*(ls: LanguageServer, time = 1.seconds) {.async.} =
137137
await ls.tick()
138138
await sleepAsync(time)
139139
await ls.tickLs()

0 commit comments

Comments
 (0)