Skip to content

Commit c6624e4

Browse files
authored
Support completions and better hovers for first class modules (#7780)
* support completions and better hovers for first class modules (and by extension module types) * changelog * fix formatting
1 parent db17fe1 commit c6624e4

File tree

9 files changed

+319
-56
lines changed

9 files changed

+319
-56
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
- Add markdown divider between module doc and module type in hover information. https://github.com/rescript-lang/rescript/pull/7775
2222
- Show docstrings before type expansions on hover. https://github.com/rescript-lang/rescript/pull/7774
23+
- Autocomplete (and improved hovers) for first-class module unpacks. https://github.com/rescript-lang/rescript/pull/7780
2324

2425
#### :bug: Bug fix
2526

analysis/src/CompletionBackEnd.ml

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -173,24 +173,71 @@ let findModuleInScope ~env ~moduleName ~scope =
173173
scope |> Scope.iterModulesAfterFirstOpen processModule;
174174
!result
175175

176+
let rec moduleItemToStructureEnv ~(env : QueryEnv.t) ~package (item : Module.t)
177+
=
178+
match item with
179+
| Module.Structure structure -> Some (env, structure)
180+
| Module.Constraint (_, moduleType) ->
181+
moduleItemToStructureEnv ~env ~package moduleType
182+
| Module.Ident p -> (
183+
match ResolvePath.resolveModuleFromCompilerPath ~env ~package p with
184+
| Some (env2, Some declared2) ->
185+
moduleItemToStructureEnv ~env:env2 ~package declared2.item
186+
| _ -> None)
187+
188+
(* Given a declared module, return the env entered into its concrete structure
189+
and the structure itself. Follows constraints and aliases *)
190+
let enterStructureFromDeclared ~(env : QueryEnv.t) ~package
191+
(declared : Module.t Declared.t) =
192+
match moduleItemToStructureEnv ~env ~package declared.item with
193+
| Some (env, s) -> Some (QueryEnv.enterStructure env s, s)
194+
| None -> None
195+
196+
let completionsFromStructureItems ~(env : QueryEnv.t)
197+
(structure : Module.structure) =
198+
StructureUtils.unique_items structure
199+
|> List.filter_map (fun (it : Module.item) ->
200+
match it.kind with
201+
| Module.Value typ ->
202+
Some
203+
(Completion.create ~env ~docstring:it.docstring
204+
~kind:(Completion.Value typ) it.name)
205+
| Module.Module {type_ = m} ->
206+
Some
207+
(Completion.create ~env ~docstring:it.docstring
208+
~kind:
209+
(Completion.Module {docstring = it.docstring; module_ = m})
210+
it.name)
211+
| Module.Type (t, _recStatus) ->
212+
Some
213+
(Completion.create ~env ~docstring:it.docstring
214+
~kind:(Completion.Type t) it.name))
215+
176216
let resolvePathFromStamps ~(env : QueryEnv.t) ~package ~scope ~moduleName ~path
177217
=
178218
(* Log.log("Finding from stamps " ++ name); *)
179219
match findModuleInScope ~env ~moduleName ~scope with
180220
| None -> None
181221
| Some declared -> (
182222
(* Log.log("found it"); *)
183-
match ResolvePath.findInModule ~env declared.item path with
184-
| None -> None
185-
| Some res -> (
186-
match res with
187-
| `Local (env, name) -> Some (env, name)
188-
| `Global (moduleName, fullPath) -> (
189-
match ProcessCmt.fileForModule ~package moduleName with
190-
| None -> None
191-
| Some file ->
192-
ResolvePath.resolvePath ~env:(QueryEnv.fromFile file) ~path:fullPath
193-
~package)))
223+
(* [""] means completion after `ModuleName.` (trailing dot). *)
224+
match path with
225+
| [""] -> (
226+
match moduleItemToStructureEnv ~env ~package declared.item with
227+
| Some (env, structure) -> Some (QueryEnv.enterStructure env structure, "")
228+
| None -> None)
229+
| _ -> (
230+
match ResolvePath.findInModule ~env declared.item path with
231+
| None -> None
232+
| Some res -> (
233+
match res with
234+
| `Local (env, name) -> Some (env, name)
235+
| `Global (moduleName, fullPath) -> (
236+
match ProcessCmt.fileForModule ~package moduleName with
237+
| None -> None
238+
| Some file ->
239+
ResolvePath.resolvePath ~env:(QueryEnv.fromFile file) ~path:fullPath
240+
~package))))
194241

