Skip to content

Commit 1c1323f

Browse files
authored
textDocument/rename implemented (#26)
* textDocument/rename implemented * Only rename symbols inside the project * Add test for prepareRename Got tests passing * Initial support for prepareRename * Use rootUri sent by editor Add --run into tests task * Finish prepareRename support nils are returned instead of errors since throwing exceptions would either cause the LSP client to get stuck in a loop (When using rpc exceptions) or have massive message with stacktrace (if normal exceptions)
1 parent e36ce58 commit 1c1323f

File tree

7 files changed

+147
-35
lines changed

7 files changed

+147
-35
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ nimble build
6161
- Document symbols
6262
- Find references
6363
- Workspace symbols
64+
- Rename symbols
6465

6566
You can install `nimlangserver` using the instuctions for your text editor below:
6667

nimlangserver.nim

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ proc getProjectFile(fileUri: string, ls: LanguageServer): Future[string] {.async
124124
rootPath = AbsoluteDir(ls.initializeParams.rootUri.uriToPath)
125125
pathRelativeToRoot = string(AbsoluteFile(fileUri).relativeTo(rootPath))
126126
mappings = ls.getWorkspaceConfiguration.await().projectMapping.get(@[])
127-
127+
128128
for mapping in mappings:
129129
if find(cstring(pathRelativeToRoot), re(mapping.fileRegex), 0, pathRelativeToRoot.len) != -1:
130130
result = string(rootPath) / mapping.projectFile
@@ -162,30 +162,45 @@ proc initialize(ls: LanguageServer, params: InitializeParams):
162162
Future[InitializeResult] {.async.} =
163163
debug "Initialize received..."
164164
ls.initializeParams = params
165-
return InitializeResult(
165+
result = InitializeResult(
166166
capabilities: ServerCapabilities(
167167
textDocumentSync: some(%TextDocumentSyncOptions(
168168
openClose: some(true),
169169
change: some(TextDocumentSyncKind.Full.int),
170170
willSave: some(false),
171171
willSaveWaitUntil: some(false),
172-
save: some(SaveOptions(includeText: some(true))))),
172+
save: some(SaveOptions(includeText: some(true))))
173+
),
173174
hoverProvider: some(true),
174175
workspace: WorkspaceCapability(
175-
workspaceFolders: some(WorkspaceFolderCapability())),
176+
workspaceFolders: some(WorkspaceFolderCapability())
177+
),
176178
completionProvider: CompletionOptions(
177179
triggerCharacters: some(@["."]),
178-
resolveProvider: some(false)),
180+
resolveProvider: some(false)
181+
),
179182
definitionProvider: some(true),
180183
declarationProvider: some(true),
181184
typeDefinitionProvider: some(true),
182185
referencesProvider: some(true),
183186
documentHighlightProvider: some(true),
184187
workspaceSymbolProvider: some(true),
185188
executeCommandProvider: ExecuteCommandOptions(
186-
commands: some(@[RESTART_COMMAND, RECOMPILE_COMMAND, CHECK_PROJECT_COMMAND])),
189+
commands: some(@[RESTART_COMMAND, RECOMPILE_COMMAND, CHECK_PROJECT_COMMAND])
190+
),
187191
documentSymbolProvider: some(true),
188-
codeActionProvider: some(true)))
192+
codeActionProvider: some(true)
193+
)
194+
)
195+
# Support rename by default, but check if we can also support prepare
196+
result.capabilities.renameProvider = %true
197+
if params.capabilities.textDocument.isSome:
198+
let docCaps = params.capabilities.textDocument.unsafeGet()
199+
# Check if the client support prepareRename
200+
if docCaps.rename.isSome and docCaps.rename.get().prepareSupport.get(false):
201+
result.capabilities.renameProvider = %* {
202+
"prepareProvider": true
203+
}
189204

190205
proc initialized(ls: LanguageServer, _: JsonNode):
191206
Future[void] {.async.} =
@@ -692,6 +707,45 @@ proc references(ls: LanguageServer, params: ReferenceParams):
692707
.filter(suggest => suggest.section != ideDef or includeDeclaration)
693708
.map(toLocation);
694709

