source: release/1.11/source/cocoa-ide/hemlock/src/symbol-completion.lisp

Last change on this file was 16688, checked in by R. Matthew Emerson, 9 years ago

Merge copyright/license header changes to 1.11 release branch.

File size: 16.2 KB
RevLine 
[7540]1;;; -*- Package: Hemlock -*-
2;;;
[16688]3;;; Copyright 2007 Clozure Associates
[7540]4;;;
[16688]5;;; Licensed under the Apache License, Version 2.0 (the "License");
6;;; you may not use this file except in compliance with the License.
7;;; You may obtain a copy of the License at
8;;;
9;;; http://www.apache.org/licenses/LICENSE-2.0
10;;;
11;;; Unless required by applicable law or agreed to in writing, software
12;;; distributed under the License is distributed on an "AS IS" BASIS,
13;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14;;; See the License for the specific language governing permissions and
15;;; limitations under the License.
16
[7540]17;;; Dynamic symbol completion
18;;; gz@clozure.com
19;;;
20;;; This uses wordchar attributes set up in completion.lisp, but otherwise is unrelated.
21
22(in-package :hemlock)
23
24;; Context maintained so repeated M-/'s can walk through all available abbreviations
25
26(defstruct (dabbrev-context (:conc-name "DABBREV."))
27 ;; The buffer this context belongs to
28 (buffer nil)
29 ;; The last expansion
30 (expansion nil)
31 ;; The thing that was expanded. This is usually a prefix of expansion, but it might
32 ;; be initials (i.e. abbrev = mvb, expansion = multiple-value-bind).
33 (abbrev "" :type simple-string)
34 ;; The package prefix if any, including the ending colon(s).
35 (prefix nil)
36 ;; The position of the end of the expansion
37 (end-mark nil)
38 ;; buffer-signature as of the time the expansion was inserted.
39 (signature nil)
40 ;; list of expansions already tried and rejected
41 (seen ())
42 ;; List of places to try next
43 (state-path '(:before-point :after-point :other-buffers :this-package :other-packages))
44 ;; Sequence of sources to go thru before changing state
45 (sources '(:last-used))
46 ;; a sequence of expansions to go thru before changing source
47 (seq (make-array 10 :fill-pointer 0 :adjustable t)))
48
49(defun symbol-completion-buffer-hook (buffer)
50 (defhvar "DAbbrev Context"
51 "Internal variable for cycling through symbol completions"
52 :buffer buffer
53 :value nil)
54 (defhvar "DAbbrev Cache"
55 "Internal variable for caching symbols in buffer"
56 :buffer buffer
57 ;; Cons of buffer sig and a vector of all symbols in buffer as of the time
58 ;; of the buffer sig.
59 :value (cons nil nil))
60 )
61
62(add-hook make-buffer-hook #'symbol-completion-buffer-hook)
63
64;; Global table of all abbrevs expanded in this session, and the last value they expanded to.
65(defvar *dabbrevs* (make-hash-table :test #'equalp))
66
67(defun dabbrev-package (context)
68 (or (dabbrev-package-for-prefix (dabbrev.prefix context))
69 ;; TODO: look for in-package form preceeding point...
70 (buffer-package (dabbrev.buffer context))))
71
72(defun dabbrev-external-symbol-p (context)
73 ;; True if explicitly looking for an external symbol.
74 (let* ((prefix (dabbrev.prefix context))
75 (prefix-len (length prefix)))
76 (or (eql prefix-len 1)
77 (and (>= prefix-len 2)
78 (not (eql (aref prefix (- prefix-len 2)) #\:))))))
79
80(defun dabbrev-package-for-prefix (prefix)
81 (when prefix
82 (let* ((prefix-len (length prefix)))
83 (if (eql prefix-len 1)
84 ccl::*keyword-package*
85 (find-package (subseq prefix 0 (if (eql (aref prefix (- prefix-len 2)) #\:)
86 (- prefix-len 2)
87 (- prefix-len 1))))))))
88
89
90;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
91;; State machine support:
92
93(defun dabbrev-next-expansion (context)
94 (cond ((> (length (dabbrev.seq context)) 0)
95 (let* ((exp (vector-pop (dabbrev.seq context))))
96 (if (find exp (dabbrev.seen context) :test #'string=)
97 (dabbrev-next-expansion context)
98 exp)))
99 ((dabbrev.sources context)
100 (dabbrev-collect-expansions (pop (dabbrev.sources context)) context)
101 (dabbrev-next-expansion context))
102 ((dabbrev.state-path context)
103 (setf (dabbrev.sources context)
104 (dabbrev-sources-in (pop (dabbrev.state-path context)) context))
105 (dabbrev-next-expansion context))
106 (t nil)))
107
108
109;; dabbrev-sources-in: maps state -> sources
110
111(defmethod dabbrev-sources-in ((state t) context)
112 (declare (ignore context))
113 (list state))
114
115(defmethod dabbrev-sources-in ((state (eql :other-buffers)) context)
[8428]116 (let* ((buffers (mapcar #'hemlock-view-buffer (hemlock-ext:all-hemlock-views))))
[7540]117 ;; Remove duplicates, always keeping the first occurance (frontmost window)
118 (loop for blist on buffers do (setf (cdr blist) (delete (car blist) (cdr blist))))
119 (delete (dabbrev.buffer context) buffers)))
120
121(defmethod dabbrev-sources-in ((state (eql :other-packages)) context)
122 (let* ((all (copy-list (list-all-packages)))
123 (this-package (dabbrev-package context))
124 (keyword-package ccl::*keyword-package*))
125 (setq all (delete this-package all))
126 (unless (eq this-package keyword-package)
127 (setq all (nconc (delete keyword-package all) (list keyword-package))))
128 all))
129
130;; dabbrev-collect-expansion: maps source -> expansions
131;; Note that in general these methods don't bother to check for dabbrev.seen
132;; or duplicates, even though they could, because there is no reason to spend
133;; time up front on checking expansions we might never get to.
134
135(defun dabbrev-match-p (context exp)
136 (let* ((abbrev (dabbrev.abbrev context))
137 (abbrev-len (length abbrev)))
138 (or (and (< abbrev-len (length exp))
139 (string-equal abbrev exp :end1 abbrev-len :end2 abbrev-len))
140 ;; Check for initials.
141 (loop
142 for char across abbrev
143 for pos = 0 then (and (setq pos (position-if-not #'alphanumericp exp :start pos))
144 (position-if #'alphanumericp exp :start (1+ pos)))
145 always (and pos (char-equal char (aref exp pos)))))))
146
147(defmethod dabbrev-collect-expansions ((source (eql :last-used)) context)
148 (let* ((abbrev (dabbrev.abbrev context))
149 (prefix (dabbrev.prefix context))
150 (abbrev-len (length abbrev))
151 (prefix-len (length prefix))
152 (string (concatenate 'string abbrev prefix)))
153 (loop
154 for end from (+ abbrev-len prefix-len) downto prefix-len
155 for key = string then (subseq string 0 end)
156 as exp = (gethash key *dabbrevs*)
157 when (and exp (dabbrev-match-p context exp))
158 do (return (vector-push-extend exp (dabbrev.seq context))))))
159
160(defmethod dabbrev-collect-expansions ((buffer buffer) context)
161 ;; TODO: need to take prefix into account - give preferences to things
162 ;; matching prefix. For now, ignore the prefix-only case here since can't
163 ;; do anything useful.
164 (unless (equal (dabbrev.abbrev context) "")
165 (let* ((vec (dabbrev-symbols-in-buffer buffer))
166 (seq (dabbrev.seq context)))
167 (loop
168 for exp across vec
169 when (dabbrev-match-p context exp)
170 do (vector-push-extend exp seq))
171 seq)))
172
173;; TODO: have a background process that does this. (Since the architecture doesn't allow locking
174;; against buffer changes, might need to do ignore-errors and just bludgeon through, checking
175;; for sig changes at end. Or perhaps could use the modification hook, if that's reliable)
176(defun dabbrev-symbols-in-buffer (buffer)
177 (let* ((cache (variable-value 'dabbrev-cache :buffer buffer)))
178 (unless (and cache (eql (car cache) (buffer-signature buffer)))
179 (let* ((hi::*current-buffer* buffer)
180 (start-mark (buffer-start-mark buffer))
181 (symbols (make-array 100 :adjustable t :fill-pointer 0)))
182 (with-mark ((word-start start-mark)
183 (word-end start-mark))
184 (loop
185 (unless (find-attribute word-end :completion-wordchar) (return))
186 (move-mark word-start word-end)
187 (unless (find-not-attribute word-end :completion-wordchar)
188 (buffer-end word-end))
189 (let* ((word (region-to-string (region word-start word-end))))
190 (unless (find word symbols :test #'equal)
191 (vector-push-extend word symbols)))))
192 (setf (variable-value 'dabbrev-cache :buffer buffer)
193 (setq cache (cons (buffer-signature buffer) (coerce symbols 'simple-vector))))))
194 (cdr cache)))
195
196(defun dabbrev-next-in-buffer (mark temp-mark temp-string)
197 ;; Leaves temp-mark at start and point-mark at end of next symbol
198 (when (find-attribute mark :completion-wordchar)
199 (move-mark temp-mark mark)
200 (unless (find-not-attribute mark :completion-wordchar)
201 (buffer-end mark))
202 (region-to-string (region temp-mark mark) temp-string)))
203
204(defun dabbrev-prev-in-buffer (mark temp-mark temp-string)
205 (when (reverse-find-attribute mark :completion-wordchar)
206 (move-mark temp-mark mark)
207 (unless (reverse-find-not-attribute mark :completion-wordchar)
208 (buffer-start mark))
209 (region-to-string (region mark temp-mark) temp-string)))
210
211(defmethod dabbrev-collect-expansions ((source (eql :before-point)) context)
212 (dabbrev-collect-expansions-1 source context))
213
214(defmethod dabbrev-collect-expansions ((source (eql :after-point)) context)
215 (dabbrev-collect-expansions-1 source context))
216
217(defun dabbrev-collect-expansions-1 (direction context)
218 (let* ((buffer (dabbrev.buffer context))
219 (point (buffer-point buffer))
220 (abbrev (dabbrev.abbrev context))
221 (abbrev-len (length abbrev))
222 (seq (dabbrev.seq context))
223 (temp-string (make-string 30)))
224 ;; TODO: need to take prefix into account - give preferences to things
225 ;; matching prefix. For now, ignore the prefix-only case here since can't
226 ;; do anything useful.
227 (when (eql abbrev-len 0)
228 (return-from dabbrev-collect-expansions-1))
229 (with-mark ((mark point) (temp-mark point))
230 (when (eq direction :before-point) (character-offset mark (- abbrev-len)))
231 (loop
232 (multiple-value-bind (word word-len)
233 (if (eq direction :before-point)
234 (dabbrev-prev-in-buffer mark temp-mark temp-string)
235 (dabbrev-next-in-buffer mark temp-mark temp-string))
236 (unless word (return))
237 (when (and (< abbrev-len word-len)
238 (string-equal word abbrev :end1 abbrev-len :end2 abbrev-len))
239 (let* ((word (subseq word 0 word-len)))
240 (unless (find word seq :test #'equal)
241 (vector-push-extend word seq)))))))
242 (nreverse seq)))
243
244(defmethod dabbrev-collect-expansions ((source (eql :this-package)) context)
245 (let* ((pkg (dabbrev-package context))
246 (seq (dabbrev.seq context)))
247 (when pkg
248 (when (dabbrev.prefix context)
249 (if (or (dabbrev-external-symbol-p context)
250 (eq pkg ccl::*keyword-package*))
251 (do-external-symbols (sym pkg)
252 (when (and (not (find sym seq :test #'eq))
253 (dabbrev-match-p context (symbol-name sym)))
254 (vector-push-extend sym seq)))
255 (ccl::do-present-symbols (sym pkg)
256 (when (and (not (find sym seq :test #'eq))
257 (dabbrev-match-p context (symbol-name sym)))
258 (vector-push-extend sym seq)))))
259 (unless (eq pkg ccl::*keyword-package*)
260 (do-symbols (sym pkg)
261 (when (and (not (find sym seq :test #'eq))
262 (dabbrev-match-p context (symbol-name sym)))
263 (vector-push-extend sym seq))))
[8774]264 (setq seq
265 (stable-sort seq #'(lambda (s1 s2)
266 (and (or (boundp s1) (fboundp s1))
267 (not (or (boundp s2) (fboundp s2)))))))
[7540]268 ;; Now convert to strings - and downcase for inserting in buffer.
269 (dotimes (i (length seq))
270 (setf (aref seq i) (string-downcase (symbol-name (aref seq i))))))
271 seq))
272
273(defmethod dabbrev-collect-expansions ((pkg package) context)
274 ;; For random packages, only need to do present symbols, since imported ones will be
275 ;; shown in their own package.
276 (let* ((seq (dabbrev.seq context)))
277 (ccl::do-present-symbols (sym pkg)
278 (let* ((name (symbol-name sym)))
279 (when (dabbrev-match-p context name)
280 (vector-push-extend (string-downcase name) seq))))
281 seq))
282
283;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
284;;
285;; the command
286
287
288(defcommand "Expand Dynamic Abbreviation" (p)
289 "Treats the symbol before point as an abbreviation and expands it.
290It checks the following in order until a suitable expansion is found:
291 - last accepted expansion for this abbreviation, if any
292 - symbols in current buffer before point
293 - symbols in current buffer after point
294 - symbols in all other editor windows, front to back
295 - symbols visible in the current package, fbound/bound symbols first
296 - symbols in all other packages (in no particular order)
297
298If called repeatedly from the same position, replaces the previous expansion
299with the next possible one.
300
301A symbol is a suitable expansion for an abbreviation if the abbreviation is
302a proper prefix of the symbol, or the abbreviation consists of the initials
303of the individual words within the symbol (e.g. mvb => multiple-value-bind).
304"
305 (declare (ignore p))
306 (let* ((buffer (current-buffer))
307 (point (buffer-point buffer))
308 (context (dabbrev-command-init buffer))
309 (abbrev (dabbrev.abbrev context))
310 (abbrev-len (length abbrev))
311 (expansion (dabbrev-next-expansion context))
312 (expansion-len (length expansion)))
313 (when (null expansion)
[11674]314 (editor-error "No~:[ more~;~] expansions for ~s"
[7540]315 (null (dabbrev.expansion context))
316 abbrev))
317 (push expansion (dabbrev.seen context))
318 (setf (dabbrev.expansion context) expansion)
319 (setf (gethash abbrev *dabbrevs*) expansion)
320 (if (and (>= expansion-len abbrev-len)
321 (string= abbrev expansion :end2 abbrev-len))
322 (insert-string point (subseq expansion abbrev-len))
323 (progn
324 (delete-characters point (- abbrev-len))
325 (insert-string point expansion)))
326 (move-mark (dabbrev.end-mark context) point)
327 (setf (dabbrev.signature context) (buffer-signature buffer))))
328
329#+gz ;; This tests the generation of completion candidates
330;; (time(hemlock::test-completions (cadr hi::*buffer-list*) "dabbrev"))
331(defun test-completions (buffer abbrev)
332 (let* ((hi::*current-buffer* buffer)
333 (point (buffer-point buffer))
334 (context (make-dabbrev-context
335 :buffer buffer
336 :abbrev abbrev
337 ;; Can use a temp mark (i.e. the kind that doesn't automatically
338 ;; update) because we only use it while buffer is unmodified.
339 :end-mark (copy-mark point :temporary))))
340 (loop as expansion = (dabbrev-next-expansion context) while expansion
341 do (push expansion (dabbrev.seen context))
342 do (setf (dabbrev.expansion context) expansion)
343 do (setf (gethash abbrev *dabbrevs*) expansion))
344 (dabbrev.seen context)))
345
346;; Reinitialize context to either restart or cycle to next completion.
347;; In the latter case, undoes the last completion in the buffer.
348(defun dabbrev-command-init (buffer)
349 (let* ((point (buffer-point buffer))
350 (context (variable-value 'dabbrev-context :buffer buffer)))
351 (if (and context
352 ;; If buffer not modified since last time
353 (eql (dabbrev.signature context) (buffer-signature buffer))
354 ;; and cursor not moved elsewhere
355 (mark= (dabbrev.end-mark context) point))
356 ;; This means rejected previous attempt, get rid of it.
357 (let* ((abbrev (dabbrev.abbrev context))
358 (abbrev-len (length abbrev))
359 (expansion (dabbrev.expansion context))
360 (expansion-len (length expansion)))
361 ;; Sanity check, because I don't totally trust buffer-signature ...
362 (with-mark ((mark point))
363 (assert (and (character-offset mark (- (length expansion)))
364 (equal (region-to-string (region mark point)) expansion))
365 () "Bug! Buffer changed unexpectedly"))
366 (if (and (>= expansion-len abbrev-len)
367 (string= abbrev expansion :end2 abbrev-len))
368 (delete-characters point (- abbrev-len expansion-len))
369 (progn
370 (delete-characters point (- expansion-len))
371 (insert-string point abbrev))))
372 ;; Else starting a new attempt, create a new context
373 (let* ((mark (copy-mark point :temporary)))
374 (multiple-value-bind (abbrev prefix) (dabbrev-get-abbrev mark point)
375 (when (and (equal abbrev "") (equal prefix ""))
376 (editor-error "Nothing to expand"))
377 (setq context (make-dabbrev-context
378 :buffer buffer
379 :abbrev abbrev
380 :prefix prefix
381 ;; Can use a temp mark (i.e. the kind that doesn't automatically
382 ;; update) because we only use it while buffer is unmodified.
383 :end-mark mark)))
384 (setf (variable-value 'dabbrev-context :buffer buffer) context)))
385 (move-mark (dabbrev.end-mark context) point)
386 context))
387
388(defun dabbrev-get-abbrev (mark point)
389 (declare (values abbrev package-prefix))
390 (move-mark mark point)
391 (unless (reverse-find-not-attribute mark :completion-wordchar)
392 (buffer-start mark))
393 (values (region-to-string (region mark point))
394 (when (eql (previous-character mark) #\:)
395 (with-mark ((temp mark))
396 (character-offset temp -1)
397 (when (eql (previous-character temp) #\:)
398 (character-offset temp -1))
399 (unless (reverse-find-not-attribute temp :completion-wordchar)
400 (buffer-start temp))
401 (region-to-string (region temp mark))))))
402
403
Note: See TracBrowser for help on using the repository browser.