diff --git a/CHANGELOG.md b/CHANGELOG.md index 2848fe649..c1e5a6240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ ## master +#### :rocket: New Feature + +- Docstring template Code Action. https://github.com/rescript-lang/rescript-vscode/pull/764 + ## 1.16.0 #### :rocket: New Feature diff --git a/analysis/src/Xform.ml b/analysis/src/Xform.ml index 915b47273..829b372a5 100644 --- a/analysis/src/Xform.ml +++ b/analysis/src/Xform.ml @@ -252,7 +252,191 @@ module AddTypeAnnotation = struct | _ -> ())) end -let parse ~filename = +module AddDocTemplate = struct + let createTemplate () = + let docContent = ["\n"; "\n"] in + let expression = + Ast_helper.Exp.constant + (Parsetree.Pconst_string (String.concat "" docContent, None)) + in + let structureItemDesc = Parsetree.Pstr_eval (expression, []) in + let structureItem = Ast_helper.Str.mk structureItemDesc in + let attrLoc = + { + Location.none with + loc_start = Lexing.dummy_pos; + loc_end = + { + Lexing.dummy_pos with + pos_lnum = Lexing.dummy_pos.pos_lnum (* force line break *); + }; + } + in + (Location.mkloc "res.doc" attrLoc, Parsetree.PStr [structureItem]) + + module Interface = struct + let mkIterator ~pos ~result = + let signature_item (iterator : Ast_iterator.iterator) + (item : Parsetree.signature_item) = + match item.psig_desc with + | Psig_value value_description as r + when Loc.hasPos ~pos value_description.pval_loc + && ProcessAttributes.findDocAttribute + value_description.pval_attributes + = None -> + result := Some (r, item.psig_loc) + | Psig_type (_, hd :: _) as r + when Loc.hasPos ~pos hd.ptype_loc + && ProcessAttributes.findDocAttribute hd.ptype_attributes = None + -> + result := Some (r, item.psig_loc) + | Psig_module {pmd_name = {loc}} as r -> + if Loc.start loc = pos then result := Some (r, item.psig_loc) + else Ast_iterator.default_iterator.signature_item iterator item + | _ -> Ast_iterator.default_iterator.signature_item iterator item + in + {Ast_iterator.default_iterator with signature_item} + + let processSigValue (valueDesc : Parsetree.value_description) loc = + let attr = createTemplate () in + let newValueBinding = + {valueDesc with pval_attributes = attr :: valueDesc.pval_attributes} + in + let signature_item_desc = Parsetree.Psig_value newValueBinding in + Ast_helper.Sig.mk ~loc signature_item_desc + + let processTypeDecl (typ : Parsetree.type_declaration) = + let attr = createTemplate () in + let newTypeDeclaration = + {typ with ptype_attributes = attr :: typ.ptype_attributes} + in + newTypeDeclaration + + let processModDecl (modDecl : Parsetree.module_declaration) loc = + let attr = createTemplate () in + let newModDecl = + {modDecl with pmd_attributes = attr :: modDecl.pmd_attributes} + in + Ast_helper.Sig.mk ~loc (Parsetree.Psig_module newModDecl) + + let xform ~path ~pos ~codeActions ~signature ~printSignatureItem = + let result = ref None in + let iterator = mkIterator ~pos ~result in + iterator.signature iterator signature; + match !result with + | Some (signatureItem, loc) -> ( + let newSignatureItem = + match signatureItem with + | Psig_value value_desc -> + Some (processSigValue value_desc value_desc.pval_loc) (* Some loc *) + | Psig_type (flag, hd :: tl) -> + let newFirstTypeDecl = processTypeDecl hd in + Some + (Ast_helper.Sig.mk ~loc + (Parsetree.Psig_type (flag, newFirstTypeDecl :: tl))) + | Psig_module modDecl -> Some (processModDecl modDecl loc) + | _ -> None + in + + match newSignatureItem with + | Some signatureItem -> + let range = rangeOfLoc signatureItem.psig_loc in + let newText = printSignatureItem ~range signatureItem in + let codeAction = + CodeActions.make ~title:"Add Documentation template" + ~kind:RefactorRewrite ~uri:path ~newText ~range + in + codeActions := codeAction :: !codeActions + | None -> ()) + | None -> () + end + + module Implementation = struct + let mkIterator ~pos ~result = + let structure_item (iterator : Ast_iterator.iterator) + (si : Parsetree.structure_item) = + match si.pstr_desc with + | Pstr_value (_, {pvb_pat = {ppat_loc}; pvb_attributes} :: _) as r + when Loc.hasPos ~pos ppat_loc + && ProcessAttributes.findDocAttribute pvb_attributes = None -> + result := Some (r, si.pstr_loc) + | Pstr_primitive value_description as r + when Loc.hasPos ~pos value_description.pval_loc + && ProcessAttributes.findDocAttribute + value_description.pval_attributes + = None -> + result := Some (r, si.pstr_loc) + | Pstr_module {pmb_name = {loc}} as r -> + if Loc.start loc = pos then result := Some (r, si.pstr_loc) + else Ast_iterator.default_iterator.structure_item iterator si + | Pstr_type (_, hd :: _) as r + when Loc.hasPos ~pos hd.ptype_loc + && ProcessAttributes.findDocAttribute hd.ptype_attributes = None + -> + result := Some (r, si.pstr_loc) + | _ -> Ast_iterator.default_iterator.structure_item iterator si + in + {Ast_iterator.default_iterator with structure_item} + + let processValueBinding (valueBinding : Parsetree.value_binding) = + let attr = createTemplate () in + let newValueBinding = + {valueBinding with pvb_attributes = attr :: valueBinding.pvb_attributes} + in + newValueBinding + + let processPrimitive (valueDesc : Parsetree.value_description) loc = + let attr = createTemplate () in + let newValueDesc = + {valueDesc with pval_attributes = attr :: valueDesc.pval_attributes} + in + Ast_helper.Str.primitive ~loc newValueDesc + + let processModuleBinding (modBind : Parsetree.module_binding) loc = + let attr = createTemplate () in + let newModBinding = + {modBind with pmb_attributes = attr :: modBind.pmb_attributes} + in + Ast_helper.Str.module_ ~loc newModBinding + + let xform ~pos ~codeActions ~path ~printStructureItem ~structure = + let result = ref None in + let iterator = mkIterator ~pos ~result in + iterator.structure iterator structure; + match !result with + | None -> () + | Some (structureItem, loc) -> ( + let newStructureItem = + match structureItem with + | Pstr_value (flag, hd :: tl) -> + let newValueBinding = processValueBinding hd in + Some + (Ast_helper.Str.mk ~loc + (Parsetree.Pstr_value (flag, newValueBinding :: tl))) + | Pstr_primitive valueDesc -> Some (processPrimitive valueDesc loc) + | Pstr_module modBind -> Some (processModuleBinding modBind loc) + | Pstr_type (flag, hd :: tl) -> + let newFirstTypeDecl = Interface.processTypeDecl hd in + Some + (Ast_helper.Str.mk ~loc + (Parsetree.Pstr_type (flag, newFirstTypeDecl :: tl))) + | _ -> None + in + + match newStructureItem with + | Some structureItem -> + let range = rangeOfLoc structureItem.pstr_loc in + let newText = printStructureItem ~range structureItem in + let codeAction = + CodeActions.make ~title:"Add Documentation template" + ~kind:RefactorRewrite ~uri:path ~newText ~range + in + codeActions := codeAction :: !codeActions + | None -> ()) + end +end + +let parseImplementation ~filename = let {Res_driver.parsetree = structure; comments} = Res_driver.parsingEngine.parseImplementation ~forPrinter:false ~filename in @@ -280,15 +464,51 @@ let parse ~filename = in (structure, printExpr, printStructureItem) +let parseInterface ~filename = + let {Res_driver.parsetree = structure; comments} = + Res_driver.parsingEngine.parseInterface ~forPrinter:false ~filename + in + let filterComments ~loc comments = + (* Relevant comments in the range of the expression *) + let filter comment = + Loc.hasPos ~pos:(Loc.start (Res_comment.loc comment)) loc + in + comments |> List.filter filter + in + let printSignatureItem ~(range : Protocol.range) + (item : Parsetree.signature_item) = + let signature_item = [item] in + signature_item + |> Res_printer.printInterface ~width:!Res_cli.ResClflags.width + ~comments:(comments |> filterComments ~loc:item.psig_loc) + |> Utils.indent range.start.character + in + (structure, printSignatureItem) + let extractCodeActions ~path ~pos ~currentFile ~debug = - match Cmt.loadFullCmtFromPath ~path with - | Some full when Files.classifySourceFile currentFile = Res -> + let codeActions = ref [] in + match Files.classifySourceFile currentFile with + | Res -> let structure, printExpr, printStructureItem = - parse ~filename:currentFile + parseImplementation ~filename:currentFile in - let codeActions = ref [] in - AddTypeAnnotation.xform ~path ~pos ~full ~structure ~codeActions ~debug; IfThenElse.xform ~pos ~codeActions ~printExpr ~path structure; AddBracesToFn.xform ~pos ~codeActions ~path ~printStructureItem structure; + AddDocTemplate.Implementation.xform ~pos ~codeActions ~path + ~printStructureItem ~structure; + + (* This Code Action needs type info *) + let () = + match Cmt.loadFullCmtFromPath ~path with + | Some full -> + AddTypeAnnotation.xform ~path ~pos ~full ~structure ~codeActions ~debug + | None -> () + in + + !codeActions + | Resi -> + let signature, printSignatureItem = parseInterface ~filename:currentFile in + AddDocTemplate.Interface.xform ~pos ~codeActions ~path ~signature + ~printSignatureItem; !codeActions - | _ -> [] + | Other -> [] diff --git a/analysis/tests/not_compiled/DocTemplate.res b/analysis/tests/not_compiled/DocTemplate.res new file mode 100644 index 000000000..2d7fe5d08 --- /dev/null +++ b/analysis/tests/not_compiled/DocTemplate.res @@ -0,0 +1,20 @@ +type a = {a: int} +// ^xfm + +type rec t = A | B +// ^xfm +and e = C +@unboxed type name = Name(string) +// ^xfm +let a = 1 +// ^xfm +let inc = x => x + 1 +// ^xfm +module T = { + // ^xfm + let b = 1 + // ^xfm +} +@module("path") +external dirname: string => string = "dirname" +//^xfm diff --git a/analysis/tests/not_compiled/DocTemplate.resi b/analysis/tests/not_compiled/DocTemplate.resi new file mode 100644 index 000000000..6940b7fe5 --- /dev/null +++ b/analysis/tests/not_compiled/DocTemplate.resi @@ -0,0 +1,20 @@ +type a = {a: int} +// ^xfm + +type rec t = A | B +// ^xfm +and e = C +@unboxed type name = Name(string) +// ^xfm +let a: int +// ^xfm +let inc: int => int +// ^xfm +module T: { + // ^xfm + let b: int + // ^xfm +} +@module("path") +external dirname: string => string = "dirname" +//^xfm diff --git a/analysis/tests/not_compiled/expected/DocTemplate.res.txt b/analysis/tests/not_compiled/expected/DocTemplate.res.txt new file mode 100644 index 000000000..5b7ed2a96 --- /dev/null +++ b/analysis/tests/not_compiled/expected/DocTemplate.res.txt @@ -0,0 +1,85 @@ +Xform not_compiled/DocTemplate.res 3:3 +can't find module DocTemplate +Hit: Add Documentation template +{"start": {"line": 3, "character": 0}, "end": {"line": 5, "character": 9}} +newText: +<--here +/** + +*/ +type rec t = A | B +// ^xfm +and e = C + +Xform not_compiled/DocTemplate.res 6:15 +can't find module DocTemplate +Hit: Add Documentation template +{"start": {"line": 6, "character": 0}, "end": {"line": 6, "character": 33}} +newText: +<--here +/** + +*/ +@unboxed +type name = Name(string) + +Xform not_compiled/DocTemplate.res 8:4 +can't find module DocTemplate +Hit: Add Documentation template +{"start": {"line": 8, "character": 0}, "end": {"line": 8, "character": 9}} +newText: +<--here +/** + +*/ +let a = 1 + +Xform not_compiled/DocTemplate.res 10:4 +can't find module DocTemplate +Hit: Add Documentation template +{"start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 20}} +newText: +<--here +/** + +*/ +let inc = x => x + 1 + +Xform not_compiled/DocTemplate.res 12:7 +can't find module DocTemplate +Hit: Add Documentation template +{"start": {"line": 12, "character": 0}, "end": {"line": 16, "character": 1}} +newText: +<--here +/** + +*/ +module T = { + // ^xfm + let b = 1 + // ^xfm +} + +Xform not_compiled/DocTemplate.res 14:6 +can't find module DocTemplate +Hit: Add Documentation template +{"start": {"line": 14, "character": 2}, "end": {"line": 14, "character": 11}} +newText: + <--here + /** + + */ + let b = 1 + +Xform not_compiled/DocTemplate.res 18:2 +can't find module DocTemplate +Hit: Add Documentation template +{"start": {"line": 17, "character": 0}, "end": {"line": 18, "character": 46}} +newText: +<--here +/** + +*/ +@module("path") +external dirname: string => string = "dirname" + diff --git a/analysis/tests/not_compiled/expected/DocTemplate.resi.txt b/analysis/tests/not_compiled/expected/DocTemplate.resi.txt new file mode 100644 index 000000000..ed2be7f56 --- /dev/null +++ b/analysis/tests/not_compiled/expected/DocTemplate.resi.txt @@ -0,0 +1,78 @@ +Xform not_compiled/DocTemplate.resi 3:3 +Hit: Add Documentation template +{"start": {"line": 3, "character": 0}, "end": {"line": 5, "character": 9}} +newText: +<--here +/** + +*/ +type rec t = A | B +// ^xfm +and e = C + +Xform not_compiled/DocTemplate.resi 6:15 +Hit: Add Documentation template +{"start": {"line": 6, "character": 0}, "end": {"line": 6, "character": 33}} +newText: +<--here +/** + +*/ +@unboxed +type name = Name(string) + +Xform not_compiled/DocTemplate.resi 8:4 +Hit: Add Documentation template +{"start": {"line": 8, "character": 0}, "end": {"line": 8, "character": 10}} +newText: +<--here +/** + +*/ +let a: int + +Xform not_compiled/DocTemplate.resi 10:4 +Hit: Add Documentation template +{"start": {"line": 10, "character": 0}, "end": {"line": 10, "character": 19}} +newText: +<--here +/** + +*/ +let inc: int => int + +Xform not_compiled/DocTemplate.resi 12:7 +Hit: Add Documentation template +{"start": {"line": 12, "character": 0}, "end": {"line": 16, "character": 1}} +newText: +<--here +/** + +*/ +module T: { + // ^xfm + let b: int + // ^xfm +} + +Xform not_compiled/DocTemplate.resi 14:6 +Hit: Add Documentation template +{"start": {"line": 14, "character": 2}, "end": {"line": 14, "character": 12}} +newText: + <--here + /** + + */ + let b: int + +Xform not_compiled/DocTemplate.resi 18:2 +Hit: Add Documentation template +{"start": {"line": 17, "character": 0}, "end": {"line": 18, "character": 46}} +newText: +<--here +/** + +*/ +@module("path") +external dirname: string => string = "dirname" + diff --git a/analysis/tests/src/expected/Xform.res.txt b/analysis/tests/src/expected/Xform.res.txt index d298604ae..9de1f9f2d 100644 --- a/analysis/tests/src/expected/Xform.res.txt +++ b/analysis/tests/src/expected/Xform.res.txt @@ -26,8 +26,24 @@ Hit: Add type annotation newText: <--here : string +Hit: Add Documentation template +{"start": {"line": 16, "character": 0}, "end": {"line": 16, "character": 18}} +newText: +<--here +/** + +*/ +let name = "hello" Xform src/Xform.res 19:5 +Hit: Add Documentation template +{"start": {"line": 19, "character": 0}, "end": {"line": 19, "character": 23}} +newText: +<--here +/** + +*/ +let annotated: int = 34 Xform src/Xform.res 26:10 Hit: Add type annotation @@ -58,6 +74,15 @@ newText: : int Xform src/Xform.res 38:5 +Hit: Add Documentation template +{"start": {"line": 37, "character": 0}, "end": {"line": 38, "character": 40}} +newText: +<--here +/** + +*/ +@react.component +let make = (~name) => React.string(name) Xform src/Xform.res 41:9 Hit: Add type annotation diff --git a/analysis/tests/test.sh b/analysis/tests/test.sh index c931641e9..e511a49ec 100755 --- a/analysis/tests/test.sh +++ b/analysis/tests/test.sh @@ -7,7 +7,7 @@ for file in src/*.{res,resi}; do fi done -for file in not_compiled/*.res; do +for file in not_compiled/*.{res,resi}; do output="$(dirname $file)/expected/$(basename $file).txt" ../rescript-editor-analysis.exe test $file &> $output # CI. We use LF, and the CI OCaml fork prints CRLF. Convert.