710+
proc prepareRename(ls: LanguageServer, params: PrepareRenameParams,
711+
id: int): Future[JsonNode] {.async.} =
712+
with (params.position, params.textDocument):
713+
let
714+
nimsuggest = await ls.getNimsuggest(uri)
715+
def = await nimsuggest.def(
716+
uriToPath(uri),
717+
ls.uriToStash(uri),
718+
line + 1,
719+
ls.getCharacter(uri, line, character)
720+
)
721+
if def.len == 0:
722+
return newJNull()
723+
# Check if the symbol belongs to the project
724+
let projectDir = ls.initializeParams.rootUri.uriToPath
725+
if def[0].filePath.isRelativeTo(projectDir):
726+
return %def[0].toLocation().range
727+
728+
return newJNull()
729+
730+
proc rename(ls: LanguageServer, params: RenameParams, id: int): Future[WorkspaceEdit] {.async.} =
731+
# We reuse the references command as to not duplicate it
732+
let references = await ls.references(ReferenceParams(
733+
context: ReferenceContext(includeDeclaration: true),
734+
textDocument: params.textDocument,
735+
position: params.position
736+
))
737+
# Build up list of edits that the client needs to perform for each file
738+
let projectDir = ls.initializeParams.rootUri.uriToPath
739+
var edits = newJObject()
740+
for reference in references:
741+
# Only rename symbols in the project.
742+
# If client supports prepareRename then an error will already have been thrown
743+
if reference.uri.uriToPath().isRelativeTo(projectDir):
744+
if reference.uri notin edits:
745+
edits[reference.uri] = newJArray()
746+
edits[reference.uri] &= %TextEdit(range: reference.range, newText: params.newName)
747+
result = WorkspaceEdit(changes: some edits)
748+
695749
proc codeAction(ls: LanguageServer, params: CodeActionParams):
696750
Future[seq[CodeAction]] {.async.} =
697751
let projectUri = await getProjectFile(params.textDocument.uri.uriToPath, ls)
@@ -849,6 +903,8 @@ proc registerHandlers*(connection: StreamConnection, pipeInput: AsyncInputStream
849903
connection.register("textDocument/hover", partial(hover, ls))
850904
connection.register("textDocument/references", partial(references, ls))
851905
connection.register("textDocument/codeAction", partial(codeAction, ls))
906+
connection.register("textDocument/prepareRename", partial(prepareRename, ls))
907+
connection.register("textDocument/rename", partial(rename, ls))
852908
connection.register("workspace/executeCommand", partial(executeCommand, ls))
853909
connection.register("workspace/symbol", partial(workspaceSymbol, ls))
854910
connection.register("textDocument/documentHighlight", partial(documentHighlight, ls))

nimlangserver.nimble

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,7 @@ requires "nim >= 1.0.0",
1616

1717
--path:"."
1818

19-
proc configForTests() =
20-
--hints: off
21-
--debuginfo
22-
--run
23-
--threads:on
24-
--silent
25-
--define:"debugLogging=on"
26-
--define:"chronicles_disable_thread_id"
27-
--define:"async_backend=asyncdispatch"
28-
--define:"chronicles_timestamps=None"
29-
--define:"debugLogging"
30-
3119
task test, "run tests":
32-
configForTests()
20+
--silent
21+
--run
3322
setCommand "c", "tests/all.nim"

protocol/types.nim

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ type
223223

224224
RenameCapability* = ref object of RootObj
225225
dynamicRegistration*: Option[bool]
226+
prepareSupport*: Option[bool]
226227

227228
PublishDiagnosticsCapability* = ref object of RootObj
228229
dynamicRegistration*: Option[bool]
@@ -315,6 +316,11 @@ type
315316
TextDocumentAndStaticRegistrationOptions* = ref object of TextDocumentRegistrationOptions
316317
id*: Option[string]
317318

319+
RenameOptions* = object
320+
# We support rename, but need to change json
321+
# depending on if the client supports prepare or not
322+
supportsPrepare*: bool
323+
318324
ServerCapabilities* = ref object of RootObj
319325
textDocumentSync*: OptionalNode # TextDocumentSyncOptions or int
320326
hoverProvider*: Option[bool]
@@ -333,7 +339,7 @@ type
333339
documentFormattingProvider*: Option[bool]
334340
documentRangeFormattingProvider*: Option[bool]
335341
documentOnTypeFormattingProvider*: DocumentOnTypeFormattingOptions
336-
renameProvider*: Option[bool]
342+
renameProvider*: JsonNode # bool or RenameOptions
337343
documentLinkProvider*: DocumentLinkOptions
338344
colorProvider*: OptionalNode # bool or ColorProviderOptions or TextDocumentAndStaticRegistrationOptions
339345
executeCommandProvider*: ExecuteCommandOptions
@@ -604,6 +610,13 @@ type
604610
position*: Position
605611
newName*: string
606612

613+
PrepareRenameParams* = ref object of RootObj
614+
textDocument*: TextDocumentIdentifier
615+
position*: Position
616+
617+
PrepareRenameResponse* = ref object of RootObj
618+
defaultBehaviour*: bool
619+
607620
SignatureHelpContext* = ref object of RootObj
608621
triggerKind*: int
609622
triggerCharacter*: Option[string]

tests/config.nims

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
--hints: off
2+
--debuginfo
3+
--threads:on
4+
--define:"debugLogging=on"
5+
--define:"chronicles_disable_thread_id"
6+
--define:"async_backend=asyncdispatch"
7+
--define:"chronicles_timestamps=None"
8+
--define:"debugLogging"

tests/projects/hw/hw.nim

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ var bbb = 100
44
bbb = 200
55
bbb = ""
66

7+
import std/options
8+
var
9+
x: Option[string]
10+
y: Option[string]
11+
712
import std/macros
813

914
macro myAssertMacroInner(arg: untyped): untyped =

tests/tnimlangserver.nim

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -148,26 +148,26 @@ suite "Suggest API selection":
148148
test "Suggest api":
149149
client.notify("textDocument/didOpen", %createDidOpenParams("projects/hw/hw.nim"))
150150
let (_, params) = suggestInit.read.waitFor
151-
doAssert %params ==
151+
check %params ==
152152
%ProgressParams(
153153
token: fmt "Creating nimsuggest for {uriToPath(helloWorldUri)}")
154-
doAssert "begin" == progress.read.waitFor[1].value.get()["kind"].getStr
155-
doAssert "end" == progress.read.waitFor[1].value.get()["kind"].getStr
154+
check "begin" == progress.read.waitFor[1].value.get()["kind"].getStr
155+
check "end" == progress.read.waitFor[1].value.get()["kind"].getStr
156156
client.notify("textDocument/didOpen",
157157
%createDidOpenParams("projects/hw/useRoot.nim"))
158158
let
159159
rootNimFileUri = "projects/hw/root.nim".fixtureUri.uriToPath
160160
rootParams2 = suggestInit.read.waitFor[1]
161161

162-
doAssert %rootParams2 ==
162+
check %rootParams2 ==
163163
%ProgressParams(token: fmt "Creating nimsuggest for {rootNimFileUri}")
164164

165-
doAssert "begin" == progress.read.waitFor[1].value.get()["kind"].getStr
166-
doAssert "end" == progress.read.waitFor[1].value.get()["kind"].getStr
165+
check "begin" == progress.read.waitFor[1].value.get()["kind"].getStr
166+
check "end" == progress.read.waitFor[1].value.get()["kind"].getStr
167167
let
168168
hoverParams = positionParams("projects/hw/hw.nim".fixtureUri, 2, 0)
169169
hover = client.call("textDocument/hover", %hoverParams).waitFor
170-
doAssert hover.kind == JNull
170+
check hover.kind == JNull
171171

172172

173173
suite "LSP features":
@@ -237,7 +237,7 @@ suite "LSP features":
237237
let
238238
hoverParams = positionParams( helloWorldUri, 2, 0)
239239
hover = client.call("textDocument/hover", %hoverParams).waitFor
240-
doAssert hover.kind == JNull
240+
check hover.kind == JNull
241241

242242
test "Definitions.":
243243
let
@@ -257,7 +257,7 @@ suite "LSP features":
257257
}
258258
}
259259
}]
260-
doAssert %locations == %expected
260+
check %locations == %expected
261261