195242
let resolveModuleWithOpens ~opens ~package ~moduleName =
196243
let rec loop opens =
@@ -219,12 +266,17 @@ let getEnvWithOpens ~scope ~(env : QueryEnv.t) ~package
219266
match resolvePathFromStamps ~env ~scope ~moduleName ~path ~package with
220267
| Some x -> Some x
221268
| None -> (
222-
match resolveModuleWithOpens ~opens ~package ~moduleName with
223-
| Some env -> ResolvePath.resolvePath ~env ~package ~path
224-
| None -> (
225-
match resolveFileModule ~moduleName ~package with
226-
| None -> None
227-
| Some env -> ResolvePath.resolvePath ~env ~package ~path))
269+
let env_opt =
270+
match resolveModuleWithOpens ~opens ~package ~moduleName with
271+
| Some envOpens -> Some envOpens
272+
| None -> resolveFileModule ~moduleName ~package
273+
in
274+
match env_opt with
275+
| None -> None
276+
| Some env -> (
277+
match path with
278+
| [""] -> Some (env, "")
279+
| _ -> ResolvePath.resolvePath ~env ~package ~path))
228280

229281
let rec expandTypeExpr ~env ~package typeExpr =
230282
match typeExpr |> Shared.digConstructor with
@@ -662,14 +714,47 @@ let getCompletionsForPath ~debug ~opens ~full ~pos ~exact ~scope
662714
localCompletionsWithOpens @ fileModules
663715
| moduleName :: path -> (
664716
Log.log ("Path " ^ pathToString path);
665-
match
666-
getEnvWithOpens ~scope ~env ~package:full.package ~opens ~moduleName path
667-
with
668-
| Some (env, prefix) ->
669-
Log.log "Got the env";
670-
let namesUsed = Hashtbl.create 10 in
671-
findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext
672-
| None -> [])
717+
(* [""] is trailing dot completion (`ModuleName.<com>`). *)
718+
match path with
719+
| [""] -> (
720+
let envFile = env in
721+
let declaredOpt =
722+
match findModuleInScope ~env:envFile ~moduleName ~scope with
723+
| Some d -> Some d
724+
| None -> (
725+
match Exported.find envFile.exported Exported.Module moduleName with
726+
| Some stamp -> Stamps.findModule envFile.file.stamps stamp
727+
| None -> None)
728+
in
729+
match declaredOpt with
730+
| Some (declared : Module.t Declared.t) when declared.isExported = false
731+
-> (
732+
match
733+
enterStructureFromDeclared ~env:envFile ~package:full.package declared
734+
with
735+
| None -> []
736+
| Some (envInModule, structure) ->
737+
completionsFromStructureItems ~env:envInModule structure)
738+
| _ -> (
739+
match
740+
getEnvWithOpens ~scope ~env ~package:full.package ~opens ~moduleName
741+
path
742+
with
743+
| Some (env, prefix) ->
744+
Log.log "Got the env";
745+
let namesUsed = Hashtbl.create 10 in
746+
findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext
747+
| None -> []))
748+
| _ -> (
749+
match
750+
getEnvWithOpens ~scope ~env ~package:full.package ~opens ~moduleName
751+
path
752+
with
753+
| Some (env, prefix) ->
754+
Log.log "Got the env";
755+
let namesUsed = Hashtbl.create 10 in
756+
findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext
757+
| None -> []))
673758

674759
(** Completions intended for piping, from a completion path. *)
675760
let completionsForPipeFromCompletionPath ~envCompletionIsMadeFrom ~opens ~pos

analysis/src/CompletionFrontEnd.ml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor
538538
p
539539
| Ppat_type _ -> ()
540540
| Ppat_unpack {txt; loc} ->
541-
scope :=
542-
!scope |> Scope.addValue ~name:txt ~loc ?contextPath:contextPathToSave
541+
scope := !scope |> Scope.addModule ~name:txt ~loc
543542
| Ppat_exception p -> scopePattern ~patternPath ?contextPath p
544543
| Ppat_extension _ -> ()
545544
| Ppat_open (_, p) -> scopePattern ~patternPath ?contextPath p

