diff --git a/CHANGELOG.md b/CHANGELOG.md index 0417098d6..69f8d9bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ #### :rocket: New Feature - Add support for the rewatch build system for incremental compilation. https://github.com/rescript-lang/rescript-vscode/pull/965 +- Code Actions: open Implementation/Interface/Compiled Js and create Interface file. https://github.com/rescript-lang/rescript-vscode/pull/767 ## 1.50.0 diff --git a/analysis/src/CodeActions.ml b/analysis/src/CodeActions.ml index 0d383685c..8fffec1ce 100644 --- a/analysis/src/CodeActions.ml +++ b/analysis/src/CodeActions.ml @@ -5,14 +5,18 @@ let stringifyCodeActions codeActions = Printf.sprintf {|%s|} (codeActions |> List.map Protocol.stringifyCodeAction |> Protocol.array) -let make ~title ~kind ~uri ~newText ~range = - let uri = uri |> Uri.fromPath |> Uri.toString in - { - Protocol.title; - codeActionKind = kind; - edit = - { - documentChanges = - [{textDocument = {version = None; uri}; edits = [{newText; range}]}]; - }; - } +let make ~title ~kind ~edit ~command = + {Protocol.title; codeActionKind = kind; edit; command} + +let makeEdit edits uri = + Protocol. + { + documentChanges = + [ + { + textDocument = + {version = None; uri = uri |> Uri.fromPath |> Uri.toString}; + edits; + }; + ]; + } diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index c861c5b01..f4113cd08 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -433,18 +433,22 @@ let test ~path = in Sys.remove currentFile; codeActions - |> List.iter (fun {Protocol.title; edit = {documentChanges}} -> - Printf.printf "Hit: %s\n" title; - documentChanges - |> List.iter (fun {Protocol.edits} -> - edits - |> List.iter (fun {Protocol.range; newText} -> - let indent = - String.make range.start.character ' ' - in - Printf.printf "%s\nnewText:\n%s<--here\n%s%s\n" - (Protocol.stringifyRange range) - indent indent newText))) + |> List.iter (fun {Protocol.title; edit} -> + match edit with + | Some {documentChanges} -> + Printf.printf "Hit: %s\n" title; + documentChanges + |> List.iter (fun {Protocol.edits} -> + edits + |> List.iter (fun {Protocol.range; newText} -> + let indent = + String.make range.start.character ' ' + in + Printf.printf + "%s\nnewText:\n%s<--here\n%s%s\n" + (Protocol.stringifyRange range) + indent indent newText)) + | None -> ()) | "c-a" -> let hint = String.sub rest 3 (String.length rest - 3) in print_endline diff --git a/analysis/src/Hint.ml b/analysis/src/Hint.ml index 227d70f38..17d940929 100644 --- a/analysis/src/Hint.ml +++ b/analysis/src/Hint.ml @@ -166,6 +166,7 @@ let codeLens ~path ~debug = single line in the editor. *) title = typeExpr |> Shared.typeToString ~lineWidth:400; + arguments = None; }; }) | _ -> None) diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index e10411664..97301529d 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -3,7 +3,7 @@ type range = {start: position; end_: position} type markupContent = {kind: string; value: string} (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#command *) -type command = {title: string; command: string} +type command = {title: string; command: string; arguments: string list option} (* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeLens *) type codeLens = {range: range; command: command option} @@ -76,12 +76,13 @@ type textDocumentEdit = { } type codeActionEdit = {documentChanges: textDocumentEdit list} -type codeActionKind = RefactorRewrite +type codeActionKind = RefactorRewrite | Empty type codeAction = { title: string; codeActionKind: codeActionKind; - edit: codeActionEdit; + edit: codeActionEdit option; + command: command option; } let null = "null" @@ -224,7 +225,7 @@ let stringifyoptionalVersionedTextDocumentIdentifier td = | Some v -> string_of_int v) (Json.escape td.uri) -let stringifyTextDocumentEdit tde = +let stringifyTextDocumentEdit (tde : textDocumentEdit) = Printf.sprintf {|{ "textDocument": %s, "edits": %s @@ -235,15 +236,37 @@ let stringifyTextDocumentEdit tde = let codeActionKindToString kind = match kind with | RefactorRewrite -> "refactor.rewrite" + | Empty -> "" let stringifyCodeActionEdit cae = Printf.sprintf {|{"documentChanges": %s}|} (cae.documentChanges |> List.map stringifyTextDocumentEdit |> array) -let stringifyCodeAction ca = - Printf.sprintf {|{"title": "%s", "kind": "%s", "edit": %s}|} ca.title - (codeActionKindToString ca.codeActionKind) - (ca.edit |> stringifyCodeActionEdit) +let stringifyCommand (command : command) = + stringifyObject + [ + ("title", Some (wrapInQuotes command.title)); + ("command", Some (wrapInQuotes command.command)); + ( "arguments", + match command.arguments with + | None -> None + | Some args -> Some (args |> List.map wrapInQuotes |> array) ); + ] + +let stringifyCodeAction (ca : codeAction) = + stringifyObject + [ + ("title", Some (wrapInQuotes ca.title)); + ("kind", Some (wrapInQuotes (codeActionKindToString ca.codeActionKind))); + ( "edit", + match ca.edit with + | None -> None + | Some edit -> Some (edit |> stringifyCodeActionEdit) ); + ( "command", + match ca.command with + | None -> None + | Some command -> Some (command |> stringifyCommand) ); + ] let stringifyHint hint = Printf.sprintf @@ -256,12 +279,6 @@ let stringifyHint hint = }|} (stringifyPosition hint.position) (Json.escape hint.label) hint.kind hint.paddingLeft hint.paddingRight - -let stringifyCommand (command : command) = - Printf.sprintf {|{"title": "%s", "command": "%s"}|} - (Json.escape command.title) - (Json.escape command.command) - let stringifyCodeLens (codeLens : codeLens) = Printf.sprintf {|{ diff --git a/analysis/src/Xform.ml b/analysis/src/Xform.ml index 699a3cfa9..eff8d37de 100644 --- a/analysis/src/Xform.ml +++ b/analysis/src/Xform.ml @@ -112,7 +112,8 @@ module IfThenElse = struct let newText = printExpr ~range newExpr in let codeAction = CodeActions.make ~title:"Replace with switch" ~kind:RefactorRewrite - ~uri:path ~newText ~range + ~command:None + ~edit:(Some (CodeActions.makeEdit [{newText; range}] path)) in codeActions := codeAction :: !codeActions end @@ -176,7 +177,8 @@ module AddBracesToFn = struct let newText = printStructureItem ~range newStructureItem in let codeAction = CodeActions.make ~title:"Add braces to function" ~kind:RefactorRewrite - ~uri:path ~newText ~range + ~command:None + ~edit:(Some (CodeActions.makeEdit [{newText; range}] path)) in codeActions := codeAction :: !codeActions end @@ -249,7 +251,8 @@ module AddTypeAnnotation = struct in let codeAction = CodeActions.make ~title:"Add type annotation" ~kind:RefactorRewrite - ~uri:path ~newText ~range + ~command:None + ~edit:(Some (CodeActions.makeEdit [{newText; range}] path)) in codeActions := codeAction :: !codeActions | _ -> ())) @@ -384,8 +387,9 @@ module ExhaustiveSwitch = struct printExpr ~range {expr with pexp_desc = Pexp_match (expr, cases)} in let codeAction = - CodeActions.make ~title:"Exhaustive switch" ~kind:RefactorRewrite - ~uri:path ~newText ~range + CodeActions.make ~command:None ~title:"Exhaustive switch" + ~kind:RefactorRewrite + ~edit:(Some (CodeActions.makeEdit [{newText; range}] path)) in codeActions := codeAction :: !codeActions)) | Some (Switch {switchExpr; completionExpr; pos}) -> ( @@ -410,8 +414,9 @@ module ExhaustiveSwitch = struct {switchExpr with pexp_desc = Pexp_match (completionExpr, cases)} in let codeAction = - CodeActions.make ~title:"Exhaustive switch" ~kind:RefactorRewrite - ~uri:path ~newText ~range + CodeActions.make ~title:"Exhaustive switch" ~command:None + ~kind:RefactorRewrite (* ~uri:path ~newText ~range *) + ~edit:(Some (CodeActions.makeEdit [{newText; range}] path)) in codeActions := codeAction :: !codeActions)) end @@ -508,7 +513,8 @@ module AddDocTemplate = struct let newText = printSignatureItem ~range signatureItem in let codeAction = CodeActions.make ~title:"Add Documentation template" - ~kind:RefactorRewrite ~uri:path ~newText ~range + ~kind:RefactorRewrite ~command:None + ~edit:(Some (CodeActions.makeEdit [{newText; range}] path)) in codeActions := codeAction :: !codeActions | None -> ()) @@ -593,13 +599,94 @@ module AddDocTemplate = struct let newText = printStructureItem ~range structureItem in let codeAction = CodeActions.make ~title:"Add Documentation template" - ~kind:RefactorRewrite ~uri:path ~newText ~range + ~kind:RefactorRewrite ~command:None + ~edit:(Some (CodeActions.makeEdit [{newText; range}] path)) in codeActions := codeAction :: !codeActions | None -> ()) end end +module ExecuteCommands = struct + let openCompiled = "rescriptls/open-compiled-file" + let openInterface = "rescriptls/open-interface-file" + let openImplementation = "rescriptls/open-implementation-file" + let createInterface = "rescriptls/create-interface-file" +end + +module OpenCompiledFile = struct + let xform ~path ~codeActions = + let uri = path |> Uri.fromPath |> Uri.toString in + let codeAction = + CodeActions.make ~title:"Open Compiled JS" ~kind:Empty ~edit:None + ~command: + (Some + Protocol. + { + title = "Open Compiled File"; + command = ExecuteCommands.openCompiled; + arguments = Some [uri]; + }) + in + codeActions := codeAction :: !codeActions +end + +module HandleImpltInter = struct + type t = Create | OpenImpl | OpenInter + let xform ~path ~codeActions = + match Files.classifySourceFile path with + | Res -> + let resiFile = path ^ "i" in + if Sys.file_exists resiFile then + let uri = resiFile |> Uri.fromPath |> Uri.toString in + let title = "Open " ^ Filename.basename uri in + let openResi = + CodeActions.make ~title ~kind:Empty ~edit:None + ~command: + (Some + Protocol. + { + title = "Open Interface File"; + command = ExecuteCommands.openInterface; + arguments = Some [uri]; + }) + in + codeActions := openResi :: !codeActions + else + let uri = path |> Uri.fromPath |> Uri.toString in + let title = "Create " ^ Filename.basename uri ^ "i" in + let createResi = + CodeActions.make ~title ~kind:Empty ~edit:None + ~command: + (Some + Protocol. + { + title = "Create Interface File"; + command = ExecuteCommands.createInterface; + arguments = Some [uri]; + }) + in + codeActions := createResi :: !codeActions + | Resi -> + let resFile = Filename.remove_extension path ^ ".res" in + if Sys.file_exists resFile then + let uri = resFile |> Uri.fromPath |> Uri.toString in + let title = "Open " ^ Filename.basename uri in + let openRes = + CodeActions.make ~title ~kind:Empty ~edit:None + ~command: + (Some + Protocol. + { + title = "Open Implementation File"; + command = ExecuteCommands.openImplementation; + arguments = Some [uri]; + }) + in + codeActions := openRes :: !codeActions + | Other -> () +end + let parseImplementation ~filename = let {Res_driver.parsetree = structure; comments} = Res_driver.parsingEngine.parseImplementation ~forPrinter:false ~filename @@ -661,6 +748,8 @@ let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug = AddBracesToFn.xform ~pos ~codeActions ~path ~printStructureItem structure; AddDocTemplate.Implementation.xform ~pos ~codeActions ~path ~printStructureItem ~structure; + OpenCompiledFile.xform ~path ~codeActions; + HandleImpltInter.xform ~path ~codeActions; (* This Code Action needs type info *) let () = @@ -680,5 +769,7 @@ let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug = let signature, printSignatureItem = parseInterface ~filename:currentFile in AddDocTemplate.Interface.xform ~pos ~codeActions ~path ~signature ~printSignatureItem; + OpenCompiledFile.xform ~path ~codeActions; + HandleImpltInter.xform ~path ~codeActions; !codeActions | Other -> [] diff --git a/analysis/tests/src/expected/CodeLens.res.txt b/analysis/tests/src/expected/CodeLens.res.txt index 06472d5e4..24cec197b 100644 --- a/analysis/tests/src/expected/CodeLens.res.txt +++ b/analysis/tests/src/expected/CodeLens.res.txt @@ -1,15 +1,27 @@ Code Lens src/CodeLens.res [{ "range": {"start": {"line": 9, "character": 4}, "end": {"line": 9, "character": 8}}, - "command": {"title": "{\"name\": string} => React.element", "command": ""} + "command": { + "title": "{\"name\": string} => React.element", + "command": "" + } }, { "range": {"start": {"line": 4, "character": 4}, "end": {"line": 4, "character": 6}}, - "command": {"title": "(~opt1: int=?, ~a: int, ~b: int, unit, ~opt2: int=?, unit, ~c: int) => int", "command": ""} + "command": { + "title": "(~opt1: int=?, ~a: int, ~b: int, unit, ~opt2: int=?, unit, ~c: int) => int", + "command": "" + } }, { "range": {"start": {"line": 2, "character": 4}, "end": {"line": 2, "character": 7}}, - "command": {"title": "(~age: int, ~name: string) => string", "command": ""} + "command": { + "title": "(~age: int, ~name: string) => string", + "command": "" + } }, { "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 7}}, - "command": {"title": "(int, int) => int", "command": ""} + "command": { + "title": "(int, int) => int", + "command": "" + } }] diff --git a/server/src/server.ts b/server/src/server.ts index cfef4f572..836ea206b 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1023,6 +1023,69 @@ function openCompiledFile(msg: p.RequestMessage): p.Message { return response; } +enum ExecuteCommands { + OpenCompiledFile = "rescriptls/open-compiled-file", + OpenInterfaceFile = "rescriptls/open-interface-file", + OpenImplentationFile = "rescriptls/open-implementation-file", + CreateInterfaceFile = "rescriptls/create-interface-file" +} + +function executeCommand(msg: p.RequestMessage): p.Message { + let params = msg.params as p.ExecuteCommandParams; + let command = params.command as ExecuteCommands; + let args = params.arguments; + + let response: p.ResponseMessage = { + jsonrpc: c.jsonrpcVersion, + id: msg.id, + result: null, + }; + + if (!args) { + return response; + } + + let uri = args[0]; + + let reqParams: p.ShowDocumentParams = { + uri, + takeFocus: true, + } + + let request: p.RequestMessage = { + jsonrpc: c.jsonrpcVersion, + id: serverSentRequestIdCounter++, + method: "window/showDocument", + params: reqParams + } + + if (command === ExecuteCommands.OpenCompiledFile) { + let message = openCompiledFile({ ...msg, params: { uri } }) + if (p.Message.isResponse(message)) { + let { uri } = message.result as p.TextDocumentIdentifier + let showDocument: p.RequestMessage = { ...request, params: { uri } } + send(showDocument); + } else { + send(message) + } + } else if (command === ExecuteCommands.CreateInterfaceFile) { + let message = createInterface({ ...msg, params: { uri } }) + if (p.Message.isResponse(message)) { + let { uri } = message.result as p.TextDocumentIdentifier + let showDocument: p.RequestMessage = { ...request, params: { uri } } + send(showDocument); + } else { + send(message) + } + } else if (command === ExecuteCommands.OpenInterfaceFile) { + send(request); + } else if (command === ExecuteCommands.OpenImplentationFile) { + send(request); + } + + return response +} + function onMessage(msg: p.Message) { if (p.Message.isNotification(msg)) { // notification message, aka the client ends it and doesn't want a reply @@ -1124,6 +1187,9 @@ function onMessage(msg: p.Message) { triggerCharacters: [".", ">", "@", "~", '"', "=", "("], resolveProvider: true, }, + executeCommandProvider: { + commands: [ExecuteCommands.OpenImplentationFile, ExecuteCommands.OpenInterfaceFile, ExecuteCommands.OpenCompiledFile, ExecuteCommands.CreateInterfaceFile] + }, semanticTokensProvider: { legend: { tokenTypes: [ @@ -1254,6 +1320,8 @@ function onMessage(msg: p.Message) { if (extName === c.resExt) { send(signatureHelp(msg)); } + } else if (msg.method === p.ExecuteCommandRequest.type.method) { + send(executeCommand(msg)); } else { let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion,