Commit
+111 -88 +/-3 browse
1 | diff --git a/lib/cmd.ml b/lib/cmd.ml |
2 | index e2d2843..c402032 100644 |
3 | --- a/lib/cmd.ml |
4 | +++ b/lib/cmd.ml |
5 | @@ -9,18 +9,20 @@ let get_notes = |
6 | Note.of_string ~content slug) |
7 | (Slug.load cfg.state_dir) |
8 | |
9 | - let filter_arg = |
10 | + let to_keys ~kind notes = |
11 | + match kind with |
12 | + | `Title -> List.map ~f:Note.get_title notes |
13 | + | `Tags -> List.concat (List.map ~f:Note.get_tags notes) |
14 | + |
15 | + let search_arg kind = |
16 | Command.Arg_type.create |
17 | ~complete:(fun _ ~part -> |
18 | let notes = get_notes in |
19 | List.filter_map |
20 | - ~f:(fun note -> |
21 | - let title = Note.get_title note in |
22 | - if String.equal part "" then Some title |
23 | - else if String.is_substring ~substring:part title then Some title |
24 | - else None) |
25 | - notes) |
26 | - (fun filter -> filter) |
27 | + ~f:(fun key -> |
28 | + if String.is_substring ~substring:part key then Some key else None) |
29 | + (to_keys ~kind notes)) |
30 | + (fun filter -> Re.Str.regexp filter) |
31 | |
32 | let key_arg = |
33 | Command.Arg_type.create |
34 | @@ -31,6 +33,8 @@ let key_arg = |
35 | string_keys) |
36 | Config.Key.of_string |
37 | |
38 | + let flag_to_op state = match state with true -> Note.And | false -> Note.Or |
39 | + |
40 | let column_list_arg = |
41 | Command.Arg_type.create (fun value -> |
42 | List.map ~f:Config.Column.of_string (String.split ~on:',' value)) |
43 | @@ -57,6 +61,20 @@ let list_style_arg = |
44 | string_keys) |
45 | Config.ListStyle.of_string |
46 | |
47 | + let filter_args = |
48 | + let open Command.Let_syntax in |
49 | + [%map_open |
50 | + let title = |
51 | + flag "title" |
52 | + (optional (search_arg `Title)) |
53 | + ~doc:"regular expression matching the note title" |
54 | + and tags = |
55 | + flag "tag" |
56 | + (listed (search_arg `Tags)) |
57 | + ~doc:"sequence of regular expressions matching note tags" |
58 | + and operator = flag "and" no_arg ~doc:"logical AND instead of default OR" in |
59 | + (title, tags, operator)] |
60 | + |
61 | (* |
62 | * commands |
63 | *) |
64 | @@ -71,15 +89,18 @@ note to stdout as plain text however the encoding can be adjusted to yaml or |
65 | json for consumption by other tools. |
66 | |}) |
67 | [%map_open |
68 | - let filter_args = anon (sequence ("filter" %: filter_arg)) |
69 | + let title, tags, operator = filter_args |
70 | and encoding = |
71 | flag "encoding" |
72 | (optional_with_default cfg.encoding encoding_arg) |
73 | ~doc:"format [json | yaml | raw] (default: raw)" |
74 | in |
75 | fun () -> |
76 | - let open Note.Search in |
77 | - let notes = find_many ~args:filter_args get_notes in |
78 | + let notes = |
79 | + Note.find_many |
80 | + ~term:{ title; tags; operator = flag_to_op operator } |
81 | + get_notes |
82 | + in |
83 | List.iter |
84 | ~f:(fun note -> |
85 | print_endline (Note.Encoding.to_string ~style:encoding note)) |
86 | @@ -142,11 +163,14 @@ let delete_note = |
87 | Delete the first note that matches the filter criteria. |
88 | |}) |
89 | [%map_open |
90 | - let filter_args = anon (sequence ("filter" %: filter_arg)) in |
91 | + let title, tags, operator = filter_args in |
92 | fun () -> |
93 | - let open Note.Search in |
94 | let notes = get_notes in |
95 | - let note = find_one ~args:filter_args notes in |
96 | + let note = |
97 | + Note.find_one |
98 | + ~term:{ title; tags; operator = flag_to_op operator } |
99 | + notes |
100 | + in |
101 | match note with |
102 | | Some note -> |
103 | Io.delete ~callback:cfg.on_modification ~title:(Note.get_title note) |
104 | @@ -161,10 +185,13 @@ let edit_note = |
105 | Select a note that matches the filter criteria and open it in your text editor. |
106 | |}) |
107 | [%map_open |
108 | - let filter_args = anon (sequence ("filter" %: filter_arg)) in |
109 | + let title, tags, operator = filter_args in |
110 | fun () -> |
111 | - let open Note.Search in |
112 | - let note = find_one ~args:filter_args get_notes in |
113 | + let note = |
114 | + Note.find_one |
115 | + ~term:{ title; tags; operator = flag_to_op operator } |
116 | + get_notes |
117 | + in |
118 | match note with |
119 | | Some note -> |
120 | Io.edit ~callback:cfg.on_modification ~editor:cfg.editor |
121 | @@ -180,7 +207,7 @@ List one or more notes that match the filter criteria, if no filter criteria |
122 | is provided then all notes will be listed. |
123 | |}) |
124 | [%map_open |
125 | - let filter_args = anon (sequence ("filter" %: filter_arg)) |
126 | + let title, tags, operator = filter_args |
127 | and style = |
128 | flag "style" |
129 | (optional_with_default cfg.list_style list_style_arg) |
130 | @@ -191,8 +218,11 @@ is provided then all notes will be listed. |
131 | ~doc:"columns to include in output" |
132 | in |
133 | fun () -> |
134 | - let open Note.Search in |
135 | - let notes = find_many ~args:filter_args get_notes in |
136 | + let notes = |
137 | + Note.find_many |
138 | + ~term:{ title; tags; operator = flag_to_op operator } |
139 | + get_notes |
140 | + in |
141 | let styles = cfg.styles in |
142 | let cells = Note.to_cells ~columns ~styles notes in |
143 | Display.to_stdout ~style cells] |
144 | diff --git a/lib/note.ml b/lib/note.ml |
145 | index 7ca26a1..4b0e286 100644 |
146 | --- a/lib/note.ml |
147 | +++ b/lib/note.ml |
148 | @@ -1,11 +1,19 @@ |
149 | open Core |
150 | |
151 | - type t = { |
152 | + type operator = And | Or |
153 | + |
154 | + and term = { |
155 | + title : Re.Str.regexp option; |
156 | + tags : Re.Str.regexp list; |
157 | + operator : operator; |
158 | + } |
159 | + |
160 | + and note = { |
161 | frontmatter : Ezjsonm.t; |
162 | content : string; |
163 | markdown : Omd.doc; |
164 | slug : Slug.t; |
165 | - parent : Slug.t option; |
166 | + parent : term option; |
167 | } |
168 | |
169 | let build ?(description = "") ?(tags = []) ?(content = "") ~title slug = |
170 | @@ -133,61 +141,39 @@ module Encoding = struct |
171 | | `Html -> Omd.to_html t.markdown |
172 | end |
173 | |
174 | - module Search = struct |
175 | - open Re.Str |
176 | - |
177 | - let dump_results results = |
178 | - List.iter |
179 | - ~f:(fun result -> |
180 | - print_endline (sprintf "%s - %d" (get_title (snd result)) (fst result))) |
181 | - results |
182 | - |
183 | - let title expr note = |
184 | - let title_string = get_title note in |
185 | - string_match expr title_string 0 |
186 | - |
187 | - let tags expr note = |
188 | - let tags = get_tags note in |
189 | - List.count ~f:(fun tag -> string_match expr tag 0) tags > 0 |
190 | - |
191 | - let content expr note = |
192 | - let words = Util.to_words [] note.markdown in |
193 | - List.count ~f:(fun word -> string_match expr word 0) words > 0 |
194 | - |
195 | - let match_and_rank ~args notes = |
196 | - let expressions = List.map ~f:regexp args in |
197 | - let matches = |
198 | - List.fold ~init:[] |
199 | - ~f:(fun accm note -> |
200 | - let has_title = |
201 | - List.count ~f:(fun expr -> title expr note) expressions > 0 |
202 | - in |
203 | - let has_tag = |
204 | - List.count ~f:(fun expr -> tags expr note) expressions > 0 |
205 | - in |
206 | - let has_content = |
207 | - List.count ~f:(fun expr -> content expr note) expressions > 0 |
208 | + let find_many ~term notes = |
209 | + let open Re.Str in |
210 | + if Option.is_none term.title && List.length term.tags = 0 then notes |
211 | + else |
212 | + List.filter |
213 | + ~f:(fun note -> |
214 | + let has_title = |
215 | + match term.title with |
216 | + | Some title -> string_match title (get_title note) 0 |
217 | + | None -> false |
218 | + in |
219 | + let has_title = Option.is_none term.title || has_title in |
220 | + let has_tags = |
221 | + let result = |
222 | + List.find |
223 | + ~f:(fun expr -> |
224 | + Option.is_some |
225 | + (List.find |
226 | + ~f:(fun tag -> string_match expr tag 0) |
227 | + (get_tags note))) |
228 | + term.tags |
229 | in |
230 | - match (has_title, has_tag, has_content) with |
231 | - | true, _, _ -> List.append accm [ (20, note) ] |
232 | - | _, true, _ -> List.append accm [ (10, note) ] |
233 | - | _, _, true -> List.append accm [ (5, note) ] |
234 | - | false, false, false -> accm) |
235 | - notes |
236 | - in |
237 | - List.rev (List.sort ~compare:(fun n1 n2 -> fst n1 - fst n2) matches) |
238 | - |
239 | - let find_one ~args notes = |
240 | - let results = match_and_rank ~args notes in |
241 | - let results = List.map ~f:snd results in |
242 | - if List.length results = 0 then None else Some (List.hd_exn results) |
243 | - |
244 | - let find_many ~args notes = |
245 | - if List.length args = 0 then notes |
246 | - else |
247 | - let results = match_and_rank ~args notes in |
248 | - List.map ~f:snd results |
249 | - end |
250 | + Option.is_some result |
251 | + in |
252 | + let has_tags = List.length term.tags = 0 || has_tags in |
253 | + match term.operator with |
254 | + | Or -> has_title || has_tags |
255 | + | And -> has_title && has_tags) |
256 | + notes |
257 | + |
258 | + let find_one ~term notes = |
259 | + let results = find_many ~term notes in |
260 | + match List.length results with 0 -> None | _ -> Some (List.hd_exn results) |
261 | |
262 | open ANSITerminal |
263 | |
264 | diff --git a/test/search.ml b/test/search.ml |
265 | index 5a47765..e7cb68a 100644 |
266 | --- a/test/search.ml |
267 | +++ b/test/search.ml |
268 | @@ -11,31 +11,38 @@ let make_notes = |
269 | ~title:"Another Very Important Note" (Slug.next state_dir) |
270 | in |
271 | let note_3 = |
272 | - Note.build ~tags:[ "fuu"; "bar"; "baz" ] ~content:"" |
273 | - ~title:"fuu" (Slug.next state_dir) |
274 | + Note.build ~tags:[ "fuu"; "bar"; "baz" ] ~content:"" ~title:"fuu" |
275 | + (Slug.next state_dir) |
276 | in |
277 | - [note_1 ; note_2 ; note_3] |
278 | - |
279 | + [ note_1; note_2; note_3 ] |
280 | |
281 | let test_filter_by_keys = |
282 | let notes = make_notes in |
283 | let result = |
284 | - Note.Search.find_many ~args:[ "fuu"; "bar"; "baz" ] |
285 | - notes |
286 | + Note.find_many |
287 | + ~term: |
288 | + { |
289 | + title = None; |
290 | + tags = |
291 | + [ Re.Str.regexp "fuu"; Re.Str.regexp "bar"; Re.Str.regexp "baz" ]; |
292 | + operator = Note.Or; |
293 | + } |
294 | + notes |
295 | in |
296 | assert (List.length result = 3) |
297 | |
298 | let test_filter_by_title_find_one = |
299 | let notes = make_notes in |
300 | let result = |
301 | - Note.Search.find_one ~args:[ "fuu" ] |
302 | - notes |
303 | + Note.find_one |
304 | + ~term:{ title = None; tags = [ Re.Str.regexp "fuu" ]; operator = Note.Or } |
305 | + notes |
306 | in |
307 | - assert (Option.is_some result) ; |
308 | + assert (Option.is_some result); |
309 | let note = Option.get result in |
310 | (* title should take priority *) |
311 | - assert ((Note.get_title note) = "fuu") |
312 | + assert (Note.get_title note = "fuu") |
313 | |
314 | - let () = |
315 | + let () = |
316 | test_filter_by_keys; |
317 | - test_filter_by_title_find_one; |
318 | + test_filter_by_title_find_one |