262262
test "References.":
263263
let referenceParams = ReferenceParams %* {
@@ -299,7 +299,7 @@ suite "LSP features":
299299
}
300300
}
301301
}]
302-
doAssert %locations == %expected
302+
check %locations == %expected
303303

304304
test "References(exclude def)":
305305
let referenceParams = ReferenceParams %* {
@@ -330,7 +330,41 @@ suite "LSP features":
330330
}
331331
}
332332
}]
333-
doAssert %locations == %expected
333+
check %locations == %expected
334+
335+
test "Prepare rename":
336+
let renameParams = PrepareRenameParams(
337+
textDocument: TextDocumentIdentifier(uri: helloWorldUri),
338+
position: Position(line: 2, character: 6)
339+
)
340+
let resp = client.call("textDocument/prepareRename", %renameParams)
341+
.waitFor()
342+
check resp == %* {
343+
"start":{"line":2,"character":4},
344+
"end":{"line":2,"character":7}
345+
}
346+
347+
348+
test "Prepare rename doesn't allow non-project symbols":
349+
let renameParams = PrepareRenameParams(
350+
textDocument: TextDocumentIdentifier(uri: helloWorldUri),
351+
position: Position(line: 8, character: 10)
352+
)
353+
let resp = client.call("textDocument/prepareRename", %renameParams)
354+
.waitFor()
355+
check resp.kind == JNull
356+
357+
test "Rename":
358+
let renameParams = RenameParams(
359+
textDocument: TextDocumentIdentifier(uri: helloWorldUri),
360+
newName: "hello",
361+
position: Position(line: 2, character: 6)
362+
)
363+
let changes = client.call("textDocument/rename", %renameParams)
364+
.waitFor().to(WorkSpaceEdit).changes.get()
365+
check changes.len == 1
366+
check changes[helloWorldUri].len == 3
367+
check changes[helloWorldUri].mapIt(it["newText"].getStr()) == "hello".repeat(3)
334368

