forked from kubernetes-el/kubernetes-el
-
Notifications
You must be signed in to change notification settings - Fork 0
/
kubernetes-ast.el
313 lines (248 loc) · 11.9 KB
/
kubernetes-ast.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
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
;;; kubernetes-ast.el --- Rendering AST. -*- lexical-binding: t; -*-
;;; Commentary:
;; Implements an interpreter for a simple layout DSL for magit sections.
;;; Code:
(require 'cl-lib)
(require 'magit-section)
(require 'subr-x)
;; Derived component support.
(defconst kubernetes-ast--components (make-hash-table :test #'eq)
"A mapping from the name of a component to its interpretation function.
When traversing a rendering AST, any list beginning with a symbol
is interpreted as a component reference. That symbol is used to
look up an interpretation function in this table. That function is
applied to any remaining elements of that cons.
The result of a function in this hash-table should be a new
rendering AST, or a string value to be inserted directly.")
(defmacro kubernetes-ast-define-component (name arglist &rest body)
"Define a rendering component.
NAME is the name of the component, which may thereafter be
referenced directly in rendering ASTs.
ARGLIST is the arguments that must be supplied to construct the
component.
BODY is the definition of the component."
(declare (indent 2))
(cl-assert (symbolp name))
(cl-assert (listp arglist))
(let ((fname (intern (format "kubernetes-ast--generated--%s" name)))
(docstring (format "Auto-generated component constructor function.
Creates instances of %s components, which may be referred to as
such in rendering ASTs." name)))
`(progn
(cl-defun ,fname ,arglist ,docstring ,@body)
(puthash ',name #',fname kubernetes-ast--components))))
(kubernetes-ast-define-component line (inner-ast)
`(,inner-ast
(padding)))
(kubernetes-ast-define-component key-value (width key value)
(cl-assert (numberp width) t)
(cl-assert (<= 0 width) t)
(cl-assert (stringp key) t)
(cl-assert (stringp value) t)
(let* ((fmt-string (concat "%-" (number-to-string width) "s"))
(str (concat (propertize (format fmt-string (concat key ": ")) 'face 'magit-section-heading)
value)))
(unless (string-blank-p (buffer-substring (line-beginning-position) (line-end-position)))
(newline))
`(copy-prop ,value (line ,str))))
(kubernetes-ast-define-component nav-prop (spec &rest inner-ast)
`(propertize (kubernetes-nav ,spec)
,inner-ast))
(kubernetes-ast-define-component copy-prop (copy-str &rest inner-ast)
(cl-assert (stringp copy-str) t)
`(propertize (kubernetes-copy ,copy-str)
,inner-ast))
;; Special operations.
(defun kubernetes-ast-put-delete-mark-on-line-at-pt (point)
(save-excursion
(goto-char point)
(goto-char (line-beginning-position))
(let* ((existing-props (text-properties-at (point)))
(props (append existing-props '(face kubernetes-delete-mark)))
(mark-str (concat (apply #'propertize "D" props)
(apply #'propertize " " existing-props))))
(cond
((member 'kubernetes-delete-mark existing-props)
nil)
((looking-at-p (rx bol space space))
(delete-char 2)
(insert mark-str))
(t
(insert mark-str))))))
;; AST interpreter.
(defconst kubernetes-ast--indentation-width 2)
(defconst kubernetes-ast--space ?\ )
(defsubst kubernetes-ast--indentation (indent-level)
(make-string (* indent-level kubernetes-ast--indentation-width) kubernetes-ast--space))
(defsubst kubernetes-ast--eval-string (s indent-level)
(let ((value (if (string-empty-p (buffer-substring (line-beginning-position) (point)))
(concat (kubernetes-ast--indentation indent-level) s)
s)))
(insert value)))
(defsubst kubernetes-ast--finalize-heading (start-pos)
;; This implementation is adapted from `magit-insert-heading'.
;; Apply heading face if no other face is set.
(let ((heading (buffer-substring start-pos (line-end-position))))
(unless (next-single-property-change 0 'face (concat "0" heading))
(add-text-properties start-pos (point) '(face magit-section-heading))))
(unless (bolp)
(insert ?\n))
;; Update containing section to point to this heading.
(setf (oref magit-insert-section--current content) (point-marker)))
(defsubst kubernetes-ast--finalize-delete-marks (start-pos)
(let ((end-line (line-number-at-pos)))
(save-excursion
(goto-char start-pos)
(kubernetes-ast-put-delete-mark-on-line-at-pt (point))
(while (< (line-number-at-pos) end-line)
(kubernetes-ast-put-delete-mark-on-line-at-pt (point))
(forward-line 1)))))
(defsubst kubernetes-ast--finalize-list-item (start-pos)
(save-excursion
(goto-char start-pos)
(goto-char (line-beginning-position))
(skip-chars-forward " ")
(unless (and (<= (+ 2 (point)) (line-end-position))
(string-equal "- " (buffer-substring (point) (+ 2 (point)))))
(delete-char -2)
(insert "- "))))
(defun kubernetes-ast--append-sentinel (instructions sentinel)
(append (list instructions) (list sentinel)))
(defun kubernetes-ast-eval (ast &optional indent-level)
"Evaluate AST as a set of instructions for inserting text into the current buffer."
;; The evaluator is implemented as a loop over an instruction stack. The
;; `instruction-stack' variable is a stack of AST instructions, the head of
;; which is the instruction to interpret. Its initial value is set to the
;; input to this function. After an instruction is interpreted, the item at
;; the top of the stack is popped. The loop ends when there are no more
;; instructions on the stack.
;;
;; If nested instructions are encountered in the AST, they are pushed onto the
;; stack, generally with a sentinel instruction to restore previous
;; interpreter state.
(let ((instruction-stack (list ast))
(indent-level (or indent-level 0)))
(while instruction-stack
(pcase (car instruction-stack)
;; Strings are inserted directly, possibly with indentation.
((and (pred stringp) s)
(kubernetes-ast--eval-string s indent-level)
(!cdr instruction-stack))
;; Padding gets some special error checking to make sure it has no inner
;; AST, since I get `padding' and `indent' mixed up all the time.
((and `(padding . ,_rest) (guard _rest))
(error "Padding takes no arguments"))
(`(padding)
(newline)
(!cdr instruction-stack))
;; Indentation
;;
;; The current indentation level is tracked by the interpreter. When an
;; `indent' directive is encountered, the indent level is incremented
;; and the inner AST is pushed to the stack with a sentinel appended.
;; When the sentinel is encountered, the indentation level is decreased.
(`(indent . ,inner-ast)
(let ((next (kubernetes-ast--append-sentinel inner-ast 'kubernetes-ast--indent-sentinel)))
(setq indent-level (1+ indent-level))
(!cdr instruction-stack)
(!cons next instruction-stack)))
(`kubernetes-ast--indent-sentinel
(setq indent-level (1- indent-level))
(!cdr instruction-stack))
;; Properties
;;
;; To propertize some inserted text, the inner AST is pushed to the
;; stack with a sentinel appended. The sentinel records the properties
;; to apply and the start position of the span. Once the sentinel is
;; encountered, the end position of the span is known and properties can
;; be applied.
(`(propertize ,spec . ,inner-ast)
(let ((next (kubernetes-ast--append-sentinel inner-ast `(kubernetes-ast--propertize-sentinel ,(point) ,spec))))
(!cdr instruction-stack)
(!cons next instruction-stack)))
(`(kubernetes-ast--propertize-sentinel ,start ,spec)
(add-text-properties start (point) spec)
(!cdr instruction-stack))
;; Deletion marks
;;
;; Deletion marks are applied to every line of the inner AST, so the
;; inner AST is pushed to the stack with a sentinel that records the
;; start position. Once the sentinel is encountered, the range of lines
;; that must be modified is known and the marks are written.
(`(mark-for-delete . ,inner-ast)
(let ((next (kubernetes-ast--append-sentinel inner-ast `(kubernetes-ast--mark-for-delete-sentinel . ,(point)))))
(!cdr instruction-stack)
(!cons next instruction-stack)))
(`(kubernetes-ast--mark-for-delete-sentinel . ,start)
(kubernetes-ast--finalize-delete-marks start)
(!cdr instruction-stack))
;; Bulleted lists
;;
;; A bulleted list is decomposed into a sequence of instructions, each
;; of which tracks its buffer positions using sentinel values.
;;
;; The bullet group is indented, and each item's start position is
;; recorded in a sentinel value. When an item's sentinel is encountered,
;; the item's dash is written to the buffer.
(`(list . ,items)
(let ((next `(indent ,@(--map `(kubernetes-ast--list-item . ,it) items))))
(!cdr instruction-stack)
(!cons next instruction-stack)))
(`(kubernetes-ast--list-item . ,inner-ast)
(let ((next (kubernetes-ast--append-sentinel inner-ast `(kubernetes-ast--list-item-sentinel . ,(point)))))
(!cdr instruction-stack)
(!cons next instruction-stack)))
(`(kubernetes-ast--list-item-sentinel . ,start)
(kubernetes-ast--finalize-list-item start)
(!cdr instruction-stack))
;; Headings
;;
;; Heading insertion requires interpretation of an inner AST to build
;; the heading text. A special sentinel is appended to the inner AST
;; that tells the interpreter to finalise the heading after interpreting
;; the inner value.
(`(heading ,inner-ast)
(unless magit-insert-section--current (error "Eval AST: Inserting a heading, but not in a section"))
(let ((next (kubernetes-ast--append-sentinel inner-ast `(kubernetes-ast--heading-sentinel . ,(point)))))
(!cdr instruction-stack)
(!cons next instruction-stack)))
(`(kubernetes-ast--heading-sentinel . ,start-pos)
(kubernetes-ast--finalize-heading start-pos)
(!cdr instruction-stack))
;; Sections
;;
;; KLUDGE: The section insertion logic in magit has complex state. It's
;; easier just to evaluate recursively than try to reproduce that logic
;; in the interpreter. This is safe so long as section nesting doesn't
;; approach `max-lisp-eval-depth'.
(`(section (,sym ,hide) . ,inner)
(!cdr instruction-stack)
(eval `(magit-insert-section (,sym nil ,hide)
(kubernetes-ast-eval ',inner ,indent-level))))
;; Custom components
;;
;; If the current instruction is a list and its head is a symbol, look
;; it up in the component definition table. If the lookup succeeds,
;; evaluate the component's constructor function to derive an AST, and
;; push that AST onto the stack.
((and `(,component . ,args)
(guard component)
(guard (symbolp component)))
(!cdr instruction-stack)
(if-let (constructor (gethash component kubernetes-ast--components))
(!cons (apply constructor args) instruction-stack)
(error "Component not defined: %s" component)))
;; Lists of instructions
;;
;; If the list being scrutinised does not begin with a symbol, it is
;; assumed to be a sequence of instructions. The items are pushed to the
;; stack.
((and (pred listp) actions)
(!cdr instruction-stack)
(setq instruction-stack (append actions instruction-stack)))
;; Heck, you've done the interpreter a frighten.
(other
(message "Stack: %s" instruction-stack)
(error "Unknown AST instruction: %s" other))))))
(provide 'kubernetes-ast)
;;; kubernetes-ast.el ends here