diff --git a/src/rescript-editor-support/Hover.re b/src/rescript-editor-support/Hover.re index fd1f0ca6..c7faa81f 100644 --- a/src/rescript-editor-support/Hover.re +++ b/src/rescript-editor-support/Hover.re @@ -78,7 +78,7 @@ let newHover = (~rootUri, ~file: SharedTypes.file, ~getModule, loc) => { showModule(~docstring, ~name, ~file, declared); | LModule(GlobalReference(moduleName, path, tip)) => let%opt file = getModule(moduleName); - let env = {Query.file, exported: file.contents.exported}; + let env = Query.fileEnv(file); let%opt (env, name) = Query.resolvePath(~env, ~path, ~getModule); let%opt stamp = Query.exportedForTip(~env, name, tip); let%opt md = Hashtbl.find_opt(file.stamps.modules, stamp); @@ -116,7 +116,7 @@ let newHover = (~rootUri, ~file: SharedTypes.file, ~getModule, loc) => { let fromType = (~docstring, typ) => { let typeString = codeBlock(typ |> Shared.typeToString); let extraTypeInfo = { - let env = {Query.file, exported: file.contents.exported}; + let env = Query.fileEnv(file); let%opt path = typ |> Shared.digConstructor; let%opt (_env, {docstring, name: {txt}, item: {decl}}) = digConstructor(~env, ~getModule, path); diff --git a/src/rescript-editor-support/NewCompletions.re b/src/rescript-editor-support/NewCompletions.re index f4da1636..8280410b 100644 --- a/src/rescript-editor-support/NewCompletions.re +++ b/src/rescript-editor-support/NewCompletions.re @@ -574,6 +574,30 @@ let getItems = module J = JsonShort; +let mkItem = (~name, ~kind, ~detail, ~docstring, ~uri, ~pos_lnum) => { + J.o([ + ("label", J.s(name)), + ("kind", J.i(kind)), + ("detail", detail |> J.s), + ( + "documentation", + J.o([ + ("kind", J.s("markdown")), + ( + "value", + J.s( + (docstring |? "No docs") + ++ "\n\n" + ++ Uri2.toString(uri) + ++ ":" + ++ string_of_int(pos_lnum), + ), + ), + ]), + ), + ]); +}; + let computeCompletions = (~full, ~maybeText, ~package, ~pos, ~state) => { let parameters = switch (maybeText) { @@ -582,19 +606,14 @@ let computeCompletions = (~full, ~maybeText, ~package, ~pos, ~state) => { switch (PartialParser.positionToOffset(text, pos)) { | None => None | Some(offset) => - switch (PartialParser.findCompletable(text, offset)) { - | None => None - | Some(Clabel(_)) => - /* Not supported yet */ - None - | Some(Cpath(parts)) => Some((text, offset, parts)) - } + Some((text, offset, PartialParser.findCompletable(text, offset))) } }; let items = switch (parameters) { | None => [] - | Some((text, offset, parts)) => + + | Some((text, offset, Some(Cpath(parts)))) => let rawOpens = PartialParser.findOpens(text, offset); let allModules = package.TopTypes.localModules @ package.dependencyModules; @@ -609,45 +628,165 @@ let computeCompletions = (~full, ~maybeText, ~package, ~pos, ~state) => { ~parts, ); /* TODO(#107): figure out why we're getting duplicates. */ - Utils.dedup(items); + items + |> Utils.dedup + |> List.map( + ( + ( + uri, + { + SharedTypes.name: {txt: name, loc: {loc_start: {pos_lnum}}}, + docstring, + item, + }, + ), + ) => + mkItem( + ~name, + ~kind=kindToInt(item), + ~detail=detail(name, item), + ~docstring, + ~uri, + ~pos_lnum, + ) + ); + + | Some((text, offset, Some(Cpipe(s)))) => + let rawOpens = PartialParser.findOpens(text, offset); + let allModules = + package.TopTypes.localModules @ package.dependencyModules; + + let getItems = parts => + getItems( + ~full, + ~package, + ~rawOpens, + ~getModule=State.fileForModule(state, ~package), + ~allModules, + ~pos, + ~parts, + ); + + let getLhsType = (~lhs, ~partialName) => { + switch (getItems([lhs])) { + | [(_uri, {SharedTypes.item: Value(t)}), ..._] => + Some((t, partialName)) + | _ => None + }; + }; + + let lhsType = + switch (Str.split(Str.regexp_string("->"), s)) { + | [lhs] => getLhsType(~lhs, ~partialName="") + | [lhs, partialName] => getLhsType(~lhs, ~partialName) + | _ => + // Only allow one -> + None + }; + + let removePackageOpens = modulePath => + switch (modulePath) { + | [toplevel, ...rest] when package.opens |> List.mem(toplevel) => rest + | _ => modulePath + }; + + let rec removeRawOpen = (rawOpen, modulePath) => + switch (rawOpen, modulePath) { + | (Tip(_), _) => Some(modulePath) + | (Nested(s, inner), [first, ...restPath]) when s == first => + removeRawOpen(inner, restPath) + | _ => None + }; + + let rec removeRawOpens = (rawOpens, modulePath) => + switch (rawOpens) { + | [rawOpen, ...restOpens] => + let newModulePath = + switch (removeRawOpen(rawOpen, modulePath)) { + | None => modulePath + | Some(newModulePath) => newModulePath + }; + removeRawOpens(restOpens, newModulePath); + | [] => modulePath + }; + + switch (lhsType) { + | Some((t, partialName)) => + let getModulePath = path => { + let rec loop = (path: Path.t) => + switch (path) { + | Pident(id) => [Ident.name(id)] + | Pdot(p, s, _) => [s, ...loop(p)] + | Papply(_) => [] + }; + switch (loop(path)) { + | [_, ...rest] => List.rev(rest) + | [] => [] + }; + }; + let modulePath = + switch (t.desc) { + | Tconstr(path, _, _) => getModulePath(path) + | Tlink({desc: Tconstr(path, _, _)}) => getModulePath(path) + | _ => [] + }; + switch (modulePath) { + | [_, ..._] => + let modulePathMinusOpens = + modulePath + |> removePackageOpens + |> removeRawOpens(rawOpens) + |> String.concat("."); + let completionName = name => + modulePathMinusOpens == "" + ? name : modulePathMinusOpens ++ "." ++ name; + let parts = modulePath @ [partialName]; + let items = getItems(parts); + items + |> List.filter(((_, {item})) => + switch (item) { + | Value(_) => true + | _ => false + } + ) + |> List.map( + ( + ( + uri, + { + SharedTypes.name: { + txt: name, + loc: {loc_start: {pos_lnum}}, + }, + docstring, + item, + }, + ), + ) => + mkItem( + ~name=completionName(name), + ~kind=kindToInt(item), + ~detail=detail(name, item), + ~docstring, + ~uri, + ~pos_lnum, + ) + ); + + | _ => [] + }; + | None => [] + }; + + | Some((_, _, Some(Clabel(_)))) => + // not supported yet + [] + + | Some((_, _, None)) => [] }; if (items == []) { J.null; } else { - items - |> List.map( - ( - ( - uri, - { - SharedTypes.name: {txt: name, loc: {loc_start: {pos_lnum}}}, - docstring, - item, - }, - ), - ) => { - J.o([ - ("label", J.s(name)), - ("kind", J.i(kindToInt(item))), - ("detail", detail(name, item) |> J.s), - ( - "documentation", - J.o([ - ("kind", J.s("markdown")), - ( - "value", - J.s( - (docstring |? "No docs") - ++ "\n\n" - ++ Uri2.toString(uri) - ++ ":" - ++ string_of_int(pos_lnum), - ), - ), - ]), - ), - ]) - }) - |> J.l; + items |> J.l; }; }; diff --git a/src/rescript-editor-support/PartialParser.re b/src/rescript-editor-support/PartialParser.re index 097320fc..da9c0c80 100644 --- a/src/rescript-editor-support/PartialParser.re +++ b/src/rescript-editor-support/PartialParser.re @@ -66,13 +66,23 @@ let rec startOfLident = (text, i) => type completable = | Clabel(string) - | Cpath(list(string)); + | Cpath(list(string)) + | Cpipe(string); let findCompletable = (text, offset) => { let mkPath = s => { - let parts = Str.split(Str.regexp_string("."), s); - let parts = s.[String.length(s) - 1] == '.' ? parts @ [""] : parts; - Cpath(parts); + let len = String.length(s); + let pipeParts = Str.split(Str.regexp_string("->"), s); + if (len > 1 + && s.[len - 2] == '-' + && s.[len - 1] == '>' + || List.length(pipeParts) > 1) { + Cpipe(s); + } else { + let parts = Str.split(Str.regexp_string("."), s); + let parts = s.[len - 1] == '.' ? parts @ [""] : parts; + Cpath(parts); + }; }; let rec loop = i => { @@ -80,6 +90,7 @@ let findCompletable = (text, offset) => { ? Some(mkPath(String.sub(text, i + 1, offset - (i + 1)))) : ( switch (text.[i]) { + | '>' when i > 0 && text.[i - 1] == '-' => loop(i - 2) | '~' => Some(Clabel(String.sub(text, i + 1, offset - (i + 1)))) | 'a'..'z' | 'A'..'Z' diff --git a/src/rescript-editor-support/ProcessExtra.re b/src/rescript-editor-support/ProcessExtra.re index 49b9c72e..cf24a627 100644 --- a/src/rescript-editor-support/ProcessExtra.re +++ b/src/rescript-editor-support/ProcessExtra.re @@ -123,10 +123,7 @@ module F = ], ); }; - let env = { - Query.file: Collector.file, - exported: Collector.file.contents.exported, - }; + let env = Query.fileEnv(Collector.file); let getTypeAtPath = getTypeAtPath(~env); diff --git a/src/rescript-editor-support/Query.re b/src/rescript-editor-support/Query.re index fafa3951..3a84d043 100644 --- a/src/rescript-editor-support/Query.re +++ b/src/rescript-editor-support/Query.re @@ -1,7 +1,5 @@ open SharedTypes; -/* TODO maybe keep track of the "current module path" */ -/* maybe add a "current module path" for debugging purposes */ type queryEnv = { file, exported, @@ -111,11 +109,7 @@ let rec resolvePath = (~env, ~path, ~getModule) => { | `Local(env, name) => Some((env, name)) | `Global(moduleName, fullPath) => let%opt file = getModule(moduleName); - resolvePath( - ~env={file, exported: file.contents.exported}, - ~path=fullPath, - ~getModule, - ); + resolvePath(~env=fileEnv(file), ~path=fullPath, ~getModule); }; }; @@ -131,11 +125,7 @@ let resolveFromStamps = (~env, ~path, ~getModule, ~pos) => { | `Local(env, name) => Some((env, name)) | `Global(moduleName, fullPath) => let%opt file = getModule(moduleName); - resolvePath( - ~env={file, exported: file.contents.exported}, - ~path=fullPath, - ~getModule, - ); + resolvePath(~env=fileEnv(file), ~path=fullPath, ~getModule); }; }; }; @@ -192,7 +182,7 @@ let resolveFromCompilerPath = (~env, ~getModule, path) => { switch (getModule(moduleName)) { | None => None | Some(file) => - let env = {file, exported: file.contents.exported}; + let env = fileEnv(file); resolvePath(~env, ~getModule, ~path); }; switch (res) { diff --git a/src/rescript-editor-support/References.re b/src/rescript-editor-support/References.re index 13407c0c..8322ab29 100644 --- a/src/rescript-editor-support/References.re +++ b/src/rescript-editor-support/References.re @@ -115,7 +115,7 @@ let definedForLoc = (~file, ~getModule, locKind) => { let%try file = getModule(moduleName) |> RResult.orError("Cannot get module " ++ moduleName); - let env = {Query.file, exported: file.contents.exported}; + let env = Query.fileEnv(file); let%try (env, name) = Query.resolvePath(~env, ~path, ~getModule) |> RResult.orError("Cannot resolve path " ++ pathToString(path)); @@ -176,7 +176,7 @@ let resolveModuleReference = switch (declared.item) { | Structure(_) => Some((file, Some(declared))) | Ident(path) => - let env = {Query.file, exported: file.contents.exported}; + let env = Query.fileEnv(file); switch (Query.fromCompilerPath(~env, path)) { | `Not_found => None | `Exported(env, name) => @@ -186,7 +186,7 @@ let resolveModuleReference = /* Some((env.file.uri, validateLoc(md.name.loc, md.extentLoc))) */ | `Global(moduleName, path) => let%opt file = getModule(moduleName); - let env = {file, Query.exported: file.contents.exported}; + let env = Query.fileEnv(file); let%opt (env, name) = Query.resolvePath(~env, ~getModule, ~path); let%opt stamp = Hashtbl.find_opt(env.exported.modules, name); let%opt md = Hashtbl.find_opt(env.file.stamps.modules, stamp); @@ -217,7 +217,7 @@ let forLocalStamp = stamp, tip, ) => { - let env = {Query.file, exported: file.contents.exported}; + let env = Query.fileEnv(file); open Infix; let%opt localStamp = switch (tip) { @@ -365,7 +365,7 @@ let allReferencesForLoc = let%try file = getModule(moduleName) |> RResult.orError("Cannot get module " ++ moduleName); - let env = {Query.file, exported: file.contents.exported}; + let env = Query.fileEnv(file); let%try (env, name) = Query.resolvePath(~env, ~path, ~getModule) |> RResult.orError("Cannot resolve path " ++ pathToString(path)); @@ -512,7 +512,7 @@ let definitionForLoc = (~pathsForModule, ~file, ~getUri, ~getModule, loc) => { ++ tipToString(tip), ); let%opt file = getModule(moduleName); - let env = {Query.file, exported: file.contents.exported}; + let env = Query.fileEnv(file); let%opt (env, name) = Query.resolvePath(~env, ~path, ~getModule); let%opt stamp = Query.exportedForTip(~env, name, tip); /** oooh wht do I do if the stamp is inside a pseudo-file? */ diff --git a/src/rescript-editor-support/RescriptEditorSupport.re b/src/rescript-editor-support/RescriptEditorSupport.re index c6c3e451..a52b4e63 100644 --- a/src/rescript-editor-support/RescriptEditorSupport.re +++ b/src/rescript-editor-support/RescriptEditorSupport.re @@ -5,7 +5,10 @@ let capabilities = J.o([ ("textDocumentSync", J.i(1)), ("hoverProvider", J.t), - ("completionProvider", J.o([("triggerCharacters", J.l([J.s(".")]))])), + ( + "completionProvider", + J.o([("triggerCharacters", J.l([J.s("."), J.s(">")]))]), + ), ("definitionProvider", J.t), ("typeDefinitionProvider", J.t), ("referencesProvider", J.t),