Skip to content

Commit b0b2ba9

Browse files
committed
Merge pull request #81 from elixir-lang/whitespace-indentation
Rewrite token emitting functions
2 parents af129fe + aecec45 commit b0b2ba9

File tree

4 files changed

+101
-261
lines changed

4 files changed

+101
-261
lines changed

elixir-mode-tests.el

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
,@body))
2222

2323
(load "test/elixir-mode-indentation-tests.el")
24-
(load "test/elixir-mode-tokenizer-hl-tests.el")
2524
(load "test/elixir-mode-font-tests.el")
2625

2726
(provide 'elixir-mode-tests)

elixir-smie.el

Lines changed: 82 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -34,215 +34,90 @@
3434
table)
3535
"Elixir mode syntax table.")
3636

37+
(defconst elixir-smie-grammar
38+
(smie-prec2->grammar
39+
(smie-merge-prec2s
40+
(smie-bnf->prec2
41+
'((id)
42+
(statements (statement)
43+
(statement ";" statements))
44+
(statement ("def" non-block-expr "do" statements "end")
45+
(non-block-expr "fn" match-statement "end")
46+
(non-block-expr "do" statements "end")
47+
("if" non-block-expr "do" statements "else" statements "end")
48+
("if" non-block-expr "do" statements "end")
49+
("if" non-block-expr "COMMA" "do:" non-block-expr)
50+
("if" non-block-expr "COMMA"
51+
"do:" non-block-expr "COMMA"
52+
"else:" non-block-expr)
53+
("try" "do" statements "after" statements "end")
54+
("try" "do" statements "catch" match-statements "end")
55+
("try" "do" statements "end")
56+
("case" non-block-expr "do" match-statements "end"))
57+
(non-block-expr (non-block-expr "OP" non-block-expr)
58+
(non-block-expr "COMMA" non-block-expr)
59+
("(" non-block-expr ")")
60+
("{" non-block-expr "}")
61+
("[" non-block-expr "]")
62+
("STRING"))
63+
(match-statements (match-statement "MATCH-STATEMENT-DELIMITER"
64+
match-statements)
65+
(match-statement))
66+
(match-statement (non-block-expr "->" statements)))
67+
'((assoc "if" "do:" "else:")
68+
(assoc "COMMA")
69+
(left "OP")))
70+
71+
(smie-precs->prec2
72+
'((left "||")
73+
(left "&&")
74+
(nonassoc "=~" "===" "!==" "==" "!=" "<=" ">=" "<" ">")
75+
(left "+" "-" "<<<" ">>>" "^^^" "~~~" "&&&" "|||")
76+
(left "*" "/"))))))
77+
78+
(defvar elixir-smie--operator-regexp
79+
(regexp-opt '("<<<" ">>>" "^^^" "~~~" "&&&" "|||" "===" "!==" "==" "!=" "<="
80+
">=" "<" ">" "&&" "||" "<>" "++" "--" "//"
81+
"/>" "=~" "|>" "->")))
82+
83+
(defvar elixir-smie-indent-basic 2)
84+
3785
(defmacro elixir-smie-debug (message &rest format-args)
3886
`(progn
3987
(when elixir-smie-verbose-p
4088
(message (format ,message ,@format-args)))
4189
nil))
4290

43-
(progn
44-
(setq elixir-syntax-class-names nil)
45-
46-
(defmacro elixir-smie-define-regexp-opt (name &rest table)
47-
`(elixir-smie-define-regexp ,name (regexp-opt (list ,@table))))
48-
49-
(defmacro elixir-smie-define-regexp (name regexp &optional flag)
50-
(let ((regex-name (intern (format "elixir-smie-%s" name))))
51-
`(progn
52-
(defconst ,regex-name
53-
,regexp)
54-
(pushnew `(,',regex-name . ,(upcase (symbol-name ',name))) elixir-syntax-class-names))))
55-
56-
(elixir-smie-define-regexp-opt op "&&" "||" "!")
57-
(elixir-smie-define-regexp dot "\\.")
58-
(elixir-smie-define-regexp comma ",")
59-
(elixir-smie-define-regexp -> "->")
60-
(elixir-smie-define-regexp << "<<")
61-
(elixir-smie-define-regexp >> ">>")
62-
(elixir-smie-define-regexp-opt parens "(" ")" "{" "}" "[" "]" "<<" ">>"))
63-
64-
(defconst elixir-smie-block-intro-keywords
65-
'(do else catch after rescue -> OP)
66-
"Keywords in which newlines cause confusion for the parser.")
67-
68-
(defun elixir-skip-comment-backward ()
69-
"Skip backwards over all whitespace and comments.
70-
71-
Return non-nil if any line breaks were skipped."
72-
(let ((start-line-no (line-number-at-pos (point))))
73-
(forward-comment (- (point)))
74-
(/= start-line-no (line-number-at-pos (point)))))
75-
76-
(defun elixir-skip-comment-forward ()
77-
"Skip forward over any whitespace and comments.
78-
79-
Return non-nil if any line breaks were skipped."
80-
(let ((start-line-no (line-number-at-pos (point))))
81-
(forward-comment (buffer-size))
82-
(/= start-line-no (line-number-at-pos (point)))))
83-
84-
(defun elixir-smie-next-token-no-lookaround (forwardp)
85-
(block elixir-smie-next-token-no-lookaround
86-
;; First, skip comments; but if any comments / newlines were
87-
;; skipped, the upper level needs to check if they were significant:
88-
(when (if forwardp
89-
(elixir-skip-comment-forward)
90-
(elixir-skip-comment-backward))
91-
(return-from elixir-smie-next-token-no-lookaround "\n"))
92-
(let* ((found-token-class (find-if
93-
(lambda (class-def)
94-
(let ((regex (symbol-value (car class-def))))
95-
(if forwardp
96-
(looking-at regex)
97-
(looking-back regex nil t))))
98-
elixir-syntax-class-names))
99-
(maybe-token
100-
(let ((current-char (if forwardp
101-
(following-char)
102-
(preceding-char))))
103-
(cond ((member current-char
104-
'(?\n ?\;))
105-
(if forwardp
106-
(forward-comment (point-max))
107-
(forward-comment (- (point))))
108-
(string current-char))
109-
(found-token-class
110-
(goto-char (if forwardp
111-
(match-end 0)
112-
(match-beginning 0)))
113-
(if (string= "PARENS" (cdr found-token-class))
114-
(buffer-substring-no-properties (match-beginning 0) (match-end 0))
115-
(cdr found-token-class)))
116-
((when (= ?\" (char-syntax (if forwardp
117-
(following-char)
118-
(preceding-char))))
119-
(if forwardp
120-
(forward-sexp)
121-
(backward-sexp))
122-
"STRING"))))))
123-
(or maybe-token
124-
(downcase
125-
(buffer-substring-no-properties
126-
(point)
127-
(if forwardp
128-
(progn (skip-syntax-forward "'w_")
129-
(point))
130-
(progn (skip-syntax-backward "'w_")
131-
(point)))))))))
91+
(defun elixir-smie--at-dot-call ()
92+
(and (eq ?w (char-syntax (following-char)))
93+
(eq (char-before) ?.)
94+
(not (eq (char-before (1- (point))) ?.))))
13295

