diff --git a/analysis/bin/main.ml b/analysis/bin/main.ml index e5075f102ed..0afe23855ab 100644 --- a/analysis/bin/main.ml +++ b/analysis/bin/main.ml @@ -134,22 +134,18 @@ let main () = | _ -> print_endline "\"ERR: Did not find root \"") | [_; "completion"; path; line; col; currentFile] -> printHeaderInfo path line col; - Commands.completion ~debug ~path + Cli.completion ~debug ~path ~pos:(int_of_string line, int_of_string col) ~currentFile | [_; "completionResolve"; path; modulePath] -> - Commands.completionResolve ~path ~modulePath + Cli.completionResolve ~path ~modulePath | [_; "definition"; path; line; col] -> - Commands.definition ~path - ~pos:(int_of_string line, int_of_string col) - ~debug + Cli.definition ~path ~pos:(int_of_string line, int_of_string col) ~debug | [_; "typeDefinition"; path; line; col] -> - Commands.typeDefinition ~path - ~pos:(int_of_string line, int_of_string col) - ~debug + Cli.typeDefinition ~path ~pos:(int_of_string line, int_of_string col) ~debug | [_; "documentSymbol"; path] -> DocumentSymbol.command ~path | [_; "hover"; path; line; col; currentFile; supportsMarkdownLinks] -> - Commands.hover ~path + Cli.hover ~path ~pos:(int_of_string line, int_of_string col) ~currentFile ~debug ~supportsMarkdownLinks: @@ -159,7 +155,7 @@ let main () = | [ _; "signatureHelp"; path; line; col; currentFile; allowForConstructorPayloads; ] -> - Commands.signatureHelp ~path + Cli.signatureHelp ~path ~pos:(int_of_string line, int_of_string col) ~currentFile ~debug ~allowForConstructorPayloads: @@ -167,13 +163,13 @@ let main () = | "true" -> true | _ -> false) | [_; "inlayHint"; path; line_start; line_end; maxLength] -> - Commands.inlayhint ~path + Cli.inlayhint ~path ~pos:(int_of_string line_start, int_of_string line_end) ~maxLength ~debug - | [_; "codeLens"; path] -> Commands.codeLens ~path ~debug + | [_; "codeLens"; path] -> Cli.codeLens ~path ~debug | [_; "codeAction"; path; startLine; startCol; endLine; endCol; currentFile] -> - Commands.codeAction ~path + Cli.codeAction ~path ~startPos:(int_of_string startLine, int_of_string startCol) ~endPos:(int_of_string endLine, int_of_string endCol) ~currentFile ~debug @@ -183,34 +179,29 @@ let main () = | "add-missing-cases" -> Codemod.AddMissingCases | _ -> raise (Failure "unsupported type") in + let source = Files.readFile path |> Option.value ~default:"" in let res = - Codemod.transform ~path + Codemod.transform ~source ~pos:(int_of_string line, int_of_string col) ~debug ~typ ~hint |> Json.escape in Printf.printf "\"%s\"" res - | [_; "diagnosticSyntax"; path] -> Commands.diagnosticSyntax ~path + | [_; "diagnosticSyntax"; path] -> Cli.diagnosticSyntax ~path | [_; "references"; path; line; col] -> - Commands.references ~path - ~pos:(int_of_string line, int_of_string col) - ~debug + Cli.references ~path ~pos:(int_of_string line, int_of_string col) ~debug | [_; "prepareRename"; path; line; col] -> - Commands.prepareRename ~path - ~pos:(int_of_string line, int_of_string col) - ~debug + Cli.prepareRename ~path ~pos:(int_of_string line, int_of_string col) ~debug | [_; "rename"; path; line; col; newName] -> - Commands.rename ~path + Cli.rename ~path ~pos:(int_of_string line, int_of_string col) ~newName ~debug - | [_; "semanticTokens"; currentFile] -> - SemanticTokens.semanticTokens ~currentFile + | [_; "semanticTokens"; currentFile] -> Cli.semanticTokens ~path:currentFile | [_; "createInterface"; path; cmiFile] -> Printf.printf "\"%s\"" (Json.escape (CreateInterface.command ~path ~cmiFile)) - | [_; "format"; path] -> - Printf.printf "\"%s\"" (Json.escape (Commands.format ~path)) - | [_; "test"; path] -> Commands.test ~path + | [_; "format"; path] -> Cli.format ~path + | [_; "test"; path] -> Cli.test ~path | [_; "cmt"; rescript_json; cmt_path] -> CmtViewer.dump rescript_json cmt_path | args when List.mem "-h" args || List.mem "--help" args -> prerr_endline help | _ -> diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml new file mode 100644 index 00000000000..bd8ced94a14 --- /dev/null +++ b/analysis/src/Cli.ml @@ -0,0 +1,385 @@ +let completion ~debug ~path ~pos ~currentFile = + let full = Cmt.loadFullCmtFromPath ~path in + let kindFile = Files.classifySourceFile currentFile in + match Files.readFile currentFile with + | None | Some "" -> Protocol.null |> print_endline + | Some source -> + Commands.completion ~debug ~source ~kindFile ~pos ~full + |> List.map Protocol.stringifyCompletionItem + |> Protocol.array |> print_endline + +let completionResolve ~path ~modulePath = + let full = Cmt.loadFullCmtFromPath ~path in + let result = + match Commands.completionResolve ~full ~modulePath with + | None -> Protocol.null + | Some content -> Protocol.wrapInQuotes content + in + print_endline result + +let inlayhint ~path ~pos ~maxLength ~debug = + let full = Cmt.loadFullCmtFromPath ~path in + let kindFile = Files.classifySourceFile path in + match Files.readFile path with + | None -> Protocol.null |> print_endline + | Some source -> ( + match Hint.inlay ~source ~kindFile ~pos ~maxLength ~full ~debug with + | Some hints -> + hints + |> List.map Protocol.stringifyHint + |> Protocol.array |> print_endline + | None -> Protocol.null |> print_endline) + +let codeLens ~path ~debug = + let full = Cmt.loadFullCmtFromPath ~path in + let kindFile = Files.classifySourceFile path in + match Files.readFile path with + | None -> Protocol.null |> print_endline + | Some source -> ( + match Hint.codeLens ~source ~kindFile ~full ~debug with + | Some lens -> + lens + |> List.map Protocol.stringifyCodeLens + |> Protocol.array |> print_endline + | None -> Protocol.null |> print_endline) + +let hover ~path ~pos ~currentFile ~debug ~supportsMarkdownLinks = + let full = Cmt.loadFullCmtFromPath ~path in + let kindFile = Files.classifySourceFile currentFile in + match Files.readFile currentFile with + | None -> Protocol.null |> print_endline + | Some source -> + let result = + match + Commands.hover ~source ~kindFile ~pos ~debug ~supportsMarkdownLinks + ~full + with + | Some value -> Protocol.stringifyHover value + | None -> Protocol.null + in + print_endline result + +let signatureHelp ~path ~pos ~currentFile ~debug ~allowForConstructorPayloads = + let full = Cmt.loadFullCmtFromPath ~path in + let kindFile = Files.classifySourceFile currentFile in + match Files.readFile currentFile with + | None -> Protocol.null |> print_endline + | Some source -> + Commands.signatureHelp ~source ~kindFile ~pos ~allowForConstructorPayloads + ~full ~debug + |> Protocol.stringifySignatureHelp |> print_endline + +let codeAction ~path ~startPos ~endPos ~currentFile ~debug = + let kindFile = Files.classifySourceFile currentFile in + match Files.readFile currentFile with + | None -> Protocol.null |> print_endline + | Some source -> + Xform.extractCodeActions ~path ~startPos ~endPos ~source ~kindFile ~debug + |> CodeActions.stringifyCodeActions |> print_endline + +let definition ~path ~pos ~debug = + let full = Cmt.loadFullCmtFromPath ~path in + print_endline + (match Commands.definition ~full ~pos ~debug with + | None -> Protocol.null + | Some location -> location |> Protocol.stringifyLocation) + +let typeDefinition ~path ~pos ~debug = + let full = Cmt.loadFullCmtFromPath ~path in + print_endline + (match Commands.typeDefinition ~full ~pos ~debug with + | None -> Protocol.null + | Some location -> location |> Protocol.stringifyLocation) + +let references ~path ~pos ~debug = + let full = Cmt.loadFullCmtFromPath ~path in + let allLocs = Commands.references ~full ~pos ~debug in + print_endline + (if allLocs = [] then Protocol.null + else + "[\n" + ^ (allLocs |> List.map Protocol.stringifyLocation |> String.concat ",\n") + ^ "\n]") + +let rename ~path ~pos ~newName ~debug = + let full = Cmt.loadFullCmtFromPath ~path in + let result = + match Commands.rename ~full ~pos ~newName ~debug with + | None -> Protocol.null + | Some (fileRenames, textDocumentEdits) -> + let fileRenamesString = + fileRenames |> List.map Protocol.stringifyRenameFile + in + let textDocumentEditsString = + textDocumentEdits |> List.map Protocol.stringifyTextDocumentEdit + in + "[\n" + ^ (fileRenamesString @ textDocumentEditsString |> String.concat ",\n") + ^ "\n]" + in + print_endline result + +let prepareRename ~path ~pos ~debug = + let full = Cmt.loadFullCmtFromPath ~path in + let result = + match Commands.prepareRename ~full ~pos ~debug with + | None -> Protocol.null + | Some (Range range) -> Protocol.stringifyRange range + | Some (Placeholder rangeph) -> + Protocol.stringifyRangeWithPlaceholder rangeph + in + print_endline result + +let format ~path = + match Files.readFile path with + | None -> Protocol.null |> print_endline + | Some source -> ( + let kindFile = Files.classifySourceFile path in + match Commands.format ~source ~kindFile with + | Ok textEdits -> ( + match textEdits with + | {newText} :: _ -> Printf.printf "\"%s\"" (Json.escape newText) + | _ -> Protocol.null |> print_endline) + | Error _ -> Protocol.null |> print_endline) + +let diagnosticSyntax ~path = + match Files.readFile path with + | None -> Protocol.array [""] |> print_endline + | Some source -> + let kindFile = Files.classifySourceFile path in + Diagnostics.document_syntax ~source ~kindFile + |> List.map Protocol.stringifyDiagnostic + |> Protocol.array |> print_endline + +let semanticTokens ~path = + match Files.readFile path with + | None -> Protocol.null |> print_endline + | Some source -> + let kindFile = Files.classifySourceFile path in + let tokens = SemanticTokens.semanticTokens ~source ~kindFile in + let data = SemanticTokens.Token.arrayToJsonString tokens.data in + Printf.printf "{\"data\":%s}" data + +let test ~path = + Uri.stripPath := true; + match Files.readFile path with + | None -> assert false + | Some text -> + let lines = text |> String.split_on_char '\n' in + let processLine i line = + let createCurrentFile () = + let currentFile, cout = + Filename.open_temp_file "def" ("txt." ^ Filename.extension path) + in + let removeLineComment l = + let len = String.length l in + let rec loop i = + if i + 2 <= len && l.[i] = '/' && l.[i + 1] = '/' then Some (i + 2) + else if i + 2 < len && l.[i] = ' ' then loop (i + 1) + else None + in + match loop 0 with + | None -> l + | Some indexAfterComment -> + String.make indexAfterComment ' ' + ^ String.sub l indexAfterComment (len - indexAfterComment) + in + lines + |> List.iteri (fun j l -> + let lineToOutput = + if j == i - 1 then removeLineComment l else l + in + Printf.fprintf cout "%s\n" lineToOutput); + close_out cout; + currentFile + in + if Str.string_match (Str.regexp "^ *//[ ]*\\^") line 0 then + let matched = Str.matched_string line in + let len = line |> String.length in + let mlen = String.length matched in + let rest = String.sub line mlen (len - mlen) in + let line = i - 1 in + let col = mlen - 1 in + if mlen >= 3 then ( + (match String.sub rest 0 3 with + | "db+" -> Log.verbose := true + | "db-" -> Log.verbose := false + | "dv+" -> Debug.debugLevel := Verbose + | "dv-" -> Debug.debugLevel := Off + | "in+" -> Cfg.inIncrementalTypecheckingMode := true + | "in-" -> Cfg.inIncrementalTypecheckingMode := false + | "ve+" -> ( + let version = String.sub rest 3 (String.length rest - 3) in + let version = String.trim version in + if Debug.verbose () then + Printf.printf "Setting version: %s\n" version; + match String.split_on_char '.' version with + | [majorRaw; minorRaw] -> + let version = (int_of_string majorRaw, int_of_string minorRaw) in + Packages.overrideRescriptVersion := Some version + | _ -> ()) + | "ve-" -> Packages.overrideRescriptVersion := None + | "def" -> + print_endline + ("Definition " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + definition ~path ~pos:(line, col) ~debug:true + | "com" -> + print_endline + ("Complete " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let currentFile = createCurrentFile () in + completion ~debug:true ~path ~pos:(line, col) ~currentFile; + Sys.remove currentFile + | "cre" -> + let modulePath = String.sub rest 3 (String.length rest - 3) in + let modulePath = String.trim modulePath in + print_endline ("Completion resolve: " ^ modulePath); + completionResolve ~path ~modulePath + | "dce" -> + print_endline ("DCE " ^ path); + Reanalyze.RunConfig.runConfig.suppress <- ["src"]; + Reanalyze.RunConfig.runConfig.unsuppress <- + [Filename.concat "src" "dce"]; + DceCommand.command () + | "doc" -> + print_endline ("DocumentSymbol " ^ path); + DocumentSymbol.command ~path + | "hig" -> + print_endline ("Highlight " ^ path); + let source = Files.readFile path |> Option.get in + let kindFile = Files.classifySourceFile path in + + SemanticTokens.command ~debug:true + ~emitter:(SemanticTokens.Token.createEmitter ()) + ~source ~kindFile + | "hov" -> + print_endline + ("Hover " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let currentFile = createCurrentFile () in + hover ~supportsMarkdownLinks:true ~path ~pos:(line, col) + ~currentFile ~debug:true; + Sys.remove currentFile + | "she" -> + print_endline + ("Signature help " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let currentFile = createCurrentFile () in + signatureHelp ~path ~pos:(line, col) ~currentFile ~debug:true + ~allowForConstructorPayloads:true; + Sys.remove currentFile + | "int" -> + print_endline ("Create Interface " ^ path); + let cmiFile = + let open Filename in + let ( ++ ) = concat in + let name = chop_extension (basename path) ^ ".cmi" in + let dir = dirname path in + dir ++ parent_dir_name ++ "lib" ++ "bs" ++ "src" ++ name + in + Printf.printf "%s" (CreateInterface.command ~path ~cmiFile) + | "ref" -> + print_endline + ("References " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + references ~path ~pos:(line, col) ~debug:true + | "pre" -> + print_endline + ("PrepareRename " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + prepareRename ~path ~pos:(line, col) ~debug:true + | "ren" -> + let newName = String.sub rest 4 (len - mlen - 4) in + let () = + print_endline + ("Rename " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col ^ " " ^ newName) + in + rename ~path ~pos:(line, col) ~newName ~debug:true + | "typ" -> + print_endline + ("TypeDefinition " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + typeDefinition ~path ~pos:(line, col) ~debug:true + | "xfm" -> + let currentFile = createCurrentFile () in + (* +2 is to ensure that the character ^ points to is what's considered the end of the selection. *) + let endCol = col + try String.index rest '^' + 2 with _ -> 0 in + let endPos = (line, endCol) in + let startPos = (line, col) in + if startPos = endPos then + print_endline + ("Xform " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col) + else + print_endline + ("Xform " ^ path ^ " start: " ^ Pos.toString startPos + ^ ", end: " ^ Pos.toString endPos); + + let source = + Files.readFile currentFile |> Option.value ~default:"" + in + let kindFile = Files.classifySourceFile currentFile in + let codeActions = + Xform.extractCodeActions ~path ~startPos ~endPos ~source ~kindFile + ~debug:true + in + Sys.remove currentFile; + codeActions + |> List.iter (fun {Protocol.title; edit = {documentChanges}} -> + Printf.printf "Hit: %s\n" title; + documentChanges + |> 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 + ("Codemod AddMissingCases" ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let source = Files.readFile path |> Option.value ~default:"" in + Codemod.transform ~source ~pos:(line, col) ~debug:true + ~typ:AddMissingCases ~hint + |> print_endline + | "dia" -> diagnosticSyntax ~path + | "hin" -> + (* Get all inlay Hint between line 1 and n. + Don't get the first line = 0. + *) + let line_start = 1 in + let line_end = 34 in + print_endline + ("Inlay Hint " ^ path ^ " " ^ string_of_int line_start ^ ":" + ^ string_of_int line_end); + inlayhint ~path ~pos:(line_start, line_end) ~maxLength:"25" + ~debug:false + | "cle" -> + print_endline ("Code Lens " ^ path); + codeLens ~path ~debug:false + | "ast" -> + print_endline + ("Dump AST " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let currentFile = createCurrentFile () in + DumpAst.dump ~pos:(line, col) ~currentFile; + Sys.remove currentFile + | "sem" -> semanticTokens ~path + | _ -> ()); + print_newline ()) + in + lines |> List.iteri processLine diff --git a/analysis/src/Codemod.ml b/analysis/src/Codemod.ml index 5c273637def..970dfb79413 100644 --- a/analysis/src/Codemod.ml +++ b/analysis/src/Codemod.ml @@ -5,8 +5,8 @@ let rec collectPatterns p = | Ppat_or (p1, p2) -> collectPatterns p1 @ [p2] | _ -> [p] -let transform ~path ~pos ~debug ~typ ~hint = - let structure, printExpr, _, _ = Xform.parseImplementation ~filename:path in +let transform ~source ~pos ~debug ~typ ~hint = + let structure, printExpr, _, _ = Xform.parseImplementation ~source in match typ with | AddMissingCases -> ( let source = "let " ^ hint ^ " = ()" in diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index 40799348ec5..6f728d767ff 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -1,17 +1,13 @@ -let completion ~debug ~path ~pos ~currentFile = - let completions = - match - Completions.getCompletions ~debug ~path ~pos ~currentFile ~forHover:false - with - | None -> [] - | Some (completions, full, _) -> - completions - |> List.map (CompletionBackEnd.completionToItem ~full) - |> List.map Protocol.stringifyCompletionItem - in - completions |> Protocol.array |> print_endline +let completion ~debug ~source ~kindFile ~pos ~full = + match + Completions.getCompletions ~debug ~source ~kindFile ~pos ~full + ~forHover:false + with + | None -> [] + | Some (completions, full, _) -> + completions |> List.map (CompletionBackEnd.completionToItem ~full) -let completionResolve ~path ~modulePath = +let completionResolve ~(full : SharedTypes.full option) ~modulePath = (* We ignore the internal module path as of now because there's currently no use case for it. But, if we wanted to move resolving documentation for regular modules and not just file modules to the completionResolve @@ -23,44 +19,26 @@ let completionResolve ~path ~modulePath = | [] -> raise (Failure "Invalid module path.") in let docstring = - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> if Debug.verbose () then Printf.printf "[completion_resolve] Could not load cmt\n"; - Protocol.null + None | Some full -> ( match ProcessCmt.fileForModule ~package:full.package moduleName with | None -> if Debug.verbose () then Printf.printf "[completion_resolve] Did not find file for module %s\n" moduleName; - Protocol.null - | Some file -> - file.structure.docstring |> String.concat "\n\n" - |> Protocol.wrapInQuotes) - in - print_endline docstring - -let inlayhint ~path ~pos ~maxLength ~debug = - let result = - match Hint.inlay ~path ~pos ~maxLength ~debug with - | Some hints -> hints |> Protocol.array - | None -> Protocol.null + None + | Some file -> Some (file.structure.docstring |> String.concat "\n\n")) in - print_endline result + docstring -let codeLens ~path ~debug = +let hover ~source ~kindFile ~pos ~supportsMarkdownLinks ~full ~debug = let result = - match Hint.codeLens ~path ~debug with - | Some lens -> lens |> Protocol.array - | None -> Protocol.null - in - print_endline result - -let hover ~path ~pos ~currentFile ~debug ~supportsMarkdownLinks = - let result = - match Cmt.loadFullCmtFromPath ~path with - | None -> Protocol.null + match full with + | None -> None | Some full -> ( match References.getLocItem ~full ~pos ~debug with | None -> ( @@ -68,12 +46,12 @@ let hover ~path ~pos ~currentFile ~debug ~supportsMarkdownLinks = Printf.printf "Nothing at that position. Now trying to use completion.\n"; match - Hover.getHoverViaCompletions ~debug ~path ~pos ~currentFile - ~forHover:true ~supportsMarkdownLinks + Hover.getHoverViaCompletions ~debug ~source ~kindFile ~pos + ~forHover:true ~supportsMarkdownLinks ~full:(Some full) with - | None -> Protocol.null - | Some hover -> hover) - | Some locItem -> ( + | None -> None + | Some hover -> Some hover) + | Some locItem -> let isModule = match locItem.locType with | LModule _ | TopLevelModule _ -> true @@ -91,34 +69,24 @@ let hover ~path ~pos ~currentFile ~debug ~supportsMarkdownLinks = (* Skip if range is all zero, unless it's a module *) (not isModule) && posIsZero loc.loc_start && posIsZero loc.loc_end in - if skipZero then Protocol.null - else - let hoverText = Hover.newHover ~supportsMarkdownLinks ~full locItem in - match hoverText with - | None -> Protocol.null - | Some s -> Protocol.stringifyHover s)) + if skipZero then None + else Hover.newHover ~supportsMarkdownLinks ~full locItem) in - print_endline result + result -let signatureHelp ~path ~pos ~currentFile ~debug ~allowForConstructorPayloads = - let result = - match - SignatureHelp.signatureHelp ~path ~pos ~currentFile ~debug - ~allowForConstructorPayloads - with - | None -> - {Protocol.signatures = []; activeSignature = None; activeParameter = None} - | Some res -> res - in - print_endline (Protocol.stringifySignatureHelp result) - -let codeAction ~path ~startPos ~endPos ~currentFile ~debug = - Xform.extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug - |> CodeActions.stringifyCodeActions |> print_endline +let signatureHelp ~source ~kindFile ~pos ~allowForConstructorPayloads ~full + ~debug = + match + SignatureHelp.signatureHelp ~debug ~source ~kindFile ~pos + ~allowForConstructorPayloads ~full + with + | None -> + {Protocol.signatures = []; activeSignature = None; activeParameter = None} + | Some res -> res -let definition ~path ~pos ~debug = +let definition ~full ~pos ~debug = let locationOpt = - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> None | Some full -> ( match References.getLocItem ~full ~pos ~debug with @@ -150,14 +118,11 @@ let definition ~path ~pos ~debug = } | Some _ -> None)) in - print_endline - (match locationOpt with - | None -> Protocol.null - | Some location -> location |> Protocol.stringifyLocation) + locationOpt -let typeDefinition ~path ~pos ~debug = +let typeDefinition ~full ~pos ~debug = let maybeLocation = - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> None | Some full -> ( match References.getLocItem ~full ~pos ~debug with @@ -172,14 +137,11 @@ let typeDefinition ~path ~pos ~debug = range = Utils.cmtLocToRange loc; })) in - print_endline - (match maybeLocation with - | None -> Protocol.null - | Some location -> location |> Protocol.stringifyLocation) + maybeLocation -let references ~path ~pos ~debug = +let references ~full ~pos ~debug = let allLocs = - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> [] | Some full -> ( match References.getLocItem ~full ~pos ~debug with @@ -194,22 +156,23 @@ let references ~path ~pos ~debug = | Some loc -> loc | None -> Uri.toTopLevelLoc uri2 in - Protocol.stringifyLocation - {uri = Uri.toString uri2; range = Utils.cmtLocToRange loc} + + { + Protocol.uri = Uri.toString uri2; + range = Utils.cmtLocToRange loc; + } :: acc) []) in - print_endline - (if allLocs = [] then Protocol.null - else "[\n" ^ (allLocs |> String.concat ",\n") ^ "\n]") + allLocs -let rename ~path ~pos ~newName ~debug = +let rename ~full ~pos ~newName ~debug = let result = - match Cmt.loadFullCmtFromPath ~path with - | None -> Protocol.null + match full with + | None -> None | Some full -> ( match References.getLocItem ~full ~pos ~debug with - | None -> Protocol.null + | None -> None | Some locItem -> let allReferences = References.allReferencesForLocItem ~full locItem in let referencesToToplevelModules = @@ -263,24 +226,16 @@ let rename ~path ~pos ~newName ~debug = textDocumentEdit :: acc) textEditsByUri [] in - let fileRenamesString = - fileRenames |> List.map Protocol.stringifyRenameFile - in - let textDocumentEditsString = - textDocumentEdits |> List.map Protocol.stringifyTextDocumentEdit - in - "[\n" - ^ (fileRenamesString @ textDocumentEditsString |> String.concat ",\n") - ^ "\n]") + Some (fileRenames, textDocumentEdits)) in - print_endline result + result -let prepareRename ~path ~pos ~debug = - match Cmt.loadFullCmtFromPath ~path with - | None -> print_endline Protocol.null +let prepareRename ~full ~pos ~debug = + match full with + | None -> None | Some full -> ( match References.getLocItem ~full ~pos ~debug with - | None -> print_endline Protocol.null + | None -> None | Some locItem -> let range = Utils.cmtLocToRange locItem.loc in let placeholderOpt = @@ -290,245 +245,37 @@ let prepareRename ~path ~pos ~debug = Some name | _ -> None in - let fields = - [("range", Some (Protocol.stringifyRange range))] - @ - match placeholderOpt with - | None -> [] - | Some s -> [("placeholder", Some (Protocol.wrapInQuotes s))] - in - print_endline (Protocol.stringifyObject fields)) + Some + (match placeholderOpt with + | None -> Protocol.Range range + | Some placeholder -> Protocol.Placeholder {range; placeholder})) -let format ~path = - if Filename.check_suffix path ".res" then - let {Res_driver.parsetree = structure; comments; diagnostics} = - Res_driver.parsing_engine.parse_implementation ~for_printer:true - ~filename:path - in - if List.length diagnostics > 0 then "" - else Res_printer.print_implementation ~comments structure - else if Filename.check_suffix path ".resi" then - let {Res_driver.parsetree = signature; comments; diagnostics} = - Res_driver.parsing_engine.parse_interface ~for_printer:true ~filename:path - in - if List.length diagnostics > 0 then "" - else Res_printer.print_interface ~comments signature - else "" - -let diagnosticSyntax ~path = - print_endline (Diagnostics.document_syntax ~path |> Protocol.array) +let format ~source ~kindFile = + let max = String.length source in + let range = + Protocol. + {start = {line = 0; character = 0}; end_ = {line = max; character = max}} + in -let test ~path = - Uri.stripPath := true; - match Files.readFile path with - | None -> assert false - | Some text -> - let lines = text |> String.split_on_char '\n' in - let processLine i line = - let createCurrentFile () = - let currentFile, cout = - Filename.open_temp_file "def" ("txt." ^ Filename.extension path) - in - let removeLineComment l = - let len = String.length l in - let rec loop i = - if i + 2 <= len && l.[i] = '/' && l.[i + 1] = '/' then Some (i + 2) - else if i + 2 < len && l.[i] = ' ' then loop (i + 1) - else None - in - match loop 0 with - | None -> l - | Some indexAfterComment -> - String.make indexAfterComment ' ' - ^ String.sub l indexAfterComment (len - indexAfterComment) - in - lines - |> List.iteri (fun j l -> - let lineToOutput = - if j == i - 1 then removeLineComment l else l - in - Printf.fprintf cout "%s\n" lineToOutput); - close_out cout; - currentFile + let result = + match kindFile with + | Files.Res -> + let {Res_driver.parsetree = structure; comments; diagnostics} = + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:true ~source in - if Str.string_match (Str.regexp "^ *//[ ]*\\^") line 0 then - let matched = Str.matched_string line in - let len = line |> String.length in - let mlen = String.length matched in - let rest = String.sub line mlen (len - mlen) in - let line = i - 1 in - let col = mlen - 1 in - if mlen >= 3 then ( - (match String.sub rest 0 3 with - | "db+" -> Log.verbose := true - | "db-" -> Log.verbose := false - | "dv+" -> Debug.debugLevel := Verbose - | "dv-" -> Debug.debugLevel := Off - | "in+" -> Cfg.inIncrementalTypecheckingMode := true - | "in-" -> Cfg.inIncrementalTypecheckingMode := false - | "ve+" -> ( - let version = String.sub rest 3 (String.length rest - 3) in - let version = String.trim version in - if Debug.verbose () then - Printf.printf "Setting version: %s\n" version; - match String.split_on_char '.' version with - | [majorRaw; minorRaw] -> - let version = (int_of_string majorRaw, int_of_string minorRaw) in - Packages.overrideRescriptVersion := Some version - | _ -> ()) - | "ve-" -> Packages.overrideRescriptVersion := None - | "def" -> - print_endline - ("Definition " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - definition ~path ~pos:(line, col) ~debug:true - | "com" -> - print_endline - ("Complete " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - let currentFile = createCurrentFile () in - completion ~debug:true ~path ~pos:(line, col) ~currentFile; - Sys.remove currentFile - | "cre" -> - let modulePath = String.sub rest 3 (String.length rest - 3) in - let modulePath = String.trim modulePath in - print_endline ("Completion resolve: " ^ modulePath); - completionResolve ~path ~modulePath - | "dce" -> - print_endline ("DCE " ^ path); - Reanalyze.RunConfig.runConfig.suppress <- ["src"]; - Reanalyze.RunConfig.runConfig.unsuppress <- - [Filename.concat "src" "dce"]; - DceCommand.command () - | "doc" -> - print_endline ("DocumentSymbol " ^ path); - DocumentSymbol.command ~path - | "hig" -> - print_endline ("Highlight " ^ path); - SemanticTokens.command ~debug:true - ~emitter:(SemanticTokens.Token.createEmitter ()) - ~path - | "hov" -> - print_endline - ("Hover " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - let currentFile = createCurrentFile () in - hover ~supportsMarkdownLinks:true ~path ~pos:(line, col) - ~currentFile ~debug:true; - Sys.remove currentFile - | "she" -> - print_endline - ("Signature help " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - let currentFile = createCurrentFile () in - signatureHelp ~path ~pos:(line, col) ~currentFile ~debug:true - ~allowForConstructorPayloads:true; - Sys.remove currentFile - | "int" -> - print_endline ("Create Interface " ^ path); - let cmiFile = - let open Filename in - let ( ++ ) = concat in - let name = chop_extension (basename path) ^ ".cmi" in - let dir = dirname path in - dir ++ parent_dir_name ++ "lib" ++ "bs" ++ "src" ++ name - in - Printf.printf "%s" (CreateInterface.command ~path ~cmiFile) - | "ref" -> - print_endline - ("References " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - references ~path ~pos:(line, col) ~debug:true - | "pre" -> - print_endline - ("PrepareRename " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - prepareRename ~path ~pos:(line, col) ~debug:true - | "ren" -> - let newName = String.sub rest 4 (len - mlen - 4) in - let () = - print_endline - ("Rename " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col ^ " " ^ newName) - in - rename ~path ~pos:(line, col) ~newName ~debug:true - | "typ" -> - print_endline - ("TypeDefinition " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - typeDefinition ~path ~pos:(line, col) ~debug:true - | "xfm" -> - let currentFile = createCurrentFile () in - (* +2 is to ensure that the character ^ points to is what's considered the end of the selection. *) - let endCol = col + try String.index rest '^' + 2 with _ -> 0 in - let endPos = (line, endCol) in - let startPos = (line, col) in - if startPos = endPos then - print_endline - ("Xform " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col) - else - print_endline - ("Xform " ^ path ^ " start: " ^ Pos.toString startPos - ^ ", end: " ^ Pos.toString endPos); - let codeActions = - Xform.extractCodeActions ~path ~startPos ~endPos ~currentFile - ~debug:true - in - Sys.remove currentFile; - codeActions - |> List.iter (fun {Protocol.title; edit = {documentChanges}} -> - Printf.printf "Hit: %s\n" title; - documentChanges - |> List.iter (fun dc -> - match dc with - | Protocol.TextDocumentEdit tde -> - Printf.printf "\nTextDocumentEdit: %s\n" - tde.textDocument.uri; + if List.length diagnostics > 0 then Error "Document has syntax errors" + else Ok (Res_printer.print_implementation ~comments structure) + | Resi -> + let {Res_driver.parsetree = signature; comments; diagnostics} = + Res_driver.parsing_engine.parse_interface_from_source ~for_printer:true + ~source + in + if List.length diagnostics > 0 then Error "Document has syntax errors" + else Ok (Res_printer.print_interface ~comments signature) + | Other -> Error "Failed to format, file not supported" + in - 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 - ("Codemod AddMissingCases" ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - Codemod.transform ~path ~pos:(line, col) ~debug:true - ~typ:AddMissingCases ~hint - |> print_endline - | "dia" -> diagnosticSyntax ~path - | "hin" -> - (* Get all inlay Hint between line 1 and n. - Don't get the first line = 0. - *) - let line_start = 1 in - let line_end = 34 in - print_endline - ("Inlay Hint " ^ path ^ " " ^ string_of_int line_start ^ ":" - ^ string_of_int line_end); - inlayhint ~path ~pos:(line_start, line_end) ~maxLength:"25" - ~debug:false - | "cle" -> - print_endline ("Code Lens " ^ path); - codeLens ~path ~debug:false - | "ast" -> - print_endline - ("Dump AST " ^ path ^ " " ^ string_of_int line ^ ":" - ^ string_of_int col); - let currentFile = createCurrentFile () in - DumpAst.dump ~pos:(line, col) ~currentFile; - Sys.remove currentFile - | "sem" -> SemanticTokens.semanticTokens ~currentFile:path - | _ -> ()); - print_newline ()) - in - lines |> List.iteri processLine + match result with + | Ok newText -> Ok [Protocol.{range; newText}] + | Error e -> Error e diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index a5c0f9ce377..cdb879290dc 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -352,8 +352,8 @@ let completePipeChain ~(inJsxContext : bool) (exp : Parsetree.expression) = |> Option.map (fun ctxPath -> (ctxPath, pexp_loc)) | _ -> None -let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor - ?findThisExprLoc text = +let completionWithParser1 ~debug ~offset ~posCursor ~kindFile ?findThisExprLoc + text = let offsetNoWhite = Utils.skipWhite text (offset - 1) in let posNoWhite = let line, col = posCursor in @@ -1783,11 +1783,12 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor } in - if Filename.check_suffix path ".res" then ( + if kindFile = Files.Res then ( let parser = - Res_driver.parsing_engine.parse_implementation ~for_printer:false + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:false in - let {Res_driver.parsetree = str} = parser ~filename:currentFile in + let {Res_driver.parsetree = str} = parser ~source:text in iterator.structure iterator str |> ignore; if blankAfterCursor = Some ' ' || blankAfterCursor = Some '\n' then ( scope := !lastScopeBeforeCursor; @@ -1796,9 +1797,11 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor (CPId {loc = Location.none; path = [""]; completionContext = Value}))); if !found = false then if debug then Printf.printf "XXX Not found!\n"; !result) - else if Filename.check_suffix path ".resi" then ( - let parser = Res_driver.parsing_engine.parse_interface ~for_printer:false in - let {Res_driver.parsetree = signature} = parser ~filename:currentFile in + else if kindFile = Resi then ( + let parser = + Res_driver.parsing_engine.parse_interface_from_source ~for_printer:false + in + let {Res_driver.parsetree = signature} = parser ~source:text in iterator.signature iterator signature |> ignore; if blankAfterCursor = Some ' ' || blankAfterCursor = Some '\n' then ( scope := !lastScopeBeforeCursor; @@ -1809,19 +1812,18 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor !result) else None -let completionWithParser ~debug ~path ~posCursor ~currentFile ~text = - match Pos.positionToOffset text posCursor with +let completionWithParser ~debug ~source ~kindFile ~posCursor = + match Pos.positionToOffset source posCursor with | Some offset -> - completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor text + completionWithParser1 ~debug ~offset ~posCursor ~kindFile source | None -> None -let findTypeOfExpressionAtLoc ~debug ~path ~posCursor ~currentFile loc = - let textOpt = Files.readFile currentFile in - match textOpt with - | None | Some "" -> None - | Some text -> ( - match Pos.positionToOffset text posCursor with +let findTypeOfExpressionAtLoc ~debug ~posCursor ~source ~kindFile loc = + match source with + | "" -> None + | source -> ( + match Pos.positionToOffset source posCursor with | Some offset -> - completionWithParser1 ~findThisExprLoc:loc ~currentFile ~debug ~offset - ~path ~posCursor text + completionWithParser1 ~findThisExprLoc:loc ~debug ~offset ~posCursor + ~kindFile source | None -> None) diff --git a/analysis/src/Completions.ml b/analysis/src/Completions.ml index c11d51673ee..ae35a5a0d34 100644 --- a/analysis/src/Completions.ml +++ b/analysis/src/Completions.ml @@ -1,11 +1,11 @@ -let getCompletions ~debug ~path ~pos ~currentFile ~forHover = - let textOpt = Files.readFile currentFile in - match textOpt with - | None | Some "" -> None - | Some text -> ( +let getCompletions ~debug ~source ~kindFile ~pos ~forHover + ~(full : SharedTypes.full option) = + match source with + | "" -> None + | source -> ( match - CompletionFrontEnd.completionWithParser ~debug ~path ~posCursor:pos - ~currentFile ~text + CompletionFrontEnd.completionWithParser ~debug ~source ~kindFile + ~posCursor:pos with | None -> None | Some (completable, scope) -> ( @@ -18,7 +18,7 @@ let getCompletions ~debug ~path ~pos ~currentFile ~forHover = scope; print_newline ()); (* Only perform expensive ast operations if there are completables *) - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> None | Some full -> let env = SharedTypes.QueryEnv.fromFile full.file in diff --git a/analysis/src/Diagnostics.ml b/analysis/src/Diagnostics.ml index 0b30d0e3321..970936c022b 100644 --- a/analysis/src/Diagnostics.ml +++ b/analysis/src/Diagnostics.ml @@ -1,4 +1,4 @@ -let document_syntax ~path = +let document_syntax ~source ~kindFile = let get_diagnostics diagnostics = diagnostics |> List.map (fun diagnostic -> @@ -8,7 +8,7 @@ let document_syntax ~path = let _, endline, endcol = Location.get_pos_info (Res_diagnostics.get_end_pos diagnostic) in - Protocol.stringifyDiagnostic + Protocol. { range = { @@ -19,16 +19,16 @@ let document_syntax ~path = severity = 1; }) in - if FindFiles.isImplementation path then + if kindFile = Files.Res then let parseImplementation = - Res_driver.parsing_engine.parse_implementation ~for_printer:false - ~filename:path + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:false ~source in get_diagnostics parseImplementation.diagnostics - else if FindFiles.isInterface path then + else if kindFile = Files.Resi then let parseInterface = - Res_driver.parsing_engine.parse_interface ~for_printer:false - ~filename:path + Res_driver.parsing_engine.parse_interface_from_source ~for_printer:false + ~source in get_diagnostics parseInterface.diagnostics else [] diff --git a/analysis/src/Hint.ml b/analysis/src/Hint.ml index 71b1b7cfe3a..81881be3bc0 100644 --- a/analysis/src/Hint.ml +++ b/analysis/src/Hint.ml @@ -31,7 +31,7 @@ let locItemToTypeHint ~full:{file; package} locItem = | `Field -> fromType t)) | _ -> None -let inlay ~path ~pos ~maxLength ~debug = +let inlay ~source ~kindFile ~pos ~maxLength ~full ~debug = let maxlen = try Some (int_of_string maxLength) with Failure _ -> None in let hints = ref [] in let start_line, end_line = pos in @@ -71,13 +71,14 @@ let inlay ~path ~pos ~maxLength ~debug = Ast_iterator.default_iterator.value_binding iterator vb in let iterator = {Ast_iterator.default_iterator with value_binding} in - (if Files.classifySourceFile path = Res then + (if kindFile = Files.Res then let parser = - Res_driver.parsing_engine.parse_implementation ~for_printer:false + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:false in - let {Res_driver.parsetree = structure} = parser ~filename:path in + let {Res_driver.parsetree = structure} = parser ~source in iterator.structure iterator structure |> ignore); - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> None | Some full -> let result = @@ -96,7 +97,7 @@ let inlay ~path ~pos ~maxLength ~debug = match locItemToTypeHint locItem ~full with | Some label -> ( let result = - Protocol.stringifyHint + Protocol. { kind = inlayKindToNumber hintKind; position; @@ -113,7 +114,7 @@ let inlay ~path ~pos ~maxLength ~debug = in Some result -let codeLens ~path ~debug = +let codeLens ~source ~kindFile ~full ~debug = let lenses = ref [] in let push loc = let range = Utils.cmtLocToRange loc in @@ -135,13 +136,14 @@ let codeLens ~path ~debug = let iterator = {Ast_iterator.default_iterator with value_binding} in (* We only print code lenses in implementation files. This is because they'd be redundant in interface files, where the definition itself will be the same thing as what would've been printed in the code lens. *) - (if Files.classifySourceFile path = Res then + (if kindFile = Files.Res then let parser = - Res_driver.parsing_engine.parse_implementation ~for_printer:false + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:false in - let {Res_driver.parsetree = structure} = parser ~filename:path in + let {Res_driver.parsetree = structure} = parser ~source in iterator.structure iterator structure |> ignore); - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> None | Some full -> let result = @@ -154,21 +156,21 @@ let codeLens ~path ~debug = with | Some {locType = Typed (_, typeExpr, _)} -> Some - (Protocol.stringifyCodeLens - { - range; - command = - Some - { - (* Code lenses can run commands. An empty command string means we just want the editor + Protocol. + { + range; + command = + Some + { + (* Code lenses can run commands. An empty command string means we just want the editor to print the text, not link to running a command. *) - command = ""; - (* Print the type with a huge line width, because the code lens always prints on a + command = ""; + (* Print the type with a huge line width, because the code lens always prints on a single line in the editor. *) - title = - typeExpr |> Shared.typeToString ~lineWidth:400; - }; - }) + title = + typeExpr |> Shared.typeToString ~lineWidth:400; + }; + } | _ -> None) in Some result diff --git a/analysis/src/Hover.ml b/analysis/src/Hover.ml index 716f5e3c002..4f1a98da27f 100644 --- a/analysis/src/Hover.ml +++ b/analysis/src/Hover.ml @@ -180,9 +180,11 @@ let hoverWithExpandedTypes ~file ~package ~supportsMarkdownLinks ?docstring (* Leverages autocomplete functionality to produce a hover for a position. This makes it (most often) work with unsaved content. *) -let getHoverViaCompletions ~debug ~path ~pos ~currentFile ~forHover - ~supportsMarkdownLinks = - match Completions.getCompletions ~debug ~path ~pos ~currentFile ~forHover with +let getHoverViaCompletions ~debug ~source ~kindFile ~pos ~forHover + ~supportsMarkdownLinks ~full = + match + Completions.getCompletions ~debug ~source ~kindFile ~pos ~forHover ~full + with | None -> None | Some (completions, ({file; package} as full), scope) -> ( let rawOpens = Scope.getRawOpens scope in @@ -193,7 +195,7 @@ let getHoverViaCompletions ~debug ~path ~pos ~currentFile ~forHover @ if typString = "" then [] else [Markdown.codeBlock typString] in - Some (Protocol.stringifyHover (String.concat "\n\n" parts)) + Some (String.concat "\n\n" parts) | {kind = Field _; env; docstring} :: _ -> ( let opens = CompletionBackEnd.getOpens ~debug ~rawOpens ~package ~env in match @@ -205,7 +207,7 @@ let getHoverViaCompletions ~debug ~path ~pos ~currentFile ~forHover hoverWithExpandedTypes ~file ~package ~docstring ~supportsMarkdownLinks typ in - Some (Protocol.stringifyHover typeString) + Some typeString | None -> None) | {env} :: _ -> ( let opens = CompletionBackEnd.getOpens ~debug ~rawOpens ~package ~env in @@ -217,7 +219,7 @@ let getHoverViaCompletions ~debug ~path ~pos ~currentFile ~forHover let typeString = hoverWithExpandedTypes ~file ~package ~supportsMarkdownLinks typ in - Some (Protocol.stringifyHover typeString) + Some typeString | None -> None) | _ -> None) diff --git a/analysis/src/Protocol.ml b/analysis/src/Protocol.ml index e3e4208628e..447584b7e9b 100644 --- a/analysis/src/Protocol.ml +++ b/analysis/src/Protocol.ml @@ -63,6 +63,10 @@ type documentSymbolItem = { range: range; children: documentSymbolItem list; } +type prepareRenameWithPlaceholder = {range: range; placeholder: string} +type prepareRename = + | Range of range + | Placeholder of prepareRenameWithPlaceholder type renameFile = {oldUri: string; newUri: string} type diagnostic = {range: range; message: string; severity: int} @@ -92,6 +96,8 @@ type codeAction = { edit: codeActionEdit; } +type semanticTokens = {data: int array} + let wrapInQuotes s = "\"" ^ Json.escape s ^ "\"" let null = "null" @@ -105,6 +111,15 @@ let stringifyRange r = (stringifyPosition r.start) (stringifyPosition r.end_) +let stringifyRangeWithPlaceholder (r : prepareRenameWithPlaceholder) = + Printf.sprintf + {|{ + "range": %s, + "placeholder": %s + }|} + (stringifyRange r.range) + (wrapInQuotes r.placeholder) + let stringifyTextEdit (te : textEdit) = Printf.sprintf {|{ diff --git a/analysis/src/SemanticTokens.ml b/analysis/src/SemanticTokens.ml index ddccba9b2b1..86feab8ef8d 100644 --- a/analysis/src/SemanticTokens.ml +++ b/analysis/src/SemanticTokens.ml @@ -29,15 +29,15 @@ module Token = struct | Property (** {x:...} *) | JsxLowercase (** div in
*) - let tokenTypeToString = function - | Operator -> "0" - | Variable -> "1" - | Type -> "2" - | JsxTag -> "3" - | Namespace -> "4" - | EnumMember -> "5" - | Property -> "6" - | JsxLowercase -> "7" + let tokenTypeToInt = function + | Operator -> 0 + | Variable -> 1 + | Type -> 2 + | JsxTag -> 3 + | Namespace -> 4 + | EnumMember -> 5 + | Property -> 6 + | JsxLowercase -> 7 let tokenTypeDebug = function | Operator -> "Operator" @@ -49,7 +49,7 @@ module Token = struct | Property -> "Property" | JsxLowercase -> "JsxLowercase" - let tokenModifiersString = "0" (* None at the moment *) + let tokenModifiers = 0 (* None at the moment *) type token = int * int * int * tokenType @@ -64,25 +64,15 @@ module Token = struct let add ~line ~char ~length ~type_ e = e.tokens <- (line, char, length, type_) :: e.tokens - let emitToken buf (line, char, length, type_) e = + let emitToken (line, char, length, type_) e = let deltaLine = line - e.lastLine in let deltaChar = if deltaLine = 0 then char - e.lastChar else char in e.lastLine <- line; e.lastChar <- char; - if Buffer.length buf > 0 then Buffer.add_char buf ','; - if - deltaLine >= 0 && deltaChar >= 0 && length >= 0 - (* Defensive programming *) - then - Buffer.add_string buf - (string_of_int deltaLine ^ "," ^ string_of_int deltaChar ^ "," - ^ string_of_int length ^ "," ^ tokenTypeToString type_ ^ "," - ^ tokenModifiersString) - - let remove_trailing_comma buffer = - let len = Buffer.length buffer in - if len > 0 && Buffer.nth buffer (len - 1) = ',' then - Buffer.truncate buffer (len - 1) + if deltaLine >= 0 && deltaChar >= 0 && length >= 0 then + Some + [|deltaLine; deltaChar; length; tokenTypeToInt type_; tokenModifiers|] + else None let emit e = let sortedTokens = @@ -90,13 +80,12 @@ module Token = struct |> List.sort (fun (l1, c1, _, _) (l2, c2, _, _) -> if l1 = l2 then compare c1 c2 else compare l1 l2) in - let buf = Buffer.create 1 in - sortedTokens |> List.iter (fun t -> e |> emitToken buf t); + let arrays = sortedTokens |> List.filter_map (fun t -> e |> emitToken t) in + Array.concat arrays - (* Valid JSON arrays cannot have trailing commas *) - remove_trailing_comma buf; - - Buffer.contents buf + let arrayToJsonString arr = + let items = Array.map string_of_int arr |> Array.to_list in + "[" ^ String.concat "," items ^ "]" end let isLowercaseId id = @@ -203,7 +192,7 @@ let emitVariant ~(name : Longident.t Location.loc) ~debug emitter = |> emitLongident ~lastToken:(Some Token.EnumMember) ~pos:(Loc.start name.loc) ~lid:name.txt ~debug -let command ~debug ~emitter ~path = +let command ~debug ~emitter ~source ~kindFile = let processTypeArg (coreType : Parsetree.core_type) = if debug then Printf.printf "TypeArg: %s\n" (Loc.toString coreType.ptyp_loc) in @@ -480,28 +469,27 @@ let command ~debug ~emitter ~path = } in - if Files.classifySourceFile path = Res then ( + if kindFile = Files.Res then ( let parser = - Res_driver.parsing_engine.parse_implementation ~for_printer:false - in - let {Res_driver.parsetree = structure; diagnostics} = - parser ~filename:path + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:false in + let {Res_driver.parsetree = structure; diagnostics} = parser ~source in if debug then Printf.printf "structure items:%d diagnostics:%d \n" (List.length structure) (List.length diagnostics); iterator.structure iterator structure |> ignore) else - let parser = Res_driver.parsing_engine.parse_interface ~for_printer:false in - let {Res_driver.parsetree = signature; diagnostics} = - parser ~filename:path + let parser = + Res_driver.parsing_engine.parse_interface_from_source ~for_printer:false in + let {Res_driver.parsetree = signature; diagnostics} = parser ~source in if debug then Printf.printf "signature items:%d diagnostics:%d \n" (List.length signature) (List.length diagnostics); iterator.signature iterator signature |> ignore -let semanticTokens ~currentFile = +let semanticTokens ~source ~kindFile = let emitter = Token.createEmitter () in - command ~emitter ~debug:false ~path:currentFile; - Printf.printf "{\"data\":[%s]}" (Token.emit emitter) + command ~emitter ~debug:false ~source ~kindFile; + Protocol.{data = Token.emit emitter} diff --git a/analysis/src/SignatureHelp.ml b/analysis/src/SignatureHelp.ml index e4c9cb11ae1..68cfc405906 100644 --- a/analysis/src/SignatureHelp.ml +++ b/analysis/src/SignatureHelp.ml @@ -33,9 +33,9 @@ let docsForLabel typeExpr ~file ~package ~supportsMarkdownLinks = in typeDefinitions |> String.concat "\n" -let findFunctionType ~currentFile ~debug ~path ~pos = +let findFunctionType ~debug ~source ~kindFile ~pos ~full = (* Start by looking at the typed info at the loc of the fn *) - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> None | Some full -> ( let {file; package} = full in @@ -72,16 +72,15 @@ let findFunctionType ~currentFile ~debug ~path ~pos = | None -> ( (* If nothing was found there, try using the unsaved completion engine *) let completables = - let textOpt = Files.readFile currentFile in - match textOpt with - | None | Some "" -> None - | Some text -> ( + match source with + | "" -> None + | source -> ( (* Leverage the completion functionality to pull out the type of the identifier doing the function application. This lets us leverage all of the smart work done in completions to find the correct type in many cases even for files not saved yet. *) match - CompletionFrontEnd.completionWithParser ~debug ~path ~posCursor:pos - ~currentFile ~text + CompletionFrontEnd.completionWithParser ~debug ~source ~kindFile + ~posCursor:pos with | None -> None | Some (completable, scope) -> @@ -238,11 +237,11 @@ let findConstructorArgs ~full ~env ~constructorName loc = | _ -> None) | _ -> None -let signatureHelp ~path ~pos ~currentFile ~debug ~allowForConstructorPayloads = - let textOpt = Files.readFile currentFile in - match textOpt with - | None | Some "" -> None - | Some text -> ( +let signatureHelp ~debug ~source ~kindFile ~pos ~allowForConstructorPayloads + ~full = + match source with + | "" -> None + | text -> ( match Pos.positionToOffset text pos with | None -> None | Some offset -> ( @@ -416,16 +415,17 @@ let signatureHelp ~path ~pos ~currentFile ~debug ~allowForConstructorPayloads = in let iterator = {Ast_iterator.default_iterator with expr; pat} in let parser = - Res_driver.parsing_engine.parse_implementation ~for_printer:false + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:false in - let {Res_driver.parsetree = structure} = parser ~filename:currentFile in + let {Res_driver.parsetree = structure} = parser ~source in iterator.structure iterator structure |> ignore; (* Handle function application, if found *) match !result with | Some (_, `FunctionCall (argAtCursor, exp, _extractedArgs)) -> ( (* Not looking for the cursor position after this, but rather the target function expression's loc. *) let pos = exp.pexp_loc |> Loc.end_ in - match findFunctionType ~currentFile ~debug ~path ~pos with + match findFunctionType ~source ~kindFile ~debug ~pos ~full with | Some (args, docstring, type_expr, package, _env, file) -> if debug then Printf.printf "argAtCursor: %s\n" @@ -525,7 +525,7 @@ let signatureHelp ~path ~pos ~currentFile ~debug ~allowForConstructorPayloads = -> ( if Debug.verbose () then Printf.printf "[signature_help] Found constructor!\n"; - match Cmt.loadFullCmtFromPath ~path with + match full with | None -> if Debug.verbose () then Printf.printf "[signature_help] Could not load cmt\n"; diff --git a/analysis/src/Xform.ml b/analysis/src/Xform.ml index ddf783c5590..8d66d8c757d 100644 --- a/analysis/src/Xform.ml +++ b/analysis/src/Xform.ml @@ -2,10 +2,10 @@ let isBracedExpr = Res_parsetree_viewer.is_braced_expr -let extractTypeFromExpr expr ~debug ~path ~currentFile ~full ~pos = +let extractTypeFromExpr expr ~debug ~source ~kindFile ~full ~pos = match expr.Parsetree.pexp_loc - |> CompletionFrontEnd.findTypeOfExpressionAtLoc ~debug ~path ~currentFile + |> CompletionFrontEnd.findTypeOfExpressionAtLoc ~debug ~source ~kindFile ~posCursor:(Pos.ofLexing expr.Parsetree.pexp_loc.loc_start) with | Some (completable, scope) -> ( @@ -377,7 +377,7 @@ module ExpandCatchAllForVariants = struct in {Ast_iterator.default_iterator with expr} - let xform ~path ~pos ~full ~structure ~currentFile ~codeActions ~debug = + let xform ~source ~kindFile ~path ~pos ~full ~structure ~codeActions ~debug = let result = ref None in let iterator = mkIterator ~pos ~result in iterator.structure iterator structure; @@ -411,7 +411,7 @@ module ExpandCatchAllForVariants = struct let currentConstructorNames = getCurrentConstructorNames cases in match switchExpr - |> extractTypeFromExpr ~debug ~path ~currentFile ~full + |> extractTypeFromExpr ~debug ~source ~kindFile ~full ~pos:(Pos.ofLexing switchExpr.pexp_loc.loc_end) with | Some (Tvariant {constructors}) -> @@ -580,8 +580,8 @@ module ExhaustiveSwitch = struct in {Ast_iterator.default_iterator with expr} - let xform ~printExpr ~path ~currentFile ~pos ~full ~structure ~codeActions - ~debug = + let xform ~printExpr ~path ~source ~kindFile ~pos ~full ~structure + ~codeActions ~debug = (* TODO: Adapt to '(' as leading/trailing character (skip one col, it's not included in the AST) *) let result = ref None in let foundSelection = ref (None, None) in @@ -605,7 +605,7 @@ module ExhaustiveSwitch = struct | Some (Selection {expr}) -> ( match expr - |> extractTypeFromExpr ~debug ~path ~currentFile ~full + |> extractTypeFromExpr ~debug ~source ~kindFile ~full ~pos:(Pos.ofLexing expr.pexp_loc.loc_start) with | None -> () @@ -631,7 +631,7 @@ module ExhaustiveSwitch = struct | Some (Switch {switchExpr; completionExpr; pos}) -> ( match completionExpr - |> extractTypeFromExpr ~debug ~path ~currentFile ~full ~pos + |> extractTypeFromExpr ~debug ~source ~kindFile ~full ~pos with | None -> () | Some extractedType -> ( @@ -840,9 +840,10 @@ module AddDocTemplate = struct end end -let parseImplementation ~filename = +let parseImplementation ~source = let {Res_driver.parsetree = structure; comments} = - Res_driver.parsing_engine.parse_implementation ~for_printer:false ~filename + Res_driver.parsing_engine.parse_implementation_from_source + ~for_printer:false ~source in let filterComments ~loc comments = (* Relevant comments in the range of the expression *) @@ -873,9 +874,10 @@ let parseImplementation ~filename = in (structure, printExpr, printStructureItem, printStandaloneStructure) -let parseInterface ~filename = +let parseInterface ~source = let {Res_driver.parsetree = structure; comments} = - Res_driver.parsing_engine.parse_interface ~for_printer:false ~filename + Res_driver.parsing_engine.parse_interface_from_source ~for_printer:false + ~source in let filterComments ~loc comments = (* Relevant comments in the range of the expression *) @@ -894,13 +896,13 @@ let parseInterface ~filename = in (structure, printSignatureItem) -let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug = +let extractCodeActions ~path ~startPos ~endPos ~source ~kindFile ~debug = let pos = startPos in let codeActions = ref [] in - match Files.classifySourceFile currentFile with - | Res -> + match kindFile with + | Files.Res -> let structure, printExpr, printStructureItem, printStandaloneStructure = - parseImplementation ~filename:currentFile + parseImplementation ~source in IfThenElse.xform ~pos ~codeActions ~printExpr ~path structure; ModuleToFile.xform ~pos ~codeActions ~path ~printStandaloneStructure @@ -914,19 +916,19 @@ let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug = match Cmt.loadFullCmtFromPath ~path with | Some full -> AddTypeAnnotation.xform ~path ~pos ~full ~structure ~codeActions ~debug; - ExpandCatchAllForVariants.xform ~path ~pos ~full ~structure ~codeActions - ~currentFile ~debug; - ExhaustiveSwitch.xform ~printExpr ~path + ExpandCatchAllForVariants.xform ~path ~source ~kindFile ~pos ~full + ~structure ~codeActions ~debug; + ExhaustiveSwitch.xform ~printExpr ~path ~source ~kindFile ~pos: (if startPos = endPos then Single startPos else Range (startPos, endPos)) - ~full ~structure ~codeActions ~debug ~currentFile + ~full ~structure ~codeActions ~debug | None -> () in !codeActions | Resi -> - let signature, printSignatureItem = parseInterface ~filename:currentFile in + let signature, printSignatureItem = parseInterface ~source in AddDocTemplate.Interface.xform ~pos ~codeActions ~path ~signature ~printSignatureItem; !codeActions diff --git a/compiler/syntax/src/res_ast_debugger.ml b/compiler/syntax/src/res_ast_debugger.ml index 5b3e5ecf01e..8f8ebdb0bb7 100644 --- a/compiler/syntax/src/res_ast_debugger.ml +++ b/compiler/syntax/src/res_ast_debugger.ml @@ -7,9 +7,15 @@ let print_engine = print_implementation = (fun ~width:_ ~filename:_ ~comments:_ structure -> Printast.implementation Format.std_formatter structure); + print_implementation_from_source = + (fun ~width:_ ~source:_ ~comments:_ structure -> + Printast.implementation Format.std_formatter structure); print_interface = (fun ~width:_ ~filename:_ ~comments:_ signature -> Printast.interface Format.std_formatter signature); + print_interface_from_source = + (fun ~width:_ ~source:_ ~comments:_ signature -> + Printast.interface Format.std_formatter signature); } module Sexp : sig @@ -962,9 +968,15 @@ module SexpAst = struct print_implementation = (fun ~width:_ ~filename:_ ~comments:_ parsetree -> parsetree |> structure |> Sexp.to_string |> print_string); + print_implementation_from_source = + (fun ~width:_ ~source:_ ~comments:_ parsetree -> + parsetree |> structure |> Sexp.to_string |> print_string); print_interface = (fun ~width:_ ~filename:_ ~comments:_ parsetree -> parsetree |> signature |> Sexp.to_string |> print_string); + print_interface_from_source = + (fun ~width:_ ~source:_ ~comments:_ parsetree -> + parsetree |> signature |> Sexp.to_string |> print_string); } end @@ -977,9 +989,19 @@ let comments_print_engine = let cmt_tbl = CommentTable.make () in CommentTable.walk_structure s cmt_tbl comments; CommentTable.log cmt_tbl); - print_interface = + Res_driver.print_implementation_from_source = + (fun ~width:_ ~source:_ ~comments s -> + let cmt_tbl = CommentTable.make () in + CommentTable.walk_structure s cmt_tbl comments; + CommentTable.log cmt_tbl); + Res_driver.print_interface = (fun ~width:_ ~filename:_ ~comments s -> let cmt_tbl = CommentTable.make () in CommentTable.walk_signature s cmt_tbl comments; CommentTable.log cmt_tbl); + Res_driver.print_interface_from_source = + (fun ~width:_ ~source:_ ~comments s -> + let cmt_tbl = CommentTable.make () in + CommentTable.walk_signature s cmt_tbl comments; + CommentTable.log cmt_tbl); } diff --git a/compiler/syntax/src/res_driver.ml b/compiler/syntax/src/res_driver.ml index 64039e76560..eddb55a1f27 100644 --- a/compiler/syntax/src/res_driver.ml +++ b/compiler/syntax/src/res_driver.ml @@ -14,10 +14,18 @@ type 'diagnostics parsing_engine = { for_printer:bool -> filename:string -> (Parsetree.structure, 'diagnostics) parse_result; + parse_implementation_from_source: + for_printer:bool -> + source:string -> + (Parsetree.structure, 'diagnostics) parse_result; parse_interface: for_printer:bool -> filename:string -> (Parsetree.signature, 'diagnostics) parse_result; + parse_interface_from_source: + for_printer:bool -> + source:string -> + (Parsetree.signature, 'diagnostics) parse_result; string_of_diagnostics: source:string -> filename:string -> 'diagnostics -> unit; } @@ -29,12 +37,24 @@ type print_engine = { comments:Res_comment.t list -> Parsetree.structure -> unit; + print_implementation_from_source: + width:int -> + source:string -> + comments:Res_comment.t list -> + Parsetree.structure -> + unit; print_interface: width:int -> filename:string -> comments:Res_comment.t list -> Parsetree.signature -> unit; + print_interface_from_source: + width:int -> + source:string -> + comments:Res_comment.t list -> + Parsetree.signature -> + unit; } let setup ~filename ~for_printer () = @@ -65,6 +85,25 @@ let parsing_engine = invalid; comments = List.rev engine.comments; }); + parse_implementation_from_source = + (fun ~for_printer ~source -> + let engine = + setup_from_source ~source ~for_printer ~display_filename:"source" () + in + let structure = Res_core.parse_implementation engine in + let invalid, diagnostics = + match engine.diagnostics with + | [] as diagnostics -> (false, diagnostics) + | _ as diagnostics -> (true, diagnostics) + in + { + filename = engine.scanner.filename; + source = engine.scanner.src; + parsetree = structure; + diagnostics; + invalid; + comments = List.rev engine.comments; + }); parse_interface = (fun ~for_printer ~filename -> let engine = setup ~filename ~for_printer () in @@ -82,6 +121,25 @@ let parsing_engine = invalid; comments = List.rev engine.comments; }); + parse_interface_from_source = + (fun ~for_printer ~source -> + let engine = + setup_from_source ~source ~display_filename:"" ~for_printer () + in + let signature = Res_core.parse_specification engine in + let invalid, diagnostics = + match engine.diagnostics with + | [] as diagnostics -> (false, diagnostics) + | _ as diagnostics -> (true, diagnostics) + in + { + filename = engine.scanner.filename; + source = engine.scanner.src; + parsetree = signature; + diagnostics; + invalid; + comments = List.rev engine.comments; + }); string_of_diagnostics = (fun ~source ~filename:_ diagnostics -> Res_diagnostics.print_report diagnostics source); @@ -127,9 +185,16 @@ let print_engine = (fun ~width ~filename:_ ~comments structure -> print_string (Res_printer.print_implementation ~width structure ~comments)); + print_implementation_from_source = + (fun ~width ~source:_ ~comments structure -> + print_string + (Res_printer.print_implementation ~width structure ~comments)); print_interface = (fun ~width ~filename:_ ~comments signature -> print_string (Res_printer.print_interface ~width signature ~comments)); + print_interface_from_source = + (fun ~width ~source:_ ~comments signature -> + print_string (Res_printer.print_interface ~width signature ~comments)); } let parse_implementation ?(ignore_parse_errors = false) sourcefile = diff --git a/compiler/syntax/src/res_driver.mli b/compiler/syntax/src/res_driver.mli index 2b717013ccb..4d6feb13de6 100644 --- a/compiler/syntax/src/res_driver.mli +++ b/compiler/syntax/src/res_driver.mli @@ -12,10 +12,18 @@ type 'diagnostics parsing_engine = { for_printer:bool -> filename:string -> (Parsetree.structure, 'diagnostics) parse_result; + parse_implementation_from_source: + for_printer:bool -> + source:string -> + (Parsetree.structure, 'diagnostics) parse_result; parse_interface: for_printer:bool -> filename:string -> (Parsetree.signature, 'diagnostics) parse_result; + parse_interface_from_source: + for_printer:bool -> + source:string -> + (Parsetree.signature, 'diagnostics) parse_result; string_of_diagnostics: source:string -> filename:string -> 'diagnostics -> unit; } @@ -41,12 +49,24 @@ type print_engine = { comments:Res_comment.t list -> Parsetree.structure -> unit; + print_implementation_from_source: + width:int -> + source:string -> + comments:Res_comment.t list -> + Parsetree.structure -> + unit; print_interface: width:int -> filename:string -> comments:Res_comment.t list -> Parsetree.signature -> unit; + print_interface_from_source: + width:int -> + source:string -> + comments:Res_comment.t list -> + Parsetree.signature -> + unit; } val parsing_engine : Res_diagnostics.t list parsing_engine diff --git a/compiler/syntax/src/res_driver_binary.ml b/compiler/syntax/src/res_driver_binary.ml index 71eb12bd483..b6c9318d5cc 100644 --- a/compiler/syntax/src/res_driver_binary.ml +++ b/compiler/syntax/src/res_driver_binary.ml @@ -6,9 +6,19 @@ let print_engine = output_string stdout Config.ast_impl_magic_number; output_value stdout filename; output_value stdout structure); + print_implementation_from_source = + (fun ~width:_ ~source:_ ~comments:_ structure -> + output_string stdout Config.ast_impl_magic_number; + output_value stdout "source"; + output_value stdout structure); print_interface = (fun ~width:_ ~filename ~comments:_ signature -> output_string stdout Config.ast_intf_magic_number; output_value stdout filename; output_value stdout signature); + print_interface_from_source = + (fun ~width:_ ~source:_ ~comments:_ signature -> + output_string stdout Config.ast_intf_magic_number; + output_value stdout "source"; + output_value stdout signature); } diff --git a/compiler/syntax/src/res_driver_ml_printer.ml b/compiler/syntax/src/res_driver_ml_printer.ml index 651ab058402..232e328bf15 100644 --- a/compiler/syntax/src/res_driver_ml_printer.ml +++ b/compiler/syntax/src/res_driver_ml_printer.ml @@ -4,7 +4,13 @@ let print_engine = print_implementation = (fun ~width:_ ~filename:_ ~comments:_ structure -> Pprintast.structure Format.std_formatter structure); + print_implementation_from_source = + (fun ~width:_ ~source:_ ~comments:_ structure -> + Pprintast.structure Format.std_formatter structure); print_interface = (fun ~width:_ ~filename:_ ~comments:_ signature -> Pprintast.signature Format.std_formatter signature); + print_interface_from_source = + (fun ~width:_ ~source:_ ~comments:_ signature -> + Pprintast.signature Format.std_formatter signature); } diff --git a/compiler/syntax/src/res_token_debugger.ml b/compiler/syntax/src/res_token_debugger.ml index e745308dd4f..23208ccdfad 100644 --- a/compiler/syntax/src/res_token_debugger.ml +++ b/compiler/syntax/src/res_token_debugger.ml @@ -142,6 +142,10 @@ let token_print_engine = { Res_driver.print_implementation = (fun ~width:_ ~filename ~comments:_ _ -> dump_tokens filename); - print_interface = + Res_driver.print_implementation_from_source = + (fun ~width:_ ~source ~comments:_ _ -> dump_tokens source); + Res_driver.print_interface = (fun ~width:_ ~filename ~comments:_ _ -> dump_tokens filename); + Res_driver.print_interface_from_source = + (fun ~width:_ ~source ~comments:_ _ -> dump_tokens source); } diff --git a/dune b/dune index 91a5df6eca9..2903c721981 100644 --- a/dune +++ b/dune @@ -1 +1 @@ -(dirs compiler tests analysis tools) +(dirs compiler tests analysis tools lsp) diff --git a/dune-project b/dune-project index c8bb3117c50..a194bdaa462 100644 --- a/dune-project +++ b/dune-project @@ -19,7 +19,8 @@ (synopsis "ReScript compiler") (depends (ocaml - (>= 5.0.0)))) + (>= 5.0.0)) + (ocaml-lsp-server (and (>= 1.26.0) :with-test-setup)))) (package (name analysis) @@ -43,3 +44,18 @@ (= 1.8.0)) analysis dune)) + +(package + (name rescript-language-server) + (synopsis "ReScript LSP") + (depends + (ocaml + (>= 4.10)) + (lsp + (>= 1.22.0)) + (eio + (>= 1.3)) + (eio_main + (>= 1.3)) + analysis + dune)) diff --git a/lsp/bin/dune b/lsp/bin/dune new file mode 100644 index 00000000000..ecd09b26ec7 --- /dev/null +++ b/lsp/bin/dune @@ -0,0 +1,5 @@ +(executable + (name main) + (package rescript-language-server) + (public_name rescript-language-server) + (libraries rescript_language_server)) diff --git a/lsp/bin/main.ml b/lsp/bin/main.ml new file mode 100644 index 00000000000..73ed8920da0 --- /dev/null +++ b/lsp/bin/main.ml @@ -0,0 +1 @@ +let () = Rescript_language_server.main () diff --git a/lsp/bin/main.mli b/lsp/bin/main.mli new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lsp/src/configuration.ml b/lsp/src/configuration.ml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lsp/src/diagnostics.ml b/lsp/src/diagnostics.ml new file mode 100644 index 00000000000..de172eda24b --- /dev/null +++ b/lsp/src/diagnostics.ml @@ -0,0 +1,5 @@ +module UriMap = Map.Make (Lsp.Uri) + +type t = Lsp.Types.Diagnostic.t list UriMap.t + +let create () = UriMap.empty diff --git a/lsp/src/document_store.ml b/lsp/src/document_store.ml new file mode 100644 index 00000000000..d818d61f60d --- /dev/null +++ b/lsp/src/document_store.ml @@ -0,0 +1,30 @@ +(* module UriMap = Map.Make (Lsp.Uri) *) + +type document = {text: string; version: int} + +type t = {documents: (Lsp.Uri.t, document) Hashtbl.t} + +let create () = {documents = Hashtbl.create 25} + +let open_document t ~uri ~text ~version = + Hashtbl.add t.documents uri {text; version}; + t + +let update_document t ~uri ~text ~version = + (match Hashtbl.find_opt t.documents uri with + | None -> + raise + (Failure (Printf.sprintf "Document not found: %s" (Lsp.Uri.to_string uri))) + | Some _ -> Hashtbl.replace t.documents uri {text; version}); + t + +let remove_document t ~uri = + Hashtbl.remove t.documents uri; + t + +let get_document t ~uri = + match Hashtbl.find_opt t.documents uri with + | Some doc -> doc + | None -> + raise + (Failure (Printf.sprintf "Document not found: %s" (Lsp.Uri.to_string uri))) diff --git a/lsp/src/dune b/lsp/src/dune new file mode 100644 index 00000000000..486415966af --- /dev/null +++ b/lsp/src/dune @@ -0,0 +1,5 @@ +(library + (name rescript_language_server) + (libraries lsp eio eio_main analysis) + (flags + (-w "-9"))) diff --git a/lsp/src/hover.ml b/lsp/src/hover.ml new file mode 100644 index 00000000000..b75d71e9197 --- /dev/null +++ b/lsp/src/hover.ml @@ -0,0 +1,30 @@ +open Lsp.Types + +let create ~(position : Position.t) ~(uri : DocumentUri.t) + (server : State.t Server.t) = + let path = DocumentUri.to_path uri in + let pos = (position.line, position.character) in + + (* NOTE: Should be a config *) + let supportsMarkdownLinks = true in + + let result = + let open Analysis in + let source = (Document_store.get_document ~uri server.state.store).text in + let debug = false in + + let kindFile = Files.classifySourceFile path in + let full = Cmt.loadFullCmtFromPath ~path in + + Commands.hover ~source ~kindFile ~pos ~debug ~supportsMarkdownLinks ~full + in + + match result with + | None -> None + | Some value -> + Some + (Hover.create + ~contents: + (`MarkupContent + (MarkupContent.create ~kind:MarkupKind.Markdown ~value)) + ()) diff --git a/lsp/src/rescript_language_server.ml b/lsp/src/rescript_language_server.ml new file mode 100644 index 00000000000..b6530ddc481 --- /dev/null +++ b/lsp/src/rescript_language_server.ml @@ -0,0 +1,79 @@ +let initialization (client_capabilities : Lsp.Types.ClientCapabilities.t) = + let open Lsp.Types in + let textDocumentSync = + `TextDocumentSyncOptions + (TextDocumentSyncOptions.create ~openClose:true + ~change:TextDocumentSyncKind.Full ~willSave:false + ~save:(`SaveOptions (SaveOptions.create ~includeText:false ())) + ~willSaveWaitUntil:false ()) + in + let capabilities = + ServerCapabilities.create ~textDocumentSync ~hoverProvider:(`Bool true) () + in + let serverInfo = + let version = "2.0.0-aplha.1" in + InitializeResult.create_serverInfo ~name:"rescript-language-server" ~version + () + in + InitializeResult.create ~capabilities ~serverInfo () + +let on_initialize (params : Lsp.Types.InitializeParams.t) (state : State.t) = + (* TODO: + * Find root project (rescript.json, package.json) using InitializeParams.workspaceFolders and save in State.t + * See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeParams + * If not found rescript.json kill the server? + * Save initializationOptions in State.t + * This options are: askToStartBuild, codeLens.enable, inlayHints.enable, etc.. + * Collect compiler diagnostics (syntax and type)? + *) + let diagnostics = Diagnostics.create () in + let initialization_info = initialization params.capabilities in + let state = State.initialize state ~params ~diagnostics in + (initialization_info, state) + +let on_request (Lsp.Client_request.E request) (server : State.t Server.t) = + let state = Server.state server in + let ok value = Ok (Lsp.Client_request.yojson_of_result request value) in + match request with + | Lsp.Client_request.Initialize params -> + let initialization_info, state = on_initialize params state in + (ok initialization_info, state) + | Shutdown -> (ok (), state) + | TextDocumentHover {position; textDocument = {uri}} -> + (ok (Hover.create ~position ~uri server), state) + | _ -> + let err = + Jsonrpc.Response.Error.make + ~code:Jsonrpc.Response.Error.Code.MethodNotFound + ~message:"Request method not supported" () + in + (Error err, state) + +let on_notification notification (server : State.t Server.t) = + let state = Server.state server in + + match notification with + | Lsp.Client_notification.TextDocumentDidOpen + {textDocument = {uri; text; version; _}} -> + let store = Document_store.open_document ~uri ~text ~version state.store in + {state with store} + (* | TextDocumentDidChange {textDocument = {uri; version; _}; contentChanges} + -> ( + match List.rev contentChanges with + | {text; _} :: _ -> state + | [] -> state) *) + | TextDocumentDidClose {textDocument = {uri; _}} -> + (* TODO: + * remove state diagnostics + * send updated diagnostics? + *) + let store = Document_store.remove_document ~uri state.store in + {state with store} + | Exit -> state + | _ -> state + +let main () = + Eio_main.run (fun env -> + let state = State.create ~store:(Document_store.create ()) in + Server.listen ~input:env#stdin ~output:env#stdout ~on_request + ~on_notification ~state ~env) diff --git a/lsp/src/server.ml b/lsp/src/server.ml new file mode 100644 index 00000000000..d2c4edc0977 --- /dev/null +++ b/lsp/src/server.ml @@ -0,0 +1,159 @@ +module Io : sig + type 'a t + + val return : 'a -> 'a t + val raise : exn -> 'a t + val await : 'a t -> 'a + val async : (sw:Eio.Switch.t -> ('a, exn) result) -> 'a t + + module O : sig + val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t + val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t + end +end = struct + type 'a t = sw:Eio.Switch.t -> ('a, exn) result Eio.Promise.t + + let await t = Eio.Switch.run @@ fun sw -> Eio.Promise.await_exn (t ~sw) + let return value ~sw:_ = Eio.Promise.create_resolved (Ok value) + let error desc ~sw:_ = Eio.Promise.create_resolved (Error desc) + + let async f ~sw = + let promise, resolver = Eio.Promise.create () in + ( Eio.Fiber.fork ~sw @@ fun () -> + try + let result = f ~sw in + Eio.Promise.resolve resolver result + with exn -> Eio.Promise.resolve resolver @@ Error exn ); + promise + + let bind t f = + async @@ fun ~sw -> + match Eio.Promise.await (t ~sw) with + | Ok value -> Eio.Promise.await @@ f value ~sw + | Error desc -> Error desc + + let raise = error + + module O = struct + let ( let+ ) x f = bind x @@ fun value -> return @@ f value + let ( let* ) = bind + end +end + +module Chan : sig + type input + type output + + val of_source : [> Eio__Flow.source_ty] Eio.Resource.t -> input + val with_sink : [> Eio__Flow.sink_ty] Eio.Resource.t -> (output -> 'a) -> 'a + + val read_line : input -> string option Io.t + val read_exactly : input -> int -> string option Io.t + val write : output -> string list -> unit Io.t +end = struct + type input = {mutex: Eio.Mutex.t; buf: Eio.Buf_read.t} + type output = {mutex: Eio.Mutex.t; buf: Eio.Buf_write.t} + + let initial_size = 1024 + let max_size = 1024 * 1024 + + let of_source source : input = + let mutex = Eio.Mutex.create () in + let buf = Eio.Buf_read.of_flow ~initial_size ~max_size source in + {mutex; buf} + + let with_sink sink f = + let mutex = Eio.Mutex.create () in + Eio.Buf_write.with_flow ~initial_size sink @@ fun buf -> f {mutex; buf} + + let read_line (input : input) = + Io.async @@ fun ~sw:_ -> + Eio.Mutex.use_rw ~protect:true input.mutex @@ fun () -> + if Eio.Buf_read.eof_seen input.buf then Ok None + else + match Eio.Buf_read.line input.buf with + | line -> Ok (Some line) + | exception End_of_file -> Ok None + + let read_exactly (input : input) size = + Io.async @@ fun ~sw:_ -> + Eio.Mutex.use_rw ~protect:true input.mutex @@ fun () -> + if Eio.Buf_read.eof_seen input.buf then Ok None + else + match Eio.Buf_read.take size input.buf with + | data -> Ok (Some data) + | exception End_of_file -> Ok None + + let write (output : output) (str : string list) = + Io.async @@ fun ~sw:_ -> + Eio.Mutex.use_rw ~protect:true output.mutex @@ fun () -> + Ok (List.iter (Eio.Buf_write.string output.buf) str) +end + +module Lsp_Io = Lsp.Io.Make (Io) (Chan) + +let notification_of_jsonrpc notification = + match Lsp.Client_notification.of_jsonrpc notification with + | Ok notification -> notification + | Error error -> raise (Lsp.Io.Error error) + +type 'a t = {channel: Chan.output; env: Eio_unix.Stdenv.base; state: 'a} + +let state t = t.state + +let respond server response = + Io.await @@ Lsp_Io.write server.channel @@ Response response + +let notification server notification = + let notification = Lsp.Server_notification.to_jsonrpc notification in + Io.await @@ Lsp_Io.write server.channel @@ Notification notification + +let log_message_notification ?(kind = Lsp.Types.MessageType.Debug) server + message = + notification server + (Lsp.Server_notification.LogMessage + (Lsp.Types.LogMessageParams.create ~type_:kind ~message)) + +let rec input_loop ~input ~state with_ = + match Io.await @@ Lsp_Io.read input with + | Some packet -> + let state = with_ state packet in + input_loop ~input ~state with_ + | exception exn -> raise (Failure "Server.input_loop") + | None -> () + +let listen ~input ~output ~on_request ~on_notification ~state ~env = + let handle_request server request = + let response, state = + match Lsp.Client_request.of_jsonrpc request with + | Error message -> + let code = Jsonrpc.Response.Error.Code.InvalidParams in + let err = Jsonrpc.Response.Error.make ~code ~message () in + (Jsonrpc.Response.{id = request.id; result = Error err}, state) + | Ok packed -> + let result, state = on_request packed server in + (Jsonrpc.Response.{id = request.id; result}, state) + in + respond server response; + state + in + let handle_notification server notification = + on_notification (notification_of_jsonrpc notification) server + in + let input = Chan.of_source input in + Chan.with_sink output (fun channel -> + let server = {channel; state; env} in + input_loop ~input ~state (fun state packet -> + match packet with + | Notification notification -> handle_notification server notification + | Request request -> handle_request server request + | Batch_call calls -> + List.fold_left + (fun state call -> + match call with + | `Request request -> handle_request server request + | `Notification notification -> + handle_notification server notification) + state calls + | Response _ -> raise (Lsp.Io.Error "unexpected response") + | Batch_response _ -> raise (Lsp.Io.Error "unexpected batch response"))) diff --git a/lsp/src/state.ml b/lsp/src/state.ml new file mode 100644 index 00000000000..e5e87932119 --- /dev/null +++ b/lsp/src/state.ml @@ -0,0 +1,13 @@ +open Lsp.Types + +type status = + | Uninitialized + | Initialized of {params: InitializeParams.t; diagnostics: Diagnostics.t} + +(* TODO: add trace, configuration *) +type t = {status: status; store: Document_store.t} + +let create ~store = {status = Uninitialized; store} + +let initialize t ~params ~diagnostics = + {t with status = Initialized {params; diagnostics}} diff --git a/package.json b/package.json index 33bc618206b..7b3de837204 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "tests/tests", "tests/tools_tests", "tests/commonjs_tests", + "tests/lsp_tests/**", "scripts/res" ], "packageManager": "yarn@4.12.0", diff --git a/rescript-language-server.opam b/rescript-language-server.opam new file mode 100644 index 00000000000..6b6aa9366a9 --- /dev/null +++ b/rescript-language-server.opam @@ -0,0 +1,31 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "ReScript LSP" +maintainer: ["Hongbo Zhang " "Cristiano Calcagno"] +authors: ["Hongbo Zhang "] +license: "LGPL-3.0-or-later" +homepage: "https://github.com/rescript-lang/rescript-compiler" +bug-reports: "https://github.com/rescript-lang/rescript-compiler/issues" +depends: [ + "ocaml" {>= "4.10"} + "lsp" {>= "1.22.0"} + "eio" {>= "1.3"} + "eio_main" {>= "1.3"} + "analysis" + "dune" {>= "3.17"} + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] diff --git a/tests/dune b/tests/dune index 01dd377b945..d9dd6567304 100644 --- a/tests/dune +++ b/tests/dune @@ -1 +1 @@ -(dirs ounit_tests syntax_benchmarks syntax_tests) +(dirs ounit_tests syntax_benchmarks syntax_tests lsp_tests) diff --git a/tests/lsp_tests/basic-workspace/Hover.res b/tests/lsp_tests/basic-workspace/Hover.res new file mode 100644 index 00000000000..230cdafee6e --- /dev/null +++ b/tests/lsp_tests/basic-workspace/Hover.res @@ -0,0 +1,286 @@ +let abc = 22 + 34 +// ^hov + +type t = (int, float) +// ^hov + +module Id = { + // ^hov + type x = int +} + +@ocaml.doc("This module is commented") +module Dep: { + @ocaml.doc("Some doc comment") + let customDouble: int => int +} = { + let customDouble = foo => foo * 2 +} + +module D = Dep +// ^hov + +let cd = D.customDouble +// ^hov + +module HoverInsideModuleWithComponent = { + let x = 2 // check that hover on x works + // ^hov + @react.component + let make = () => React.null +} + +@ocaml.doc("Doc comment for functionWithTypeAnnotation") +let functionWithTypeAnnotation: unit => int = () => 1 +// ^hov + +@react.component +let make = (~name) => React.string(name) +// ^hov + +module C2 = { + @react.component + let make2 = (~name: string) => React.string(name) + // ^hov +} + +let num = 34 +// ^hov + +module type Logger = { + // ^hov + let log: string => unit +} + +module JsLogger: Logger = { + // ^hov + let log = (msg: string) => Console.log(msg) + let _oneMore = 3 +} + +module JJ = JsLogger +// ^def + +module IdDefinedTwice = { + // ^hov + let _x = 10 + let y = 20 + let _x = 10 +} + +module A = { + let x = 13 +} + +module B = A +// ^hov + +module C = B +// ^hov + +module Comp = { + @react.component + let make = (~children: React.element) => children +} + +module Comp1 = Comp + +let _ = + +
+
+ +// ^hov + +let _ = + +
+
+ +// ^hov + +type r<'a> = {i: 'a, f: float} + +let _get = r => r.f +. r.i +// ^hov + +let withAs = (~xx as yyy) => yyy + 1 +// ^hov + +module AA = { + type cond<'a> = [< #str(string)] as 'a + let fnnxx = (b: cond<_>) => true ? b : b +} + +let funAlias = AA.fnnxx + +let typeOk = funAlias +// ^hov + +let typeDuplicate = AA.fnnxx +// ^hov + +@live let dd = 34 +// ^hov + +let arity0a = () => { + //^hov + let f = () => 3 + f +} + +let arity0b = ((), ()) => 3 +// ^hov + +let arity0c = ((), ()) => 3 +// ^hov + +let arity0d = () => { + // ^hov + let f = () => 3 + f +} + +/**doc comment 1*/ +let docComment1 = 12 +// ^hov + +/** doc comment 2 */ +let docComment2 = 12 +// ^hov + +module ModWithDocComment = { + /*** module level doc comment 1 */ + + /** doc comment for x */ + let x = 44 + + /*** module level doc comment 2 */ +} + +module TypeSubstitutionRecords = { + type foo<'a> = {content: 'a, zzz: string} + type bar = {age: int} + type foobar = foo + + let x1: foo = {content: {age: 42}, zzz: ""} + // ^hov + let x2: foobar = {content: {age: 42}, zzz: ""} + // ^hov + + // x1.content. + // ^com + + // x2.content. + // ^com + + type foo2<'b> = foo<'b> + type foobar2 = foo2 + + let y1: foo2 = {content: {age: 42}, zzz: ""} + let y2: foobar2 = {content: {age: 42}, zzz: ""} + + // y1.content. + // ^com + + // y2.content. + // ^com +} + +module CompV4 = { + type props<'n, 's> = {n?: 'n, s: 's} + let make = props => { + let _ = props.n == Some(10) + React.string(props.s) + } +} + +let mk = CompV4.make +// ^hov + +type useR = {x: int, y: list>>} + +let testUseR = (v: useR) => v +// ^hov + +let usr: useR = { + x: 123, + y: list{}, +} + +// let f = usr +// ^hov + +module NotShadowed = { + /** Stuff */ + let xx_ = 10 + + /** More Stuff */ + let xx = xx_ +} + +module Shadowed = { + /** Stuff */ + let xx = 10 + + /** More Stuff */ + let xx = xx +} + +let _ = NotShadowed.xx +// ^hov + +let _ = Shadowed.xx +// ^hov + +type recordWithDocstringField = { + /** Mighty fine field here. */ + someField: bool, +} + +let x: recordWithDocstringField = { + someField: true, +} + +// x.someField +// ^hov + +let someField = x.someField +// ^hov + +type variant = + /** Cool variant! */ + | CoolVariant + /** Other cool variant */ + | OtherCoolVariant + +let coolVariant = CoolVariant +// ^hov + +type payloadVariants = InlineRecord({field1: int, field2: bool}) | Args(int, bool) + +let payloadVariant = InlineRecord({field1: 1, field2: true}) +// ^hov + +let payloadVariant2 = Args(1, true) +// ^hov + +module RecursiveVariants = { + type rec t = Action1(int) | Action2(float) | Batch(array) +} + +let recursiveVariant = RecursiveVariants.Action1(1) +// ^hov + +// Hover on unsaved +// let fff = "hello"; fff +// ^hov + +// switch x { | {someField} => someField } +// ^hov + +module Arr = Belt.Array +// ^hov + +type aliased = variant +// ^hov diff --git a/tests/lsp_tests/basic-workspace/Hover.res.js b/tests/lsp_tests/basic-workspace/Hover.res.js new file mode 100644 index 00000000000..84e61d64681 --- /dev/null +++ b/tests/lsp_tests/basic-workspace/Hover.res.js @@ -0,0 +1,288 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.mjs"; +import * as JsxRuntime from "react/jsx-runtime"; + +let Id = {}; + +function customDouble(foo) { + return (foo << 1); +} + +let Dep = { + customDouble: customDouble +}; + +function Hover$HoverInsideModuleWithComponent(props) { + return null; +} + +let HoverInsideModuleWithComponent = { + x: 2, + make: Hover$HoverInsideModuleWithComponent +}; + +function functionWithTypeAnnotation() { + return 1; +} + +function Hover(props) { + return props.name; +} + +function Hover$C2$make2(props) { + return props.name; +} + +let C2 = { + make2: Hover$C2$make2 +}; + +function log(msg) { + console.log(msg); +} + +let JsLogger = { + log: log +}; + +let IdDefinedTwice = { + y: 20, + _x: 10 +}; + +let A = { + x: 13 +}; + +function Hover$Comp(props) { + return props.children; +} + +let Comp = { + make: Hover$Comp +}; + +JsxRuntime.jsxs(Hover$Comp, { + children: [ + JsxRuntime.jsx("div", {}), + JsxRuntime.jsx("div", {}) + ] +}); + +JsxRuntime.jsxs(Hover$Comp, { + children: [ + JsxRuntime.jsx("div", {}), + JsxRuntime.jsx("div", {}) + ] +}); + +function _get(r) { + return r.f + r.i; +} + +function withAs(yyy) { + return yyy + 1 | 0; +} + +function fnnxx(b) { + return b; +} + +let AA = { + fnnxx: fnnxx +}; + +function arity0a() { + return () => 3; +} + +function arity0b(param, param$1) { + return 3; +} + +function arity0c(param, param$1) { + return 3; +} + +function arity0d() { + return () => 3; +} + +let ModWithDocComment = { + x: 44 +}; + +let TypeSubstitutionRecords_x1 = { + content: { + age: 42 + }, + zzz: "" +}; + +let TypeSubstitutionRecords_x2 = { + content: { + age: 42 + }, + zzz: "" +}; + +let TypeSubstitutionRecords_y1 = { + content: { + age: 42 + }, + zzz: "" +}; + +let TypeSubstitutionRecords_y2 = { + content: { + age: 42 + }, + zzz: "" +}; + +let TypeSubstitutionRecords = { + x1: TypeSubstitutionRecords_x1, + x2: TypeSubstitutionRecords_x2, + y1: TypeSubstitutionRecords_y1, + y2: TypeSubstitutionRecords_y2 +}; + +function make(props) { + Primitive_object.equal(props.n, 10); + return props.s; +} + +let CompV4 = { + make: make +}; + +function testUseR(v) { + return v; +} + +let NotShadowed = { + xx_: 10, + xx: 10 +}; + +let Shadowed = { + xx: 10 +}; + +let RecursiveVariants = {}; + +let abc = 56; + +let D; + +let cd = customDouble; + +let make$1 = Hover; + +let num = 34; + +let JJ; + +let B; + +let C; + +let Comp1; + +let funAlias = fnnxx; + +let typeOk = fnnxx; + +let typeDuplicate = fnnxx; + +let dd = 34; + +let docComment1 = 12; + +let docComment2 = 12; + +let mk = make; + +let usr = { + x: 123, + y: /* [] */0 +}; + +let x = { + someField: true +}; + +let someField = true; + +let coolVariant = "CoolVariant"; + +let payloadVariant = { + TAG: "InlineRecord", + field1: 1, + field2: true +}; + +let payloadVariant2 = { + TAG: "Args", + _0: 1, + _1: true +}; + +let recursiveVariant = { + TAG: "Action1", + _0: 1 +}; + +let Arr; + +export { + abc, + Id, + Dep, + D, + cd, + HoverInsideModuleWithComponent, + functionWithTypeAnnotation, + make$1 as make, + C2, + num, + JsLogger, + JJ, + IdDefinedTwice, + A, + B, + C, + Comp, + Comp1, + _get, + withAs, + AA, + funAlias, + typeOk, + typeDuplicate, + dd, + arity0a, + arity0b, + arity0c, + arity0d, + docComment1, + docComment2, + ModWithDocComment, + TypeSubstitutionRecords, + CompV4, + mk, + testUseR, + usr, + NotShadowed, + Shadowed, + x, + someField, + coolVariant, + payloadVariant, + payloadVariant2, + RecursiveVariants, + recursiveVariant, + Arr, +} +/* Not a pure module */ diff --git a/tests/lsp_tests/basic-workspace/package.json b/tests/lsp_tests/basic-workspace/package.json new file mode 100644 index 00000000000..950bea0a1f9 --- /dev/null +++ b/tests/lsp_tests/basic-workspace/package.json @@ -0,0 +1,14 @@ +{ + "name": "@tests/lsp-tests-basic-workspace", + "type": "module", + "private": true, + "scripts": { + "build": "rescript build", + "clean": "rescript clean", + "dev": "rescript -w" + }, + "dependencies": { + "@rescript/react": "workspace:^", + "rescript": "workspace:^" + } +} diff --git a/tests/lsp_tests/basic-workspace/rescript.json b/tests/lsp_tests/basic-workspace/rescript.json new file mode 100644 index 00000000000..76742555703 --- /dev/null +++ b/tests/lsp_tests/basic-workspace/rescript.json @@ -0,0 +1,13 @@ +{ + "name": "@tests/lsp-tests-basic-workspace", + "sources": { + "dir": "." + }, + "package-specs": { + "module": "esmodule", + "in-source": true + }, + "suffix": ".res.js", + "dependencies": ["@rescript/react"], + "jsx": { "version": 4 } +} diff --git a/tests/lsp_tests/dune b/tests/lsp_tests/dune new file mode 100644 index 00000000000..fd9baef10d0 --- /dev/null +++ b/tests/lsp_tests/dune @@ -0,0 +1,10 @@ +(executable + (name test) + (package rescript-language-server) + (public_name lsp-tests) + (libraries lsp jsonrpc yojson eio eio_main eio.unix) + (flags + (-w "-9-32-33"))) + +(dirs + (:standard \ ignored_dir basic-workspace)) diff --git a/tests/lsp_tests/expected/Hover.res.expected b/tests/lsp_tests/expected/Hover.res.expected new file mode 100644 index 00000000000..c71369a28a4 --- /dev/null +++ b/tests/lsp_tests/expected/Hover.res.expected @@ -0,0 +1,331 @@ +Request textDocument/hover Line: 1 Character: 4 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover Line: 4 Character: 5 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\ntype t = (int, float)\n```" + } +} + +Request textDocument/hover Line: 7 Character: 7 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule Id: {\n type x = int\n}\n```" + } +} + +Request textDocument/hover Line: 20 Character: 11 +Response +{ + "contents": { + "kind": "markdown", + "value": "\nThis module is commented\n---\n\n```\n \n```\n```rescript\nmodule Dep: {\n let customDouble: int => int\n}\n```" + } +} + +Request textDocument/hover Line: 23 Character: 11 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint => int\n```\n---\nSome doc comment" + } +} + +Request textDocument/hover Line: 27 Character: 6 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover Line: 34 Character: 4 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nunit => int\n```\n---\nDoc comment for functionWithTypeAnnotation" + } +} + +Request textDocument/hover Line: 38 Character: 13 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nstring\n```" } } + +Request textDocument/hover Line: 43 Character: 15 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nstring\n```" } } + +Request textDocument/hover Line: 47 Character: 10 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover Line: 50 Character: 13 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule type Logger = {\n let log: string => unit\n}\n```" + } +} + +Request textDocument/hover Line: 55 Character: 7 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule type Logger = {\n let log: string => unit\n}\n```" + } +} + +Command `def` not implemented!Request textDocument/hover Line: 64 Character: 9 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule IdDefinedTwice: {\n let y: int\n let _x: int\n}\n```" + } +} + +Request textDocument/hover Line: 75 Character: 7 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule A: {\n let x: int\n}\n```" + } +} + +Request textDocument/hover Line: 78 Character: 7 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule A: {\n let x: int\n}\n```" + } +} + +Request textDocument/hover Line: 92 Character: 10 +Response +null + +Request textDocument/hover Line: 99 Character: 10 +Response +null + +Request textDocument/hover Line: 104 Character: 25 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nfloat\n```" } } + +Request textDocument/hover Line: 107 Character: 21 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover Line: 117 Character: 16 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nAA.cond<([< #str(string)] as 'a)> => AA.cond<'a>\n```\n\n---\n\n```\n \n```\n```rescript\ntype AA.cond<'a> = 'a\n constraint 'a = [< #str(string)]\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C110%2C2%5D)\n" + } +} + +Request textDocument/hover Line: 120 Character: 25 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nAA.cond<([< #str(string)] as 'a)> => AA.cond<'a>\n```\n\n---\n\n```\n \n```\n```rescript\ntype AA.cond<'a> = 'a\n constraint 'a = [< #str(string)]\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C110%2C2%5D)\n" + } +} + +Request textDocument/hover Line: 123 Character: 3 +Response +{ + "contents": { + "kind": "markdown", + "value": "The `@live` decorator is for reanalyze, a static analysis tool for ReScript that can do dead code analysis.\n\n`@live` tells the dead code analysis that the value should be considered live, even though it might appear to be dead. This is typically used in case of FFI where there are indirect ways to access values. It can be added to everything that could otherwise be considered unused by the dead code analysis - values, functions, arguments, records, individual record fields, and so on.\n\n[Read more and see examples in the documentation](https://rescript-lang.org/syntax-lookup#live-decorator).\n\nHint: Did you know you can run an interactive code analysis in your project by running the command `> ReScript: Start Code Analyzer`? Try it!" + } +} + +Request textDocument/hover Line: 132 Character: 4 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\n(unit, unit) => int\n```" + } +} + +Request textDocument/hover Line: 135 Character: 4 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\n(unit, unit) => int\n```" + } +} + +Request textDocument/hover Line: 138 Character: 5 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nunit => unit => int\n```" + } +} + +Request textDocument/hover Line: 145 Character: 9 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\ndoc comment 1" + } +} + +Request textDocument/hover Line: 149 Character: 6 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\n doc comment 2 " + } +} + +Request textDocument/hover Line: 166 Character: 23 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nfoo\n```\n\n---\n\n```\n \n```\n```rescript\ntype foo<'a> = {content: 'a, zzz: string}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C161%2C2%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype bar = {age: int}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C162%2C2%5D)\n" + } +} + +Request textDocument/hover Line: 168 Character: 22 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nfoobar\n```\n\n---\n\n```\n \n```\n```rescript\ntype foobar = foo\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C163%2C2%5D)\n" + } +} + +Command `com` not implemented!Command `com` not implemented!Command `com` not implemented!Command `com` not implemented!Request textDocument/hover Line: 198 Character: 4 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nCompV4.props => React.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype CompV4.props<'n, 's> = {n?: 'n, s: 's}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C190%2C2%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.element = Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Fdependencies%2Frescript-react%2Fsrc%2FReact.res%22%2C0%2C0%5D)\n" + } +} + +Request textDocument/hover Line: 203 Character: 16 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nuseR\n```\n\n---\n\n```\n \n```\n```rescript\ntype useR = {x: int, y: list>>}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C200%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype r<'a> = {i: 'a, f: float}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C101%2C0%5D)\n" + } +} + +Request textDocument/hover Line: 211 Character: 13 +Response +null + +Request textDocument/hover Line: 230 Character: 20 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\n More Stuff " + } +} + +Request textDocument/hover Line: 233 Character: 17 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\n More Stuff " + } +} + +Request textDocument/hover Line: 245 Character: 6 +Response +null + +Request textDocument/hover Line: 248 Character: 19 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nbool\n```\n---\n Mighty fine field here. " + } +} + +Request textDocument/hover Line: 257 Character: 20 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nvariant\nCoolVariant\n```\n---\n Cool variant! \n\n---\n\n```\n \n```\n```rescript\ntype variant = CoolVariant | OtherCoolVariant\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C250%2C0%5D)\n" + } +} + +Request textDocument/hover Line: 262 Character: 22 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\npayloadVariants\nInlineRecord({field1: int, field2: bool})\n```\n\n---\n\n```\n \n```\n```rescript\ntype payloadVariants =\n | InlineRecord({field1: int, field2: bool})\n | Args(int, bool)\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C259%2C0%5D)\n" + } +} + +Request textDocument/hover Line: 265 Character: 23 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\npayloadVariants\nArgs(int, bool)\n```\n\n---\n\n```\n \n```\n```rescript\ntype payloadVariants =\n | InlineRecord({field1: int, field2: bool})\n | Args(int, bool)\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C259%2C0%5D)\n" + } +} + +Request textDocument/hover Line: 272 Character: 42 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nRecursiveVariants.t\nAction1(int)\n```\n\n---\n\n```\n \n```\n```rescript\ntype RecursiveVariants.t =\n | Action1(int)\n | Action2(float)\n | Batch(array)\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C268%2C2%5D)\n" + } +} + +Request textDocument/hover Line: 276 Character: 23 +Response +null + +Request textDocument/hover Line: 279 Character: 33 +Response +null + +Request textDocument/hover Line: 282 Character: 8 +Response +{ + "contents": { + "kind": "markdown", + "value": "\n [`Belt.Array`]()\n\n **mutable array**: Utilities functions\n\n---\n\n```\n \n```\n```rescript\nmodule Array: {\n module Id\n module Array\n module SortArray\n module MutableQueue\n module MutableStack\n module List\n module Range\n module Set\n module Map\n module MutableSet\n module MutableMap\n module HashSet\n module HashMap\n module Option\n module Result\n module Int\n module Float\n}\n```" + } +} + +Request textDocument/hover Line: 285 Character: 6 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\ntype aliased = variant\n```\n\n---\n\n```\n \n```\n```rescript\ntype variant = CoolVariant | OtherCoolVariant\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C250%2C0%5D)\n" + } +} + diff --git a/tests/lsp_tests/test.ml b/tests/lsp_tests/test.ml new file mode 100644 index 00000000000..378671874ee --- /dev/null +++ b/tests/lsp_tests/test.ml @@ -0,0 +1,258 @@ +module Helper = struct + (** Helpers for spawning the ReScript language server in tests, sending + LSP requests/notifications over stdio, and reading responses back. *) + + let server_binary = "_build/default/lsp/bin/main.exe" + + type t = { + proc: [`Generic | `Unix] Eio.Process.ty Eio.Resource.t; + stdin: Eio_unix.sink_ty Eio.Resource.t; + stdout: Eio.Buf_read.t; + mutable next_id: int; + } + + let frame (json : Yojson.Safe.t) : string = + let body = Yojson.Safe.to_string json in + Printf.sprintf "Content-Length: %d\r\n\r\n%s" (String.length body) body + + let read_headers buf = + let rec loop acc = + match Eio.Buf_read.line buf with + | "" -> Some acc + | line -> + let acc = + match String.index_opt line ':' with + | None -> acc + | Some i -> + let k = String.sub line 0 i in + let v = + String.trim (String.sub line (i + 1) (String.length line - i - 1)) + in + (k, v) :: acc + in + loop acc + | exception End_of_file -> if acc = [] then None else Some acc + in + loop [] + + let read_message buf = + match read_headers buf with + | None -> None + | Some headers -> + let len = int_of_string (List.assoc "Content-Length" headers) in + let body = Eio.Buf_read.take len buf in + Some (Yojson.Safe.from_string body) + + let start ~sw ~env = + let mgr = Eio.Stdenv.process_mgr env in + let stdin_r, stdin_w = Eio_unix.pipe sw in + let stdout_r, stdout_w = Eio_unix.pipe sw in + let proc = + Eio.Process.spawn ~sw mgr ~stdin:stdin_r ~stdout:stdout_w + ~executable:server_binary [] + in + Eio.Resource.close stdin_r; + Eio.Resource.close stdout_w; + let stdout = Eio.Buf_read.of_flow ~max_size:(16 * 1024 * 1024) stdout_r in + {proc; stdin = stdin_w; stdout; next_id = 0} + + let send_packet t (packet : Jsonrpc.Packet.t) = + let json = Jsonrpc.Packet.yojson_of_t packet in + Eio.Flow.copy_string (frame json) t.stdin + + let next_id t = + t.next_id <- t.next_id + 1; + t.next_id + + (** Send a typed LSP request and return the assigned id. *) + let send_request t (req : 'r Lsp.Client_request.t) = + let id = `Int (next_id t) in + let jsonrpc_req = Lsp.Client_request.to_jsonrpc_request req ~id in + send_packet t (Jsonrpc.Packet.Request jsonrpc_req); + id + + (** Send a typed LSP notification. *) + let send_notification t (notif : Lsp.Client_notification.t) = + let jsonrpc_notif = Lsp.Client_notification.to_jsonrpc notif in + send_packet t (Jsonrpc.Packet.Notification jsonrpc_notif) + + (** Read packets until we find the response matching [id]. Server + notifications/requests received in the meantime are discarded. *) + let rec read_response t id = + match read_message t.stdout with + | None -> failwith "Helper.read_response: unexpected EOF" + | Some json -> ( + match Jsonrpc.Packet.t_of_yojson json with + | Response resp when resp.id = id -> resp + | _ -> read_response t id) + + (** Send a typed request and synchronously wait for its response, decoded + back into the request's result type. *) + let request (type r) t (req : r Lsp.Client_request.t) : r = + let id = send_request t req in + let resp = read_response t id in + match resp.result with + | Ok json -> Lsp.Client_request.response_of_json req json + | Error err -> failwith ("LSP error response: " ^ err.message) + + (** Read the next packet of any kind. Useful when waiting for a server + notification (e.g. publishDiagnostics). *) + (* let read_packet t = + match read_message t.stdout with + | None -> failwith "Helper.read_packet: unexpected EOF" + | Some json -> Jsonrpc.Packet.t_of_yojson json *) + + let stop t = + (try Eio.Resource.close t.stdin with _ -> ()); + Eio.Process.await t.proc + + (** Run [f] with a freshly started server, ensuring the process is stopped + and the switch is released afterwards. *) + let with_server ~env f = + Eio.Switch.run @@ fun sw -> + let t = start ~sw ~env in + Fun.protect ~finally:(fun () -> ignore (stop t)) (fun () -> f t) +end + +open Lsp +open Types + +type caret_comment = { + path: string; + line: int; (* line of the comment *) + col: int; (* column of the ^ character *) + command: string; (* e.g. "hov" *) + text: string; +} + +module StringMap = Map.Make (String) + +let find_caret_comments ~fs ~dir = + let results = ref [] in + + (* Read all .res files in directory *) + Eio.Path.with_open_dir + Eio.Path.(fs / dir) + (fun dir_handle -> + Eio.Path.read_dir dir_handle + |> List.filter (String.ends_with ~suffix:".res") + |> List.iter (fun filename -> + let path = Eio.Path.(dir_handle / filename) in + let content = Eio.Path.load path in + let lines = String.split_on_char '\n' content in + + List.iteri + (fun line_idx line -> + (* Match lines like "// ^command" *) + match String.trim line with + | s when String.length s > 3 && String.sub s 0 3 = "// " -> ( + let rest = String.sub s 3 (String.length s - 3) in + match String.index_opt rest '^' with + | None -> () + | Some caret_in_rest -> + (* Column of ^ in original line *) + let prefix_len = + String.length line - String.length (String.trim line) + in + let col = prefix_len + 3 + caret_in_rest in + let command = + let after = caret_in_rest + 1 in + if after < String.length rest then + String.trim + (String.sub rest after (String.length rest - after)) + else "" + in + results := + { + (* TODO: rewrite this *) + path = Sys.getcwd () ^ "/" ^ dir ^ "/" ^ snd path; + line = line_idx; + col; + command; + text = content; + } + :: !results) + | _ -> ()) + lines)); + + List.rev !results + +let run_test ~fs ~dir server = + let comments = find_caret_comments ~fs ~dir in + + let send_request payload method_ (caret_comment : caret_comment) = + let request_str = + Printf.sprintf "%s Line: %d Character: %d" method_ caret_comment.line + caret_comment.col + in + let response = Helper.request server payload in + let response_str = + Client_request.yojson_of_result payload response + |> Yojson.Safe.pretty_to_string ~std:false + in + Printf.sprintf "Request %s\nResponse\n%s\n\n" request_str response_str + in + + let open_document ~uri ~text = + Helper.send_notification server + (Client_notification.TextDocumentDidOpen + (DidOpenTextDocumentParams.create + ~textDocument: + (TextDocumentItem.create ~uri ~languageId:"rescript" ~version:0 + ~text))) + in + + let comment_to_lsp (caret_comment : caret_comment) = + let uri = DocumentUri.of_path caret_comment.path in + let textDocument = TextDocumentIdentifier.create ~uri in + + let character = caret_comment.col in + let line = caret_comment.line - 1 in + let position = Position.create ~line ~character in + let text = caret_comment.text in + + match caret_comment.command with + | "hov" -> + open_document ~uri ~text; + send_request + (Client_request.TextDocumentHover + (HoverParams.create ~textDocument ~position ())) + "textDocument/hover" caret_comment + (* | "cmp" -> + let context = + CompletionContext.create ~triggerCharacter:">" + ~triggerKind:CompletionTriggerKind.TriggerCharacter () + in + send_request + (Client_request.TextDocumentCompletion + (CompletionParams.create ~textDocument ~position ~context ())) + "textDocument/completion" caret_comment *) + | other -> Printf.sprintf "Command `%s` not implemented!" other + in + + let grouped = + List.fold_left + (fun acc comment -> + let others = + Option.value ~default:[] (StringMap.find_opt comment.path acc) + in + StringMap.add comment.path (comment :: others) acc) + StringMap.empty comments + in + + StringMap.iter + (fun path comments -> + let filename = Filename.basename path ^ ".expected" in + let save_path = Filename.concat "tests/lsp_tests/expected" filename in + let content = List.rev_map comment_to_lsp comments |> String.concat "" in + let file = Eio.Path.(fs / save_path) in + Eio.Path.save ~create:(`Or_truncate 0o644) file content) + grouped + +let main () = + Eio_main.run @@ fun env -> + Helper.with_server ~env @@ fun server -> + run_test ~fs:env#fs ~dir:"tests/lsp_tests/basic-workspace" server; + Helper.stop server |> ignore + +let () = main () diff --git a/yarn.lock b/yarn.lock index af3369dbea2..29db1fe74d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -734,6 +734,15 @@ __metadata: languageName: unknown linkType: soft +"@tests/lsp-tests-basic-workspace@workspace:tests/lsp_tests/basic-workspace": + version: 0.0.0-use.local + resolution: "@tests/lsp-tests-basic-workspace@workspace:tests/lsp_tests/basic-workspace" + dependencies: + "@rescript/react": "workspace:^" + rescript: "workspace:^" + languageName: unknown + linkType: soft + "@tests/reanalyze-benchmark@workspace:tests/analysis_tests/tests-reanalyze/deadcode-benchmark": version: 0.0.0-use.local resolution: "@tests/reanalyze-benchmark@workspace:tests/analysis_tests/tests-reanalyze/deadcode-benchmark"