diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 4341c4f82..8487625c7 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -140,30 +140,51 @@ let typeDefinition ~path ~pos ~debug = | Some location -> location |> Protocol.stringifyLocation) let references ~path ~pos ~debug = - let allLocs = + let allLocs, prompt_segments = match Cmt.loadFullCmtFromPath ~path with - | None -> [] + | None -> ([], []) | Some full -> ( match References.getLocItem ~full ~pos ~debug with - | None -> [] + | None -> ([], []) | Some locItem -> + let itemName = + match locItem.locType with + | Typed (name, _, _) -> name + | _ -> "UnknownName" + in + let prompt = Prompt.createForReferences itemName in let allReferences = References.allReferencesForLocItem ~full locItem in - allReferences - |> List.fold_left - (fun acc {References.uri = uri2; locOpt} -> - let loc = - match locOpt with - | Some loc -> loc - | None -> Uri.toTopLevelLoc uri2 - in - Protocol.stringifyLocation - {uri = Uri.toString uri2; range = Utils.cmtLocToRange loc} - :: acc) - []) + let references = + allReferences + |> List.fold_left + (fun acc {References.uri = uri2; locOpt} -> + let loc = + match locOpt with + | Some loc -> loc + | None -> Uri.toTopLevelLoc uri2 + in + prompt + |> Prompt.addSnippet ~isDefinition:(locItem.loc = loc) + ~pos:(Loc.start loc) ~uri:uri2; + Protocol.stringifyLocation + {uri = Uri.toString uri2; range = Utils.cmtLocToRange loc} + :: acc) + [] + in + (references, Prompt.toSegments prompt)) in + print_endline (if allLocs = [] then Protocol.null - else "[\n" ^ (allLocs |> String.concat ",\n") ^ "\n]") + else + let prompt = + prompt_segments + |> List.map Protocol.wrapInQuotes + |> Protocol.array_newline + in + let references = allLocs |> Protocol.array_newline in + Printf.sprintf "{\"prompt\":\n%s,\n\"references\":\n%s}\n" prompt + references) let rename ~path ~pos ~newName ~debug = let result = diff --git a/analysis/src/Prompt.ml b/analysis/src/Prompt.ml new file mode 100644 index 000000000..4d6b6068b --- /dev/null +++ b/analysis/src/Prompt.ml @@ -0,0 +1,94 @@ +type lines = string array +type snippet = {uri: Uri.t; pos: Pos.t; code: string} +type t = { + mutable definition: snippet option; + mutable files: (string, lines) Hashtbl.t; + mutable preamble: string; + mutable snippets: snippet list; +} + +let readFile {files} name = + match Hashtbl.find_opt files name with + | None -> ( + match Files.readFile name with + | None -> None + | Some text -> Some (text |> String.split_on_char '\n' |> Array.of_list)) + | Some lines -> Some lines + +let snippetRadius (lines : lines) = min (Array.length lines) 10 / 2 + +let addSnippet ~isDefinition ~pos ~(uri : Uri.t) prompt = + match readFile prompt (Uri.toPath uri) with + | None -> () (* ignore files not found *) + | Some lines -> + let lineNum = fst pos in + let radius = snippetRadius lines in + let firstLine = max 0 (lineNum - radius) in + let lastLine = min (Array.length lines - 1) (lineNum + radius) in + let linesInRadius = Array.sub lines firstLine (lastLine - firstLine) in + let code = linesInRadius |> Array.to_list |> String.concat "\n" in + let snippet = {uri; pos; code} in + if isDefinition then prompt.definition <- Some snippet + else prompt.snippets <- snippet :: prompt.snippets + +let printSnippet buf {uri; pos; code} = + Buffer.add_string buf + ("{\"file\": " + ^ Filename.basename (Uri.toString uri) + ^ ", \"line\": " + ^ string_of_int (1 + fst pos) + ^ ", \"code\":\n"); + Buffer.add_string buf code; + Buffer.add_string buf "\"}\n" + +let createForReferences name = + let quoted = "\"" ^ name ^ "\"" in + let backticked = "`" ^ name ^ "`" in + let preamble = + [ + {|A Snippet has the form: {"file": "Hello.res", "line":23, "code": "...the code..."} where the first line of code is line 23.|}; + "Find Uses of " ^ quoted + ^ " given snippets for its Definition and its Users."; + ""; + {|The input has form: + Definition: + snippet + + Use1: + snippet1 + + Use2: + snippet2 + ...|}; + ""; + "You will produce output of the form:"; + "- `File.res` line `12`: function `foo` calls " ^ backticked ^ " ..."; + "- `File2.res` line `34`: function `bar` uses " ^ backticked ^ " to ..."; + ""; + "Ignore any code in the Snippets that is not directly relevant to using " + ^ quoted ^ "."; + "Add enough details to understand at high level how " ^ quoted + ^ " is used targeted at a person who is trying to undersand the codebase."; + ] + |> String.concat "\n" + in + {definition = None; files = Hashtbl.create 1; preamble; snippets = []} + +let toSegments {definition; preamble; snippets} = + let segments = ref [] in + let addSegment s = segments := s :: !segments in + addSegment (preamble ^ "\n"); + (match definition with + | None -> () + | Some snippet -> + let buf = Buffer.create 1 in + Buffer.add_string buf "Definition:\n"; + printSnippet buf snippet; + addSegment (Buffer.contents buf)); + snippets + |> List.iteri (fun i s -> + let buf = Buffer.create 1 in + Buffer.add_string buf ("Use" ^ string_of_int (i + 1) ^ ":\n"); + printSnippet buf s; + addSegment (Buffer.contents buf)); + !segments |> List.rev diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index 1f23f522e..b6323f6ef 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -85,6 +85,7 @@ type codeAction = { let null = "null" let array l = "[" ^ String.concat ", " l ^ "]" +let array_newline l = "[" ^ String.concat ",\n" l ^ "]" let stringifyPosition p = Printf.sprintf {|{"line": %i, "character": %i}|} p.line p.character diff --git a/analysis/tests/src/CompletionExpressions.res b/analysis/tests/src/CompletionExpressions.res index d61259101..9eb3e9afd 100644 --- a/analysis/tests/src/CompletionExpressions.res +++ b/analysis/tests/src/CompletionExpressions.res @@ -250,3 +250,25 @@ external commitLocalUpdate: (~updater: RecordSourceSelectorProxy.t => unit) => u // commitLocalUpdate(~updater=) // ^com + + + + + + + + + + + + + + + + + +let x1 = 1 +let x2 = 2 +let x3 = 3 +let x4 = 4 + diff --git a/analysis/tests/src/CompletionInferValues.res b/analysis/tests/src/CompletionInferValues.res index 829c26032..b658f8b67 100644 --- a/analysis/tests/src/CompletionInferValues.res +++ b/analysis/tests/src/CompletionInferValues.res @@ -31,7 +31,7 @@ let reactEventFn = (cb: ReactEvent.Mouse.t => unit) => { // reactEventFn(event => { event->pr }); // ^com -module Div = { +module Div = { @react.component let make = (~onMouseEnter: option unit>=?) => { let _ = onMouseEnter diff --git a/analysis/tests/src/expected/Cross.res.txt b/analysis/tests/src/expected/Cross.res.txt index 6f5ad3e43..bbcea11eb 100644 --- a/analysis/tests/src/expected/Cross.res.txt +++ b/analysis/tests/src/expected/Cross.res.txt @@ -1,21 +1,36 @@ References src/Cross.res 0:17 -[ -{"uri": "Cross.res", "range": {"start": {"line": 0, "character": 15}, "end": {"line": 0, "character": 25}}}, +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"UnknownName\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `UnknownName` ...\n- `File2.res` line `34`: function `bar` uses `UnknownName` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"UnknownName\".\nAdd enough details to understand at high level how \"UnknownName\" is used targeted at a person who is trying to undersand the codebase.\n", +"Definition:\n{\"file\": Cross.res, \"line\": 1, \"code\":\nlet crossRef = References.x\n// ^ref\n\nlet crossRef2 = References.x\n\"}\n", +"Use1:\n{\"file\": Cross.res, \"line\": 4, \"code\":\nlet crossRef = References.x\n// ^ref\n\nlet crossRef2 = References.x\n\nmodule Ref = References\n\nlet crossRef3 = References.x\"}\n", +"Use2:\n{\"file\": Cross.res, \"line\": 6, \"code\":\nlet crossRef = References.x\n// ^ref\n\nlet crossRef2 = References.x\n\nmodule Ref = References\n\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\"}\n", +"Use3:\n{\"file\": Cross.res, \"line\": 8, \"code\":\n\nlet crossRef2 = References.x\n\nmodule Ref = References\n\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\"}\n", +"Use4:\n{\"file\": References.res, \"line\": 1, \"code\":\nlet x = 12\n// ^ref\n\nlet a = x\n\"}\n"], +"references": +[{"uri": "Cross.res", "range": {"start": {"line": 0, "character": 15}, "end": {"line": 0, "character": 25}}}, {"uri": "Cross.res", "range": {"start": {"line": 3, "character": 16}, "end": {"line": 3, "character": 26}}}, {"uri": "Cross.res", "range": {"start": {"line": 5, "character": 13}, "end": {"line": 5, "character": 23}}}, {"uri": "Cross.res", "range": {"start": {"line": 7, "character": 16}, "end": {"line": 7, "character": 26}}}, -{"uri": "References.res", "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}} -] +{"uri": "References.res", "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}}]} + References src/Cross.res 9:31 -[ -{"uri": "Cross.res", "range": {"start": {"line": 9, "character": 28}, "end": {"line": 9, "character": 51}}}, +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"UnknownName\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `UnknownName` ...\n- `File2.res` line `34`: function `bar` uses `UnknownName` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"UnknownName\".\nAdd enough details to understand at high level how \"UnknownName\" is used targeted at a person who is trying to undersand the codebase.\n", +"Definition:\n{\"file\": Cross.res, \"line\": 10, \"code\":\n\nmodule Ref = References\n\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\"}\n", +"Use1:\n{\"file\": Cross.res, \"line\": 13, \"code\":\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\nmodule RefWithInterface = ReferencesWithInterface\n\nlet crossRefWithInterface3 = ReferencesWithInterface.x\"}\n", +"Use2:\n{\"file\": Cross.res, \"line\": 15, \"code\":\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\nmodule RefWithInterface = ReferencesWithInterface\n\nlet crossRefWithInterface3 = ReferencesWithInterface.x\n\nlet _ = RenameWithInterface.x\"}\n", +"Use3:\n{\"file\": Cross.res, \"line\": 17, \"code\":\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\nmodule RefWithInterface = ReferencesWithInterface\n\nlet crossRefWithInterface3 = ReferencesWithInterface.x\n\nlet _ = RenameWithInterface.x\n// ^ren RenameWithInterfacePrime\n\"}\n", +"Use4:\n{\"file\": ReferencesWithInterface.res, \"line\": 1, \"code\":\nlet x = 2\"}\n", +"Use5:\n{\"file\": ReferencesWithInterface.resi, \"line\": 1, \"code\":\nlet x: int\"}\n"], +"references": +[{"uri": "Cross.res", "range": {"start": {"line": 9, "character": 28}, "end": {"line": 9, "character": 51}}}, {"uri": "Cross.res", "range": {"start": {"line": 12, "character": 29}, "end": {"line": 12, "character": 52}}}, {"uri": "Cross.res", "range": {"start": {"line": 14, "character": 26}, "end": {"line": 14, "character": 49}}}, {"uri": "Cross.res", "range": {"start": {"line": 16, "character": 29}, "end": {"line": 16, "character": 52}}}, {"uri": "ReferencesWithInterface.res", "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}}, -{"uri": "ReferencesWithInterface.resi", "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}} -] +{"uri": "ReferencesWithInterface.resi", "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}}]} + Rename src/Cross.res 18:13 RenameWithInterfacePrime [ diff --git a/analysis/tests/src/expected/References.res.txt b/analysis/tests/src/expected/References.res.txt index ea3108dae..95461d1b3 100644 --- a/analysis/tests/src/expected/References.res.txt +++ b/analysis/tests/src/expected/References.res.txt @@ -1,33 +1,56 @@ References src/References.res 0:4 -[ -{"uri": "Cross.res", "range": {"start": {"line": 0, "character": 26}, "end": {"line": 0, "character": 27}}}, +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"x\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `x` ...\n- `File2.res` line `34`: function `bar` uses `x` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"x\".\nAdd enough details to understand at high level how \"x\" is used targeted at a person who is trying to undersand the codebase.\n", +"Definition:\n{\"file\": References.res, \"line\": 1, \"code\":\nlet x = 12\n// ^ref\n\nlet a = x\n\"}\n", +"Use1:\n{\"file\": Cross.res, \"line\": 1, \"code\":\nlet crossRef = References.x\n// ^ref\n\nlet crossRef2 = References.x\n\"}\n", +"Use2:\n{\"file\": Cross.res, \"line\": 4, \"code\":\nlet crossRef = References.x\n// ^ref\n\nlet crossRef2 = References.x\n\nmodule Ref = References\n\nlet crossRef3 = References.x\"}\n", +"Use3:\n{\"file\": Cross.res, \"line\": 8, \"code\":\n\nlet crossRef2 = References.x\n\nmodule Ref = References\n\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\"}\n", +"Use4:\n{\"file\": References.res, \"line\": 4, \"code\":\nlet x = 12\n// ^ref\n\nlet a = x\n\nlet b = a\n\nlet c = x\"}\n", +"Use5:\n{\"file\": References.res, \"line\": 8, \"code\":\n\nlet a = x\n\nlet b = a\n\nlet c = x\n\nlet foo = (~xx) => xx + 1\n// ^ref\n\"}\n"], +"references": +[{"uri": "Cross.res", "range": {"start": {"line": 0, "character": 26}, "end": {"line": 0, "character": 27}}}, {"uri": "Cross.res", "range": {"start": {"line": 3, "character": 27}, "end": {"line": 3, "character": 28}}}, {"uri": "Cross.res", "range": {"start": {"line": 7, "character": 27}, "end": {"line": 7, "character": 28}}}, {"uri": "References.res", "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}}, {"uri": "References.res", "range": {"start": {"line": 3, "character": 8}, "end": {"line": 3, "character": 9}}}, -{"uri": "References.res", "range": {"start": {"line": 7, "character": 8}, "end": {"line": 7, "character": 9}}} -] +{"uri": "References.res", "range": {"start": {"line": 7, "character": 8}, "end": {"line": 7, "character": 9}}}]} + References src/References.res 9:19 -[ -{"uri": "References.res", "range": {"start": {"line": 9, "character": 11}, "end": {"line": 9, "character": 14}}}, -{"uri": "References.res", "range": {"start": {"line": 9, "character": 19}, "end": {"line": 9, "character": 21}}} -] +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"xx\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `xx` ...\n- `File2.res` line `34`: function `bar` uses `xx` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"xx\".\nAdd enough details to understand at high level how \"xx\" is used targeted at a person who is trying to undersand the codebase.\n", +"Definition:\n{\"file\": References.res, \"line\": 10, \"code\":\n\nlet b = a\n\nlet c = x\n\nlet foo = (~xx) => xx + 1\n// ^ref\n\nmodule M: {\n let aa: int\"}\n", +"Use1:\n{\"file\": References.res, \"line\": 10, \"code\":\n\nlet b = a\n\nlet c = x\n\nlet foo = (~xx) => xx + 1\n// ^ref\n\nmodule M: {\n let aa: int\"}\n"], +"references": +[{"uri": "References.res", "range": {"start": {"line": 9, "character": 11}, "end": {"line": 9, "character": 14}}}, +{"uri": "References.res", "range": {"start": {"line": 9, "character": 19}, "end": {"line": 9, "character": 21}}}]} + References src/References.res 20:12 -[ -{"uri": "References.res", "range": {"start": {"line": 13, "character": 2}, "end": {"line": 13, "character": 13}}}, +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"aa\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `aa` ...\n- `File2.res` line `34`: function `bar` uses `aa` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"aa\".\nAdd enough details to understand at high level how \"aa\" is used targeted at a person who is trying to undersand the codebase.\n", +"Definition:\n{\"file\": References.res, \"line\": 21, \"code\":\n let aa = 10\n}\n\nlet bb = M.aa\nlet cc = bb\nlet dd = M.aa\n// ^ref\n\nlet _ = \"}\n", +"Use1:\n{\"file\": References.res, \"line\": 14, \"code\":\n\nlet foo = (~xx) => xx + 1\n// ^ref\n\nmodule M: {\n let aa: int\n} = {\n let aa = 10\n}\n\"}\n", +"Use2:\n{\"file\": References.res, \"line\": 19, \"code\":\n let aa: int\n} = {\n let aa = 10\n}\n\nlet bb = M.aa\nlet cc = bb\nlet dd = M.aa\n// ^ref\n\"}\n"], +"references": +[{"uri": "References.res", "range": {"start": {"line": 13, "character": 2}, "end": {"line": 13, "character": 13}}}, {"uri": "References.res", "range": {"start": {"line": 18, "character": 11}, "end": {"line": 18, "character": 13}}}, -{"uri": "References.res", "range": {"start": {"line": 20, "character": 11}, "end": {"line": 20, "character": 13}}} -] +{"uri": "References.res", "range": {"start": {"line": 20, "character": 11}, "end": {"line": 20, "character": 13}}}]} + References src/References.res 23:15 getLocItem #4: heuristic for within fragments: take make as makeProps does not work the type is not great but jump to definition works -[ -{"uri": "ReferencesInner.res", "range": {"start": {"line": 1, "character": 28}, "end": {"line": 1, "character": 32}}}, +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"make\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `make` ...\n- `File2.res` line `34`: function `bar` uses `make` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"make\".\nAdd enough details to understand at high level how \"make\" is used targeted at a person who is trying to undersand the codebase.\n", +"Use1:\n{\"file\": ReferencesInner.res, \"line\": 2, \"code\":\n@react.component\"}\n", +"Use2:\n{\"file\": References.res, \"line\": 24, \"code\":\nlet bb = M.aa\nlet cc = bb\nlet dd = M.aa\n// ^ref\n\nlet _ = \"}\n", +"Use3:\n{\"file\": ComponentInner.res, \"line\": 2, \"code\":\n@react.component\nlet make = () => React.null\"}\n", +"Use4:\n{\"file\": ComponentInner.resi, \"line\": 2, \"code\":\n@react.component\nlet make: unit => React.element\"}\n"], +"references": +[{"uri": "ReferencesInner.res", "range": {"start": {"line": 1, "character": 28}, "end": {"line": 1, "character": 32}}}, {"uri": "References.res", "range": {"start": {"line": 23, "character": 19}, "end": {"line": 23, "character": 23}}}, {"uri": "ComponentInner.res", "range": {"start": {"line": 1, "character": 4}, "end": {"line": 1, "character": 8}}}, -{"uri": "ComponentInner.resi", "range": {"start": {"line": 1, "character": 4}, "end": {"line": 1, "character": 8}}} -] +{"uri": "ComponentInner.resi", "range": {"start": {"line": 1, "character": 4}, "end": {"line": 1, "character": 8}}}]} + diff --git a/analysis/tests/src/expected/ReferencesWithInterface.res.txt b/analysis/tests/src/expected/ReferencesWithInterface.res.txt index 33f2d105d..38e8296a6 100644 --- a/analysis/tests/src/expected/ReferencesWithInterface.res.txt +++ b/analysis/tests/src/expected/ReferencesWithInterface.res.txt @@ -1,9 +1,16 @@ References src/ReferencesWithInterface.res 0:4 -[ -{"uri": "Cross.res", "range": {"start": {"line": 9, "character": 52}, "end": {"line": 9, "character": 53}}}, +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"x\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `x` ...\n- `File2.res` line `34`: function `bar` uses `x` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"x\".\nAdd enough details to understand at high level how \"x\" is used targeted at a person who is trying to undersand the codebase.\n", +"Definition:\n{\"file\": ReferencesWithInterface.res, \"line\": 1, \"code\":\nlet x = 2\"}\n", +"Use1:\n{\"file\": Cross.res, \"line\": 10, \"code\":\n\nmodule Ref = References\n\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\"}\n", +"Use2:\n{\"file\": Cross.res, \"line\": 13, \"code\":\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\nmodule RefWithInterface = ReferencesWithInterface\n\nlet crossRefWithInterface3 = ReferencesWithInterface.x\"}\n", +"Use3:\n{\"file\": Cross.res, \"line\": 17, \"code\":\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\nmodule RefWithInterface = ReferencesWithInterface\n\nlet crossRefWithInterface3 = ReferencesWithInterface.x\n\nlet _ = RenameWithInterface.x\n// ^ren RenameWithInterfacePrime\n\"}\n", +"Use4:\n{\"file\": ReferencesWithInterface.resi, \"line\": 1, \"code\":\nlet x: int\"}\n"], +"references": +[{"uri": "Cross.res", "range": {"start": {"line": 9, "character": 52}, "end": {"line": 9, "character": 53}}}, {"uri": "Cross.res", "range": {"start": {"line": 12, "character": 53}, "end": {"line": 12, "character": 54}}}, {"uri": "Cross.res", "range": {"start": {"line": 16, "character": 53}, "end": {"line": 16, "character": 54}}}, {"uri": "ReferencesWithInterface.resi", "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}}, -{"uri": "ReferencesWithInterface.res", "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}} -] +{"uri": "ReferencesWithInterface.res", "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}}]} + diff --git a/analysis/tests/src/expected/ReferencesWithInterface.resi.txt b/analysis/tests/src/expected/ReferencesWithInterface.resi.txt index 3e96fbc75..d67a7fee5 100644 --- a/analysis/tests/src/expected/ReferencesWithInterface.resi.txt +++ b/analysis/tests/src/expected/ReferencesWithInterface.resi.txt @@ -1,9 +1,16 @@ References src/ReferencesWithInterface.resi 0:4 -[ -{"uri": "Cross.res", "range": {"start": {"line": 9, "character": 52}, "end": {"line": 9, "character": 53}}}, +{"prompt": +["A Snippet has the form: {\"file\": \"Hello.res\", \"line\":23, \"code\": \"...the code...\"} where the first line of code is line 23.\nFind Uses of \"x\" given snippets for its Definition and its Users.\n\nThe input has form:\n Definition:\n snippet\n \n Use1:\n snippet1\n \n Use2:\n snippet2\n ...\n\nYou will produce output of the form:\n- `File.res` line `12`: function `foo` calls `x` ...\n- `File2.res` line `34`: function `bar` uses `x` to ...\n\nIgnore any code in the Snippets that is not directly relevant to using \"x\".\nAdd enough details to understand at high level how \"x\" is used targeted at a person who is trying to undersand the codebase.\n", +"Definition:\n{\"file\": ReferencesWithInterface.resi, \"line\": 1, \"code\":\nlet x: int\"}\n", +"Use1:\n{\"file\": Cross.res, \"line\": 10, \"code\":\n\nmodule Ref = References\n\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\"}\n", +"Use2:\n{\"file\": Cross.res, \"line\": 13, \"code\":\nlet crossRef3 = References.x\n\nlet crossRefWithInterface = ReferencesWithInterface.x\n// ^ref\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\nmodule RefWithInterface = ReferencesWithInterface\n\nlet crossRefWithInterface3 = ReferencesWithInterface.x\"}\n", +"Use3:\n{\"file\": Cross.res, \"line\": 17, \"code\":\n\nlet crossRefWithInterface2 = ReferencesWithInterface.x\n\nmodule RefWithInterface = ReferencesWithInterface\n\nlet crossRefWithInterface3 = ReferencesWithInterface.x\n\nlet _ = RenameWithInterface.x\n// ^ren RenameWithInterfacePrime\n\"}\n", +"Use4:\n{\"file\": ReferencesWithInterface.res, \"line\": 1, \"code\":\nlet x = 2\"}\n"], +"references": +[{"uri": "Cross.res", "range": {"start": {"line": 9, "character": 52}, "end": {"line": 9, "character": 53}}}, {"uri": "Cross.res", "range": {"start": {"line": 12, "character": 53}, "end": {"line": 12, "character": 54}}}, {"uri": "Cross.res", "range": {"start": {"line": 16, "character": 53}, "end": {"line": 16, "character": 54}}}, {"uri": "ReferencesWithInterface.res", "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}}, -{"uri": "ReferencesWithInterface.resi", "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}} -] +{"uri": "ReferencesWithInterface.resi", "range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 5}}}]} + diff --git a/package-lock.json b/package-lock.json index 4da8af3eb..6609182af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,11 @@ "version": "1.18.0", "hasInstallScript": true, "license": "MIT", + "dependencies": { + "gpt-3-encoder": "^1.1.4", + "openai": "^3.2.1", + "vscode-uri": "^3.0.7" + }, "devDependencies": { "@types/node": "^14.14.41", "@types/vscode": "1.68.0", @@ -31,6 +36,75 @@ "integrity": "sha512-duBwEK5ta/eBBMJMQ7ECMEsMvlE3XJdRGh3xoS1uOO4jl2Z4LPBl5vx8WvBP10ERAgDRmIt/FaSD4RHyBGbChw==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gpt-3-encoder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/gpt-3-encoder/-/gpt-3-encoder-1.1.4.tgz", + "integrity": "sha512-fSQRePV+HUAhCn7+7HL7lNIXNm6eaFWFbNLOOGtmSJ0qJycyQvj60OvRlH7mee8xAMjBDNRdMXlMwjAbMTDjkg==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -43,6 +117,34 @@ "node": ">=10" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/openai": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", + "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", + "dependencies": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, "node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -71,6 +173,11 @@ "node": ">=4.2.0" } }, + "node_modules/vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -91,6 +198,52 @@ "integrity": "sha512-duBwEK5ta/eBBMJMQ7ECMEsMvlE3XJdRGh3xoS1uOO4jl2Z4LPBl5vx8WvBP10ERAgDRmIt/FaSD4RHyBGbChw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "requires": { + "follow-redirects": "^1.14.8" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "gpt-3-encoder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/gpt-3-encoder/-/gpt-3-encoder-1.1.4.tgz", + "integrity": "sha512-fSQRePV+HUAhCn7+7HL7lNIXNm6eaFWFbNLOOGtmSJ0qJycyQvj60OvRlH7mee8xAMjBDNRdMXlMwjAbMTDjkg==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -100,6 +253,28 @@ "yallist": "^4.0.0" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "openai": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", + "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", + "requires": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -115,6 +290,11 @@ "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", "dev": true }, + "vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 5c0a83918..6757a542c 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,14 @@ "type": "object", "title": "ReScript", "properties": { + "rescript.settings.openaiKey": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "OpenAI API key" + }, "rescript.settings.allowBuiltInFormatter": { "scope": "language-overridable", "type": "boolean", @@ -221,5 +229,10 @@ "@types/vscode": "1.68.0", "semver": "^7.3.7", "typescript": "^4.7.3" + }, + "dependencies": { + "gpt-3-encoder": "^1.1.4", + "openai": "^3.2.1", + "vscode-uri": "^3.0.7" } } diff --git a/server/src/server.ts b/server/src/server.ts index 98ec1779f..2215e4d74 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -25,8 +25,13 @@ import { fileURLToPath } from "url"; import { ChildProcess } from "child_process"; import { WorkspaceEdit } from "vscode-languageserver"; import { filesDiagnostics } from "./utils"; +import { URI } from "vscode-uri"; +import * as os from "os"; +const { Configuration, OpenAIApi } = require("openai"); +const { encode, decode } = require("gpt-3-encoder"); interface extensionConfiguration { + openaiKey: string | null; allowBuiltInFormatter: boolean; askToStartBuild: boolean; inlayHints: { @@ -53,6 +58,7 @@ let extensionClientCapabilities: extensionClientCapabilities = {}; // All values here are temporary, and will be overridden as the server is // initialized, and the current config is received from the client. let extensionConfiguration: extensionConfiguration = { + openaiKey: null, allowBuiltInFormatter: false, askToStartBuild: true, inlayHints: { @@ -536,18 +542,93 @@ function typeDefinition(msg: p.RequestMessage) { return response; } +var promptNum = 0; + +function showMarkdown(content: string, title: string) { + promptNum++; + let tempFileName = "prompt_" + promptNum + "____" + process.pid + ".md"; + let tmpname = path.join(os.tmpdir(), tempFileName); + fs.writeFileSync(tmpname, content, { encoding: "utf-8" }); + let command = `command:markdown.showPreview?${encodeURIComponent( + JSON.stringify([URI.parse(tmpname)]) + )}`; + let params: p.ShowMessageParams = { + type: p.MessageType.Warning, + message: `${title} is ready. [Open it](${command}).`, + }; + let message: p.NotificationMessage = { + jsonrpc: c.jsonrpcVersion, + method: "window/showMessage", + params: params, + }; + send(message); +} + function references(msg: p.RequestMessage) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references let params = msg.params as p.ReferenceParams; let filePath = fileURLToPath(params.textDocument.uri); - let result: typeof p.ReferencesRequest.type = utils.getReferencesForPosition( - filePath, - params.position - ); + let result = utils.getReferencesForPosition(filePath, params.position); + let references: typeof p.ReferencesRequest.type = result.references; + let segments: string[] = result.prompt; + + type model = { name: string; maxTokens: number }; + let gpt3: model = { name: "gpt-3.5-turbo", maxTokens: 4000 }; + let gpt3_16k: model = { name: "gpt-3.5-turbo-16k", maxTokens: 16000 }; + let gpt4: model = { name: "gpt-4", maxTokens: 4000 }; + + let model = gpt3; + let prompt = ""; + let numTokens = 0; + while (true) { + let nextSegment = segments.shift(); + if (!nextSegment) break; + let newPrompt = prompt + "\n" + nextSegment; + let newTokens = encode(newPrompt).length; + if (newTokens < model.maxTokens) { + prompt = newPrompt; + numTokens = newTokens; + } else break; + } + let key = extensionConfiguration.openaiKey; + if (!key) { + showMarkdown( + "Please set the `OpenAI API Key` in the `settings.json` file.", + "OpenAI API Key not set." + ); + } else { + const configuration = new Configuration({ + apiKey: extensionConfiguration.openaiKey, + }); + const openai = new OpenAIApi(configuration); + let foo = async () => { + var completion; + var err; + try { + completion = await openai.createChatCompletion({ + model: model.name, + messages: [{ role: "user", content: prompt }], + }); + } catch (error) { + let e: any = error; + if (e.response) { + err = `status:${e.response.status}, data:${JSON.stringify( + e.response.data + )}`; + } else { + err = `Error with OpenAI API request: ${e.message}`; + } + } + let reply = err || completion.data.choices[0].message.content; + showMarkdown(reply, "The Reply"); + }; + foo(); + } + let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion, id: msg.id, - result, + result: references, // error: code and message set in case an exception happens during the definition request. }; return response;