diff --git a/CHANGELOG.md b/CHANGELOG.md index 6baeb4237..f968d0863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Add support for prop completion for JSX V4 https://github.com/rescript-lang/rescript-vscode/pull/579 - Add support for create interface file for JSX V4 https://github.com/rescript-lang/rescript-vscode/pull/580 - Expand one level of type definition on hover. Dig into record/variant body. https://github.com/rescript-lang/rescript-vscode/pull/584 +- Add clickable links to type definitions in hovers. https://github.com/rescript-lang/rescript-vscode/pull/585 #### :bug: Bug Fix diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index 90b82b1c8..cf657cec4 100644 --- a/analysis/src/Cli.ml +++ b/analysis/src/Cli.ml @@ -7,7 +7,7 @@ API examples: ./rescript-editor-analysis.exe definition src/MyFile.res 9 3 ./rescript-editor-analysis.exe typeDefinition src/MyFile.res 9 3 ./rescript-editor-analysis.exe documentSymbol src/Foo.res - ./rescript-editor-analysis.exe hover src/MyFile.res 10 2 + ./rescript-editor-analysis.exe hover src/MyFile.res 10 2 true ./rescript-editor-analysis.exe references src/MyFile.res 10 2 ./rescript-editor-analysis.exe rename src/MyFile.res 10 2 foo ./rescript-editor-analysis.exe diagnosticSyntax src/MyFile.res @@ -39,9 +39,9 @@ Options: ./rescript-editor-analysis.exe documentSymbol src/MyFile.res - hover: get inferred type for MyFile.res at line 10 column 2: + hover: get inferred type for MyFile.res at line 10 column 2 (supporting markdown links): - ./rescript-editor-analysis.exe hover src/MyFile.res 10 2 + ./rescript-editor-analysis.exe hover src/MyFile.res 10 2 true references: get all references to item in MyFile.res at line 10 column 2: @@ -95,10 +95,14 @@ let main () = ~pos:(int_of_string line, int_of_string col) ~debug:false | [_; "documentSymbol"; path] -> DocumentSymbol.command ~path - | [_; "hover"; path; line; col; currentFile] -> + | [_; "hover"; path; line; col; currentFile; supportsMarkdownLinks] -> Commands.hover ~path ~pos:(int_of_string line, int_of_string col) ~currentFile ~debug:false + ~supportsMarkdownLinks: + (match supportsMarkdownLinks with + | "true" -> true + | _ -> false) | [_; "inlayHint"; path; line_start; line_end; maxLength] -> Commands.inlayhint ~path ~pos:(int_of_string line_start, int_of_string line_end) diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 5454ab056..f0ad3efd2 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -36,7 +36,7 @@ let codeLens ~path ~debug = let result = Hint.codeLens ~path ~debug |> Protocol.array in print_endline result -let hover ~path ~pos ~currentFile ~debug = +let hover ~path ~pos ~currentFile ~debug ~supportsMarkdownLinks = let result = match Cmt.fullFromPath ~path with | None -> Protocol.null @@ -81,7 +81,7 @@ let hover ~path ~pos ~currentFile ~debug = in if skipZero then Protocol.null else - let hoverText = Hover.newHover ~full locItem in + let hoverText = Hover.newHover ~supportsMarkdownLinks ~full locItem in match hoverText with | None -> Protocol.null | Some s -> Protocol.stringifyHover s)) @@ -341,7 +341,8 @@ let test ~path = ("Hover " ^ path ^ " " ^ string_of_int line ^ ":" ^ string_of_int col); let currentFile = createCurrentFile () in - hover ~path ~pos:(line, col) ~currentFile ~debug:true; + hover ~supportsMarkdownLinks:true ~path ~pos:(line, col) + ~currentFile ~debug:true; Sys.remove currentFile | "int" -> print_endline ("Create Interface " ^ path); diff --git a/analysis/src/Hover.ml b/analysis/src/Hover.ml index a8e1cf619..2d0271dec 100644 --- a/analysis/src/Hover.ml +++ b/analysis/src/Hover.ml @@ -2,6 +2,50 @@ open SharedTypes let codeBlock code = Printf.sprintf "```rescript\n%s\n```" code +(* Light weight, hopefully-enough-for-the-purpose fn to encode URI components. + Built to handle the reserved characters listed in + https://en.wikipedia.org/wiki/Percent-encoding. Note that this function is not + general purpose, rather it's currently only for URL encoding the argument list + passed to command links in markdown. *) +let encodeURIComponent text = + let ln = String.length text in + let buf = Buffer.create ln in + let rec loop i = + if i < ln then ( + (match text.[i] with + | '"' -> Buffer.add_string buf "%22" + | '\'' -> Buffer.add_string buf "%22" + | ':' -> Buffer.add_string buf "%3A" + | ';' -> Buffer.add_string buf "%3B" + | '/' -> Buffer.add_string buf "%2F" + | '\\' -> Buffer.add_string buf "%5C" + | ',' -> Buffer.add_string buf "%2C" + | '&' -> Buffer.add_string buf "%26" + | '[' -> Buffer.add_string buf "%5B" + | ']' -> Buffer.add_string buf "%5D" + | '#' -> Buffer.add_string buf "%23" + | '$' -> Buffer.add_string buf "%24" + | '+' -> Buffer.add_string buf "%2B" + | '=' -> Buffer.add_string buf "%3D" + | '?' -> Buffer.add_string buf "%3F" + | '@' -> Buffer.add_string buf "%40" + | '%' -> Buffer.add_string buf "%25" + | c -> Buffer.add_char buf c); + loop (i + 1)) + in + loop 0; + Buffer.contents buf + +type link = {startPos: Protocol.position; file: string; label: string} + +let linkToCommandArgs link = + Printf.sprintf "[\"%s\",%i,%i]" link.file link.startPos.line + link.startPos.character + +let makeGotoCommand link = + Printf.sprintf "[%s](command:rescript-vscode.go_to_location?%s)" link.label + (encodeURIComponent (linkToCommandArgs link)) + let showModuleTopLevel ~docstring ~name (topLevel : Module.item list) = let contents = topLevel @@ -36,7 +80,7 @@ let rec showModule ~docstring ~(file : File.t) ~name | Some {item = Ident path} -> Some ("Unable to resolve module reference " ^ Path.name path) -let newHover ~full:{file; package} locItem = +let newHover ~full:{file; package} ~supportsMarkdownLinks locItem = match locItem.locType with | TypeDefinition (name, decl, _stamp) -> let typeDef = Shared.declToString name decl in @@ -101,9 +145,15 @@ let newHover ~full:{file; package} locItem = let fromConstructorPath ~env path = match References.digConstructor ~env ~package path with | None -> None - | Some (_env, {name = {txt}; item = {decl}}) -> + | Some (env, {extentLoc; item = {decl}}) -> if Utils.isUncurriedInternal path then None - else Some (decl |> Shared.declToString txt |> codeBlock) + else + Some + ( decl + |> Shared.declToString ~printNameAsIs:true + (SharedTypes.pathIdentToString path), + extentLoc, + env ) in let fromType ~docstring typ = let typeString = codeBlock (typ |> Shared.typeToString) in @@ -142,7 +192,28 @@ let newHover ~full:{file; package} locItem = | None -> (env, [typ]) in let constructors = Shared.findTypeConstructors typesToSearch in - constructors |> List.filter_map (fromConstructorPath ~env:envToSearch) + constructors + |> List.filter_map (fun constructorPath -> + match + constructorPath |> fromConstructorPath ~env:envToSearch + with + | None -> None + | Some (typString, extentLoc, env) -> + let startLine, startCol = Pos.ofLexing extentLoc.loc_start in + let linkToTypeDefinitionStr = + if supportsMarkdownLinks then + "\nGo to: " + ^ makeGotoCommand + { + label = "Type definition"; + file = Uri.toString env.file.uri; + startPos = {line = startLine; character = startCol}; + } + else "" + in + Some + (Shared.markdownSpacing ^ codeBlock typString + ^ linkToTypeDefinitionStr ^ "\n\n---\n")) in let typeString = typeString :: typeDefinitions |> String.concat "\n\n" in (typeString, docstring) diff --git a/analysis/src/PrintType.ml b/analysis/src/PrintType.ml index 3da8293c0..f06239f60 100644 --- a/analysis/src/PrintType.ml +++ b/analysis/src/PrintType.ml @@ -3,8 +3,8 @@ let printExpr ?(lineWidth = 60) typ = Res_doc.toString ~width:lineWidth (Res_outcome_printer.printOutTypeDoc (Printtyp.tree_of_typexp false typ)) -let printDecl ~recStatus name decl = +let printDecl ?printNameAsIs ~recStatus name decl = Printtyp.reset_names (); Res_doc.toString ~width:60 - (Res_outcome_printer.printOutSigItemDoc + (Res_outcome_printer.printOutSigItemDoc ?printNameAsIs (Printtyp.tree_of_type_declaration (Ident.create name) decl recStatus)) diff --git a/analysis/src/Shared.ml b/analysis/src/Shared.ml index 37280d50f..af5f3748e 100644 --- a/analysis/src/Shared.ml +++ b/analysis/src/Shared.ml @@ -63,8 +63,8 @@ let findTypeConstructors (tel : Types.type_expr list) = tel |> List.iter loop; !paths |> List.rev -let declToString ?(recStatus = Types.Trec_not) name t = - PrintType.printDecl ~recStatus name t +let declToString ?printNameAsIs ?(recStatus = Types.Trec_not) name t = + PrintType.printDecl ?printNameAsIs ~recStatus name t let cacheTypeToString = ref false let typeTbl = Hashtbl.create 1 @@ -78,3 +78,5 @@ let typeToString ?lineWidth (t : Types.type_expr) = Hashtbl.replace typeTbl (t.id, t) s; s | Some s -> s + +let markdownSpacing = "\n```\n \n```\n" diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index 314f7670b..03ad6935e 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -315,6 +315,13 @@ type path = string list let pathToString (path : path) = path |> String.concat "." +let rec pathIdentToString (p : Path.t) = + match p with + | Pident {name} -> name + | Pdot (nextPath, id, _) -> + Printf.sprintf "%s.%s" (pathIdentToString nextPath) id + | Papply _ -> "" + type locKind = | LocalReference of int * Tip.t | GlobalReference of string * string list * Tip.t diff --git a/analysis/tests/src/expected/Auto.res.txt b/analysis/tests/src/expected/Auto.res.txt index b9ad6f2e9..c26bf07ed 100644 --- a/analysis/tests/src/expected/Auto.res.txt +++ b/analysis/tests/src/expected/Auto.res.txt @@ -1,3 +1,3 @@ Hover src/Auto.res 2:13 -{"contents": "```rescript\n(Belt.List.t<'a>, 'a => 'b) => Belt.List.t<'b>\n```\n\n```rescript\ntype t<'a> = list<'a>\n```\n\n\n Returns a new list with `f` applied to each element of `someList`.\n\n ```res example\n list{1, 2}->Belt.List.map(x => x + 1) // list{3, 4}\n ```\n"} +{"contents": "```rescript\n(Belt.List.t<'a>, 'a => 'b) => Belt.List.t<'b>\n```\n\n\n```\n \n```\n```rescript\ntype Belt.List.t<'a> = list<'a>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22belt_List.mli%22%2C34%2C0%5D)\n\n---\n\n\n\n Returns a new list with `f` applied to each element of `someList`.\n\n ```res example\n list{1, 2}->Belt.List.map(x => x + 1) // list{3, 4}\n ```\n"} diff --git a/analysis/tests/src/expected/Definition.res.txt b/analysis/tests/src/expected/Definition.res.txt index 9e2ec3299..18dd479df 100644 --- a/analysis/tests/src/expected/Definition.res.txt +++ b/analysis/tests/src/expected/Definition.res.txt @@ -8,7 +8,7 @@ Hover src/Definition.res 14:14 {"contents": "```rescript\n('a => 'b, list<'a>) => list<'b>\n```\n\n [List.map f [a1; ...; an]] applies function [f] to [a1, ..., an],\n and builds the list [[f a1; ...; f an]]\n with the results returned by [f]. Not tail-recursive. "} Hover src/Definition.res 18:14 -{"contents": "```rescript\n(Belt.List.t<'a>, 'a => 'b) => Belt.List.t<'b>\n```\n\n```rescript\ntype t<'a> = list<'a>\n```\n\n\n Returns a new list with `f` applied to each element of `someList`.\n\n ```res example\n list{1, 2}->Belt.List.map(x => x + 1) // list{3, 4}\n ```\n"} +{"contents": "```rescript\n(Belt.List.t<'a>, 'a => 'b) => Belt.List.t<'b>\n```\n\n\n```\n \n```\n```rescript\ntype Belt.List.t<'a> = list<'a>\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22belt_List.mli%22%2C34%2C0%5D)\n\n---\n\n\n\n Returns a new list with `f` applied to each element of `someList`.\n\n ```res example\n list{1, 2}->Belt.List.map(x => x + 1) // list{3, 4}\n ```\n"} Hover src/Definition.res 23:3 {"contents": "```rescript\n(. int, int) => int\n```"} diff --git a/analysis/tests/src/expected/Div.res.txt b/analysis/tests/src/expected/Div.res.txt index 8d5f1ef99..b9b76fdbc 100644 --- a/analysis/tests/src/expected/Div.res.txt +++ b/analysis/tests/src/expected/Div.res.txt @@ -1,6 +1,6 @@ Hover src/Div.res 0:10 getLocItem #3: heuristic for