-
Notifications
You must be signed in to change notification settings - Fork 6
/
input-editing.lisp
1041 lines (942 loc) · 43.7 KB
/
input-editing.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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; -*- Mode: Lisp; Package: CLIM-INTERNALS -*-
;;; (c) copyright 2001 by
;;; Tim Moore ([email protected])
;;; (c) copyright 2006-2008 by
;;; Troels Henriksen ([email protected])
;;; This library is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU Library General Public
;;; License as published by the Free Software Foundation; either
;;; version 2 of the License, or (at your option) any later version.
;;;
;;; This library 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
;;; Library General Public License for more details.
;;;
;;; You should have received a copy of the GNU Library General Public
;;; License along with this library; if not, write to the
;;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;;; Boston, MA 02111-1307 USA.
;;; This file provides definitions of every part of input-editing that
;;; can be defined without actually having loaded the input
;;; editor. This is so more input-editor using code can be loaded
;;; before loading Drei.
(in-package :clim-internals)
(defvar *use-goatee* nil
"If true, use the Goatee editing component instead of Drei. The
Goatee component is faster and more mature than Drei.")
(defvar *activation-gestures* nil
"The set of currently active activation gestures. The global
value of this must be NIL. The exact format of
`*activation-gestures*' is unspecified. `*activation-gestures*'
and the elements in it may have dynamic extent.")
(defvar *standard-activation-gestures* '(:newline :return)
"The default set of activation gestures. The exact set of
standard activation is unspecified, but must include the gesture
that corresponds to the #\Newline character. ")
(defvar *delimiter-gestures* nil
"The set of currently active delimiter gestures. The global
value of this must be NIL. The exact format of
`*delimiter-gestures*' is unspecified. `*delimiter-gestures*' and
the elements in it may have dynamic extent.")
(with-system-redefinition-allowed
(when (and (fboundp 'interactive-stream-p)
(not (typep (fdefinition 'interactive-stream-p)
'generic-function)))
(fmakunbound 'interactive-stream-p))
(defgeneric interactive-stream-p (stream)
(:method (stream)
(cl:interactive-stream-p stream))))
(defclass standard-input-editing-mixin ()
((%typeout-record :accessor typeout-record
:initform nil
:documentation "The output record (if any)
that is the typeout information for this
input-editing-stream. `With-input-editor-typeout' manages this
output record."))
(:documentation "A mixin implementing some useful standard
behavior for input-editing streams."))
(defmethod typeout-record :around ((stream standard-input-editing-mixin))
;; Can't do this in an initform, since we need to proper position...
(or (call-next-method)
(let ((record
(make-instance 'standard-sequence-output-record
:x-position 0
:y-position (bounding-rectangle-min-y
(input-editing-stream-output-record stream)))))
(stream-add-output-record (encapsulating-stream-stream stream)
record)
(setf (typeout-record stream) record))))
;;; These helper functions take the arguments of ACCEPT so that they
;;; can be used directly by ACCEPT.
(defun make-activation-gestures
(&key (activation-gestures nil activation-gestures-p)
(additional-activation-gestures nil additional-activations-p)
(existing-activation-gestures *activation-gestures*)
&allow-other-keys)
(cond (additional-activations-p
(append additional-activation-gestures existing-activation-gestures))
(activation-gestures-p
activation-gestures)
(t (or existing-activation-gestures
*standard-activation-gestures*))))
(defun make-delimiter-gestures
(&key (delimiter-gestures nil delimiter-gestures-p)
(additional-delimiter-gestures nil additional-delimiters-p)
(existing-delimiter-gestures *delimiter-gestures*)
&allow-other-keys)
(cond (additional-delimiters-p
(append additional-delimiter-gestures existing-delimiter-gestures))
(delimiter-gestures-p
delimiter-gestures)
(t existing-delimiter-gestures)))
(defmacro with-activation-gestures ((gestures &key override) &body body)
"Specifies a list of gestures that terminate input during the
execution of `body'. `Body' may have zero or more declarations as
its first forms. `Gestures' must be either a single gesture name
or a form that evaluates to a list of gesture names.
If the boolean `override' is true, then `gestures' will override
the current activation gestures. If it is false (the default),
then gestures will be added to the existing set of activation
gestures. `with-activation-gestures' must bind
`*activation-gestures*' to the new set of activation gestures.
See also the `:activation-gestures' and
`:additional-activation-gestures' options to `accept'."
;; XXX Guess this implies that gestures need to be defined at
;; compile time. Sigh. We permit both CLIM 2.0-style gesture names
;; and CLIM 2.2 style characters.
(let ((gesture-form (cond ((or (and (symbolp gestures)
(gethash gestures *gesture-names*))
(characterp gestures))
`(list ',gestures))
(t gestures)))
(gestures (gensym))
(override-var (gensym)))
`(let* ((,gestures ,gesture-form) ;Preserve evaluation order of arguments
(,override-var ,override)
(*activation-gestures* (apply #'make-activation-gestures
(if ,override-var
:activation-gestures
:additional-activation-gestures)
(list ,gestures))))
,@body)))
(defmacro with-delimiter-gestures ((gestures &key override) &body body)
"Specifies a list of gestures that terminate an individual
token, but not the entire input, during the execution of
`body'. `Body' may have zero or more declarations as its first
forms. `Gestures' must be either a single gesture name or a form
that evaluates to a list of gesture names.
If the boolean `override' is true, then `gestures' will override
the current delimiter gestures. If it is false (the default),
then gestures will be added to the existing set of delimiter
gestures. `With-delimiter-gestures' must bind
`*delimiter-gestures*' to the new set of delimiter
gestures.
See also the `:delimiter-gestures' and
`:additional-delimiter-gestures' options to `accept'."
;; XXX Guess this implies that gestures need to be defined at
;; compile time. Sigh. We permit both CLIM 2.0-style gesture names
;; and CLIM 2.2 style characters.
(let ((gesture-form (cond ((or (and (symbolp gestures)
(gethash gestures *gesture-names*))
(characterp gestures))
`(list ',gestures))
(t gestures)))
(gestures (gensym))
(override-var (gensym)))
`(let* ((,gestures ,gesture-form) ;Preserve evaluation order of arguments
(,override-var ,override)
(*delimiter-gestures* (make-delimiter-gestures
(if ,override-var
:delimiter-gestures
:additional-delimiter-gestures)
,gestures)))
,@body)))
(defun activation-gesture-p (gesture)
"Returns true if the gesture object `gesture' is an activation
gesture, otherwise returns false."
(loop for gesture-name in *activation-gestures*
when (gesture-matches-spec-p gesture gesture-name)
do (return t)
finally (return nil)))
(defun delimiter-gesture-p (gesture)
"Returns true if the gesture object `gesture' is a delimiter
gesture, otherwise returns false."
(loop for gesture-name in *delimiter-gestures*
when (gesture-matches-spec-p gesture gesture-name)
do (return t)
finally (return nil)))
(defmacro with-input-editor-typeout ((&optional (stream t) &rest args
&key erase)
&body body)
"Clear space above the input-editing stream `stream' and
evaluate `body', capturing output done to `stream'. Place will be
obtained above the input-editing area and the output put
there. Nothing will be displayed until `body' finishes. `Stream'
is not evaluated and must be a symbol. If T (the default),
`*standard-input*' will be used. `Stream' will be bound to an
`extended-output-stream' while `body' is being evaluated."
(declare (ignore erase))
(check-type stream symbol)
(let ((stream (if (eq stream t) '*standard-output* stream)))
`(invoke-with-input-editor-typeout
,stream
#'(lambda (,stream)
,@body)
,@args)))
(defgeneric invoke-with-input-editor-typeout (stream continuation &key erase)
(:documentation "Call `continuation' with a single argument, a
stream to do input-editor-typeout on."))
(defun sheet-move-output-vertically (sheet y delta-y)
"Move the output records of `sheet', starting at vertical
device unit offset `y' or below, down by `delta-y' device units,
then repaint `sheet'."
(unless (zerop delta-y)
(with-bounding-rectangle* (sheet-x1 sheet-y1 sheet-x2 sheet-y2) sheet
(declare (ignore sheet-x1 sheet-y1))
(map-over-output-records-overlapping-region
#'(lambda (record)
(multiple-value-bind (record-x record-y) (output-record-position record)
(when (> (+ record-y (bounding-rectangle-height record)) y)
(setf (output-record-position record)
(values record-x (+ record-y delta-y))))))
(stream-output-history sheet)
(make-bounding-rectangle 0 y sheet-x2 sheet-y2))
;; Only repaint within the visible region...
(with-bounding-rectangle* (viewport-x1 viewport-y1 viewport-x2 viewport-y2)
(or (pane-viewport-region sheet) sheet)
(declare (ignore viewport-y1))
(repaint-sheet sheet (make-bounding-rectangle viewport-x1 (- y (abs delta-y))
viewport-x2 viewport-y2))))))
(defmethod invoke-with-input-editor-typeout ((editing-stream standard-input-editing-mixin)
(continuation function) &key erase)
(with-accessors ((stream-typeout-record typeout-record)) editing-stream
;; Can't do this in an initform, as we need to set the proper
;; output record position.
(let* ((encapsulated-stream (encapsulating-stream-stream editing-stream))
(old-min-y (bounding-rectangle-min-y stream-typeout-record))
(old-height (bounding-rectangle-height stream-typeout-record))
(new-typeout-record (with-output-to-output-record (encapsulated-stream
'standard-sequence-output-record
record)
(unless erase
;; Steal the children of the old typeout record.
(map nil #'(lambda (child)
(setf (output-record-parent child) nil
(output-record-position child) (values 0 0))
(add-output-record child record))
(output-record-children stream-typeout-record))
;; Make sure new output is done
;; after the stolen children.
(stream-increment-cursor-position
encapsulated-stream 0 old-height))
(funcall continuation encapsulated-stream))))
(with-sheet-medium (medium encapsulated-stream)
(setf (output-record-position new-typeout-record) (values 0 old-min-y))
;; Calculate the height difference between the old typeout and the new.
(let ((delta-y (- (bounding-rectangle-height new-typeout-record) old-height)))
(multiple-value-bind (typeout-x typeout-y)
(output-record-position new-typeout-record)
(declare (ignore typeout-x))
;; Clear the old typeout...
(clear-output-record stream-typeout-record)
;; Move stuff for the new typeout record...
(sheet-move-output-vertically encapsulated-stream typeout-y delta-y)
;; Reuse the old stream-typeout-record...
(add-output-record new-typeout-record stream-typeout-record)
;; Now, let there be light!
(repaint-sheet encapsulated-stream stream-typeout-record)))))))
(defun clear-typeout (&optional (stream t))
"Blank out the input-editor typeout displayed on `stream',
defaulting to T for `*standard-output*'."
(with-input-editor-typeout (stream :erase t)
(declare (ignore stream))))
(defmacro with-input-editing ((&optional (stream t)
&rest args
&key input-sensitizer (initial-contents "")
(class ''standard-input-editing-stream class-provided-p))
&body body)
"Establishes a context in which the user can edit the input
typed in on the interactive stream `stream'. `Body' is then
executed in this context, and the values returned by `body' are
returned as the values of `with-input-editing'. `Body' may have
zero or more declarations as its first forms.
The stream argument is not evaluated, and must be a symbol that
is bound to an input stream. If stream is T (the default),
`*standard-input*' is used. If stream is a stream that is not an
interactive stream, then `with-input-editing' is equivalent to
progn.
`input-sensitizer', if supplied, is a function of two arguments,
a stream and a continuation function; the function has dynamic
extent. The continuation, supplied by CLIM, is responsible for
displaying output corresponding to the user's input on the
stream. The input-sensitizer function will typically call
`with-output-as-presentation' in order to make the output
produced by the continuation sensitive.
If `initial-contents' is supplied, it must be either a string or
a list of two elements, an object and a presentation type. If it
is a string, the string will be inserted into the input buffer
using `replace-input'. If it is a list, the printed
representation of the object will be inserted into the input
buffer using `presentation-replace-input'."
(setq stream (stream-designator-symbol stream '*standard-input*))
(with-keywords-removed (args (:input-sensitizer :initial-contents :class))
`(invoke-with-input-editing ,stream
#'(lambda (,stream) ,@body)
,input-sensitizer ,initial-contents
,(if class-provided-p
class
`(if *use-goatee* 'goatee-input-editing-stream
,class))
,@args)))
(defmacro with-input-position ((stream) &body body)
(let ((stream-var (gensym "STREAM")))
`(let* ((,stream-var ,stream)
(*current-input-stream* (and (typep ,stream-var
'input-editing-stream)
,stream-var))
(*current-input-position* (and *current-input-stream*
(stream-scan-pointer ,stream-var))))
,@body)))
(defun input-editing-rescan-loop (editing-stream continuation)
(let ((start-scan-pointer (stream-scan-pointer editing-stream)))
(loop (block rescan
(handler-bind ((rescan-condition
#'(lambda (c)
(declare (ignore c))
(reset-scan-pointer editing-stream start-scan-pointer)
;; Input-editing contexts above may be interested...
(return-from rescan nil))))
(return-from input-editing-rescan-loop
(funcall continuation editing-stream)))))))
(defgeneric finalize (editing-stream input-sensitizer)
(:documentation "Do any cleanup on an editing stream that is no
longer supposed to be used for editing, like turning off the
cursor, etc."))
(defmethod finalize ((stream input-editing-stream) input-sensitizer)
(clear-typeout stream)
(redraw-input-buffer stream))
(defgeneric invoke-with-input-editing
(stream continuation input-sensitizer initial-contents class)
(:documentation "Implements `with-input-editing'. `Class' is
the class of the input-editing stream to create, if necessary."))
(defmethod invoke-with-input-editing
(stream continuation input-sensitizer initial-contents class)
(declare (ignore input-sensitizer initial-contents class))
(funcall continuation stream))
(defmethod invoke-with-input-editing ((stream input-editing-stream)
continuation input-sensitizer
initial-contents class)
(unless (stream-rescanning-p stream)
(if (stringp initial-contents)
(replace-input stream initial-contents)
(presentation-replace-input stream
(first initial-contents)
(second initial-contents)
(stream-default-view stream))))
(call-next-method))
(defmethod invoke-with-input-editing :around ((stream extended-output-stream)
continuation
input-sensitizer
initial-contents
class)
(declare (ignore continuation input-sensitizer initial-contents class))
(letf (((cursor-visibility (stream-text-cursor stream)) nil))
(call-next-method)))
(defmethod invoke-with-input-editing :around (stream
continuation
input-sensitizer
initial-contents
class)
(declare (ignore continuation input-sensitizer initial-contents class))
(with-activation-gestures (*standard-activation-gestures*)
(call-next-method)))
(defgeneric input-editing-stream-output-record (stream)
(:documentation "Return the output record showing the display of the
input-editing stream `stream' values. This function does not
appear in the spec but is used by the command processing code for
layout and to implement a general with-input-editor-typeout."))
(defmethod input-editor-format ((stream t) format-string &rest format-args)
(unless (and (typep stream '#.*string-input-stream-class*)
(input-stream-p stream))
(apply #'format stream format-string format-args)))
(defun make-room (buffer pos n)
(let ((fill (fill-pointer buffer)))
(when (> (+ fill n)
(array-dimension buffer 0))
(adjust-array buffer (list (+ fill n))))
(incf (fill-pointer buffer) n)
(replace buffer buffer :start1 (+ pos n) :start2 pos :end2 fill)))
;;; Defaults for replace-input and presentation-replace-input.
(defvar *current-input-stream* nil)
(defvar *current-input-position* 0)
(defun read-token (stream &key
(input-wait-handler *input-wait-handler*)
(pointer-button-press-handler
*pointer-button-press-handler*)
click-only)
"Reads characters from the interactive stream `stream' until it
encounters a delimiter or activation gesture, or a pointer
gesture. Returns the accumulated string that was delimited by the
delimiter or activation gesture, leaving the delimiter
unread.
If the first character of typed input is a quotation mark (#\"),
then `read-token' will ignore delimiter gestures until another
quotation mark is seen. When the closing quotation mark is seen,
`read-token' will proceed as above.
`Click-only' is ignored for now.
`Input-wait-handler' and `pointer-button-press-handler' are as
for 34stream-read-gesture"
(declare (ignore click-only)) ;XXX For now
(let ((result (make-array 1
:adjustable t
:fill-pointer 0
:element-type 'character))
(in-quotes nil))
;; The spec says that read-token ignores delimiter gestures if the
;; first character is #\", until it sees another. OK... what about
;; other occurences of #\"? Guess we'll just accumulate them.
(loop for first-char = t then nil
for gesture = (read-gesture
:stream stream
:input-wait-handler input-wait-handler
:pointer-button-press-handler
pointer-button-press-handler)
do (cond ((or (null gesture)
(activation-gesture-p gesture)
(typep gesture 'pointer-button-event)
(and (not in-quotes)
(delimiter-gesture-p gesture)))
(loop-finish))
((characterp gesture)
(if (eql gesture #\")
(cond (first-char
(setq in-quotes t))
(in-quotes
(setq in-quotes nil))
(t (vector-push-extend gesture result)))
(vector-push-extend gesture result)))
(t nil))
finally (progn
(when gesture
(unread-gesture gesture :stream stream))
;; Return a simple string. XXX Would returning an
;; adjustable string be so bad?
(return (subseq result 0))))))
(defun write-token (token stream &key acceptably)
"This function is the opposite of `read-token' given the string
token, it writes it to the interactive stream stream. If
`acceptably' is true and there are any characters in the token
that are delimiter gestures (see the macro
`with-delimiter-gestures'), then `write-token' will surround the
token with quotation marks (#\").
Typically, `present' methods will use `write-token' instead of
`write-string'."
(let ((put-in-quotes (and acceptably (some #'delimiter-gesture-p token))))
(when put-in-quotes
(write-char #\" stream))
(write-string token stream)
(when put-in-quotes
(write-char #\" stream))))
;;; Signalling Errors Inside present (sic)
(define-condition simple-parse-error (simple-condition parse-error)
()
(:documentation "The error that is signalled by
`simple-parse-error'. This is a subclass of `parse-error'.
This condition handles two initargs, `:format-string' and
`:format-arguments', which are used to specify a control string
and arguments for a call to `format'."))
(defun simple-parse-error (format-string &rest format-args)
"Signals a `simple-parse-error' error while parsing an input
token. Does not return. `Format-string' and `format-args' are as
for format."
(error 'simple-parse-error
:format-control format-string :format-arguments format-args))
(define-condition input-not-of-required-type (parse-error)
((string :reader not-required-type-string :initarg :string)
(type :reader not-required-type-type :initarg :type))
(:report (lambda (condition stream)
(format stream "Input ~S is not of required type ~S"
(not-required-type-string condition)
(not-required-type-type condition))))
(:documentation "The error that is signalled by
`input-not-of-required-type'. This is a subclass of
`parse-error'.
This condition handles two initargs, `:string' and `:type', which
specify a string to be used in an error message and the expected
presentation type."))
(defun input-not-of-required-type (object type)
"Reports that input does not satisfy the specified type by
signalling an `input-not-of-required-type' error. `Object' is a
parsed object or an unparsed token (a string). `Type' is a
presentation type specifier. Does not return."
(error 'input-not-of-required-type :string object :type type))
;;; 24.5 Completion
(defvar *completion-gestures* '(:complete)
"A list of the gesture names that cause `complete-input' to
complete the user's input as fully as possible. The exact global
contents of this list is unspecified, but must include the
`:complete' gesture name.")
(defvar *help-gestures* '(:help)
"A list of the gesture names that cause `accept' and
`complete-input' to display a (possibly input context-sensitive)
help message, and for some presentation types a list of
possibilities as well. The exact global contents of this list is
unspecified, but must include the `:help' gesture name.")
(defvar *possibilities-gestures* '(:possibilities)
"A list of the gesture names that cause `complete-input' to
display a (possibly input context-sensitive) help message and a
list of possibilities. The exact global contents of this list is
unspecified, but must include the `:possibilities' gesture
name.")
(define-condition simple-completion-error (simple-parse-error)
((input-so-far :reader completion-error-input-so-far
:initarg :input-so-far))
(:documentation "The error that is signalled by
`complete-input' when no completion is found. This is a subclass
of `simple-parse-error'."))
;;; wrapper around event-matches-gesture-name-p to match against characters too.
(defgeneric gesture-matches-spec-p (gesture spec)
(:documentation "Match a gesture against a gesture name or character."))
(defmethod gesture-matches-spec-p (gesture (spec symbol))
(event-matches-gesture-name-p gesture spec))
(defmethod gesture-matches-spec-p ((gesture character) (spec character))
(char-equal gesture spec))
(defmethod gesture-matches-spec-p (gesture spec)
(declare (ignore gesture spec))
nil)
(defun gesture-match (gesture list)
"Returns t if gesture matches any gesture spec in list."
(some #'(lambda (name)
(gesture-matches-spec-p gesture name))
list))
;;; Helpers for complete-input, which is just getting too long.
(defun complete-gesture-p (gesture)
(or (delimiter-gesture-p gesture) (activation-gesture-p gesture)))
;;; Break out rescanning case for complete-input.
;;;
;;; funky logic; we don't know if we're still rescanning until after the call
;;; to read-gesture.
(defun complete-input-rescan (stream func partial-completers so-far
allow-any-input)
(when (stream-rescanning-p stream)
(loop for gesture = (read-gesture :stream stream :timeout 0)
while (and gesture (stream-rescanning-p stream))
if (complete-gesture-p gesture)
do (let (input success object nmatches)
(when (gesture-match gesture partial-completers)
(setf (values input success object nmatches)
(funcall func (subseq so-far 0) :complete-limited)))
(unless (and (numberp nmatches) (> nmatches 0))
;; Not a partial match; better be a total match
(setf (values input success object)
(funcall func (subseq so-far 0) :complete))
(if (or success allow-any-input)
(progn
(unread-gesture gesture :stream stream)
(return-from complete-input-rescan
(values object t input)))
;; This used to be an error, but no one thought
;; that was a really great idea.
(signal 'simple-completion-error
:format-control "complete-input: While rescanning,~
can't match ~A~A"
:format-arguments (list so-far gesture)
:input-so-far so-far))))
end
do (vector-push-extend gesture so-far)
finally (when gesture
(unread-gesture gesture :stream stream))))
nil)
(defun possibilities-for-menu (possibilities)
(loop for (display object) in possibilities
collect `(,display :value ,object)))
(defun possibility-printer (possibility ptype stream)
"A default function for printing a possibility. Suitable for
used as value of `:possibility-printer' in calls to
`complete-input'"
(with-output-as-presentation (stream possibility ptype)
(write-string (first possibility) stream)))
(defun print-possibilities (possibilities possibility-printer stream)
"Write `possibitilies' to `stream', using
`possibility-printer'. `Possibilities' must be a list of
input-completion possibilities. `Stream' must be an input-editing
stream. Output will be done to its typeout."
(with-input-editor-typeout (stream :erase t)
(surrounding-output-with-border (stream :shape :drop-shadow :background +cornsilk1+)
(surrounding-output-with-border (stream :shape :rectangle)
(let ((ptype `(completion ,possibilities)))
(format-items possibilities
:stream stream
:printer #'(lambda (possibility stream)
(funcall possibility-printer
possibility
ptype
stream))))))))
;;; Helper returns gesture (or nil if gesture shouldn't be part of the input)
;;; and completion mode, if any.
(defvar *completion-possibilities-continuation* nil)
(defun read-completion-gesture (stream
partial-completers
help-displays-possibilities)
(flet ((possibilitiesp (gesture)
(or (gesture-match gesture *possibilities-gestures*)
(and help-displays-possibilities
(gesture-match gesture *help-gestures*)))))
(let ((*completion-possibilities-continuation*
#'(lambda ()
(return-from read-completion-gesture
(values nil :possibilities)))))
(handler-bind ((accelerator-gesture
#'(lambda (c)
(let ((gesture (accelerator-gesture-event c)))
(when (possibilitiesp gesture)
(return-from read-completion-gesture
(values nil :possibilities)))))))
(let ((gesture (read-gesture :stream stream)))
(values gesture
(cond ((possibilitiesp gesture)
:possibilities)
((gesture-match gesture partial-completers)
:complete-limited)
((gesture-match gesture *completion-gestures*)
:complete-maximal)
((complete-gesture-p gesture)
:complete)
(t nil))))))))
(defparameter *trace-complete-input* nil)
(defun complete-input (stream func &key
partial-completers allow-any-input
(possibility-printer #'possibility-printer)
(help-displays-possibilities t))
(let ((so-far (make-array 1 :element-type 'character :adjustable t
:fill-pointer 0))
(*accelerator-gestures* (append *help-gestures*
*possibilities-gestures*
*accelerator-gestures*)))
(with-input-position (stream)
(flet ((insert-input (input)
(adjust-array so-far (length input)
:fill-pointer (length input))
(replace so-far input)
;; XXX: Relies on non-specified behavior of :rescan.
(replace-input stream input :rescan nil)))
(multiple-value-bind (object success input)
(complete-input-rescan stream func partial-completers
so-far allow-any-input)
(when success
(return-from complete-input (values object success input))))
(loop
(multiple-value-bind (gesture mode)
(read-completion-gesture stream
partial-completers
help-displays-possibilities)
(if mode
(multiple-value-bind
(input success object nmatches possibilities)
(funcall func (subseq so-far 0) mode)
(when (and (zerop nmatches)
(eq mode :complete-limited)
(complete-gesture-p gesture))
;; Gesture is both a partial completer and a
;; delimiter e.g., #\space. If no partial match,
;; try again with a total match.
(setf (values input success object nmatches possibilities)
(funcall func (subseq so-far 0) :complete))
(setf mode :complete))
;; Preserve the delimiter
(when (and success (eq mode :complete))
(unread-gesture gesture :stream stream))
;; Get completion from menu
(when *trace-complete-input*
(format *trace-output* "nmatches = ~A, mode = ~A~%"
nmatches mode))
(when (and (> nmatches 0) (eq mode :possibilities))
(print-possibilities possibilities possibility-printer stream)
(redraw-input-buffer stream)
(let ((possibility
(handler-case
(with-input-context (`(completion ,possibilities) :override nil)
(object type event)
(prog1 nil (read-gesture :stream stream :peek-p t))
(t object))
(abort-gesture () nil))))
(if possibility
(setf (values input success object nmatches)
(values (first possibility) t (second possibility) 1))
(setf success nil
nmatches 0))))
(unless (and (eq mode :complete) (not success))
(if (> nmatches 0)
(insert-input input)
(beep)))
(cond ((and success (eq mode :complete))
(return-from complete-input
(values object success input)))
((activation-gesture-p gesture)
(if allow-any-input
(return-from complete-input
(values nil t (subseq so-far 0)))
(error 'simple-completion-error
:format-control "Input ~S does not match"
:format-arguments (list so-far)
:input-so-far so-far)))))
(vector-push-extend gesture so-far))))))))
;;; helper function
(defun left-prefix (string1 string2 &key (end nil))
"Returns the common prefix of string1 and string2, up to end"
(let* ((end1 (if end
(min (length string1) end)
nil))
(end2 (if end
(min (length string2) end)
nil))
(mismatch (mismatch string1 string2 :test #'char-equal
:end1 end1 :end2 end2)))
(cond (mismatch
(subseq string1 0 mismatch))
(end
(subseq string1 0 end))
(t string1))))
(defun complete-from-generator (initial-string generator delimiters &key
(action :complete)
(predicate (constantly t)))
(when (eq action :possibilities)
(return-from complete-from-generator
(complete-from-generator-possibilities initial-string
generator
predicate)))
(let ((initial-string-len (length initial-string))
(candidate-match nil)
(matches 0)
(object nil)
(identical nil)
(identical-match nil)
(identical-object nil)
(actual-match nil))
(flet ((suggester (str obj)
(unless (funcall predicate obj)
(return-from suggester nil))
(let ((partial-match-end
(and (eq action :complete-limited)
(>= (length str) initial-string-len)
(position-if #'(lambda (c) (member c delimiters))
str
:start initial-string-len))))
(when (and (eq action :complete-limited)
(null partial-match-end))
(return-from suggester nil))
(unless partial-match-end
(setq partial-match-end (1- (length str))))
(let ((mismatch-initial (mismatch initial-string str
:test #'char-equal)))
(cond ((and mismatch-initial
(>= mismatch-initial (length initial-string)))
(incf matches)
(unless candidate-match
(setq object obj))
(setf candidate-match
(cond (candidate-match
(left-prefix candidate-match
str
:end (1+ partial-match-end)))
(partial-match-end
(subseq str 0 (1+ partial-match-end)))
(t str))
actual-match str))
((null mismatch-initial)
(incf matches)
;; If there's a longer match we want to find it.
(if (eq action :complete-maximal)
(progn
(setf identical-match str)
(setf identical-object obj))
(progn
(setf candidate-match str)
(setf object obj)))
(setf identical t)))))))
(funcall generator initial-string #'suggester)
(let ((partial-match-before-end (and (eq action :complete-limited)
(eql matches 1)
(< (length candidate-match)
(length actual-match)))))
(values (or candidate-match identical-match initial-string)
(or (and identical
(or (not (eq action :complete-maximal))
(eql matches 1)))
(and (eql matches 1)
(not partial-match-before-end)))
(if (eq action :complete-maximal)
(cond ((and (eql matches 2) identical-match)
object)
((and identical-match (eql matches 1))
identical-object)
((eql matches 1)
object))
(and (or identical (and (eql matches 1)
(not partial-match-before-end)))
object))
matches
nil)))))
;;; The possibilities action is different enough that I don't want to add to
;;; the spaghetti above...
(defun complete-from-generator-possibilities
(initial-string generator predicate)
(let ((possibilities nil)
(nmatches 0)
(initial-len (length initial-string)))
(flet ((suggester (str obj)
(unless (funcall predicate obj)
(return-from suggester nil))
(when (>= (or (mismatch initial-string str :test #'char-equal)
(length initial-string))
initial-len)
(incf nmatches)
(push (list str obj) possibilities))))
(funcall generator initial-string #'suggester)
(if (and (eql nmatches 1)
(string-equal initial-string (caar possibilities)))
;; return values are as from complete-from-generator, qv.
(values (caar possibilities)
t
(cdar possibilities)
nmatches
possibilities)
(values initial-string nil nil nmatches (sort possibilities #'string-lessp :key #'car))))))
(defun complete-from-possibilities (initial-string completions delimiters &key
(action :complete)
(predicate (constantly t))
(name-key #'car)
(value-key #'second))
(flet ((generator (input-string suggester)
(declare (ignore input-string))
(do-sequence (possibility completions)
(funcall suggester
(funcall name-key possibility)
(funcall value-key possibility)))))
(complete-from-generator initial-string #'generator delimiters
:action action
:predicate predicate)))
(defun suggest (completion object)
"Specifies one possibility for
`completing-from-suggestions'. `Completion' is a string, the
printed representation of object. `Object' is the internal
representation.
Calling this function outside of the body of
`completing-from-suggestions' is an error."
(declare (ignore completion object))
(error
"SUGGEST called outside of lexical scope of COMPLETING-FROM-SUGGESTIONS" ))
(defmacro completing-from-suggestions ((stream &rest args) &body body)
"Reads input from the input editing stream `stream', completing
over a set of possibilities generated by calls to `suggest'
within `body'. `Body' may have zero or more declarations as its
first forms.
`Completing-from-suggestions' returns three values, `object',
`success', and `string'.
The stream argument is not evaluated, and must be a symbol that
is bound to a stream. If `stream' t is (the default),
`*standard-input*' is used. `Partial-completers',
`allow-any-input', and `possibility-printer' are as for
`complete-input'.
Implementations will probably use `complete-from-generator' to
implement this."
(when (eq stream t)
(setq stream '*standard-input*))
(let ((generator (gensym "GENERATOR"))
(input-string (gensym "INPUT-STRING"))
(suggester (gensym "SUGGESTER")))
`(flet ((,generator (,input-string ,suggester)
(declare (ignore ,input-string))
(flet ((suggest (completion object)
(funcall ,suggester completion object)))
,@body)))
;; This sucks, but we can't use args to the macro directly because
;; we want the partial-delimiters argument and we need to insure its
;; proper evaluation order with everything else.
(let* ((complete-input-args (list ,@args))
(partial-completers (getf complete-input-args
:partial-completers
nil)))
(apply #'complete-input
,stream
#'(lambda (so-far mode)
(complete-from-generator so-far
#',generator
partial-completers
:action mode))
complete-input-args)))))
;;; Infrasructure for detecting empty input, thus allowing accept-1
;;; to supply a default.
(defmacro handle-empty-input ((stream) input-form &body handler-forms)
"Establishes a context on `stream' (a `standard-input-editing-stream') in
which empty input entered in `input-form' may transfer control to
`handler-forms'. Empty input is assumed when a simple-parse-error is
signalled and there is a delimeter gesture or activation gesture in the
stream at the position where `input-form' began its input. The gesture that
caused the transfer remains to be read in `stream'. Control is transferred to
the outermost `handle-empty-input' form that is empty.
Note: noise strings in the buffer, such as the prompts of recursive calls to
`accept', cause input to not be empty. However, the prompt generated by
`accept' is generally not part of its own empty input context."
(with-gensyms (input-cont handler-cont)
`(flet ((,input-cont ()
,input-form)
(,handler-cont ()
,@handler-forms))
(declare (dynamic-extent #',input-cont #',handler-cont))
(invoke-handle-empty-input ,stream #',input-cont #',handler-cont))))
(define-condition empty-input-condition (simple-condition)
((stream :reader empty-input-condition-stream :initarg :stream)))
;;; The code that signalled the error might have consumed the gesture, or
;;; not.
;;; XXX Actually, it would be a violation of the `accept' protocol to consume
;;; the gesture, but who knows what random accept methods are doing.
(defun empty-input-p
(stream begin-scan-pointer activation-gestures delimiter-gestures)
(let ((scan-pointer (stream-scan-pointer stream))
(fill-pointer (fill-pointer (stream-input-buffer stream))))
;; activated?
(cond ((and (eql begin-scan-pointer scan-pointer)
(eql scan-pointer fill-pointer))
t)