analysis/src/Hover.ml

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,16 +287,29 @@ let newHover ~full:{file; package} ~supportsMarkdownLinks locItem =
287287
| Const_int32 _ -> "int32"
288288
| Const_int64 _ -> "int64"
289289
| Const_bigint _ -> "bigint"))
290-
| Typed (_, t, locKind) ->
290+
| Typed (_, t, locKind) -> (
291291
let fromType ?docstring ?constructor typ =
292292
hoverWithExpandedTypes ~file ~package ~supportsMarkdownLinks ?docstring
293293
?constructor typ
294294
in
295-
Some
296-
(match References.definedForLoc ~file ~package locKind with
297-
| None -> t |> fromType
298-
| Some (docstring, res) -> (
299-
match res with
300-
| `Declared | `Field -> t |> fromType ~docstring
301-
| `Constructor constructor ->
302-
t |> fromType ~docstring:constructor.docstring ~constructor))
295+
(* Expand first-class modules to the underlying module type signature. *)
296+
let t = Shared.dig t in
297+
match t.desc with
298+
| Tpackage (path, _lids, _tys) -> (
299+
let env = QueryEnv.fromFile file in
300+
match ResolvePath.resolveModuleFromCompilerPath ~env ~package path with
301+
| None -> Some (fromType t)
302+
| Some (envForModule, Some declared) ->
303+
let name = Path.name path in
304+
showModule ~docstring:declared.docstring ~name ~file:envForModule.file
305+
~package (Some declared)
306+
| Some (_, None) -> Some (fromType t))
307+
| _ ->
308+
Some
309+
(match References.definedForLoc ~file ~package locKind with
310+
| None -> t |> fromType
311+
| Some (docstring, res) -> (
312+
match res with
313+
| `Declared | `Field -> t |> fromType ~docstring
314+
| `Constructor constructor ->
315+
t |> fromType ~docstring:constructor.docstring ~constructor)))

analysis/src/ProcessCmt.ml

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -431,30 +431,84 @@ let rec getModulePath mod_desc =
431431
| Tmod_constraint (expr, _typ, _constraint, _coercion) ->
432432
getModulePath expr.mod_desc
433433

434-
let rec forStructureItem ~env ~(exported : Exported.t) item =
434+
let rec forStructureItem ~(env : SharedTypes.Env.t) ~(exported : Exported.t)
435+
item =
435436
match item.Typedtree.str_desc with
436437
| Tstr_value (_isRec, bindings) ->
437438
let items = ref [] in
438439
let rec handlePattern attributes pat =
439440
match pat.Typedtree.pat_desc with
440441
| Tpat_var (ident, name)
441442
| Tpat_alias (_, ident, name) (* let x : t = ... *) ->
442-
let item = pat.pat_type in
443-
let declared =
444-
addDeclared ~name ~stamp:(Ident.binding_time ident) ~env
445-
~extent:pat.pat_loc ~item attributes
446-
(Exported.add exported Exported.Value)
447-
Stamps.addValue
443+
(* Detect first-class module unpack patterns and register them as modules. *)
444+
let unpack_loc_opt =
445+
match
446+
pat.pat_extra
447+
|> Utils.filterMap (function
448+
| Typedtree.Tpat_unpack, loc, _ -> Some loc
449+
| _ -> None)
450+
with
451+
| loc :: _ -> Some loc
452+
| [] -> None
448453
in
449-
items :=
450-
{
451-
Module.kind = Module.Value declared.item;
452-
name = declared.name.txt;
453-
docstring = declared.docstring;
454-
deprecated = declared.deprecated;
455-
loc = declared.extentLoc;
456-
}
457-
:: !items
454+
if unpack_loc_opt <> None then
455+
match (Shared.dig pat.pat_type).desc with
456+
| Tpackage (path, _, _) ->
457+
let declared =
458+
ProcessAttributes.newDeclared ~item:(Module.Ident path)
459+
~extent:(Option.get unpack_loc_opt)
460+
~name ~stamp:(Ident.binding_time ident) ~modulePath:NotVisible
461+
false attributes
462+
in
463+
Stamps.addModule env.stamps (Ident.binding_time ident) declared;
464+
items :=
465+
{
466+
Module.kind =
467+
Module
468+
{
469+
type_ = declared.item;
470+
isModuleType = isModuleType declared;
471+
};
472+
name = declared.name.txt;
473+
docstring = declared.docstring;
474+
deprecated = declared.deprecated;
475+
loc = declared.extentLoc;
476+
}
477+
:: !items
478+
| _ ->
479+
let item = pat.pat_type in
480+
let declared =
481+
addDeclared ~name ~stamp:(Ident.binding_time ident) ~env
482+
~extent:pat.pat_loc ~item attributes
483+
(Exported.add exported Exported.Value)
484+
Stamps.addValue
485+
in
486+
items :=
487+
{
488+
Module.kind = Module.Value declared.item;
489+
name = declared.name.txt;
490+
docstring = declared.docstring;
491+
deprecated = declared.deprecated;
492+
loc = declared.extentLoc;
493+
}
494+
:: !items
495+
else
496+
let item = pat.pat_type in
497+
let declared =
498+
addDeclared ~name ~stamp:(Ident.binding_time ident) ~env
499+
~extent:pat.pat_loc ~item attributes
500+
(Exported.add exported Exported.Value)
501+
Stamps.addValue
502+
in
503+
items :=
504+
{
505+
Module.kind = Module.Value declared.item;
506+
name = declared.name.txt;
507+
docstring = declared.docstring;
508+
deprecated = declared.deprecated;
509+
loc = declared.extentLoc;
510+
}
511+
:: !items
458512
| Tpat_tuple pats | Tpat_array pats | Tpat_construct (_, _, pats) ->
459513
pats |> List.iter (fun p -> handlePattern [] p)
460514
| Tpat_or (p, _, _) -> handlePattern [] p