335369
test "didChange then sending hover.":
336370
let didChangeParams = DidChangeTextDocumentParams %* {
@@ -360,6 +394,7 @@ suite "LSP features":
360394
"uri": fixtureUri("projects/hw/hw.nim")
361395
}
362396
}
397+
363398
let actualEchoCompletionItem =
364399
to(waitFor client.call("textDocument/completion", %completionParams),
365400
seq[CompletionItem])
@@ -369,7 +404,7 @@ suite "LSP features":
369404
doAssert actualEchoCompletionItem.kind.get == 3
370405
doAssert actualEchoCompletionItem.detail.get().contains("proc")
371406
doAssert actualEchoCompletionItem.documentation.isSome
372-
407+
373408
test "Shutdown":
374409
let
375410
nullValue = newJNull()
@@ -410,7 +445,12 @@ suite "Null configuration:":
410445
"processId": %getCurrentProcessId(),
411446
"rootUri": fixtureUri("projects/hw/"),
412447
"capabilities": {
413-
"workspace": {"configuration": true}
448+
"workspace": {"configuration": true},
449+
"textDocument": {
450+
"rename": {
451+
"prepareSupport": true
452+
}
453+
}
414454
}
415455
}
416456

@@ -530,4 +570,4 @@ suite "LSP expand":
530570
# }},
531571
# "content":"proc helloProc(): string =\n result = \"Hello\"\n"
532572
# }
533-
# doAssert %expected == %expandResult
573+
# doAssert %expected == %expandResult

0 commit comments

Comments
 (0)