Author: Kevin Schoon [kevinschoon@gmail.com]
Hash: e8de5684659e65094a3582d55ce1ebba6845f49e
Timestamp: Fri, 02 Jul 2021 23:47:52 +0000 (3 years ago)

+107 -45 +/-4 browse
helper functions for autocomplete
1diff --git a/bin/note.ml b/bin/note.ml
2index eb988cf..cf884f0 100644
3--- a/bin/note.ml
4+++ b/bin/note.ml
5 @@ -12,33 +12,28 @@ let options : Note.options =
6 editor = cfg.editor;
7 }
8
9- let get_title (note : Note.t) = (note |> Note.frontmatter).path
10-
11- let get_tags (note : Note.t) = (note |> Note.frontmatter).tags
12-
13- let to_keys ~kind notes =
14- match kind with
15- | `Title -> List.map ~f:get_title notes
16- | `Tags -> List.concat (List.map ~f:get_tags notes)
17-
18- let name_arg =
19- Command.Arg_type.create
20- ~complete:(fun _ ~part -> [ part ])
21- (fun filter -> filter)
22-
23- let tag_arg =
24- Command.Arg_type.create
25- ~complete:(fun _ ~part -> [ part ])
26- (fun filter -> filter)
27-
28- let key_arg =
29- Command.Arg_type.create
30- ~complete:(fun _ ~part ->
31- let string_keys = List.map ~f:Config.Key.to_string Config.Key.all in
32- List.filter
33- ~f:(fun key -> String.is_substring ~substring:part key)
34- string_keys)
35- Config.Key.of_string
36+ module Args = struct
37+ let path =
38+ Command.Arg_type.create
39+ ~complete:(fun _ ~part ->
40+ options |> Note.Completion.suggest_paths ~hint:part)
41+ (fun filter -> filter)
42+
43+ let tag =
44+ Command.Arg_type.create
45+ ~complete:(fun _ ~part ->
46+ options |> Note.Completion.suggest_tags ~hint:part)
47+ (fun filter -> filter)
48+
49+ let config_key =
50+ Command.Arg_type.create
51+ ~complete:(fun _ ~part ->
52+ let string_keys = List.map ~f:Config.Key.to_string Config.Key.all in
53+ List.filter
54+ ~f:(fun key -> String.is_substring ~substring:part key)
55+ string_keys)
56+ Config.Key.of_string
57+ end
58
59 (*
60 * commands
61 @@ -52,14 +47,15 @@ let config_get =
62 let open Command.Let_syntax in
63 Command.basic ~summary:"get a config value"
64 [%map_open
65- let key = anon ("key" %: key_arg) in
66+ let key = anon ("key" %: Args.config_key) in
67 fun () -> print_endline (Config.get cfg key)]
68
69 let config_set =
70 let open Command.Let_syntax in
71 Command.basic ~summary:"set a config value"
72 [%map_open
73- let key = anon ("key" %: key_arg) and value = anon ("value" %: string) in
74+ let key = anon ("key" %: Args.config_key)
75+ and value = anon ("value" %: string) in
76 fun () ->
77 let cfg = Config.set cfg key value in
78 Config.save cfg]
79 @@ -73,7 +69,7 @@ List one or more notes that match the filter criteria, if no filter criteria
80 is provided then all notes will be listed.
81 |})
82 [%map_open
83- let paths = anon (sequence ("path" %: string)) in
84+ let paths = anon (sequence ("path" %: Args.path)) in
85 fun () ->
86 let paths = match paths with [] -> [ "/" ] | paths -> paths in
87 paths
88 @@ -94,8 +90,8 @@ on_modification callback will be invoked if the file is committed to disk.
89 let stdin =
90 flag "stdin" (optional bool)
91 ~doc:"read content from stdin and copy it into the note body"
92- and path = anon ("path" %: name_arg)
93- and tags = flag "tag" (listed tag_arg) ~doc:"tag"
94+ and path = anon ("path" %: Args.path)
95+ and tags = flag "tag" (listed Args.tag) ~doc:"tag"
96 and description =
97 flag "description" (optional string) ~doc:"description"
98 in
99 @@ -112,7 +108,7 @@ let remove_note =
100 Command.basic ~summary:"remove an existing note"
101 ~readme:(fun () -> {||})
102 [%map_open
103- let path = anon ("path" %: name_arg) in
104+ let path = anon ("path" %: Args.path) in
105 fun () ->
106 let message =
107 Format.sprintf "Are you sure you want to delete note %s?" path
108 @@ -131,8 +127,8 @@ let edit_note =
109 Select a note that matches the filter criteria and open it in your text editor.
110 |})
111 [%map_open
112- let path = anon ("path" %: name_arg)
113- and _ = flag "tag" (listed tag_arg) ~doc:"tag"
114+ let path = anon ("path" %: Args.path)
115+ and _ = flag "tag" (listed Args.tag) ~doc:"tag"
116 and _ =
117 flag "description" (optional_with_default "" string) ~doc:"description"
118 in
119 @@ -147,7 +143,7 @@ List one or more notes that match the filter criteria, if no filter criteria
120 is provided then all notes will be listed.
121 |})
122 [%map_open
123- let paths = anon (sequence ("path" %: string)) in
124+ let paths = anon (sequence ("path" %: Args.path)) in
125 fun () ->
126 let paths = match paths with [] -> [ "/" ] | paths -> paths in
127 paths
128 diff --git a/lib/note.ml b/lib/note.ml
129index 831ea96..dd75a78 100644
130--- a/lib/note.ml
131+++ b/lib/note.ml
132 @@ -233,3 +233,22 @@ module Adapter = struct
133 end
134
135 include Adapter
136+
137+ module Completion = struct
138+ let suggest_paths ~hint options =
139+ options.state_dir |> Manifest.load_or_init
140+ |> Manifest.list ~path:(hint |> Filename.dirname)
141+ |> List.map ~f:(fun item -> item.path)
142+ |> List.filter ~f:(fun path -> path |> String.is_substring ~substring:hint)
143+
144+ let suggest_tags ~hint options =
145+ let manifest = options.state_dir |> Manifest.load_or_init in
146+ manifest.items
147+ |> List.concat_map ~f:(fun item ->
148+ let frontmatter =
149+ item.slug |> Slug.to_string |> In_channel.read_all |> of_string
150+ |> frontmatter
151+ in
152+ frontmatter.tags)
153+ |> List.filter ~f:(fun tag -> tag |> String.is_substring ~substring:hint)
154+ end
155 diff --git a/lib/note.mli b/lib/note.mli
156index 978ef91..6b680bc 100644
157--- a/lib/note.mli
158+++ b/lib/note.mli
159 @@ -22,8 +22,8 @@ val to_json : t -> Ezjsonm.value
160 (* get a note as json data with structured data extracted from it *)
161
162 val frontmatter : t -> Frontmatter.t
163-
164 (* get decoded frontmatter structure *)
165+
166 val content : t -> string
167 (* get the raw text content without frontmatter heading *)
168
169 @@ -60,3 +60,12 @@ val remove : path:string -> options -> unit
170
171 val edit : path:string -> options -> unit
172 (* edit an existing note opening it in the configured editor *)
173+
174+ (* helper functions for autocomplete *)
175+ module Completion : sig
176+ val suggest_paths : hint:string -> options -> string list
177+ (* suggest paths for autocomplete *)
178+
179+ val suggest_tags : hint:string -> options -> string list
180+ (* suggest tags for autocomplete *)
181+ end
182 diff --git a/test/note_test.ml b/test/note_test.ml
183index e2d5857..ea4270e 100644
184--- a/test/note_test.ml
185+++ b/test/note_test.ml
186 @@ -2,23 +2,24 @@ open Core
187 open Note_lib
188
189 let parsing () =
190- let n1 = {|
191+ let n1 =
192+ {|
193 ---
194 path: /fuu
195 tags: ["bar"]
196 description: "baz"
197 ---
198- # Hello World|} in
199+ # Hello World|}
200+ in
201 let n1 = n1 |> Note.of_string in
202- Alcotest.(check string) "path" "/fuu" (n1 |> Note.frontmatter).path ;
203+ Alcotest.(check string) "path" "/fuu" (n1 |> Note.frontmatter).path;
204 let tag = (n1 |> Note.frontmatter).tags |> List.hd_exn in
205- Alcotest.(check string) "tag" "bar" tag ;
206- let description = (Option.value_exn (n1 |> Note.frontmatter).description) in
207- Alcotest.(check string) "description" "baz" description ;
208- let content = (n1 |> Note.content) in
209+ Alcotest.(check string) "tag" "bar" tag;
210+ let description = Option.value_exn (n1 |> Note.frontmatter).description in
211+ Alcotest.(check string) "description" "baz" description;
212+ let content = n1 |> Note.content in
213 Alcotest.(check string) "content" "\n# Hello World" content
214
215-
216 let adapter () =
217 let options : Note.options =
218 {
219 @@ -42,9 +43,46 @@ let adapter () =
220 "note removed" 1
221 (tree |> Note.flatten ~accm:[] |> List.length)
222
223+ let suggest_path () =
224+ let options : Note.options =
225+ {
226+ state_dir = Filename.temp_dir "note-test" "";
227+ editor = "true";
228+ on_modification = None;
229+ }
230+ in
231+ options |> Note.create ~content:(Some "fuu") ~path:"/fuu";
232+ options |> Note.create ~content:(Some "bar") ~path:"/fuu/bar";
233+ options |> Note.create ~content:(Some "baz") ~path:"/fuu/baz";
234+ let suggestions = options |> Note.Completion.suggest_paths ~hint:"/f" in
235+ let result = List.nth_exn suggestions 0 in
236+ Alcotest.(check string) "suggestion" "/fuu" result;
237+ let suggestions = options |> Note.Completion.suggest_paths ~hint:"/fuu/b" in
238+ let result = List.nth_exn suggestions 0 in
239+ Alcotest.(check string) "suggestion" "/fuu/baz" result;
240+ let result = List.nth_exn suggestions 1 in
241+ Alcotest.(check string) "suggestion" "/fuu/bar" result
242+
243+ let suggest_tags () =
244+ let options : Note.options =
245+ {
246+ state_dir = Filename.temp_dir "note-test" "";
247+ editor = "true";
248+ on_modification = None;
249+ }
250+ in
251+ options |> Note.create ~tags:[ "aa"; "bb" ] ~content:(Some "") ~path:"/fuu";
252+ options |> Note.create ~tags:[ "cc"; "dd" ] ~content:(Some "") ~path:"/bar";
253+ let result = options |> Note.Completion.suggest_tags ~hint:"a" in
254+ Alcotest.(check string) "tag aa" "aa" (List.nth_exn result 0)
255+
256 let () =
257 Alcotest.run "Note"
258 [
259 ("parse", [ Alcotest.test_case "parse" `Quick parsing ]);
260 ("adapter", [ Alcotest.test_case "adapter" `Quick adapter ]);
261+ ( "path_suggestion",
262+ [ Alcotest.test_case "suggest path" `Quick suggest_path ] );
263+ ( "tag_suggestion",
264+ [ Alcotest.test_case "suggest tags" `Quick suggest_tags ] );
265 ]