analysis/src/ProcessExtra.ml

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,25 @@ let typ ~env ~extra (iter : Tast_iterator.iterator) (item : Typedtree.core_type)
358358

359359
let pat ~(file : File.t) ~env ~extra (iter : Tast_iterator.iterator)
360360
(pattern : Typedtree.pattern) =
361+
(* Detect first-class module unpack in a pattern and return the module path
362+
if present. Used to register a synthetic module declaration *)
363+
let unpacked_module_path_opt () =
364+
let has_unpack =
365+
match
366+
pattern.pat_extra
367+
|> List.filter_map (function
368+
| Typedtree.Tpat_unpack, _, _ -> Some ()
369+
| _ -> None)
370+
with
371+
| _ :: _ -> true
372+
| [] -> false
373+
in
374+
if not has_unpack then None
375+
else
376+
match (Shared.dig pattern.pat_type).desc with
377+
| Tpackage (path, _, _) -> Some path
378+
| _ -> None
379+
in
361380
let addForPattern stamp name =
362381
if Stamps.findValue file.stamps stamp = None then (
363382
let declared =
@@ -376,13 +395,27 @@ let pat ~(file : File.t) ~env ~extra (iter : Tast_iterator.iterator)
376395
addForRecord ~env ~extra ~recordType:pattern.pat_type items
377396
| Tpat_construct (lident, constructor, _) ->
378397
addForConstructor ~env ~extra pattern.pat_type lident constructor
379-
| Tpat_alias (_inner, ident, name) ->
398+
| Tpat_alias (_inner, ident, name) -> (
380399
let stamp = Ident.binding_time ident in
381-
addForPattern stamp name
382-
| Tpat_var (ident, name) ->
400+
match unpacked_module_path_opt () with
401+
| Some path ->
402+
let declared =
403+
ProcessAttributes.newDeclared ~item:(Module.Ident path) ~extent:name.loc
404+
~name ~stamp ~modulePath:NotVisible false pattern.pat_attributes
405+
in
406+
Stamps.addModule file.stamps stamp declared
407+
| None -> addForPattern stamp name)
408+
| Tpat_var (ident, name) -> (
383409
(* Log.log("Pattern " ++ name.txt); *)
384410
let stamp = Ident.binding_time ident in
385-
addForPattern stamp name
411+
match unpacked_module_path_opt () with
412+
| Some path ->
413+
let declared =
414+
ProcessAttributes.newDeclared ~item:(Module.Ident path) ~extent:name.loc
415+
~name ~stamp ~modulePath:NotVisible false pattern.pat_attributes
416+
in
417+
Stamps.addModule file.stamps stamp declared
418+
| None -> addForPattern stamp name)
386419
| _ -> ());
387420
Tast_iterator.default_iterator.pat iter pattern
388421

analysis/src/StructureUtils.ml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
open SharedTypes
2+
3+
let unique_items (structure : Module.structure) : Module.item list =
4+
let namesUsed = Hashtbl.create 10 in
5+
structure.items
6+
|> List.filter (fun (it : Module.item) ->
7+
if Hashtbl.mem namesUsed it.name then false
8+
else (
9+
Hashtbl.add namesUsed it.name ();
10+
true))

0 commit comments

Comments
 (0)