133-
(defun elixir-smie-next-token (forwardp)
134-
(block elixir-smie-next-token
135-
(let ((current-token (elixir-smie-next-token-no-lookaround forwardp)))
136-
(when (string= "\n" current-token)
137-
;; This is a newline; if the previous token isn't an OP2, this
138-
;; means the line end marks the end of a statement & we get to
139-
;; scan forward until there's a non-newline token; otherwise,
140-
;; make this line ending something that probably ends the
141-
;; statement (but see below).
142-
(if (save-excursion
143-
(block nil
144-
(let ((token (elixir-smie-next-token-no-lookaround nil)))
145-
(while (and (not (= (point) (point-min)))
146-
(string= "\n" token))
147-
(setq token (elixir-smie-next-token-no-lookaround nil)))
148-
(when (member (intern token) elixir-smie-block-intro-keywords)
149-
(return t)))))
150-
;; it's a continuation line, return the next token after the newline:
151-
(return-from elixir-smie-next-token (elixir-smie-next-token forwardp))
152-
(setq current-token ";")))
153-
154-
;; When reading match statements (the ones with expr -> statements),
155-
;; we need to drop non-; delimiters so the parser knows when a
156-
;; match statement ends and another begins, so scan around point to
157-
;; see if there are any -> within the current block's scope.
158-
159-
;; If the current token is a ";", scan forward to see if the current
160-
;; potential statement contains a "->". If so, scan back to find a
161-
;; "do". If there is a -> there, emit a match-statement-delimiter
162-
;; instead of the ";".
163-
(if (and (string= ";" current-token)
164-
;; Scan ahead:
165-
(let ((level 0)
166-
token)
167-
(save-excursion
168-
(block nil
169-
(while
170-
(and
171-
;; Cursor is not at the end of the buffer...
172-
(not (= (point) (point-max)))
173-
;; ...and the current token is not an empty string...
174-
(not (string= "" token))
175-
;; ...nor a newline nor a semicolon.
176-
(not (or (string= "\n" token) (string= ";" token))))
177-
(setq token (elixir-smie-next-token-no-lookaround t))
178-
;; If we're at the top level and the token is "->",
179-
;; return t
180-
(cond ((and (= level 0) (string= "->" token))
181-
(return t))
182-
;; If token is "do" or "fn", increment level
183-
((find token '("do" "fn") :test 'string=)
184-
(incf level))
185-
;; If token is "end", decrement level
186-
((string= token "end")
187-
(decf level)))))))
188-
;; Scan behind:
189-
(let (token)
190-
(save-excursion
191-
(block nil
192-
(while
193-
(and
194-
;; Cursor is not at the beginning of buffer...
195-
(not (= (point) (point-min)))
196-
;; ...and token is neither empty string, nor "do"/"fn"
197-
(not (string= "" token))
198-
(not (string= "do" token))
199-
(not (string= "fn" token)))
200-
(setq token (elixir-smie-next-token-no-lookaround nil))
201-
(when (string= "->" token)
202-
(return t)))
203-
(when (string= token "do") t)))))
204-
"MATCH-STATEMENT-DELIMITER"
205-
current-token))))
96+
(defun elixir-smie--implicit-semi-p ()
97+
(not (or (memq (char-before) '(?\{ ?\[))
98+
(looking-back elixir-smie--operator-regexp (- (point) 3) t))))
20699

207100
(defun elixir-smie-forward-token ()
208-
(elixir-smie-next-token t))
101+
(cond
102+
((and (looking-at "[\n#]") (elixir-smie--implicit-semi-p))
103+
(if (eolp) (forward-char 1) (forward-comment 1))
104+
";")
105+
((looking-at elixir-smie--operator-regexp)
106+
(goto-char (match-end 0))
107+
"OP")
108+
(t (smie-default-forward-token))))
209109

