diff --git a/CHANGELOG.md b/CHANGELOG.md index 29be466e2..80fed106c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Statically linked Linux binaries - Emit `%todo` instead of `failwith("TODO")` when we can (ReScript >= v11.1). https://github.com/rescript-lang/rescript-vscode/pull/981 - Complete `%todo`. https://github.com/rescript-lang/rescript-vscode/pull/981 +- Add code action for extracting a locally defined module into its own file. https://github.com/rescript-lang/rescript-vscode/pull/983 ## 1.50.0 diff --git a/analysis/src/CodeActions.ml b/analysis/src/CodeActions.ml index 0d383685c..013395b3a 100644 --- a/analysis/src/CodeActions.ml +++ b/analysis/src/CodeActions.ml @@ -13,6 +13,15 @@ let make ~title ~kind ~uri ~newText ~range = edit = { documentChanges = - [{textDocument = {version = None; uri}; edits = [{newText; range}]}]; + [ + TextDocumentEdit + { + Protocol.textDocument = {version = None; uri}; + edits = [{newText; range}]; + }; + ]; }; } + +let makeWithDocumentChanges ~title ~kind ~documentChanges = + {Protocol.title; codeActionKind = kind; edit = {documentChanges}} diff --git a/analysis/src/Codemod.ml b/analysis/src/Codemod.ml index 5204f4825..cef82fac3 100644 --- a/analysis/src/Codemod.ml +++ b/analysis/src/Codemod.ml @@ -6,7 +6,7 @@ let rec collectPatterns p = | _ -> [p] let transform ~path ~pos ~debug ~typ ~hint = - let structure, printExpr, _ = Xform.parseImplementation ~filename:path in + let structure, printExpr, _, _ = Xform.parseImplementation ~filename:path in match typ with | AddMissingCases -> ( let source = "let " ^ hint ^ " = ()" in diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index ab668b2a6..8b725497c 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -447,15 +447,23 @@ let test ~path = |> 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 dc -> + match dc with + | Protocol.TextDocumentEdit tde -> + Printf.printf "\nTextDocumentEdit: %s\n" + tde.textDocument.uri; + + tde.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) + | CreateFile cf -> + Printf.printf "\nCreateFile: %s\n" cf.uri)) | "c-a" -> let hint = String.sub rest 3 (String.length rest - 3) in print_endline diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index e10411664..dfa2b191d 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -75,7 +75,14 @@ type textDocumentEdit = { edits: textEdit list; } -type codeActionEdit = {documentChanges: textDocumentEdit list} +type createFileOptions = {overwrite: bool option; ignoreIfExists: bool option} +type createFile = {uri: string; options: createFileOptions option} + +type documentChange = + | TextDocumentEdit of textDocumentEdit + | CreateFile of createFile + +type codeActionEdit = {documentChanges: documentChange list} type codeActionKind = RefactorRewrite type codeAction = { @@ -232,13 +239,41 @@ let stringifyTextDocumentEdit tde = (stringifyoptionalVersionedTextDocumentIdentifier tde.textDocument) (tde.edits |> List.map stringifyTextEdit |> array) +let stringifyCreateFile cf = + stringifyObject + [ + ("kind", Some (wrapInQuotes "create")); + ("uri", Some (wrapInQuotes cf.uri)); + ( "options", + match cf.options with + | None -> None + | Some options -> + Some + (stringifyObject + [ + ( "overwrite", + match options.overwrite with + | None -> None + | Some ov -> Some (string_of_bool ov) ); + ( "ignoreIfExists", + match options.ignoreIfExists with + | None -> None + | Some i -> Some (string_of_bool i) ); + ]) ); + ] + +let stringifyDocumentChange dc = + match dc with + | TextDocumentEdit tde -> stringifyTextDocumentEdit tde + | CreateFile cf -> stringifyCreateFile cf + let codeActionKindToString kind = match kind with | RefactorRewrite -> "refactor.rewrite" let stringifyCodeActionEdit cae = Printf.sprintf {|{"documentChanges": %s}|} - (cae.documentChanges |> List.map stringifyTextDocumentEdit |> array) + (cae.documentChanges |> List.map stringifyDocumentChange |> array) let stringifyCodeAction ca = Printf.sprintf {|{"title": "%s", "kind": "%s", "edit": %s}|} ca.title diff --git a/analysis/src/Xform.ml b/analysis/src/Xform.ml index 699a3cfa9..8cba4ccfa 100644 --- a/analysis/src/Xform.ml +++ b/analysis/src/Xform.ml @@ -117,6 +117,75 @@ module IfThenElse = struct codeActions := codeAction :: !codeActions end +module ModuleToFile = struct + let mkIterator ~pos ~changed ~path ~printStandaloneStructure = + let structure_item (iterator : Ast_iterator.iterator) + (structure_item : Parsetree.structure_item) = + (match structure_item.pstr_desc with + | Pstr_module + {pmb_loc; pmb_name; pmb_expr = {pmod_desc = Pmod_structure structure}} + when structure_item.pstr_loc |> Loc.hasPos ~pos -> + let range = rangeOfLoc structure_item.pstr_loc in + let newTextInCurrentFile = "" in + let textForExtractedFile = + printStandaloneStructure ~loc:pmb_loc structure + in + let moduleName = pmb_name.txt in + let newFilePath = + Uri.fromPath + (Filename.concat (Filename.dirname path) moduleName ^ ".res") + in + changed := + Some + (CodeActions.makeWithDocumentChanges ~title:"Extract module as file" + ~kind:RefactorRewrite + ~documentChanges: + [ + Protocol.CreateFile + { + uri = newFilePath |> Uri.toString; + options = + Some + {overwrite = Some false; ignoreIfExists = Some true}; + }; + TextDocumentEdit + { + textDocument = + {uri = newFilePath |> Uri.toString; version = None}; + edits = + [ + { + newText = textForExtractedFile; + range = + { + start = {line = 0; character = 0}; + end_ = {line = 0; character = 0}; + }; + }; + ]; + }; + TextDocumentEdit + { + textDocument = {uri = path; version = None}; + edits = [{newText = newTextInCurrentFile; range}]; + }; + ]); + () + | _ -> ()); + Ast_iterator.default_iterator.structure_item iterator structure_item + in + + {Ast_iterator.default_iterator with structure_item} + + let xform ~pos ~codeActions ~path ~printStandaloneStructure structure = + let changed = ref None in + let iterator = mkIterator ~pos ~path ~changed ~printStandaloneStructure in + iterator.structure iterator structure; + match !changed with + | None -> () + | Some codeAction -> codeActions := codeAction :: !codeActions +end + module AddBracesToFn = struct (* Add braces to fn without braces *) @@ -626,7 +695,12 @@ let parseImplementation ~filename = ~comments:(comments |> filterComments ~loc:item.pstr_loc) |> Utils.indent range.start.character in - (structure, printExpr, printStructureItem) + let printStandaloneStructure ~(loc : Location.t) structure = + structure + |> Res_printer.printImplementation ~width:!Res_cli.ResClflags.width + ~comments:(comments |> filterComments ~loc) + in + (structure, printExpr, printStructureItem, printStandaloneStructure) let parseInterface ~filename = let {Res_driver.parsetree = structure; comments} = @@ -654,10 +728,12 @@ let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug = let codeActions = ref [] in match Files.classifySourceFile currentFile with | Res -> - let structure, printExpr, printStructureItem = + let structure, printExpr, printStructureItem, printStandaloneStructure = parseImplementation ~filename:currentFile in IfThenElse.xform ~pos ~codeActions ~printExpr ~path structure; + ModuleToFile.xform ~pos ~codeActions ~path ~printStandaloneStructure + structure; AddBracesToFn.xform ~pos ~codeActions ~path ~printStructureItem structure; AddDocTemplate.Implementation.xform ~pos ~codeActions ~path ~printStructureItem ~structure; diff --git a/analysis/tests/not_compiled/expected/DocTemplate.res.txt b/analysis/tests/not_compiled/expected/DocTemplate.res.txt index 5b7ed2a96..ce8487127 100644 --- a/analysis/tests/not_compiled/expected/DocTemplate.res.txt +++ b/analysis/tests/not_compiled/expected/DocTemplate.res.txt @@ -1,6 +1,8 @@ Xform not_compiled/DocTemplate.res 3:3 can't find module DocTemplate Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.res {"start": {"line": 3, "character": 0}, "end": {"line": 5, "character": 9}} newText: <--here @@ -14,6 +16,8 @@ and e = C Xform not_compiled/DocTemplate.res 6:15 can't find module DocTemplate Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.res {"start": {"line": 6, "character": 0}, "end": {"line": 6, "character": 33}} newText: <--here @@ -26,6 +30,8 @@ type name = Name(string) Xform not_compiled/DocTemplate.res 8:4 can't find module DocTemplate Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.res {"start": {"line": 8, "character": 0}, "end": {"line": 8, "character": 9}} newText: <--here @@ -37,6 +43,8 @@ let a = 1 Xform not_compiled/DocTemplate.res 10:4 can't find module DocTemplate Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.res {"start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 20}} newText: <--here @@ -48,6 +56,8 @@ let inc = x => x + 1 Xform not_compiled/DocTemplate.res 12:7 can't find module DocTemplate Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.res {"start": {"line": 12, "character": 0}, "end": {"line": 16, "character": 1}} newText: <--here @@ -59,10 +69,30 @@ module T = { let b = 1 // ^xfm } +Hit: Extract module as file + +CreateFile: T.res + +TextDocumentEdit: T.res +{"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}} +newText: +<--here +// ^xfm +let b = 1 +// ^xfm + + +TextDocumentEdit: not_compiled/DocTemplate.res +{"start": {"line": 12, "character": 0}, "end": {"line": 16, "character": 1}} +newText: +<--here + Xform not_compiled/DocTemplate.res 14:6 can't find module DocTemplate Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.res {"start": {"line": 14, "character": 2}, "end": {"line": 14, "character": 11}} newText: <--here @@ -70,10 +100,30 @@ newText: */ let b = 1 +Hit: Extract module as file + +CreateFile: T.res + +TextDocumentEdit: T.res +{"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}} +newText: +<--here +// ^xfm +let b = 1 +// ^xfm + + +TextDocumentEdit: not_compiled/DocTemplate.res +{"start": {"line": 12, "character": 0}, "end": {"line": 16, "character": 1}} +newText: +<--here + Xform not_compiled/DocTemplate.res 18:2 can't find module DocTemplate Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.res {"start": {"line": 17, "character": 0}, "end": {"line": 18, "character": 46}} newText: <--here diff --git a/analysis/tests/not_compiled/expected/DocTemplate.resi.txt b/analysis/tests/not_compiled/expected/DocTemplate.resi.txt index ed2be7f56..ef4987a7c 100644 --- a/analysis/tests/not_compiled/expected/DocTemplate.resi.txt +++ b/analysis/tests/not_compiled/expected/DocTemplate.resi.txt @@ -1,5 +1,7 @@ Xform not_compiled/DocTemplate.resi 3:3 Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.resi {"start": {"line": 3, "character": 0}, "end": {"line": 5, "character": 9}} newText: <--here @@ -12,6 +14,8 @@ and e = C Xform not_compiled/DocTemplate.resi 6:15 Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.resi {"start": {"line": 6, "character": 0}, "end": {"line": 6, "character": 33}} newText: <--here @@ -23,6 +27,8 @@ type name = Name(string) Xform not_compiled/DocTemplate.resi 8:4 Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.resi {"start": {"line": 8, "character": 0}, "end": {"line": 8, "character": 10}} newText: <--here @@ -33,6 +39,8 @@ let a: int Xform not_compiled/DocTemplate.resi 10:4 Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.resi {"start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 19}} newText: <--here @@ -43,6 +51,8 @@ let inc: int => int Xform not_compiled/DocTemplate.resi 12:7 Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.resi {"start": {"line": 12, "character": 0}, "end": {"line": 16, "character": 1}} newText: <--here @@ -57,6 +67,8 @@ module T: { Xform not_compiled/DocTemplate.resi 14:6 Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.resi {"start": {"line": 14, "character": 2}, "end": {"line": 14, "character": 12}} newText: <--here @@ -67,6 +79,8 @@ newText: Xform not_compiled/DocTemplate.resi 18:2 Hit: Add Documentation template + +TextDocumentEdit: DocTemplate.resi {"start": {"line": 17, "character": 0}, "end": {"line": 18, "character": 46}} newText: <--here diff --git a/analysis/tests/src/Xform.res b/analysis/tests/src/Xform.res index e0a7610d7..e2623ad9b 100644 --- a/analysis/tests/src/Xform.res +++ b/analysis/tests/src/Xform.res @@ -65,3 +65,11 @@ let bar = () => { } @res.partial Inner.foo(1) } + +module ExtractableModule = { + /** Doc comment. */ + type t = int + // A comment here + let doStuff = a => a + 1 + // ^xfm +} \ No newline at end of file diff --git a/analysis/tests/src/expected/ExhaustiveSwitch.res.txt b/analysis/tests/src/expected/ExhaustiveSwitch.res.txt index 627db72ac..bf1cc8447 100644 --- a/analysis/tests/src/expected/ExhaustiveSwitch.res.txt +++ b/analysis/tests/src/expected/ExhaustiveSwitch.res.txt @@ -119,6 +119,8 @@ Path getV Package opens Pervasives.JsxModules.place holder Resolved opens 1 pervasives Hit: Exhaustive switch + +TextDocumentEdit: ExhaustiveSwitch.res {"start": {"line": 33, "character": 3}, "end": {"line": 33, "character": 10}} newText: <--here @@ -138,6 +140,8 @@ Path vvv Package opens Pervasives.JsxModules.place holder Resolved opens 1 pervasives Hit: Exhaustive switch + +TextDocumentEdit: ExhaustiveSwitch.res {"start": {"line": 36, "character": 3}, "end": {"line": 36, "character": 6}} newText: <--here diff --git a/analysis/tests/src/expected/Xform.res.txt b/analysis/tests/src/expected/Xform.res.txt index 30154b591..d14d0409e 100644 --- a/analysis/tests/src/expected/Xform.res.txt +++ b/analysis/tests/src/expected/Xform.res.txt @@ -8,6 +8,8 @@ Path kind Package opens Pervasives.JsxModules.place holder Resolved opens 1 pervasives Hit: Replace with switch + +TextDocumentEdit: Xform.res {"start": {"line": 6, "character": 0}, "end": {"line": 11, "character": 1}} newText: <--here @@ -20,6 +22,8 @@ switch kind { Xform src/Xform.res 13:15 Hit: Replace with switch + +TextDocumentEdit: Xform.res {"start": {"line": 13, "character": 0}, "end": {"line": 13, "character": 79}} newText: <--here @@ -30,11 +34,15 @@ switch kind { Xform src/Xform.res 16:5 Hit: Add type annotation + +TextDocumentEdit: Xform.res {"start": {"line": 16, "character": 8}, "end": {"line": 16, "character": 8}} newText: <--here : string Hit: Add Documentation template + +TextDocumentEdit: Xform.res {"start": {"line": 16, "character": 0}, "end": {"line": 16, "character": 18}} newText: <--here @@ -45,6 +53,8 @@ let name = "hello" Xform src/Xform.res 19:5 Hit: Add Documentation template + +TextDocumentEdit: Xform.res {"start": {"line": 19, "character": 0}, "end": {"line": 19, "character": 23}} newText: <--here @@ -55,6 +65,8 @@ let annotated: int = 34 Xform src/Xform.res 26:10 Hit: Add type annotation + +TextDocumentEdit: Xform.res {"start": {"line": 26, "character": 10}, "end": {"line": 26, "character": 11}} newText: <--here @@ -62,6 +74,8 @@ newText: Xform src/Xform.res 30:9 Hit: Add braces to function + +TextDocumentEdit: Xform.res {"start": {"line": 26, "character": 0}, "end": {"line": 32, "character": 3}} newText: <--here @@ -76,6 +90,8 @@ let foo = x => { Xform src/Xform.res 34:21 Hit: Add type annotation + +TextDocumentEdit: Xform.res {"start": {"line": 34, "character": 24}, "end": {"line": 34, "character": 24}} newText: <--here @@ -83,6 +99,8 @@ newText: Xform src/Xform.res 38:5 Hit: Add Documentation template + +TextDocumentEdit: Xform.res {"start": {"line": 37, "character": 0}, "end": {"line": 38, "character": 40}} newText: <--here @@ -94,6 +112,8 @@ let make = (~name) => React.string(name) Xform src/Xform.res 41:9 Hit: Add type annotation + +TextDocumentEdit: Xform.res {"start": {"line": 41, "character": 11}, "end": {"line": 41, "character": 11}} newText: <--here @@ -110,6 +130,8 @@ Path name Package opens Pervasives.JsxModules.place holder Resolved opens 1 pervasives Hit: Add braces to function + +TextDocumentEdit: Xform.res {"start": {"line": 48, "character": 0}, "end": {"line": 48, "character": 25}} newText: <--here @@ -119,6 +141,8 @@ let noBraces = () => { Xform src/Xform.res 52:34 Hit: Add braces to function + +TextDocumentEdit: Xform.res {"start": {"line": 51, "character": 0}, "end": {"line": 54, "character": 1}} newText: <--here @@ -131,6 +155,8 @@ let nested = () => { Xform src/Xform.res 62:6 Hit: Add braces to function + +TextDocumentEdit: Xform.res {"start": {"line": 58, "character": 4}, "end": {"line": 62, "character": 7}} newText: <--here @@ -141,3 +167,25 @@ newText: } } +Xform src/Xform.res 72:5 +Hit: Extract module as file + +CreateFile: ExtractableModule.res + +TextDocumentEdit: ExtractableModule.res +{"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}} +newText: +<--here +/** Doc comment. */ +type t = int +// A comment here +let doStuff = a => a + 1 +// ^xfm + + +TextDocumentEdit: src/Xform.res +{"start": {"line": 68, "character": 0}, "end": {"line": 74, "character": 1}} +newText: +<--here + +