package merlin-lib

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file enclosing.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
(*
  This computes the "enclosing" ranges a given location. Those ranges do not
  necessarily correspond to a node in the AST.

  The ranges are purely syntactic, however the AST used here is the
  typedtree. This creates several issues, where some information has been lost
  during a translation phase. We worked around those limitations, but for the
  record, here is a (potentially non-exhaustive) list of things that would be
  improved by switching to the (non-preprocessed) parsetree instead:

   - [let%map ... in ...] could be recogized without having to recognize the
     rewritten expression

   - Parenthesized expression could recover their inner/outer locations without
     [node_loc_stack] (which we could get rid of)

   - Let bindings turned into match node in the typedtree would not need special
     handling.

   On the other hand, a "parsetree" version of [MBrowse] does not exist yet, and
   we use it to get the node at cursor, as well as some of its helper
   functions. So, reader, whenever the parsetree version get added, rewrite this
   module!
*)

open Std

let ( << ) (x : Location.t) (y : Location.t) =
  x.loc_end.pos_cnum <= y.loc_start.pos_cnum

let contains (loc1 : Location.t) (loc2 : Location.t) =
  if Location.is_none loc1 then false
  else
    loc1.loc_start.pos_cnum <= loc2.loc_start.pos_cnum
    && loc2.loc_end.pos_cnum <= loc1.loc_end.pos_cnum

let ( <= ) x y = contains y x

let merge (loc1 : Location.t) (loc2 : Location.t) =
  let resolve_non_pos (pos1 : Lexing.position) (pos2 : Lexing.position) f =
    if pos1.pos_cnum < 0 then pos2 else if pos2.pos_cnum < 0 then pos1 else f ()
  in
  let min_pos pos1 pos2 =
    resolve_non_pos pos1 pos2 @@ fun () ->
    if pos1.pos_cnum < pos2.pos_cnum then pos1 else pos2
  in
  let max_pos pos1 pos2 =
    resolve_non_pos pos1 pos2 @@ fun () ->
    if pos1.pos_cnum < pos2.pos_cnum then pos2 else pos1
  in
  let loc_start = min_pos loc1.loc_start loc2.loc_start in
  let loc_end = max_pos loc1.loc_end loc2.loc_end in
  (* I don't think loc_ghosts are a problem, as this is generated by Mbrowse
     module it should not contain any ghost location (apart from the initial
     [Location.none]). *)
  { Location.loc_start; loc_end; loc_ghost = false }

let ( ++ ) = merge

(* In some cases, we don't want to go "up" to a full node, but include new
   location little by little. For instance:

   {[
   let a = 1 in
   let b = 2 in
   let c = 3 in ...
   ]}

   We don't want to go go up from [2] to [let b = 2 in let c 3 in ...], we
   want intermediate stages that do not correspond to actual nodes:
   [let b = 2 in], then [let b = 2 in let c = 3 in], ...

   Similar example can be done with chains of [;] sequences, and other
   cases. All those cases share a similar setting: they look like a sequence (of
   [let ... in ...], of [;], etc) that are actually represented with binary
   operators, with "right" priority for parentheses.

   In all those cases, we split the location in the left part and the right part
   (using [merlin_loc] to split at the right point), include the left part
   first, and continue "going down-right".
*)

let rec go_down_right acc ~current_loc (expr : Typedtree.expression) =
  if Mbrowse.node_loc (Expression expr) <= current_loc then (current_loc, acc)
  else
    let go right_expr =
      (* The location without [begin ... end] or parenthesis delimiters. *)
      let undelimited_loc =
        match Mbrowse.node_loc_stack (Expression expr) with
        | [] -> Some (Mbrowse.node_loc (Expression expr))
        | innermost :: _ ->
          (* If we are in a delimited environment, and we don't come from below,
             we don't want to continue. Example, where brackets are current_loc
             and curlies are innermost:

             {[
               [let x = 1 in
               {let y = 2 in]
                x + y}
             ]}*)
          if current_loc <= innermost then Some innermost else None
      in
      match undelimited_loc with
      | None -> (current_loc, acc)
      | Some full_loc ->
        let left_loc =
          let right_merlin_loc =
            Mbrowse.node_merlin_loc (Expression right_expr)
          in
          { full_loc with loc_end = right_merlin_loc.loc_start }
        in
        let current_loc = current_loc ++ left_loc in
        let acc = current_loc :: acc in
        let is_splittable =
          Mbrowse.node_loc_stack (Expression right_expr) |> List.is_empty
        in
        if not is_splittable then (current_loc, acc)
        else go_down_right acc ~current_loc right_expr
    in
    match expr with
    (* [let x = exp1 in][ exp2] *)
    | { exp_desc = Texp_let (_, _, right_expr); _ } -> go right_expr
    (* [exp1 ;][ exp2] *)
    | { exp_desc = Texp_sequence (_, right_expr); _ } -> go right_expr
    (* [let%map x = exp1 in][ exp2] *)
    | { exp_desc =
          Texp_apply
            ( _map_or_bind,
              [ (Nolabel, Arg value_binded);
                ( Labelled "f",
                  Arg
                    { exp_desc =
                        Texp_function
                          ( [ { fp_kind = Tparam_pat pat; _ } ],
                            Tfunction_body right_expr );
                      _
                    } )
              ] );
        _
      }
      when pat.pat_loc << value_binded.exp_loc
           && value_binded.exp_loc << right_expr.exp_loc -> go right_expr
    (* [let () = exp1 in][ exp2] *)
    | { exp_desc =
          Texp_match
            ( exp,
              [ { c_lhs; c_rhs = right_expr; c_guard = None; c_cont = _ } ],
              [],
              _ );
        _
      }
      when c_lhs.pat_loc << exp.exp_loc -> go right_expr
    (* [let* x = exp1 in][ exp2] *)
    | { exp_desc = Texp_letop { body = { c_rhs = right_expr; _ }; _ }; _ } ->
      go right_expr
    | _ -> (current_loc, acc)

