-
-
Notifications
You must be signed in to change notification settings - Fork 93
/
racket-show.el
226 lines (198 loc) · 9.29 KB
/
racket-show.el
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
;;; racket-show.el -*- lexical-binding: t -*-
;; Copyright (c) 2013-2022 by Greg Hendershott.
;; Portions Copyright (C) 1985-1986, 1999-2013 Free Software Foundation, Inc.
;; Author: Greg Hendershott
;; URL: https://github.com/greghendershott/racket-mode
;; SPDX-License-Identifier: GPL-3.0-or-later
(require 'racket-util)
(require 'racket-custom)
(require 'pos-tip nil t) ;noerror
(require 'cl-macs)
(require 'face-remap)
(defun racket-show (str &optional pos transient-p)
"Apply STR and POS to functions in the variable `racket-show-functions'.
See that for meaning of STR and POS.
When TRANSIENT-P, we automatically hide before the next command
runs. Otherwise, the UI might remain visible indefinitely --
depending on how a racket-show function displays --- until a
subsequent call to `racket-show' to hide or to show a new value.
Either behavior could be desirable depending on the caller's use
case. For example `racket-xp-mode' wants the display to remain
visible, if possible, even when the user chooses a command to
select another window; only point motion hides or shows a
different annotation."
(unless (string-or-null-p str)
(signal 'wrong-type-argument `(string-or-null-p ,str)))
(when (racket--non-empty-string-p str)
(unless (number-or-marker-p pos)
(signal 'wrong-type-argument `(number-or-marker-p ,pos))))
(run-hook-with-args 'racket-show-functions str pos)
(if transient-p
(add-hook 'pre-command-hook #'racket-show--pre-command-hook nil t)
(remove-hook 'pre-command-hook #'racket-show--pre-command-hook t)))
(defun racket-show--pre-command-hook ()
"Hide and remove ourselves as a pre-command-hook."
(run-hook-with-args 'racket-show-functions "" nil)
(remove-hook 'pre-command-hook #'racket-show--pre-command-hook t))
(defun racket-show-echo-area (str &optional _pos)
"Show things in the echo area.
A value for the variable `racket-show-functions'.
This does /not/ add STR to the \"*Messages*\" log buffer."
(when str
(let ((message-log-max nil)) ;don't log
(message "%s" str))))
(defun racket-show-header-line (str &optional _pos)
"Show things using a buffer header line.
A value for the variable `racket-show-functions'.
When there is nothing to show, keep a blank header-line. That
way, the buffer below doesn't \"jump up and down\" by a line as
messages appear and disappear. Only when V is nil do we remove
the header line."
(setq-local header-line-format
(and str
(format "%s" (racket--only-first-line str)))))
(defun racket--only-first-line (str)
(save-match-data
(string-match (rx (group (* (not (any ?\n))))) str)
(match-string 1 str)))
(defun racket-show-pos-tip (str &optional pos)
"Show things using `pos-tip-show' if available.
A value for the variable `racket-show-functions'."
(when (and (fboundp 'x-hide-tip)
(fboundp 'x-show-tip)
(not (memq window-system (list nil 'pc)))
(fboundp 'pos-tip-show)
(fboundp 'pos-tip-hide))
(if (racket--non-empty-string-p str)
(pos-tip-show str nil pos)
(pos-tip-hide))))
(defvar-local racket--pseudo-tooltip-overlays nil)
(defun racket-show-pseudo-tooltip (str &optional pos)
"Show using an overlay that resembles a tooltip.
This is nicer than `racket-show-pos-tip' because it:
- Doesn't flicker while navigating.
- Doesn't disappear after a timeout.
- Performs well when `x-gtk-use-system-tooltips' is nil.
On the other hand, this does not look as nice when displaying
text that spans multiple lines or is too wide to fit the window.
In that case, we simply left-justify everything and do not draw
any border."
(racket--delete-pseudo-tooltip-overlays)
(when (racket--non-empty-string-p str)
(setq-local racket--pseudo-tooltip-overlays
(racket--make-pseudo-tooltip-overlays str pos))))
(defun racket--delete-pseudo-tooltip-overlays ()
(dolist (ov racket--pseudo-tooltip-overlays)
(delete-overlay ov))
(setq-local racket--pseudo-tooltip-overlays nil))
(defun racket--make-pseudo-tooltip-overlays (text pos)
"Create one or more overlays for a pseudo tooltip, returning them in a list."
(if (or (string-match-p "\n" text)
(< (window-width) (+ (string-width text) 2))
(and text-scale-mode (< 0 text-scale-mode-amount)))
;; When text is multi-line or too wide, we don't try to simulate
;; a tooltip, exactly. Instead we simply "insert" left
;; justified, before the next line.
(let* ((text (propertize (concat text "\n")
'face
`(:inherit default
:foreground ,(face-foreground 'tooltip)
:background ,(face-background 'tooltip))))
(eol (racket--eol pos))
(ov (make-overlay eol (1+ eol))))
(overlay-put ov 'after-string text)
(list ov))
;; Else we simulate a tooltip. The only question is where, and the
;; overlay(s) necessary to achieve that.
(let*
((text (propertize (concat " " text " ")
'face
`(:inherit default
:foreground ,(face-foreground 'tooltip)
:background ,(face-background 'tooltip)
:box (:line-width -1))))
(text-width (string-width text))
(bol (racket--bol pos))
(eol (racket--eol pos)))
;; If there is room after end of same line, show there.
(if (< (+ text-width 1) (- (window-width) (- eol bol)))
(let ((ov (make-overlay (1- eol) eol)))
(overlay-put ov 'after-string (concat " " text))
(list ov))
;; Otherwise we simulate a tooltip displayed one line below
;; pos, and one column right (although it might start further
;; left depending on window-width) "over" any existing text.
(let*
(;; Position the tooltip on the next line, indented to `pos'
;; -- but not so far it ends off right edge.
(indent (max 0 (min (- pos bol)
(- (window-width) text-width 2))))
(beg (+ eol indent 1))
(next-eol (racket--eol (1+ eol))))
;; If the tip starts before next-eol, create an overlay with
;; the 'display property, covering the span of the tooltip
;; text but not beyond next-eol.
;;
;; As a further wrinkle, when the overlay does not cover the
;; entire rest of the line, our new text might not be
;; exactly the same pixel width as the text we replace --
;; causing the remaining text to shift. This can happen e.g.
;; due to Unicode characters like λ. Furthermore, our
;; replacement text can be two pixels wider because :box
;; (:line-width -1) doesn't seem to work as advertised.
;;
;; To avoid this, we add _another_ overlay simply to replace
;; the character following our tooltip with a space of the
;; necessary pixel width to keep things aligned. Although
;; covering the character with a space isn't great -- even
;; if you justify it as a sort of "shadow" (?) -- it's
;; better than having the remainder of the line jiggle as
;; the tooltip apears and disappears.
(if (< beg next-eol)
(cl-flet ((text-pixel-width
(beg end)
(car (window-text-pixel-size nil beg end))))
(let* ((end (min next-eol (+ beg text-width)))
(ov (make-overlay beg end))
(old (text-pixel-width (1+ eol) end))
(_ (overlay-put ov 'display text))
(new (text-pixel-width (1+ eol) end))
(diff (- new old)))
(cons
ov
(when (and (not (zerop diff))
(< end next-eol))
(let* ((ov-spacer (make-overlay end (1+ end)))
(width (text-pixel-width end (1+ end)))
(space-width (abs (- width diff))))
(overlay-put ov-spacer
'display
`(space
:width (,space-width)))
(list ov-spacer))))))
;; Else the tip starts after next-eol. So, create an overlay
;; on the newline, and use an after-string, where we prefix
;; enough blank spaces before the tooltip text itself to get
;; the desired indent.
(let* ((ov (make-overlay (1- next-eol) next-eol))
(blanks (make-string (- beg next-eol) 32)))
(overlay-put ov 'after-string (concat blanks text))
(list ov))))))))
(defun racket--bol (pos)
"Given POS return line beginning position."
(save-excursion
(goto-char pos)
(if visual-line-mode
(beginning-of-visual-line)
(beginning-of-line))
(point)))
(defun racket--eol (pos)
"Given POS return line ending position."
(save-excursion
(goto-char pos)
(if visual-line-mode
(end-of-visual-line)
(end-of-line))
(point)))
(provide 'racket-show)
;; racket-show.el ends here