Commit
+107 -45 +/-4 browse
1 | diff --git a/bin/note.ml b/bin/note.ml |
2 | index 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 |
129 | index 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 |
156 | index 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 |
183 | index 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 | ] |