let add_intermediate_locs (current_loc, acc) locs =
  List.fold_left
    ~f:(fun (current_loc, acc) loc ->
      let current_loc = current_loc ++ loc in
      (current_loc, current_loc :: acc))
    ~init:(current_loc, acc) locs

let with_extra ~current_loc acc extra node =
  let loc_stack = Mbrowse.node_loc_stack node in
  let node_loc = Mbrowse.node_loc node in
  let node_locs = loc_stack @ [ node_loc ] in
  (* Exclude locations not fully containing current_loc. This is to avoid when
       the cursor is eg on a closing parenthesis, including the innermost
       location would not take the closing parenthesis. *)
  let node_locs = List.filter ~f:(fun loc -> current_loc <= loc) node_locs in
  let acc = add_intermediate_locs (current_loc, acc) node_locs in
  add_intermediate_locs acc (List.rev_map ~f:(fun (_, loc, _) -> loc) extra)

let expr_locs acc ~current_loc (exp : Typedtree.expression) =
  match exp with
  | { exp_extra; _ } -> with_extra ~current_loc acc exp_extra (Expression exp)

let pat_locs acc ~current_loc pat =
  match pat with
  | { Typedtree.pat_extra; _ } ->
    with_extra ~current_loc acc pat_extra (Pattern pat)

let typ_locs acc ~current_loc typ =
  let loc_stack =
    Mbrowse.node_loc_stack (Core_type typ)
    |> List.filter ~f:(fun loc -> current_loc <= loc)
  in
  let typ_loc = Mbrowse.node_loc (Core_type typ) in
  add_intermediate_locs (current_loc, acc) (loc_stack @ [ typ_loc ])

let rec expand_node acc ~current_loc (nodes : Browse_raw.node list) =
  match nodes with
  | [] -> List.rev acc
  (* Skipping node already included *)
  | node :: nodes when Mbrowse.node_loc node <= current_loc ->
    expand_node acc ~current_loc nodes
  (* skipping "[let x = 5] in exp2" location.
     TODO: would we want to add "let [x = 5] in exp2"? *)
  | Value_binding _ :: (Expression _ :: _ as nodes) ->
    expand_node acc ~current_loc nodes
  | Expression exp :: nodes ->
    let current_loc, acc = go_down_right acc ~current_loc exp in
    let current_loc, acc = expr_locs acc ~current_loc exp in
    expand_node acc ~current_loc nodes
  | Pattern pat :: nodes ->
    let current_loc, acc = pat_locs acc ~current_loc pat in
    expand_node acc ~current_loc nodes
  | Core_type typ :: nodes ->
    let current_loc, acc = typ_locs acc ~current_loc typ in
    expand_node acc ~current_loc nodes
  | node :: q ->
    let loc = Mbrowse.node_loc node in
    let current_loc = current_loc ++ loc in
    let acc = current_loc :: acc in
    expand_node acc ~current_loc q

let locs current_loc (mbrowse : Mbrowse.t) =
  mbrowse |> List.map ~f:snd
  |> expand_node [] ~current_loc
  |> List.fold_left ~init:[] ~f:(fun acc loc ->
      match acc with
      | hd :: _ as acc when Location_aux.compare hd loc = 0 -> acc
      | _ -> loc :: acc)
  |> List.rev