210110
(defun elixir-smie-backward-token ()
211-
(elixir-smie-next-token nil))
212-
213-
(defconst elixir-smie-grammar
214-
(smie-prec2->grammar
215-
(smie-bnf->prec2
216-
'((id)
217-
(statements (statement)
218-
(statement ";" statements))
219-
(statement ("def" non-block-expr "do" statements "end")
220-
(non-block-expr "fn" match-statement "end")
221-
(non-block-expr "do" statements "end")
222-
("if" non-block-expr "do" statements "else" statements "end")
223-
("if" non-block-expr "do" statements "end")
224-
("if" non-block-expr "COMMA" "do:" non-block-expr)
225-
("if" non-block-expr "COMMA"
226-
"do:" non-block-expr "COMMA"
227-
"else:" non-block-expr)
228-
("try" "do" statements "after" statements "end")
229-
("try" "do" statements "catch" match-statements "end")
230-
("try" "do" statements "end")
231-
("case" non-block-expr "do" match-statements "end"))
232-
(non-block-expr (non-block-expr "OP" non-block-expr)
233-
(non-block-expr "COMMA" non-block-expr)
234-
("(" statements ")")
235-
("{" statements "}")
236-
("[" statements "]")
237-
("STRING"))
238-
(match-statements (match-statement "MATCH-STATEMENT-DELIMITER" match-statements)
239-
(match-statement))
240-
(match-statement (non-block-expr "->" statements)))
241-
'((assoc "if" "do:" "else:")
242-
(assoc "COMMA")
243-
(left "OP")))))
244-
245-
(defvar elixir-smie-indent-basic 2)
111+
(let ((pos (point)))
112+
(forward-comment (- (point)))
113+
(cond
114+
((and (> pos (line-end-position))
115+
(elixir-smie--implicit-semi-p))
116+
";")
117+
((looking-back elixir-smie--operator-regexp (- (point) 3) t)
118+
(goto-char (match-beginning 0))
119+
"OP")
120+
(t (smie-default-backward-token)))))
246121

