source: branches/acode-rewrite/source/cocoa-ide/hemlock/src/searchcoms.lisp

Last change on this file was 16082, checked in by Gary Byers, 11 years ago

Merge trunk changes into this branch. Expect some things to explode.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 20.8 KB
RevLine 
[7551]1;;; -*- Log: hemlock.log; Package: Hemlock -*-
[6]2;;;
3;;; **********************************************************************
4;;; This code was written as part of the CMU Common Lisp project at
5;;; Carnegie Mellon University, and has been placed in the public domain.
6;;;
7#+CMU (ext:file-comment
8 "$Header$")
9;;;
10;;; **********************************************************************
11;;;
12;;; This file contains searching and replacing commands.
13;;;
14
15(in-package :hemlock)
16
17
18
19
20;;;; Some global state.
[16082]21
22(defparameter *isearch-is-global* nil "True if incremental search string is not per-buffer but global.")
[6]23
24(defvar *last-search-string* () "Last string searched for.")
25(defvar *last-search-pattern*
26 (new-search-pattern :string-insensitive :forward "Foo")
[7509]27 "Search pattern we keep around so we don't cons them all the time.")
[6]28(defvar *search-wrapped-p* nil "True if search wrapped")
29
30(defhvar "String Search Ignore Case"
31 "When set, string searching commands use case insensitive."
32 :value t)
33
34(defun get-search-pattern (string direction)
35 (declare (simple-string string))
36 (when (zerop (length string)) (editor-error))
37 (setq *last-search-string* string)
38 (setq *last-search-pattern*
39 (new-search-pattern (if (value string-search-ignore-case)
40 :string-insensitive
41 :string-sensitive)
42 direction string *last-search-pattern*)))
43
[8428]44
45(defun note-current-selection-set-by-search ()
[6]46 (hemlock-ext:note-selection-set-by-search (current-buffer)))
47
48
49;;;; Vanilla searching.
50
51(defcommand "Forward Search" (p &optional string)
52 "Do a forward search for a string.
53 Prompt for the string and leave the point after where it is found."
54 "Searches for the specified String in the current buffer."
55 (declare (ignore p))
56 (if (not string)
57 (setq string (prompt-for-string :prompt "Search: "
58 :default *last-search-string*
59 :help "String to search for")))
60 (let* ((pattern (get-search-pattern string :forward))
[7475]61 (point (current-point))
[6]62 (mark (copy-mark point))
[7048]63 ;; find-pattern moves point to start of match, and returns is # chars matched
64 (won (find-pattern point pattern)))
65 (cond (won (move-mark mark point)
[8428]66 (character-offset point won)
[6]67 (push-buffer-mark mark t)
[7048]68 (note-current-selection-set-by-search))
69 (t (delete-mark mark)
[6]70 (editor-error)))
71 (clear-echo-area)))
72
[7048]73(defcommand "Reverse Search" (p &optional string)
[6]74 "Do a backward search for a string.
75 Prompt for the string and leave the point before where it is found."
76 "Searches backwards for the specified String in the current buffer."
77 (declare (ignore p))
78 (if (not string)
79 (setq string (prompt-for-string :prompt "Reverse Search: "
80 :default *last-search-string*
81 :help "String to search for")))
82 (let* ((pattern (get-search-pattern string :backward))
83 (point (current-point))
[7048]84 (mark (copy-mark point))
85 (won (find-pattern point pattern)))
86 (cond (won (move-mark mark point)
[8428]87 (character-offset mark won)
[6]88 (push-buffer-mark mark t)
[7048]89 (note-current-selection-set-by-search))
90 (t (delete-mark mark)
[6]91 (editor-error)))
92 (clear-echo-area)))
93
[8428]94
[6]95
[8428]96
97;;;; Replacement commands:
98
99(defmode "Query/Replace" :precedence :highest
[6]100 :documentation "Type one of the following single-character commands:"
[8428]101 ;; Make anything that's not otherwise overridden exit query/replace
[6]102 :default-command "Query/Replace Exit and Redo")
[8428]103
104(add-hook abort-hook 'abort-query/replace-mode)
105
106(defhvar "Case Replace"
[6]107 "If this is true then \"Query Replace\" will try to preserve case when
[16082]108 doing replacements."
109 :value t)
110
111(defun search-match-type (mark match-len)
112 (cond ((or (eql match-len 0) (not (upper-case-p (next-character mark))))
113 :exact)
114 ((or (eql match-len 1) (prog2
115 (mark-after mark)
116 (upper-case-p (next-character mark))
117 (mark-before mark)))
[6]118 :upcase)
119 (t :capitalize)))
120
121(defcommand "Replace String" (p &optional
122 (target (prompt-for-string
123 :prompt "Replace String: "
124 :help "Target string"
125 :default *last-search-string*))
126 (replacement (prompt-for-string
127 :prompt "With: "
128 :help "Replacement string")))
[8428]129 "Replaces the specified Target string with the specified Replacement
130 string in the current buffer for all occurrences after the point or within
131 the active region, depending on whether it is active."
132 (let ((qrs (query/replace-init :count p :target target :replacement replacement
[6]133 :undo-name "Replace String")))
[8428]134 (query/replace-all qrs)
135 (query/replace-finish qrs)))
136
137(defun current-query-replace-state ()
[6]138 (or (value query/replace-state)
139 (error "Query/Replace command invoked outside Query Replace")))
140
141(defcommand "Query Replace" (p &optional
142 (target (prompt-for-string
143 :prompt "Query Replace: "
144 :help "Target string"
145 :default *last-search-string*))
146 (replacement (prompt-for-string
147 :prompt "With: "
148 :help "Replacement string")))
[8428]149 "Replaces the Target string with the Replacement string if confirmation
150 from the keyboard is given. If the region is active, limit queries to
151 occurrences that occur within it, otherwise use point to end of buffer."
152 (let* ((buffer (current-buffer))
153 (qrs (query/replace-init :count p :target target :replacement replacement
154 :undo-name "Query Replace")))
155 (setf (buffer-minor-mode (current-buffer) "Query/Replace") t)
156 (unless (hemlock-bound-p 'query/replace-state :buffer buffer)
157 (defhvar "Query/Replace State"
158 "Internal variable containing current state of Query/Replace"
[6]159 :buffer buffer))
160 (setf (value query/replace-state) qrs)
161 (query/replace-find-next qrs)))
162
163(defstruct (replace-undo (:constructor make-replace-undo (mark region)))
164 mark
165 region)
166
167(setf (documentation 'replace-undo-mark 'function)
168 "Return the mark where a replacement was made.")
169(setf (documentation 'replace-undo-region 'function)
[8428]170 "Return region deleted due to replacement.")
171
172
173(defstruct (query-replace-state (:conc-name "QRS-"))
174 count
175 target
176 replacement
177 undo-name
178 dumb-p
179 upper
180 cap
181 start-mark
[6]182 last-found
[8428]183 stop-mark
184 undo-data)
185
186(defun query/replace-init (&key count target replacement undo-name)
187 (when (and count (minusp count))
188 (editor-error "Replacement count is negative."))
189 (let* ((point (current-point))
190 (region (get-count-region))
[6]191 (start-mark (copy-mark (region-start region) :temporary))
[8428]192 (end-mark (copy-mark (region-end region) :left-inserting)))
193 (move-mark point start-mark)
194 (get-search-pattern target :forward)
195 (make-query-replace-state
196 :count (or count -1)
197 :target target
198 :replacement replacement
199 :undo-name undo-name
200 :dumb-p (not (and (every #'(lambda (ch) (or (not (both-case-p ch))
201 (lower-case-p ch)))
202 (the string replacement))
203 (value case-replace)))
204 :upper (string-upcase replacement)
205 :cap (string-capitalize replacement)
206 :start-mark start-mark
[6]207 :last-found (copy-mark start-mark :temporary)
208 :stop-mark end-mark
[8428]209 :undo-data nil)))
210
211
212(defun query/replace-find-next (qrs &key (interactive t))
213 (let* ((point (current-point))
214 (won (and (not (zerop (qrs-count qrs)))
215 (find-pattern point *last-search-pattern* (qrs-stop-mark qrs)))))
216 (if won
217 (progn
218 (decf (qrs-count qrs))
219 (move-mark (qrs-last-found qrs) (current-point))
220 (character-offset point (length (qrs-target qrs)))
221 (when interactive
222 (message "Query Replace (type ? for help): "))
223 T)
224 (progn
[6]225 (when interactive
[8428]226 (end-query/replace-mode))
227 nil))))
228
229(defun query/replace-replace (qrs)
230 (let* ((replacement (qrs-replacement qrs))
231 (point (current-point))
232 (length (length (qrs-target qrs))))
[16082]233 (with-mark ((undo-mark1 point :left-inserting)
234 (undo-mark2 point :left-inserting))
235 (character-offset undo-mark1 (- length))
236 (let ((string (ecase (if (qrs-dumb-p qrs) :exact (search-match-type undo-mark1 length))
[8428]237 (:exact replacement)
238 (:upcase (qrs-upper qrs))
239 (:capitalize (qrs-cap qrs)))))
240 (push (make-replace-undo
241 ;; Save :right-inserting, so the INSERT-STRING at mark below
242 ;; doesn't move the copied mark the past replacement.
243 (copy-mark undo-mark1 :right-inserting)
244 (delete-and-save-region (region undo-mark1 undo-mark2)))
245 (qrs-undo-data qrs))
246 (insert-string point string)))))
247
248(defun query/replace-all (qrs)
249 (loop
250 while (query/replace-find-next qrs :interactive nil)
251 do (query/replace-replace qrs)))
252
253(defun query/replace-finish (qrs &key (report t))
254 (let* ((undo-data (nreverse (qrs-undo-data qrs)))
255 (count (length undo-data))
256 (replacement-len (length (qrs-replacement qrs))))
257 (save-for-undo (qrs-undo-name qrs)
258 #'(lambda ()
259 (dolist (ele undo-data)
260 (setf (mark-kind (replace-undo-mark ele)) :left-inserting))
261 (dolist (ele undo-data)
262 (let ((mark (replace-undo-mark ele)))
263 (delete-characters mark replacement-len)
264 (ninsert-region mark (replace-undo-region ele)))))
265 #'(lambda ()
266 (dolist (ele undo-data)
267 (delete-mark (replace-undo-mark ele)))))
268 (unless (mark= (current-point) (qrs-start-mark qrs))
[11674]269 (push-buffer-mark (qrs-start-mark qrs)))
[8428]270 (delete-mark (qrs-stop-mark qrs))
271 (when report
272 (message "~D occurrence~:P replaced." count))))
273
274
275(defun abort-query/replace-mode ()
276 (when (buffer-minor-mode (current-buffer) "Query/Replace")
277 (end-query/replace-mode :report nil)))
278
279(defun end-query/replace-mode (&key (report t))
280 (let* ((qrs (current-query-replace-state)))
281 (query/replace-finish qrs :report report)
282 (setf (buffer-minor-mode (current-buffer) "Query/Replace") nil)))
283
284(defcommand "Query/Replace This" (p)
285 "Replace this occurence"
286 (declare (ignore p))
287 (let ((qrs (current-query-replace-state)))
288 (query/replace-replace qrs)
289 (query/replace-find-next qrs)))
290
291(defcommand "Query/Replace Skip" (p)
292 "Don't replace this occurence, but continue"
293 (declare (ignore p))
294 (let ((qrs (current-query-replace-state)))
295 (query/replace-find-next qrs)))
296
297(defcommand "Query/Replace All" (p)
298 "Replace this and all remaining occurences"
299 (declare (ignore p))
300 (let ((qrs (current-query-replace-state)))
301 (query/replace-replace qrs)
302 (query/replace-all qrs))
303 (end-query/replace-mode))
304
305(defcommand "Query/Replace Last" (p)
306 "Replace this occurrence, then exit"
307 (declare (ignore p))
308 (let ((qrs (current-query-replace-state)))
309 (query/replace-replace qrs))
310 (end-query/replace-mode))
311
312(defcommand "Query/Replace Exit" (p)
313 "Exit Query Replace mode"
314 (declare (ignore p))
315 (end-query/replace-mode))
316
317(defcommand "Query/Replace Abort" (p)
318 "Abort Query/Replace mode"
319 (declare (ignore p))
320 (abort-current-command "Query/Replace aborted"))
321
322(defcommand "Query/Replace Help" (p)
323 "Describe Query/Replace commands"
324 (describe-mode-command p "Query/Replace"))
325
326;; The transparent-p flag takes care of executing the key normally when we're done,
327;; as long as we don't take a non-local exit.
328(defcommand ("Query/Replace Exit and Redo" :transparent-p t) (p)
329 "Exit Query Replace and then execute the key normally"
[6]330 (declare (ignore p))
331 (end-query/replace-mode))
332
333;;;; Occurrence searching.
334
335(defcommand "List Matching Lines" (p &optional string)
336 "Prompts for a search string and lists all matching lines after the point or
337 within the current-region, depending on whether it is active or not.
338 With an argument, lists p lines before and after each matching line."
339 "Prompts for a search string and lists all matching lines after the point or
340 within the current-region, depending on whether it is active or not.
341 With an argument, lists p lines before and after each matching line."
342 (unless string
343 (setf string (prompt-for-string :prompt "List Matching: "
344 :default *last-search-string*
345 :help "String to search for")))
346 (let ((pattern (get-search-pattern string :forward))
347 (matching-lines nil)
348 (region (get-count-region)))
349 (with-mark ((mark (region-start region))
350 (end-mark (region-end region)))
351 (loop
352 (when (or (null (find-pattern mark pattern)) (mark> mark end-mark))
353 (return))
354 (setf matching-lines
[6790]355 (nconc matching-lines (list-lines mark (or p 0))))
[6]356 (unless (line-offset mark 1 0)
357 (return))))
358 (with-pop-up-display (s :height (length matching-lines) :title (format nil "Lines matching ~s" string))
359 (dolist (line matching-lines)
360 (write-line line s)))))
361
362;;; LIST-LINES creates a lists of strings containing (num) lines before the
363;;; line that the point is on, the line that the point is on, and (num)
364;;; lines after the line that the point is on. If (num) > 0, a string of
365;;; dashes will be added to make life easier for List Matching Lines.
366;;;
367(defun list-lines (mark num)
368 (if (<= num 0)
369 (list (line-string (mark-line mark)))
370 (with-mark ((mark mark)
371 (beg-mark mark))
372 (unless (line-offset beg-mark (- num))
373 (buffer-start beg-mark))
374 (unless (line-offset mark num)
375 (buffer-end mark))
376 (let ((lines (list "--------")))
377 (loop
378 (push (line-string (mark-line mark)) lines)
379 (when (same-line-p mark beg-mark)
380 (return lines))
381 (line-offset mark -1))))))
382
383(defcommand "Delete Matching Lines" (p &optional string)
384 "Deletes all lines that match the search pattern using delete-region. If
385 the current region is active, limit the search to it. The argument is
386 ignored."
387 "Deletes all lines that match the search pattern using delete-region. If
388 the current region is active, limit the search to it. The argument is
389 ignored."
390 (declare (ignore p))
391 (unless string
392 (setf string (prompt-for-string :prompt "Delete Matching: "
393 :default *last-search-string*
394 :help "String to search for")))
395 (let* ((region (get-count-region))
396 (pattern (get-search-pattern string :forward))
397 (start-mark (region-start region))
398 (end-mark (region-end region)))
399 (with-mark ((bol-mark start-mark :left-inserting)
400 (eol-mark start-mark :right-inserting))
401 (loop
402 (unless (and (find-pattern bol-mark pattern) (mark< bol-mark end-mark))
403 (return))
404 (move-mark eol-mark bol-mark)
405 (line-start bol-mark)
406 (unless (line-offset eol-mark 1 0)
407 (buffer-end eol-mark))
408 (delete-region (region bol-mark eol-mark))))))
409
410(defcommand "Delete Non-Matching Lines" (p &optional string)
411 "Deletes all lines that do not match the search pattern using delete-region.
412 If the current-region is active, limit the search to it. The argument is
413 ignored."
414 "Deletes all lines that do not match the search pattern using delete-region.
415 If the current-region is active, limit the search to it. The argument is
416 ignored."
417 (declare (ignore p))
418 (unless string
419 (setf string (prompt-for-string :prompt "Delete Non-Matching:"
420 :default *last-search-string*
421 :help "String to search for")))
422 (let* ((region (get-count-region))
423 (start-mark (region-start region))
424 (stop-mark (region-end region))
425 (pattern (get-search-pattern string :forward)))
426 (with-mark ((beg-mark start-mark :left-inserting)
427 (end-mark start-mark :right-inserting))
428 (loop
429 (move-mark end-mark beg-mark)
430 (cond ((and (find-pattern end-mark pattern) (mark< end-mark stop-mark))
431 (line-start end-mark)
432 (delete-region (region beg-mark end-mark))
433 (unless (line-offset beg-mark 1 0)
434 (return)))
435 (t
436 (delete-region (region beg-mark stop-mark))
437 (return)))))))
438
439(defcommand "Count Occurrences" (p &optional string)
440 "Prompts for a search string and counts occurrences of it after the point or
441 within the current-region, depending on whether it is active or not. The
442 argument is ignored."
443 "Prompts for a search string and counts occurrences of it after the point or
444 within the current-region, depending on whether it is active or not. The
445 argument is ignored."
446 (declare (ignore p))
447 (unless string
448 (setf string (prompt-for-string
449 :prompt "Count Occurrences: "
450 :default *last-search-string*
451 :help "String to search for")))
452 (message "~D occurrence~:P"
453 (count-occurrences-region (get-count-region) string)))
454
455(defun count-occurrences-region (region string)
456 (let ((pattern (get-search-pattern string :forward))
457 (end-mark (region-end region)))
458 (let ((occurrences 0))
459 (with-mark ((mark (region-start region)))
460 (loop
461 (let ((won (find-pattern mark pattern)))
462 (when (or (null won) (mark> mark end-mark))
463 (return))
[16082]464 (incf occurrences)
465 (character-offset mark won))))
466 occurrences)))
467
468
469;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
470
471(defcommand "List Definitions" (p)
472 "List definitions in the buffer, or in the current region if there is one"
473 (declare (ignore p))
474 (show-list-definitions-window (current-view)))
475
476;;; TODO: It could be nice to track and raise an existing window. That
477;;; could mean keeping track of each window per buffer and region, or just
478;;; keeping one per buffer and updating it if the region changes. As it
479;;; works now, a new window will be opened each time.
480(defun show-list-definitions-window (view)
481 (let* ((region (if (region-active-p)
482 (current-region)
483 (buffer-region (current-buffer))))
484 (definitions (collect-definition-lines region)))
485 (flet ((defn-action (defn)
486 (gui::execute-in-gui (lambda ()
487 (hemlock-ext:select-view view)
488 (hi::handle-hemlock-event view
489 (lambda ()
490 ;; TODO: only leave mark if we're far away, or maybe if last command
491 ;; was not list-definitions...
492 (destructuring-bind (line-text posn type) defn
493 (declare (ignore type))
494 (or (move-to-definition posn line-text t)
495 (loud-message "Could find definition"))))))))
496 (defn-printer (defn stream)
497 (write-string (car defn) stream)))
498 (hemlock-ext:open-sequence-dialog
499 :title (format nil "Definitions in ~s" (buffer-name (current-buffer)))
500 :sequence definitions
501 :action #'defn-action
502 :printer #'defn-printer))))
503
504(defun collect-definition-lines (&optional (region (buffer-region (current-buffer))))
505 (let ((line (mark-line (region-start region)))
506 (last-line (mark-line (region-end region)))
507 (lines '()))
508 (with-mark ((mark (region-start region)))
509 (labels ((collect-line (line)
510 (move-to-position mark 0 line)
511 (push (list (line-string line)
512 (hi::get-line-origin line)
513 (get-definition-type mark))
514 lines))
515 (starts-with-definition (line)
516 (let ((string (line-string line)))
517 (and (plusp (length string))
518 (char= #\( (char string 0))))))
519 (loop
520 (when (starts-with-definition line)
521 (collect-line line))
522 (when (eq line last-line)
523 (return (nreverse lines)))
524 (setf line (line-next line)))))))
525
526(defun get-definition-type (mark)
527 (let ((buffer (mark-buffer mark)))
528 (mark-after mark)
529 (let ((str (symbol-at-mark mark)))
530 (when str
531 (multiple-value-bind (sym error)
532 (let* ((*package* (ccl:require-type (or (buffer-package buffer) *package*) 'package)))
533 (ignore-errors (values (read-from-string str))))
534 (if error
535 (intern (string-upcase str) *package*)
536 sym))))))
537
538(defun move-to-definition (posn line-text &optional (leave-mark t))
539 (flet ((ssearch (mark string direction)
540 (find-pattern mark (new-search-pattern :string-insensitive
541 direction
542 string))))
543 (declare (inline ssearch))
544 (with-mark ((mark (current-point)))
545 (or (move-to-absolute-position mark posn) (buffer-end mark))
546 (when (or (ssearch mark line-text :forward)
547 (ssearch mark line-text :backward))
548 (if leave-mark
549 (move-point-leaving-mark mark)
550 (move-mark (current-point-collapsing-selection) mark))))))
551
552
553;; Interface for getting this functionality outside of the editor.
554;; Returns a list of (string number symbol) where string is the first line of the definition,
555;; number is the absolute position in the buffer of the start of the line, and symbol is the
556;; definition type (eg. DEFUN, DEFVAR, HI:DEFCOMMAND, etc).
557(defun definitions-in-document (ns-doc)
558 (gui::execute-in-buffer (gui::hemlock-buffer ns-doc) #'collect-definition-lines))
Note: See TracBrowser for help on using the repository browser.