]> code.delx.au - gnu-emacs-elpa/blob - packages/wisi/wisi.el
release ada-mode 5.1.7, wisi 1.1.0; minor format changes in ada-ref-man (take 2)
[gnu-emacs-elpa] / packages / wisi / wisi.el
1 ;;; wisi.el --- Utilities for implementing an indentation/navigation engine using a generalized LALR parser
2 ;;
3 ;; Copyright (C) 2012 - 2014 Free Software Foundation, Inc.
4 ;;
5 ;; Author: Stephen Leake <stephen_leake@member.fsf.org>
6 ;; Maintainer: Stephen Leake <stephen_leake@member.fsf.org>
7 ;; Keywords: parser
8 ;; indentation
9 ;; navigation
10 ;; Version: 1.1.0
11 ;; package-requires: ((cl-lib "0.4") (emacs "24.2"))
12 ;; URL: http://stephe-leake.org/emacs/ada-mode/emacs-ada-mode.html
13 ;;
14 ;; This file is part of GNU Emacs.
15 ;;
16 ;; GNU Emacs is free software: you can redistribute it and/or modify
17 ;; it under the terms of the GNU General Public License as published by
18 ;; the Free Software Foundation, either version 3 of the License, or
19 ;; (at your option) any later version.
20 ;;
21 ;; GNU Emacs is distributed in the hope that it will be useful,
22 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 ;; GNU General Public License for more details.
25 ;;
26 ;; You should have received a copy of the GNU General Public License
27 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
28 ;;
29
30 ;;; Commentary:
31
32 ;;;; History: see NEWS-wisi.text
33 ;;
34 ;;;; indentation algorithm overview
35 ;;
36 ;; This design is inspired in part by experience writing a SMIE
37 ;; indentation engine for Ada, and the wisent parser.
38 ;;
39 ;; The general approach to indenting a given token is to find the
40 ;; start of the statement it is part of, or some other relevant point
41 ;; in the statement, and indent relative to that. So we need a parser
42 ;; that lets us find statement indent points from arbitrary places in
43 ;; the code.
44 ;;
45 ;; For example, the grammar for Ada as represented by the EBNF in LRM
46 ;; Annex P is not LALR(1), so we use a generalized LALR(1) parser (see
47 ;; wisi-parse, wisi-compile).
48 ;;
49 ;; The parser actions cache indentation and other information as text
50 ;; properties of tokens in statements.
51 ;;
52 ;; An indentation engine moves text in the buffer, as does user
53 ;; editing, so we can't rely on character positions remaining
54 ;; constant. So the parser actions use markers to store
55 ;; positions. Text properties also move with the text.
56 ;;
57 ;; The stored information includes a marker at each statement indent
58 ;; point. Thus, the indentation algorithm is: find the previous token
59 ;; with cached information, and either indent from it, or fetch from
60 ;; it the marker for a previous statement indent point, and indent
61 ;; relative to that.
62 ;;
63 ;; Since we have a cache (the text properties), we need to consider
64 ;; when to invalidate it. Ideally, we invalidate only when a change to
65 ;; the buffer would change the result of a parse that crosses that
66 ;; change, or starts after that change. Changes in whitespace
67 ;; (indentation and newlines) do not affect an Ada parse. Other
68 ;; languages are sensitive to newlines (Bash for example) or
69 ;; indentation (Python). Adding comments does not change a parse,
70 ;; unless code is commented out. For now we invalidate the cache after
71 ;; the edit point if the change involves anything other than
72 ;; whitespace.
73 ;;
74 ;;; Handling parse errors:
75 ;;
76 ;; When a parse fails, the cache information before the failure point
77 ;; is only partly correct, and there is no cache informaiton after the
78 ;; failure point.
79 ;;
80 ;; However, in the case where a parse previously succeeded, and the
81 ;; current parse fails due to editing, we keep the preceding cache
82 ;; information by setting wisi-cache-max to the edit point in
83 ;; wisi-before change; the parser does not apply actions before that
84 ;; point.
85 ;;
86 ;; This allows navigation and indentation in the text preceding the
87 ;; edit point, and saves some time.
88 ;;
89 ;;;; comparison to the SMIE parser
90 ;;
91 ;; The central problem to be solved in building the SMIE parser is
92 ;; grammar precedence conflicts; the general solution is refining
93 ;; keywords so that each new keyword can be assigned a unique
94 ;; precedence. This means ad hoc code must be written to determine the
95 ;; correct refinement for each language keyword from the surrounding
96 ;; tokens. In effect, for a complex language like Ada, the knowledge
97 ;; of the language grammar is mostly embedded in the refinement code;
98 ;; only a small amount is in the refined grammar. Implementing a SMIE
99 ;; parser for a new language involves the same amount of work as the
100 ;; first language.
101 ;;
102 ;; Using a generalized LALR parser avoids that particular problem;
103 ;; assuming the language is already defined by a grammar, it is only a
104 ;; matter of a format change to teach the wisi parser the
105 ;; language. The problem in a wisi indentation engine is caching the
106 ;; output of the parser in a useful way, since we can't start the
107 ;; parser from arbitrary places in the code (as we can with the SMIE
108 ;; parser). A second problem is determining when to invalidate the
109 ;; cache. But these problems are independent of the language being
110 ;; parsed, so once we have one wisi indentation engine working,
111 ;; adapting it to new languages should be quite simple.
112 ;;
113 ;; The SMIE parser does not find the start of each statement, only the
114 ;; first language keyword in each statement; additional code must be
115 ;; written to find the statement start and indent points. The wisi
116 ;; parser finds the statement start and indent points directly.
117 ;;
118 ;; In SMIE, it is best if each grammar rule is a complete statement,
119 ;; so forward-sexp will traverse the entire statement. If nested
120 ;; non-terminals are used, forward-sexp may stop inside one of the
121 ;; nested non-terminals. This problem does not occur with the wisi
122 ;; parser.
123 ;;
124 ;; A downside of the wisi parser is conflicts in the grammar; they can
125 ;; be much more difficult to resolve than in the SMIE parser. The
126 ;; generalized parser helps by handling conflicts, but it does so by
127 ;; running multiple parsers in parallel, persuing each choice in the
128 ;; conflict. If the conflict is due to a genuine ambiguity, both paths
129 ;; will succeed, which causes the parse to fail, since it is not clear
130 ;; which set of text properties to store. Even if one branch
131 ;; ultimately fails, running parallel parsers over large sections of
132 ;; code is slow. Finally, this approach can lead to exponential growth
133 ;; in the number of parsers. So grammar conflicts must still be
134 ;; analyzed and minimized.
135 ;;
136 ;; In addition, the complete grammar must be specified; in smie, it is
137 ;; often possible to specify a subset of the grammar.
138 ;;
139 ;;;; grammar compiler and parser
140 ;;
141 ;; Since we are using a generalized LALR(1) parser, we cannot use any
142 ;; of the wisent grammar functions. We use OpenToken wisi-generate
143 ;; to compile BNF to Elisp source (similar to
144 ;; semantic-grammar-create-package), and wisi-compile-grammar to
145 ;; compile that to the parser table.
146 ;;
147 ;; Semantic provides a complex lexer, more complicated than we need
148 ;; for indentation. So we use the elisp lexer, which consists of
149 ;; `forward-comment', `skip-syntax-forward', and `scan-sexp'. We wrap
150 ;; that in functions that return tokens in the form wisi-parse
151 ;; expects.
152 ;;
153 ;;;; lexer
154 ;;
155 ;; The lexer is `wisi-forward-token'. It relies on syntax properties,
156 ;; so syntax-propertize must be called on the text to be lexed before
157 ;; wisi-forward-token is called. In general, it is hard to determine
158 ;; an appropriate end-point for syntax-propertize, other than
159 ;; point-max. So we call (syntax-propertize point-max) in wisi-setup,
160 ;; and also call syntax-propertize in wisi-after-change.
161 ;;
162 ;;;; code style
163 ;;
164 ;; 'wisi' was originally short for "wisent indentation engine", but
165 ;; now is just a name.
166 ;;
167 ;; not using lexical-binding because we support Emacs 23
168 ;;
169 ;;;;;
170
171 ;;; Code:
172
173 (require 'cl-lib)
174 (require 'wisi-parse)
175
176 ;; WORKAROUND: for some reason, this condition doesn't work in batch mode!
177 ;; (when (and (= emacs-major-version 24)
178 ;; (= emacs-minor-version 2))
179 (require 'wisi-compat-24.2)
180 ;;)
181
182 (defcustom wisi-font-lock-size-threshold 100000
183 "Max size (in characters) for using wisi parser results for syntax highlighting."
184 :type 'integer
185 :group 'wisi
186 :safe 'integerp)
187 (make-variable-buffer-local 'wisi-font-lock-size-threshold)
188
189 ;;;; lexer
190
191 (defvar-local wisi-class-list nil)
192 (defvar-local wisi-keyword-table nil)
193 (defvar-local wisi-punctuation-table nil)
194 (defvar-local wisi-punctuation-table-max-length 0)
195 (defvar-local wisi-string-double-term nil);; string delimited by double quotes
196 (defvar-local wisi-string-quote-escape-doubled nil
197 "Non-nil if a string delimiter is escaped by doubling it (as in Ada).")
198 (defvar-local wisi-string-quote-escape nil
199 "Cons '(delim . character) where 'character' escapes quotes in strings delimited by 'delim'.")
200 (defvar-local wisi-string-single-term nil) ;; string delimited by single quotes
201 (defvar-local wisi-symbol-term nil)
202 (defvar-local wisi-number-term nil)
203 (defvar-local wisi-number-p nil)
204
205 (defun wisi-number-p (token-text)
206 "Return t if TOKEN-TEXT plus text after point matches the
207 syntax for a real literal; otherwise nil. point is after
208 TOKEN-TEXT; move point to just past token."
209 ;; typical literals:
210 ;; 1234
211 ;; 1234.5678
212 ;; 1234.5678e+99
213 ;;
214 (let ((end (point)))
215 ;; starts with a simple integer
216 (when (string-match "^[0-9]+" token-text)
217 (when (looking-at "\\.[0-9]+")
218 ;; real number
219 (goto-char (setq end (match-end 0)))
220 (when (looking-at "[Ee][+-][0-9]+")
221 ;; exponent
222 (goto-char (setq end (match-end 0)))))
223
224 t
225 )))
226
227 (defun wisi-forward-token ()
228 "Move point forward across one token, skipping leading whitespace and comments.
229 Return the corresponding token, in format: (token start . end) where:
230
231 `token' is a token symbol (not string) from `wisi-punctuation-table',
232 `wisi-keyword-table', `wisi-string-double-term', `wisi-string-double-term' or `wisi-symbol-term'.
233
234 `start, end' are the character positions in the buffer of the start
235 and end of the token text.
236
237 If at end of buffer, returns `wisent-eoi-term'."
238 (forward-comment (point-max))
239 ;; skips leading whitespace, comment, trailing whitespace.
240
241 (let ((start (point))
242 ;; (info "(elisp)Syntax Table Internals" "*info elisp syntax*")
243 (syntax (syntax-class (syntax-after (point))))
244 token-id token-text)
245 (cond
246 ((eobp)
247 (setq token-id wisent-eoi-term))
248
249 ((eq syntax 1)
250 ;; punctuation. Find the longest matching string in wisi-punctuation-table
251 (forward-char 1)
252 (let ((next-point (point))
253 temp-text temp-id done)
254 (while (not done)
255 (setq temp-text (buffer-substring-no-properties start (point)))
256 (setq temp-id (car (rassoc temp-text wisi-punctuation-table)))
257 (when temp-id
258 (setq token-id temp-id
259 next-point (point)))
260 (if (or
261 (eobp)
262 (= (- (point) start) wisi-punctuation-table-max-length))
263 (setq done t)
264 (forward-char 1))
265 )
266 (goto-char next-point)))
267
268 ((memq syntax '(4 5)) ;; open, close parenthesis
269 (forward-char 1)
270 (setq token-text (buffer-substring-no-properties start (point)))
271 (setq token-id (symbol-value (intern-soft token-text wisi-keyword-table))))
272
273 ((eq syntax 7)
274 ;; string quote, either single or double. we assume point is
275 ;; before the start quote, not the end quote
276 (let ((delim (char-after (point)))
277 (forward-sexp-function nil))
278 (condition-case err
279 (progn
280 (forward-sexp)
281
282 ;; point is now after the end quote; check for an escaped quote
283 (while (or
284 (and wisi-string-quote-escape-doubled
285 (eq (char-after (point)) delim))
286 (and (eq delim (car wisi-string-quote-escape))
287 (eq (char-before (1- (point))) (cdr wisi-string-quote-escape))))
288 (forward-sexp))
289 (setq token-id (if (= delim ?\") wisi-string-double-term wisi-string-single-term)))
290 (scan-error
291 ;; Something screwed up; we should not get here if
292 ;; syntax-propertize works properly.
293 (error "wisi-forward-token: forward-sexp failed %s" err)
294 ))))
295
296 (t ;; assuming word syntax
297 (skip-syntax-forward "w_'")
298 (setq token-text (buffer-substring-no-properties start (point)))
299 (setq token-id
300 (or (symbol-value (intern-soft (downcase token-text) wisi-keyword-table))
301 (and (functionp wisi-number-p)
302 (funcall wisi-number-p token-text)
303 (setq token-text (buffer-substring-no-properties start (point)))
304 wisi-number-term)
305 wisi-symbol-term))
306 )
307 );; cond
308
309 (unless token-id
310 (signal 'wisi-parse-error
311 (wisi-error-msg "unrecognized token '%s'" (buffer-substring-no-properties start (point)))))
312
313 (cons token-id (cons start (point)))
314 ))
315
316 (defun wisi-backward-token ()
317 "Move point backward across one token, skipping whitespace and comments.
318 Return (nil start . end) - same structure as
319 wisi-forward-token, but does not look up symbol."
320 (forward-comment (- (point)))
321 ;; skips leading whitespace, comment, trailing whitespace.
322
323 ;; (info "(elisp)Syntax Table Internals" "*info elisp syntax*")
324 (let ((end (point))
325 (syntax (syntax-class (syntax-after (1- (point))))))
326 (cond
327 ((bobp) nil)
328
329 ((memq syntax '(4 5)) ;; open, close parenthesis
330 (backward-char 1))
331
332 ((eq syntax 7)
333 ;; a string quote. we assume we are after the end quote, not the start quote
334 (let ((forward-sexp-function nil))
335 (forward-sexp -1)))
336
337 (t
338 (if (zerop (skip-syntax-backward "."))
339 (skip-syntax-backward "w_'")))
340 )
341 (cons nil (cons (point) end))
342 ))
343
344 ;;;; token info cache
345 ;;
346 ;; the cache stores the results of parsing as text properties on
347 ;; keywords, for use by the indention and motion engines.
348
349 (cl-defstruct
350 (wisi-cache
351 (:constructor wisi-cache-create)
352 (:copier nil))
353 nonterm;; nonterminal from parse (set by wisi-statement-action)
354
355 token
356 ;; terminal symbol from wisi-keyword-table or
357 ;; wisi-punctuation-table, or lower-level nonterminal from parse
358 ;; (set by wisi-statement-action)
359
360 last ;; pos of last char in token, relative to first (0 indexed)
361
362 class
363 ;; arbitrary lisp symbol, used for indentation and navigation.
364 ;; some classes are defined by wisi:
365 ;;
366 ;; 'block-middle - a block keyword (ie: if then else end), not at the start of a statement
367 ;;
368 ;; 'block-start - a block keyword at the start of a statement
369 ;;
370 ;; 'statement-start - the start of a statement
371 ;;
372 ;; 'open-paren
373 ;;
374 ;; others are language-specific
375
376 containing
377 ;; Marker at the containing keyword for this token.
378 ;; A containing keyword is an indent point; the start of a
379 ;; statement, or 'begin', 'then' or 'else' for a block of
380 ;; statements, etc.
381 ;; nil only for first token in buffer
382
383 prev ;; marker at previous motion token in statement; nil if none
384 next ;; marker at next motion token in statement; nil if none
385 end ;; marker at token at end of current statement
386 )
387
388 (defvar-local wisi-parse-table nil)
389
390 (defvar-local wisi-parse-failed nil
391 "Non-nil when a recent parse has failed - cleared when parse succeeds.")
392
393 (defvar-local wisi-parse-try nil
394 "Non-nil when parse is needed - cleared when parse succeeds.")
395
396 (defvar-local wisi-change-need-invalidate nil
397 "When non-nil, buffer position to invalidate from.
398 Used in before/after change functions.")
399
400 (defvar-local wisi-end-caches nil
401 "List of buffer positions of caches in current statement that need wisi-cache-end set.")
402
403 (defun wisi-delete-cache (after)
404 (with-silent-modifications
405 (remove-text-properties after (point-max) '(wisi-cache nil))
406 ;; We don't remove 'font-lock-face; that's annoying to the user,
407 ;; since they won't be restored until a parse for some other
408 ;; reason, and they are likely to be right anyway.
409 ))
410
411 (defun wisi-invalidate-cache(&optional after)
412 "Invalidate parsing caches for the current buffer from AFTER to end of buffer."
413 (interactive)
414 (if (not after)
415 (setq after (point-min))
416 (setq after
417 (save-excursion
418 (goto-char after)
419 (line-beginning-position))))
420 (when (> wisi-debug 0) (message "wisi-invalidate %s:%d" (current-buffer) after))
421 (setq wisi-cache-max after)
422 (setq wisi-parse-try t)
423 (syntax-ppss-flush-cache after)
424 (wisi-delete-cache after)
425 )
426
427 (defun wisi-before-change (begin end)
428 "For `before-change-functions'."
429 ;; begin . end is range of text being deleted
430
431 ;; If jit-lock-after-change is before wisi-after-change in
432 ;; after-change-functions, it might use any invalid caches in the
433 ;; inserted text.
434 ;;
435 ;; So we check for that here, and ensure it is after
436 ;; wisi-after-change, which deletes the invalid caches
437 (when (boundp 'jit-lock-mode)
438 (when (memq 'wisi-after-change (memq 'jit-lock-after-change after-change-functions))
439 (setq after-change-functions (delete 'wisi-after-change after-change-functions))
440 (add-hook 'after-change-functions 'wisi-after-change nil t))
441 )
442
443 (setq wisi-change-need-invalidate nil)
444
445 (when (> end begin)
446 (save-excursion
447 ;; (info "(elisp)Parser State")
448 (let* ((begin-state (syntax-ppss begin))
449 (end-state (syntax-ppss end))
450 ;; syntax-ppss has moved point to "end".
451 (word-end (progn (skip-syntax-forward "w_")(point))))
452
453 ;; Remove grammar face from word(s) containing change region;
454 ;; might be changing to/from a keyword. See
455 ;; test/ada_mode-interactive_common.adb Obj_1
456 (goto-char begin)
457 (skip-syntax-backward "w_")
458 (with-silent-modifications
459 (remove-text-properties (point) word-end '(font-lock-face nil fontified nil)))
460
461 (if (<= wisi-cache-max begin)
462 ;; Change is in unvalidated region; either the parse was
463 ;; failing, or there is more than one top-level grammar
464 ;; symbol in buffer.
465 (when wisi-parse-failed
466 ;; The parse was failing, probably due to bad syntax; this
467 ;; change may have fixed it, so try reparse.
468 (setq wisi-parse-try t))
469
470 ;; else change is in validated region
471 ;;
472 ;; don't invalidate parse for whitespace, string, or comment changes
473 (cond
474 ((and
475 (nth 3 begin-state); in string
476 (nth 3 end-state)))
477 ;; no easy way to tell if there is intervening non-string
478
479 ((and
480 (nth 4 begin-state); in comment
481 (nth 4 end-state))
482 ;; too hard to detect case where there is intervening
483 ;; code; no easy way to go to end of comment if not
484 ;; newline
485 )
486
487 ;; Deleting whitespace generally does not require parse, but
488 ;; deleting all whitespace between two words does; check that
489 ;; there is whitespace on at least one side of the deleted
490 ;; text.
491 ;;
492 ;; We are not in a comment (checked above), so treat
493 ;; comment end as whitespace in case it is newline, except
494 ;; deleting a comment end at begin means commenting the
495 ;; current line; requires parse.
496 ((and
497 (eq (car (syntax-after begin)) 0) ; whitespace
498 (memq (car (syntax-after (1- end))) '(0 12)) ; whitespace, comment end
499 (or
500 (memq (car (syntax-after (1- begin))) '(0 12))
501 (memq (car (syntax-after end)) '(0 12)))
502 (progn
503 (goto-char begin)
504 (skip-syntax-forward " >" end)
505 (eq (point) end))))
506
507 (t
508 (setq wisi-change-need-invalidate
509 (progn
510 ;; note that because of the checks above, this never
511 ;; triggers a parse, so it's fast
512 (goto-char begin)
513 (wisi-goto-statement-start)
514 (point))))
515 )))
516 ))
517 )
518
519 (defun wisi-after-change (begin end length)
520 "For `after-change-functions'."
521 ;; begin . end is range of text being inserted (empty if equal);
522 ;; length is the size of the deleted text.
523
524 ;; (syntax-ppss-flush-cache begin) is in before-change-functions
525
526 (syntax-propertize end) ;; see comments above on "lexer" re syntax-propertize
527
528 ;; Remove caches on inserted text, which could have caches from
529 ;; before the failed parse (or another buffer), and are in any case
530 ;; invalid. No point in removing 'fontified; that's handled by
531 ;; jit-lock.
532
533 (with-silent-modifications
534 (remove-text-properties begin end '(wisi-cache nil font-lock-face nil)))
535
536 ;; Also remove grammar face from word(s) containing change region;
537 ;; might be changing to/from a keyword. See
538 ;; test/ada_mode-interactive_common.adb Obj_1
539 (save-excursion
540 ;; (info "(elisp)Parser State")
541 (let ((need-invalidate wisi-change-need-invalidate)
542 begin-state end-state word-end)
543 (when (> end begin)
544 (setq begin-state (syntax-ppss begin))
545 (setq end-state (syntax-ppss end))
546 ;; syntax-ppss has moved point to "end".
547 (skip-syntax-forward "w_")
548 (setq word-end (point))
549 (goto-char begin)
550 (skip-syntax-backward "w_")
551 (with-silent-modifications
552 (remove-text-properties (point) word-end '(font-lock-face nil fontified nil))))
553
554 (if (<= wisi-cache-max begin)
555 ;; Change is in unvalidated region
556 (when wisi-parse-failed
557 ;; The parse was failing, probably due to bad syntax; this
558 ;; change may have fixed it, so try reparse.
559 (setq wisi-parse-try t))
560
561 ;; Change is in validated region
562 (cond
563 (wisi-change-need-invalidate
564 ;; wisi-before change determined the removed text alters the
565 ;; parse
566 )
567
568 ((= end begin)
569 (setq need-invalidate nil))
570
571 ((and
572 (nth 3 begin-state); in string
573 (nth 3 end-state))
574 ;; no easy way to tell if there is intervening non-string
575 (setq need-invalidate nil))
576
577 ((or
578 (nth 4 begin-state)
579 (nth 4 end-state)); in comment
580 ;; no easy way to detect intervening code
581 (setq need-invalidate nil)
582 ;; no caches to remove
583 )
584
585 ;; Adding whitespace generally does not require parse, but in
586 ;; the middle of word it does; check that there was
587 ;; whitespace on at least one side of the inserted text.
588 ;;
589 ;; We are not in a comment (checked above), so treat
590 ;; comment end as whitespace in case it is newline
591 ((and
592 (or
593 (memq (car (syntax-after (1- begin))) '(0 12)); whitespace, comment end
594 (memq (car (syntax-after end)) '(0 12)))
595 (progn
596 (goto-char begin)
597 (skip-syntax-forward " >" end)
598 (eq (point) end)))
599 (setq need-invalidate nil))
600
601 (t
602 (setq need-invalidate
603 (progn
604 (goto-char begin)
605 ;; note that because of the checks above, this never
606 ;; triggers a parse, so it's fast
607 (wisi-goto-statement-start)
608 (point))))
609 )
610
611 (if need-invalidate
612 (wisi-invalidate-cache need-invalidate)
613
614 ;; else move cache-max by the net change length.
615 (setq wisi-cache-max
616 (+ wisi-cache-max (- end begin length))) )
617 ))
618 ))
619
620 (defun wisi-get-cache (pos)
621 "Return `wisi-cache' struct from the `wisi-cache' text property at POS.
622 If accessing cache at a marker for a token as set by `wisi-cache-tokens', POS must be (1- mark)."
623 (get-text-property pos 'wisi-cache))
624
625 (defvar-local wisi-parse-error-msg nil)
626
627 (defun wisi-goto-error ()
628 "Move point to position in last error message (if any)."
629 (when (string-match ":\\([0-9]+\\):\\([0-9]+\\):" wisi-parse-error-msg)
630 (let ((line (string-to-number (match-string 1 wisi-parse-error-msg)))
631 (col (string-to-number (match-string 2 wisi-parse-error-msg))))
632 (push-mark)
633 (goto-char (point-min))
634 (forward-line (1- line))
635 (forward-char col))))
636
637 (defun wisi-show-parse-error ()
638 "Show last wisi-parse error."
639 (interactive)
640 (cond
641 (wisi-parse-failed
642 (wisi-goto-error)
643 (message wisi-parse-error-msg))
644
645 (wisi-parse-try
646 (message "need parse"))
647
648 (t
649 (message "parse succeeded"))
650 ))
651
652 (defvar wisi-post-parse-succeed-hook nil
653 "Hook run after parse succeeds.")
654
655 (defun wisi-validate-cache (pos)
656 "Ensure cached data is valid at least up to POS in current buffer."
657 (let ((msg (when (> wisi-debug 0) (format "wisi: parsing %s:%d ..." (buffer-name) (line-number-at-pos pos)))))
658 (when (and wisi-parse-try
659 (< wisi-cache-max pos))
660 (when (> wisi-debug 0)
661 (message msg))
662
663 (setq wisi-parse-try nil)
664 (setq wisi-parse-error-msg nil)
665 (setq wisi-end-caches nil)
666
667 (if (> wisi-debug 1)
668 ;; let debugger stop in wisi-parse
669 (progn
670 (save-excursion
671 (wisi-parse wisi-parse-table 'wisi-forward-token)
672 (setq wisi-cache-max (point))
673 (setq wisi-parse-failed nil))
674 (run-hooks 'wisi-post-parse-succeed-hook))
675
676 ;; else capture errors from bad syntax, so higher level
677 ;; functions can try to continue and/or we don't bother the
678 ;; user.
679 (condition-case err
680 (progn
681 (save-excursion
682 (wisi-parse wisi-parse-table 'wisi-forward-token)
683 (setq wisi-cache-max (point))
684 (setq wisi-parse-failed nil))
685 (run-hooks 'wisi-post-parse-succeed-hook))
686 (wisi-parse-error
687 ;; delete caches past wisi-cache-max added by failed parse
688 (wisi-delete-cache wisi-cache-max)
689 (setq wisi-parse-failed t)
690 (setq wisi-parse-error-msg (cdr err)))
691 ))
692 (if wisi-parse-error-msg
693 ;; error
694 (when (> wisi-debug 0)
695 (message "%s error" msg)
696 (wisi-goto-error)
697 (error wisi-parse-error-msg))
698 ;; no msg; success
699 (when (> wisi-debug 0)
700 (message "%s done" msg)))
701 )))
702
703 (defun wisi-fontify-region (begin end)
704 "For `jit-lock-functions'."
705 (when (< (point-max) wisi-font-lock-size-threshold)
706 (wisi-validate-cache end)))
707
708 (defun wisi-get-containing-cache (cache)
709 "Return cache from (wisi-cache-containing CACHE)."
710 (let ((containing (wisi-cache-containing cache)))
711 (and containing
712 (wisi-get-cache (1- containing)))))
713
714 (defun wisi-cache-region (cache)
715 "Return region designated by cache.
716 Point must be at cache."
717 (cons (point) (+ (point) (wisi-cache-last cache))))
718
719 (defun wisi-cache-text (cache)
720 "Return property-less buffer substring designated by cache.
721 Point must be at cache."
722 (buffer-substring-no-properties (point) (+ (point) (wisi-cache-last cache))))
723
724 ;;;; parse actions
725
726 (defun wisi-set-end (start-mark end-mark)
727 "Set END-MARK on all caches in `wisi-end-caches' in range START-MARK END-MARK,
728 delete from `wisi-end-caches'."
729 (let ((i 0)
730 pos cache)
731 (while (< i (length wisi-end-caches))
732 (setq pos (nth i wisi-end-caches))
733 (setq cache (wisi-get-cache pos))
734
735 (if (and (>= pos start-mark)
736 (< pos end-mark))
737 (progn
738 (setf (wisi-cache-end cache) end-mark)
739 (setq wisi-end-caches (delq pos wisi-end-caches)))
740
741 ;; else not in range
742 (setq i (1+ i)))
743 )))
744
745 (defvar wisi-tokens nil)
746 ;; keep byte-compiler happy; `wisi-tokens' is bound in action created
747 ;; by wisi-semantic-action
748
749 (defun wisi-statement-action (pairs)
750 "Cache information in text properties of tokens.
751 Intended as a grammar non-terminal action.
752
753 PAIRS is a vector of the form [TOKEN-NUMBER CLASS TOKEN-NUMBER
754 CLASS ...] where TOKEN-NUMBER is the (1 indexed) token number in
755 the production, CLASS is the wisi class of that token. Use in a
756 grammar action as:
757 (wisi-statement-action [1 'statement-start 7 'statement-end])"
758 (save-excursion
759 (let ((first-item t)
760 first-keyword-mark
761 (override-start nil)
762 (i 0))
763 (while (< i (length pairs))
764 (let* ((number (1- (aref pairs i)))
765 (region (cdr (aref wisi-tokens number)));; wisi-tokens is let-bound in wisi-parse-reduce
766 (token (car (aref wisi-tokens number)))
767 (class (aref pairs (setq i (1+ i))))
768 (mark
769 ;; Marker one char into token, so indent-line-to
770 ;; inserts space before the mark, not after
771 (when region (copy-marker (1+ (car region)))))
772 cache)
773
774 (setq i (1+ i))
775
776 (unless (memq class wisi-class-list)
777 (error "%s not in wisi-class-list" class))
778
779 (if region
780 (progn
781 (if (setq cache (wisi-get-cache (car region)))
782 ;; We are processing a previously set non-terminal; ie generic_formal_part in
783 ;;
784 ;; generic_package_declaration : generic_formal_part package_specification SEMICOLON
785 ;; (wisi-statement-action 1 'block-start 2 'block-middle 3 'statement-end)
786 ;;
787 ;; or simple_statement in
788 ;;
789 ;; statement : label_opt simple_statement
790 ;;
791 ;; override nonterm, class, containing
792 ;; set end only if not set yet (due to failed parse)
793 (progn
794 (cl-case (wisi-cache-class cache)
795 (block-start
796 (setf (wisi-cache-class cache)
797 (cond
798 ((eq override-start nil)
799 (cond
800 ((memq class '(block-start statement-start)) 'block-start)
801 (t 'block-middle)))
802
803 ((memq override-start '(block-start statement-start)) 'block-start)
804
805 (t (error "unexpected override-start"))
806 )))
807 (t
808 (setf (wisi-cache-class cache) (or override-start class)))
809 )
810 (setf (wisi-cache-nonterm cache) $nterm)
811 (setf (wisi-cache-containing cache) first-keyword-mark)
812 (unless (wisi-cache-end cache)
813 (if wisi-end-caches
814 (push (car region) wisi-end-caches)
815 (setq wisi-end-caches (list (car region)))
816 ))
817 )
818
819 ;; else create new cache
820 (with-silent-modifications
821 (put-text-property
822 (car region)
823 (1+ (car region))
824 'wisi-cache
825 (wisi-cache-create
826 :nonterm $nterm;; $nterm defined in wisi-semantic-action
827 :token token
828 :last (- (cdr region) (car region))
829 :class (or override-start class)
830 :containing first-keyword-mark)
831 ))
832 (if wisi-end-caches
833 (push (car region) wisi-end-caches)
834 (setq wisi-end-caches (list (car region)))
835 ))
836
837 (when first-item
838 (setq first-item nil)
839 (when (or override-start
840 ;; FIXME: why block-middle here?
841 (memq class '(block-middle block-start statement-start)))
842 (setq override-start nil)
843 (setq first-keyword-mark mark)))
844
845 (when (eq class 'statement-end)
846 (wisi-set-end (1- first-keyword-mark) (copy-marker (1+ (car region)))))
847 )
848
849 ;; region is nil when a production is empty; if the first
850 ;; token is a start, override the class on the next token.
851 (when (and first-item
852 (memq class '(block-middle block-start statement-start)))
853 (setq override-start class)))
854 ))
855 )))
856
857 (defun wisi-containing-action (containing-token contained-token)
858 "Set containing marks in all tokens in CONTAINED-TOKEN with null containing mark to marker pointing to CONTAINING-TOKEN.
859 If CONTAINING-TOKEN is empty, the next token number is used."
860 ;; wisi-tokens is is bound in action created by wisi-semantic-action
861 (let* ((containing-region (cdr (aref wisi-tokens (1- containing-token))))
862 (contained-region (cdr (aref wisi-tokens (1- contained-token)))))
863
864 (unless containing-region ;;
865 (signal 'wisi-parse-error
866 (wisi-error-msg
867 "wisi-containing-action: containing-region '%s' is empty. grammar error; bad action"
868 (wisi-token-text (aref wisi-tokens (1- containing-token))))))
869
870 (unless (or (not contained-region) ;; contained-token is empty
871 (wisi-get-cache (car containing-region)))
872 (signal 'wisi-parse-error
873 (wisi-error-msg
874 "wisi-containing-action: containing-token '%s' has no cache. grammar error; missing action"
875 (wisi-token-text (aref wisi-tokens (1- containing-token))))))
876
877 (while (not containing-region)
878 ;; containing-token is empty; use next
879 (setq containing-region (cdr (aref wisi-tokens containing-token))))
880
881 (when contained-region
882 ;; nil when empty production, may not contain any caches
883 (save-excursion
884 (goto-char (cdr contained-region))
885 (let ((cache (wisi-backward-cache))
886 (mark (copy-marker (1+ (car containing-region)))))
887 (while cache
888
889 ;; skip blocks that are already marked
890 (while (and (>= (point) (car contained-region))
891 (markerp (wisi-cache-containing cache)))
892 (goto-char (1- (wisi-cache-containing cache)))
893 (setq cache (wisi-get-cache (point))))
894
895 (if (or (and (= (car containing-region) (car contained-region))
896 (<= (point) (car contained-region)))
897 (< (point) (car contained-region)))
898 ;; done
899 (setq cache nil)
900
901 ;; else set mark, loop
902 (setf (wisi-cache-containing cache) mark)
903 (setq cache (wisi-backward-cache)))
904 ))))))
905
906 (defun wisi-match-class-token (cache class-tokens)
907 "Return t if CACHE matches CLASS-TOKENS.
908 CLASS-TOKENS is a vector [number class token_id class token_id ...].
909 number is ignored."
910 (let ((i 1)
911 (done nil)
912 (result nil)
913 class token)
914 (while (and (not done)
915 (< i (length class-tokens)))
916 (setq class (aref class-tokens i))
917 (setq token (aref class-tokens (setq i (1+ i))))
918 (setq i (1+ i))
919 (when (and (eq class (wisi-cache-class cache))
920 (eq token (wisi-cache-token cache)))
921 (setq result t
922 done t))
923 )
924 result))
925
926 (defun wisi-motion-action (token-numbers)
927 "Set prev/next marks in all tokens given by TOKEN-NUMBERS.
928 TOKEN-NUMBERS is a vector with each element one of:
929
930 number: the token number; mark that token
931
932 vector [number class token_id]:
933 vector [number class token_id class token_id ...]:
934 mark all tokens in number nonterminal matching (class token_id) with nil prev/next."
935 (save-excursion
936 (let (prev-keyword-mark
937 prev-cache
938 cache
939 mark
940 (i 0))
941 (while (< i (length token-numbers))
942 (let ((token-number (aref token-numbers i))
943 region)
944 (setq i (1+ i))
945 (cond
946 ((numberp token-number)
947 (setq region (cdr (aref wisi-tokens (1- token-number))))
948 (when region
949 (setq cache (wisi-get-cache (car region)))
950 (setq mark (copy-marker (1+ (car region))))
951
952 (when (and prev-keyword-mark
953 cache
954 (null (wisi-cache-prev cache)))
955 (setf (wisi-cache-prev cache) prev-keyword-mark)
956 (setf (wisi-cache-next prev-cache) mark))
957
958 (setq prev-keyword-mark mark)
959 (setq prev-cache cache)
960 ))
961
962 ((vectorp token-number)
963 ;; token-number may contain 0, 1, or more 'class token_id' pairs
964 ;; the corresponding region may be empty
965 ;; there must have been a prev keyword
966 (setq region (cdr (aref wisi-tokens (1- (aref token-number 0)))))
967 (when region ;; not an empty token
968 ;; We must search for all targets at the same time, to
969 ;; get the motion order right.
970 (goto-char (car region))
971 (setq cache (or (wisi-get-cache (point))
972 (wisi-forward-cache)))
973 (while (< (point) (cdr region))
974 (when (wisi-match-class-token cache token-number)
975 (when (null (wisi-cache-prev cache))
976 (setf (wisi-cache-prev cache) prev-keyword-mark))
977 (when (null (wisi-cache-next cache))
978 (setq mark (copy-marker (1+ (point))))
979 (setf (wisi-cache-next prev-cache) mark)
980 (setq prev-keyword-mark mark)
981 (setq prev-cache cache)))
982
983 (setq cache (wisi-forward-cache))
984 )))
985
986 (t
987 (error "unexpected token-number %s" token-number))
988 )
989
990 ))
991 )))
992
993 (defun wisi-extend-action (number)
994 "Extend text of cache at token NUMBER to cover all of token NUMBER.
995 Also override token with new token."
996 (let* ((token-region (aref wisi-tokens (1- number)));; wisi-tokens is let-bound in wisi-parse-reduce
997 (token (car token-region))
998 (region (cdr token-region))
999 cache)
1000
1001 (when region
1002 (setq cache (wisi-get-cache (car region)))
1003 (setf (wisi-cache-last cache) (- (cdr region) (car region)))
1004 (setf (wisi-cache-token cache) token)
1005 )
1006 ))
1007
1008 (defun wisi-face-action-1 (face region &optional no-override)
1009 "Apply FACE to REGION. If NO-OVERRIDE is non-nil, don't override existing face."
1010 (when region
1011 ;; We allow overriding a face property, because we don't want to
1012 ;; delete them in wisi-invalidate (see comments there). On the
1013 ;; other hand, it can be an error, so keep this debug
1014 ;; code. However, note that font-lock-face properties must be
1015 ;; removed first, or the buffer must be fresh (never parsed).
1016 ;;
1017 ;; Grammar sets no-override when a higher-level production might
1018 ;; override a face in a lower-level production; that's not an
1019 ;; error.
1020 (let (cur-face
1021 (do-set t))
1022 (when (or no-override
1023 (> wisi-debug 1))
1024 (setq cur-face (get-text-property (car region) 'font-lock-face))
1025 (if cur-face
1026 (if no-override
1027 (setq do-set nil)
1028 (message "%s:%d overriding face %s with %s on '%s'"
1029 (buffer-file-name)
1030 (line-number-at-pos (car region))
1031 face
1032 cur-face
1033 (buffer-substring-no-properties (car region) (cdr region))))
1034
1035 ))
1036 (when do-set
1037 (with-silent-modifications
1038 (add-text-properties
1039 (car region) (cdr region)
1040 (list
1041 'font-lock-face face
1042 'fontified t))))
1043 )))
1044
1045 (defun wisi-face-action (pairs &optional no-override)
1046 "Cache face information in text properties of tokens.
1047 Intended as a grammar non-terminal action.
1048
1049 PAIRS is a vector of the form [token-number face token-number face ...]
1050 token-number may be an integer, or a vector [integer token_id token_id ...]
1051
1052 For an integer token-number, apply face to the first cached token
1053 in the range covered by wisi-tokens[token-number]. If there are
1054 no cached tokens, apply face to entire wisi-tokens[token-number]
1055 region.
1056
1057 For a vector token-number, apply face to the first cached token
1058 in the range matching one of token_id covered by
1059 wisi-tokens[token-number].
1060
1061 If NO-OVERRIDE is non-nil, don't override existing face."
1062 (let (number region face (tokens nil) cache (i 0) (j 1))
1063 (while (< i (length pairs))
1064 (setq number (aref pairs i))
1065 (setq face (aref pairs (setq i (1+ i))))
1066 (cond
1067 ((integerp number)
1068 (setq region (cdr (aref wisi-tokens (1- number))));; wisi-tokens is let-bound in wisi-parse-reduce
1069 (when region
1070 (save-excursion
1071 (goto-char (car region))
1072 (setq cache (or (wisi-get-cache (point))
1073 (wisi-forward-cache)))
1074 (if (< (point) (cdr region))
1075 (when cache
1076 (wisi-face-action-1 face (wisi-cache-region cache) no-override))
1077
1078 ;; no caches in region; just apply face to region
1079 (wisi-face-action-1 face region no-override))
1080 )))
1081
1082 ((vectorp number)
1083 (setq region (cdr (aref wisi-tokens (1- (aref number 0)))))
1084 (when region
1085 (while (< j (length number))
1086 (setq tokens (cons (aref number j) tokens))
1087 (setq j (1+ j)))
1088 (save-excursion
1089 (goto-char (car region))
1090 (setq cache (wisi-forward-find-token tokens (cdr region) t))
1091 ;; might be looking for IDENTIFIER in name, but only have "*".
1092 (when cache
1093 (wisi-face-action-1 face (wisi-cache-region cache) no-override))
1094 )))
1095 )
1096 (setq i (1+ i))
1097
1098 )))
1099
1100 (defun wisi-face-list-action (pairs &optional no-override)
1101 "Cache face information in text properties of tokens.
1102 Intended as a grammar non-terminal action.
1103
1104 PAIRS is a vector of the form [token-number face token-number face ...]
1105 token-number is an integer. Apply face to all cached tokens
1106 in the range covered by wisi-tokens[token-number].
1107
1108 If NO-OVERRIDE is non-nil, don't override existing face."
1109 (let (number region face cache (i 0))
1110 (while (< i (length pairs))
1111 (setq number (aref pairs i))
1112 (setq face (aref pairs (setq i (1+ i))))
1113 (setq region (cdr (aref wisi-tokens (1- number))));; wisi-tokens is let-bound in wisi-parse-reduce
1114 (when region
1115 (save-excursion
1116 (goto-char (car region))
1117 (setq cache (or (wisi-get-cache (point))
1118 (wisi-forward-cache)))
1119 (while (<= (point) (cdr region))
1120 (when cache
1121 (wisi-face-action-1 face (wisi-cache-region cache) no-override))
1122 (setq cache (wisi-forward-cache))
1123 )))
1124
1125 (setq i (1+ i))
1126
1127 )))
1128
1129 ;;;; motion
1130 (defun wisi-backward-cache ()
1131 "Move point backward to the beginning of the first token preceding point that has a cache.
1132 Returns cache, or nil if at beginning of buffer."
1133 (let (cache pos)
1134 (setq pos (previous-single-property-change (point) 'wisi-cache))
1135 ;; There are three cases:
1136 ;;
1137 ;; 1) caches separated by non-cache chars: 'if ... then'
1138 ;; pos is before 'f', cache is on 'i'
1139 ;;
1140 ;; 2) caches not separated: ');'
1141 ;; pos is before ';', cache is on ';'
1142 ;;
1143 ;; 3) at bob; pos is nil
1144 ;;
1145 (if pos
1146 (progn
1147 (setq cache (get-text-property pos 'wisi-cache))
1148 (if cache
1149 ;; case 2
1150 (goto-char pos)
1151 ;; case 1
1152 (setq cache (get-text-property (1- pos) 'wisi-cache))
1153 (goto-char (1- pos))))
1154 ;; at bob
1155 (goto-char (point-min))
1156 (setq cache nil))
1157 cache
1158 ))
1159
1160 (defun wisi-forward-cache ()
1161 "Move point forward to the beginning of the first token after point that has a cache.
1162 Returns cache, or nil if at end of buffer."
1163 (let (cache pos)
1164 (when (get-text-property (point) 'wisi-cache)
1165 ;; on a cache; get past it
1166 (goto-char (1+ (point))))
1167
1168 (setq cache (get-text-property (point) 'wisi-cache))
1169 (if cache
1170 nil
1171
1172 (setq pos (next-single-property-change (point) 'wisi-cache))
1173 (if pos
1174 (progn
1175 (goto-char pos)
1176 (setq cache (get-text-property pos 'wisi-cache)))
1177 ;; at eob
1178 (goto-char (point-max))
1179 (setq cache nil))
1180 )
1181 cache
1182 ))
1183
1184 (defun wisi-forward-find-class (class limit)
1185 "Search forward for a token that has a cache with CLASS.
1186 Return cache, or nil if at end of buffer.
1187 If LIMIT (a buffer position) is reached, throw an error."
1188 (let ((cache (wisi-forward-cache)))
1189 (while (not (eq class (wisi-cache-class cache)))
1190 (setq cache (wisi-forward-cache))
1191 (when (>= (point) limit)
1192 (error "cache with class %s not found" class)))
1193 cache))
1194
1195 (defun wisi-forward-find-token (token limit &optional noerror)
1196 "Search forward for a token that has a cache with TOKEN.
1197 If point is at a matching token, return that token.
1198 TOKEN may be a list; stop on any cache that has a member of the list.
1199 Return cache, or nil if at end of buffer.
1200 If LIMIT (a buffer position) is reached, then if NOERROR is nil, throw an
1201 error, if non-nil, return nil."
1202 (let ((token-list (cond
1203 ((listp token) token)
1204 (t (list token))))
1205 (cache (wisi-get-cache (point)))
1206 (done nil))
1207 (while (not (or done
1208 (and cache
1209 (memq (wisi-cache-token cache) token-list))))
1210 (setq cache (wisi-forward-cache))
1211 (when (>= (point) limit)
1212 (if noerror
1213 (progn
1214 (setq done t)
1215 (setq cache nil))
1216 (error "cache with token %s not found" token))))
1217 cache))
1218
1219 (defun wisi-forward-find-nonterm (nonterm limit)
1220 "Search forward for a token that has a cache with NONTERM.
1221 NONTERM may be a list; stop on any cache that has a member of the list.
1222 Return cache, or nil if at end of buffer.
1223 If LIMIT (a buffer position) is reached, throw an error."
1224 (let ((nonterm-list (cond
1225 ((listp nonterm) nonterm)
1226 (t (list nonterm))))
1227 (cache (wisi-forward-cache)))
1228 (while (not (memq (wisi-cache-nonterm cache) nonterm-list))
1229 (setq cache (wisi-forward-cache))
1230 (when (>= (point) limit)
1231 (error "cache with nonterm %s not found" nonterm)))
1232 cache))
1233
1234 (defun wisi-goto-cache-next (cache)
1235 (goto-char (1- (wisi-cache-next cache)))
1236 (wisi-get-cache (point))
1237 )
1238
1239 (defun wisi-forward-statement-keyword ()
1240 "If not at a cached token, move forward to next
1241 cache. Otherwise move to cache-next, or next cache if nil.
1242 Return cache found."
1243 (wisi-validate-cache (point-max)) ;; ensure there is a next cache to move to
1244 (let ((cache (wisi-get-cache (point))))
1245 (if cache
1246 (let ((next (wisi-cache-next cache)))
1247 (if next
1248 (goto-char (1- next))
1249 (wisi-forward-token)
1250 (wisi-forward-cache)))
1251 (wisi-forward-cache))
1252 )
1253 (wisi-get-cache (point))
1254 )
1255
1256 (defun wisi-backward-statement-keyword ()
1257 "If not at a cached token, move backward to prev
1258 cache. Otherwise move to cache-prev, or prev cache if nil."
1259 (wisi-validate-cache (point))
1260 (let ((cache (wisi-get-cache (point))))
1261 (if cache
1262 (let ((prev (wisi-cache-prev cache)))
1263 (if prev
1264 (goto-char (1- prev))
1265 (wisi-backward-cache)))
1266 (wisi-backward-cache))
1267 ))
1268
1269 (defun wisi-goto-containing (cache &optional error)
1270 "Move point to containing token for CACHE, return cache at that point.
1271 If ERROR, throw error when CACHE has no container; else return nil."
1272 (cond
1273 ((markerp (wisi-cache-containing cache))
1274 (goto-char (1- (wisi-cache-containing cache)))
1275 (wisi-get-cache (point)))
1276 (t
1277 (when error
1278 (error "already at outermost containing token")))
1279 ))
1280
1281 (defun wisi-goto-containing-paren (cache)
1282 "Move point to just after the open-paren containing CACHE.
1283 Return cache for paren, or nil if no containing paren."
1284 (while (and cache
1285 (not (eq (wisi-cache-class cache) 'open-paren)))
1286 (setq cache (wisi-goto-containing cache)))
1287 (when cache
1288 (forward-char 1))
1289 cache)
1290
1291 (defun wisi-goto-start (cache)
1292 "Move point to containing ancestor of CACHE that has class block-start or statement-start.
1293 Return start cache."
1294 (when
1295 ;; cache nil at bob, or on cache in partially parsed statement
1296 (while (and cache
1297 (not (memq (wisi-cache-class cache) '(block-start statement-start))))
1298 (setq cache (wisi-goto-containing cache)))
1299 )
1300 cache)
1301
1302 (defun wisi-goto-end-1 (cache)
1303 (goto-char (1- (wisi-cache-end cache))))
1304
1305 (defun wisi-goto-statement-start ()
1306 "Move point to token at start of statement point is in or after.
1307 Return start cache."
1308 (interactive)
1309 (wisi-validate-cache (point))
1310 (let ((cache (wisi-get-cache (point))))
1311 (unless cache
1312 (setq cache (wisi-backward-cache)))
1313 (wisi-goto-start cache)))
1314
1315 (defun wisi-goto-statement-end ()
1316 "Move point to token at end of statement point is in or before."
1317 (interactive)
1318 (wisi-validate-cache (point))
1319 (let ((cache (or (wisi-get-cache (point))
1320 (wisi-forward-cache))))
1321 (when (wisi-cache-end cache)
1322 ;; nil when cache is statement-end
1323 (wisi-goto-end-1 cache))
1324 ))
1325
1326 (defun wisi-next-statement-cache (cache)
1327 "Move point to CACHE-next, return cache; error if nil."
1328 (when (not (markerp (wisi-cache-next cache)))
1329 (error "no next statement cache"))
1330 (goto-char (1- (wisi-cache-next cache)))
1331 (wisi-get-cache (point)))
1332
1333 (defun wisi-prev-statement-cache (cache)
1334 "Move point to CACHE-next, return cache; error if nil."
1335 (when (not (markerp (wisi-cache-prev cache)))
1336 (error "no prev statement cache"))
1337 (goto-char (1- (wisi-cache-prev cache)))
1338 (wisi-get-cache (point)))
1339
1340 ;;;; indentation
1341
1342 (defun wisi-comment-indent ()
1343 "For `comment-indent-function'. Indent single line comment to
1344 the comment on the previous line."
1345 ;; This should only be called by comment-indent-new-line or
1346 ;; fill-comment-paragraph, so there will be a preceding comment line
1347 ;; that we can trust.
1348 (save-excursion
1349 (forward-comment -1)
1350 (if (looking-at comment-start)
1351 (current-column)
1352 (error "wisi-comment-indent called after non-comment"))))
1353
1354 (defun wisi-indent-current (offset)
1355 "Return indentation OFFSET relative to indentation of current line."
1356 (+ (current-indentation) offset)
1357 )
1358
1359 (defun wisi-indent-paren (offset)
1360 "Return indentation OFFSET relative to preceding open paren."
1361 (save-excursion
1362 (goto-char (nth 1 (syntax-ppss)))
1363 (+ (current-column) offset)))
1364
1365 (defun wisi-indent-start (offset cache)
1366 "Return indentation of OFFSET relative to containing ancestor
1367 of CACHE with class statement-start or block-start."
1368 (wisi-goto-start cache)
1369 (+ (current-indentation) offset))
1370
1371 (defun wisi-indent-statement ()
1372 "Indent region given by `wisi-goto-start' on cache at or before point, then wisi-cache-end."
1373 (wisi-validate-cache (point))
1374
1375 (save-excursion
1376 (let ((cache (or (wisi-get-cache (point))
1377 (wisi-backward-cache))))
1378 (when cache
1379 ;; can be nil if in header comment
1380 (let ((start (progn (wisi-goto-start cache) (point)))
1381 (end (progn
1382 (when (wisi-cache-end cache)
1383 ;; nil when cache is statement-end
1384 (goto-char (1- (wisi-cache-end cache))))
1385 (point))))
1386 (indent-region start end)
1387 ))
1388 )))
1389
1390 (defvar-local wisi-indent-calculate-functions nil
1391 "Functions to calculate indentation. Each called with point
1392 before a token at the beginning of a line (at current
1393 indentation); return indentation column for that token, or
1394 nil. May move point. Calling stops when first function returns
1395 non-nil.")
1396
1397 (defvar-local wisi-post-parse-fail-hook
1398 "Function to reindent portion of buffer.
1399 Called from `wisi-indent-line' when a parse succeeds after
1400 failing; assumes user was editing code that is now syntactically
1401 correct. Must leave point at indentation of current line.")
1402
1403 (defvar-local wisi-indent-failed nil
1404 "Non-nil when wisi-indent-line fails due to parse failing; cleared when indent succeeds.")
1405
1406 (defun wisi-indent-line ()
1407 "Indent current line using the wisi indentation engine."
1408 (interactive)
1409
1410 (let ((savep (point))
1411 indent)
1412 (save-excursion
1413 (back-to-indentation)
1414 (when (>= (point) savep) (setq savep nil))
1415
1416 (when (>= (point) wisi-cache-max)
1417 (wisi-validate-cache (line-end-position))) ;; include at lease the first token on this line
1418
1419 (if (> (point) wisi-cache-max)
1420 ;; parse failed
1421 (progn
1422 ;; no indent info at point. Assume user is
1423 ;; editing; indent to previous line, fix it
1424 ;; after parse succeeds
1425 (setq wisi-indent-failed t)
1426 (forward-line -1);; safe at bob
1427 (back-to-indentation)
1428 (setq indent (current-column)))
1429
1430 ;; parse succeeded
1431 (when wisi-indent-failed
1432 ;; previous parse failed
1433 (setq wisi-indent-failed nil)
1434 (run-hooks 'wisi-post-parse-fail-hook))
1435
1436 (when (> (point) wisi-cache-max)
1437 (error "wisi-post-parse-fail-hook invalidated parse."))
1438
1439 (setq indent
1440 (with-demoted-errors
1441 (or (run-hook-with-args-until-success 'wisi-indent-calculate-functions) 0))
1442 )
1443 ))
1444
1445 (if savep
1446 ;; point was inside line text; leave it there
1447 (save-excursion (indent-line-to indent))
1448 ;; point was before line text; move to start of text
1449 (indent-line-to indent))
1450 ))
1451
1452 ;;;; debug
1453 (defun wisi-parse-buffer ()
1454 (interactive)
1455 (syntax-propertize (point-max))
1456 (wisi-invalidate-cache)
1457 (wisi-validate-cache (point-max)))
1458
1459 (defun wisi-show-cache ()
1460 "Show cache at point."
1461 (interactive)
1462 (message "%s" (wisi-get-cache (point))))
1463
1464 (defun wisi-show-token ()
1465 "Move forward across one keyword, show token_id."
1466 (interactive)
1467 (let ((token (wisi-forward-token)))
1468 (message "%s" (car token))))
1469
1470 (defun wisi-show-containing-or-previous-cache ()
1471 (interactive)
1472 (let ((cache (wisi-get-cache (point))))
1473 (if cache
1474 (message "containing %s" (wisi-goto-containing cache t))
1475 (message "previous %s" (wisi-backward-cache)))
1476 ))
1477
1478 (defun wisi-show-cache-max ()
1479 (interactive)
1480 (push-mark)
1481 (goto-char wisi-cache-max))
1482
1483 ;;;;; setup
1484
1485 (defun wisi-setup (indent-calculate post-parse-fail class-list keyword-table token-table parse-table)
1486 "Set up a buffer for parsing files with wisi."
1487 (setq wisi-class-list class-list)
1488 (setq wisi-string-double-term (car (symbol-value (intern-soft "string-double" token-table))))
1489 (setq wisi-string-single-term (car (symbol-value (intern-soft "string-single" token-table))))
1490 (setq wisi-symbol-term (car (symbol-value (intern-soft "symbol" token-table))))
1491
1492 (let ((numbers (cadr (symbol-value (intern-soft "number" token-table)))))
1493 (setq wisi-number-term (car numbers))
1494 (setq wisi-number-p (cdr numbers)))
1495
1496 (setq wisi-punctuation-table (symbol-value (intern-soft "punctuation" token-table)))
1497 (setq wisi-punctuation-table-max-length 0)
1498 (let (fail)
1499 (dolist (item wisi-punctuation-table)
1500 (when item ;; default matcher can be nil
1501
1502 ;; check that all chars used in punctuation tokens have punctuation syntax
1503 (mapc (lambda (char)
1504 (when (not (= ?. (char-syntax char)))
1505 (setq fail t)
1506 (message "in %s, %c does not have punctuation syntax"
1507 (car item) char)))
1508 (cdr item))
1509
1510 (when (< wisi-punctuation-table-max-length (length (cdr item)))
1511 (setq wisi-punctuation-table-max-length (length (cdr item)))))
1512 )
1513 (when fail
1514 (error "aborting due to punctuation errors")))
1515
1516 (setq wisi-keyword-table keyword-table)
1517 (setq wisi-parse-table parse-table)
1518
1519 (setq wisi-indent-calculate-functions indent-calculate)
1520 (set (make-local-variable 'indent-line-function) 'wisi-indent-line)
1521
1522 (setq wisi-post-parse-fail-hook post-parse-fail)
1523 (setq wisi-indent-failed nil)
1524
1525 (add-hook 'before-change-functions 'wisi-before-change nil t)
1526 (add-hook 'after-change-functions 'wisi-after-change nil t)
1527
1528 (when (functionp 'jit-lock-register)
1529 (jit-lock-register 'wisi-fontify-region))
1530
1531 ;; see comments on "lexer" above re syntax-propertize
1532 (syntax-propertize (point-max))
1533
1534 (wisi-invalidate-cache)
1535 )
1536
1537 (provide 'wisi)
1538 ;;; wisi.el ends here