Skip to content

Code action: Extract module to file #983

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

11 changes: 10 additions & 1 deletion analysis/src/CodeActions.ml
Original file line number Diff line number Diff line change
@@ -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}}
2 changes: 1 addition & 1 deletion analysis/src/Codemod.ml
Original file line number Diff line number Diff line change
@@ -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
26 changes: 17 additions & 9 deletions analysis/src/Commands.ml
Original file line number Diff line number Diff line change
@@ -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 ->
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section just improves the test output a bit now that we have several document change actions.

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
39 changes: 37 additions & 2 deletions analysis/src/Protocol.ml
Original file line number Diff line number Diff line change
@@ -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}
Comment on lines +78 to +85
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
80 changes: 78 additions & 2 deletions analysis/src/Xform.ml
Original file line number Diff line number Diff line change
@@ -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;
50 changes: 50 additions & 0 deletions analysis/tests/not_compiled/expected/DocTemplate.res.txt
Original file line number Diff line number Diff line change
@@ -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,21 +69,61 @@ 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
/**

*/
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
14 changes: 14 additions & 0 deletions analysis/tests/not_compiled/expected/DocTemplate.resi.txt
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions analysis/tests/src/Xform.res
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions analysis/tests/src/expected/ExhaustiveSwitch.res.txt
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions analysis/tests/src/expected/Xform.res.txt
Original file line number Diff line number Diff line change
@@ -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,13 +65,17 @@ 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
(x: option<T.r>)

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,13 +90,17 @@ 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
: int

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