/
completions.lisp
326 lines (289 loc) · 12 KB
/
completions.lisp
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
;;;; completions.lisp ---
;;;
;;; Filename: completions.lisp
;;; Description: Completions for the stumpwm IDO module
;;; Author: Alessandro Piras
;;; Maintainer:
;;; Created: Tue Apr 10 16:24:32 2012 (+0200)
;;; Last-Updated: Mon Apr 16 14:04:41 2012 (+0200)
;;; By: Alessandro Piras
;;; Update #: 4
;;; URL:
;;; Keywords:
;;; Compatibility:
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;; Commentary:
;;; Completions Sets for stumpwm IDO module.
;;;
;;; The sets are implemented as `completions-set' structures (csets).
;;; These structures have three fields:
;;; SET: A data structure representing a set of values from which to
;;; choose the matches, or a function that takes an input string
;;; and a cursor position and returns such a set.
;;; FILTER-FN: A function that, given the input string, the cursor
;;; position, the set data structure stored in or returned
;;; by the SET field and a MATCH-FN predicate and returns a
;;; list containing all the elements in set that match the
;;; input string according to the MATCH-FN predicate.
;;; SORT-FN: A function that takes the result of FILTER-FN and sorts
;;; them. Defaults to `identity'.
;;;
;;; Input Matchers: The MATCH-FN predicates (defined in matchers.lisp)
;;; An input matcher is a function that takes the input string, the
;;; cursor position and a pstring, and returns non nil if the pstring
;;; matches the input string, nil otherwise.
;;;
;;; This module offers some helpers to easily create csets for some
;;; common cases:
;;;
;;; 1. The cset is a sequence, or a function that returns one:
;;; this sets can be created using `sequence-cset'.
;;; 2. The cset is the union of other csets:
;;; this set can be created using `cset-union'.
;;; 3. The cset has the same values as another completions set, but
;;; the input should have some prefix string to trigger the
;;; completion: this set can be created using `prefix-cset'.
;;;
;;; Example: creating a cset *MYSET* that completes comma-prefixed
;;; stumpwm commands or shell commands:
;;; Let *commands-set* be the cset of command completions, and
;;; *shell-commands-set* be the cset of shell commands.
;;; We can create the set *MYSET* as follows:
;;; (defparameter *myset* (cset-union (prefix-cset *commands-set*)
;;; *shell-commands-set*))
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;; Change Log:
;;;
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;; This program is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU General Public License as
;;; published by the Free Software Foundation; either version 3, or
;;; (at your option) any later version.
;;;
;;; This program is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;;; General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with this program; see the file COPYING. If not, write to
;;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;;; Floor, Boston, MA 02110-1301, USA.
;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;
;;;; Code:
;;; TODO: add documentation
(in-package :stumpwm-ido)
(declaim (optimize (speed 0) (debug 3)))
;;; Set of completions
(defstruct (completions-set (:conc-name cset-))
filter-fn ; (filter-fn match-fn input-string cursor-position set-data)
(sort-fn #'identity) ; (sort-fn input-string cursor-position completions-list)
(transform-input-fn #'identity) ; (transform-input-fn input-string cursor-position set-data)
(expand-completion-fn #'pstr:pstring-string)
set)
(defun ido-unsorted-input-completions-1 (input-string cursor-position cset match-fn)
"Returns the completions in COMPLETIONS-SET matching INPUT-STRING
and CURSOR-POSITION according to the string matching function
MATCH-FN."
(funcall (cset-filter-fn cset)
match-fn
(funcall (cset-transform-input-fn cset) input-string)
cursor-position
(let ((set (cset-set cset)))
(typecase set
(function (funcall set input-string cursor-position))
(t set)))))
(defun ido-unsorted-input-completions (input-string cursor-position cset match-fn-or-list)
(typecase match-fn-or-list
(function (ido-unsorted-input-completions-1 input-string cursor-position cset match-fn-or-list))
(list (case (first match-fn-or-list)
;; Try the matching rules in the list until at least
;; one match is found, and return all the matches in the
;; set according to that matching function.
(:or (or (ido-unsorted-input-completions input-string cursor-position
cset
(second match-fn-or-list))
(when (first (cddr match-fn-or-list))
(ido-unsorted-input-completions input-string cursor-position
cset
(cons :or (cddr match-fn-or-list))))))
;; Return the completions matching all the matching rules
;; specified.
(:and (reduce (lambda (s1 s2) (intersection s1 s2 :test #'pstr:pstring-equal))
(mapcar (lambda (match-rule)
(ido-unsorted-input-completions input-string cursor-position
cset
match-rule))
(cdr match-fn-or-list))))
;; Return the completions matching according to any of the
;; matching rules specified. The matches for the first
;; predicate will appear first in the completion list.
(:union (remove-duplicates (mappend (lambda (match-fn)
(ido-unsorted-input-completions input-string
cursor-position
cset
match-fn))
(cdr match-fn-or-list)))
:test #'pstr:pstring-equal
:from-end t)))))
(defun ido-input-completions (input-string cursor-position cset match-fn-or-list)
(let* ((completions (funcall (cset-sort-fn cset)
(ido-unsorted-input-completions input-string cursor-position
cset
match-fn-or-list)))
(item (find input-string completions :key #'pstr:pstring-string :test #'string-equal)))
(if item (cons item (remove item completions))
completions)))
;;;; Some helpers for completions set definition
;;; Filter function for sets expressed as sequences
(defun sequence-filter (match-fn input-string cursor-position sequence)
(remove-if-not (curry match-fn input-string cursor-position)
sequence))
(defun sequence-cset (sequence-or-function)
"Takes a sequence or a function that returns one, and returns a
completion set."
(make-completions-set :filter-fn #'sequence-filter
:set sequence-or-function))
;;; Prefix input: switch completion set using prefixes
(defun prefix-input-string (plen input-string include-prefix-p)
(if include-prefix-p
input-string
(subseq input-string plen)))
(defun prefix-cursor-pos (plen cursor-pos include-prefix-p)
(if include-prefix-p
cursor-pos
(- cursor-pos plen)))
(defun prefix-filter (prefix cset include-prefix-p)
(let ((plen (length prefix))
(filter-fn (cset-filter-fn cset)))
(lambda (match-fn input-string cursor-position set)
(let ((p (search prefix input-string)))
(when (and (numberp p) (zerop p))
(funcall filter-fn
match-fn
(prefix-input-string plen input-string include-prefix-p)
(prefix-cursor-pos plen cursor-position include-prefix-p)
set))))))
(defun prefix-set (prefix cset include-prefix-p)
(let ((set (cset-set cset))
(plen (length prefix)))
(typecase set
(function
(lambda (input-string cursor-position)
(funcall set
(prefix-input-string plen input-string include-prefix-p)
(prefix-cursor-pos plen cursor-position include-prefix-p))))
(t set))))
(defun prefix-cset (prefix cset &key include-prefix-p)
(make-completions-set :filter-fn (prefix-filter prefix cset include-prefix-p)
:set (prefix-set prefix cset include-prefix-p)))
;;; Union cset: match from more than one set
(defun cset-union-filter (csets)
(let ((filter-fn-list (mapcar #'cset-filter-fn csets)))
(lambda (match-fn input-string cursor-position setlist)
(reduce #'append
(mapcar (lambda (filter-fn set)
(funcall filter-fn
match-fn input-string cursor-position set))
filter-fn-list setlist)))))
(defun cset-union-set (csets)
(let ((sets (mapcar #'cset-set csets)))
(lambda (input-string cursor-position)
(mapcar (lambda (set)
(typecase set
(function (funcall set input-string cursor-position))
(t set)))
sets))))
(defun cset-union (&rest csets)
(make-completions-set :filter-fn (cset-union-filter csets)
:set (cset-union-set csets)))
;;;; Some predefined sets
;;; Stumpwm Command Set
(defun commands (&rest args)
(declare (ignore args))
(let (res)
(maphash-keys (lambda (k) (push (pstr:pstring-propertize (symbol-name k)
:value k)
res))
stumpwm::*command-hash*)
(reverse res)))
(defparameter *command-completions*
(sequence-cset #'commands))
;;; File Path Set
(defun pathname-set-filter (match-fn input-string cursor-position set)
(remove-if-not (curry match-fn (file-namestring input-string) cursor-position)
set))
(defun pathname->string (pathname)
(let ((fn (file-namestring pathname)))
(if (emptyp fn)
(concatenate 'string (car (last (pathname-directory pathname))) "/")
fn)))
(defun pathname->completion (pathname)
(let ((untyped-completion (pstr:pstring-propertize (pathname->string pathname)
:value pathname)))
(if (fad:directory-pathname-p pathname)
(pstr:pstring-propertize untyped-completion
:match-type 1)
untyped-completion)))
(defun list-directory (dir)
(directory (concatenate 'string dir "*.*") :resolve-symlinks nil))
(defun pathname-set (input-string cursor-position)
(declare (ignore cursor-position))
(mapcar #'pathname->completion
(list-directory (directory-namestring input-string))))
(defun pathname-completion< (path1 path2)
(apply #'string-lessp (mapcar (lambda (path)
(let ((p (remove-if-not #'alphanumericp (pstr:pstring-string path))))
(if (emptyp p) path p)))
(list path1 path2))))
(defun pathname-set-sort (completions-list)
(sort completions-list #'pathname-completion<))
(defparameter *pathname-completions*
(make-completions-set :filter-fn #'sequence-filter
:sort-fn #'pathname-set-sort
:transform-input-fn #'file-namestring
:expand-completion-fn (lambda (cmp)
(namestring
(pstr:pstring-get-property cmp :value 0)))
:set #'pathname-set))
(defun lcps (prefix strings &key (test #'eql))
"Longest Common Prefixed Substring. Returns the longest common
substring starting with PREFIX in STRINGS."
(let* ((positions (mapcar (lambda (cmp)
(+ (length prefix)
(search prefix cmp :test test)))
strings))
(postfixes (mapcar #'subseq strings positions))
(shortest-postfix (reduce #'(lambda (x1 x2)
(if (< (length x1) (length x2))
x1
x2))
postfixes)))
(let ((end (dotimes (i (length shortest-postfix) i)
(unless (every (lambda (postfix)
(funcall test (elt postfix i)
(elt shortest-postfix i)))
postfixes)
(return-from nil i)))))
(concatenate 'string prefix (subseq shortest-postfix 0 end)))))
(defun ido-cset-expand-input (input-string cursor-pos cset match-fn)
(let ((compls (ido-input-completions input-string cursor-pos cset match-fn)))
(lcps input-string
(mapcar (cset-expand-completion-fn cset)
compls)
:test #'char-equal))) ;FIXME -> should check if case sensitivity is active or not
;;; TODO Find a way to list only files with a given property (ex. executables)
;;; a portable way would be better!
;; (defun is-exe (path)
;; (and (not (fad:directory-exists-p path)) (is-executable path)))
;; (defun is-executable (path) (boole boole-and (sb-posix:stat-mode (sb-posix:stat path)) #o0111))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; completions.lisp ends here