247122
(defun verbose-elixir-smie-rules (kind token)
248123
(let ((value (elixir-smie-rules kind token)))
@@ -256,35 +131,25 @@ Return non-nil if any line breaks were skipped."
256131

257132
(defun elixir-smie-rules (kind token)
258133
(pcase (cons kind token)
259-
(`(:after . "STRING")
260-
(if (smie-rule-prev-p "do:")
261-
(smie-rule-parent 0)
262-
nil))
263-
(`(:elem . basic)
264-
(if (smie-rule-hanging-p)
265-
0
266-
elixir-smie-indent-basic))
267134
(`(:after . "OP")
268-
(unless (smie-rule-sibling-p)
269-
elixir-smie-indent-basic))
135+
(cond
136+
((smie-rule-sibling-p) nil)
137+
((smie-rule-hanging-p) (smie-rule-parent elixir-smie-indent-basic))
138+
(t elixir-smie-indent-basic)))
270139
(`(:before . "def") elixir-smie-indent-basic)
271140
;; If the parent token of `->' is `fn', then we want to align to the
272141
;; parent, and offset by `elixir-smie-indent-basic'. Otherwise, indent
273142
;; normally. This helps us work with/indent anonymous function blocks
274143
;; correctly.
275-
(`(:after . "->")
276-
(when (smie-rule-hanging-p)
277-
(if (smie-rule-parent-p "fn")
278-
(smie-rule-parent elixir-smie-indent-basic)
279-
elixir-smie-indent-basic)))
280-
(`(:after . "do")
281-
elixir-smie-indent-basic)
282-
(`(:list-intro . ,(or `"do" `";")) t)
144+
(`(:before . ";")
145+
(cond
146+
((smie-rule-parent-p "after" "catch" "def" "defmodule" "defp" "do" "else"
147+
"fn" "if" "rescue" "try" "unless")
148+
(smie-rule-parent elixir-smie-indent-basic))))
283149
(`(:after . ";")
284150
(if (smie-rule-parent-p "if")
285151
(smie-rule-parent 0)))))
286152

287-
288153
(define-minor-mode elixir-smie-mode
289154
"SMIE-based indentation and syntax for Elixir"
290155
nil nil nil nil

test/elixir-mode-indentation-tests.el

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -176,26 +176,36 @@ end
176176

177177
(elixir-def-indentation-test indents-records-correctly ()
178178
"
179-
defrecord Money, [:currency_unit, :amount] do
180-
foo
179+
defmodule MyModule do
180+
require Record
181+
Record.defrecord :money, [:currency_unit, :amount]
182+
183+
Record.defrecord :animal, [:species, :name]
181184
end
182185
"
183186
"
184-
defrecord Money, [:currency_unit, :amount] do
185-
foo
187+
defmodule MyModule do
188+
require Record
189+
Record.defrecord :money, [:currency_unit, :amount]
190+
191+
Record.defrecord :animal, [:species, :name]
186192
end
187193
")
188194

189195
(elixir-def-indentation-test indents-continuation-lines ()
190196
"
191-
has_something(x) &&
192-
has_something(y) ||
193-
has_something(z)
194-
"
195-
"
197+
def foo do
196198
has_something(x) &&
197199
has_something(y) ||
198200
has_something(z)
201+
end
202+
"
203+
"
204+
def foo do
205+
has_something(x) &&
206+
has_something(y) ||
207+
has_something(z)
208+
end
199209
")
200210

201211
(elixir-def-indentation-test indents-continuation-lines-with-comments/1

0 commit comments

Comments
 (0)