branch: elpa/julia-mode commit 10b0eff520eab0248b3687be3b4125dd743c47cf Author: nverno <noah.v.pe...@gmail.com> Commit: nverno <noah.v.pe...@gmail.com>
beginning/end of defun functions --- julia-mode-tests.el | 219 ++++++++++++++++++++++++++++++++++++++++++++-------- julia-mode.el | 135 ++++++++++++++++++++++++++++++-- 2 files changed, 315 insertions(+), 39 deletions(-) diff --git a/julia-mode-tests.el b/julia-mode-tests.el index 4f24513..f5436c7 100644 --- a/julia-mode-tests.el +++ b/julia-mode-tests.el @@ -65,14 +65,37 @@ (font-lock-fontify-buffer))) (should (eq ,face (get-text-property ,pos 'face))))) +(defmacro julia--should-move-point (text fun from to &optional end arg) + "With TEXT in `julia-mode', after calling FUN, the point should move FROM\ +to TO. If FROM is a string, move the point to matching string before calling +function FUN. If TO is a string, match resulting point to point a beginning of +matching line or end of match if END is non-nil. Optional ARG is passed to FUN." + (declare (indent defun)) + `(with-temp-buffer + (julia-mode) + (insert ,text) + (indent-region (point-min) (point-max)) + (goto-char (point-min)) + (if (stringp ,from) + (re-search-forward ,from) + (goto-char ,from)) + (funcall ,fun ,arg) + (should (eq (point) (if (stringp ,to) + (progn (goto-char (point-min)) + (re-search-forward ,to) + (if ,end (goto-char (match-end 0)) + (goto-char (match-beginning 0)) + (point-at-bol))) + ,to))))) + (ert-deftest julia--test-indent-if () "We should indent inside if bodies." (julia--should-indent - " + " if foo bar end" - " + " if foo bar end")) @@ -80,13 +103,13 @@ end")) (ert-deftest julia--test-indent-else () "We should indent inside else bodies." (julia--should-indent - " + " if foo bar else baz end" - " + " if foo bar else @@ -96,23 +119,23 @@ end")) (ert-deftest julia--test-indent-toplevel () "We should not indent toplevel expressions. " (julia--should-indent - " + " foo() bar()" - " + " foo() bar()")) (ert-deftest julia--test-indent-nested-if () "We should indent for each level of indentation." (julia--should-indent - " + " if foo if bar bar end end" - " + " if foo if bar bar @@ -151,11 +174,11 @@ end")) (ert-deftest julia--test-indent-function () "We should indent function bodies." (julia--should-indent - " + " function foo() bar end" - " + " function foo() bar end")) @@ -163,11 +186,11 @@ end")) (ert-deftest julia--test-indent-begin () "We should indent after a begin keyword." (julia--should-indent - " + " @async begin bar end" - " + " @async begin bar end")) @@ -175,10 +198,10 @@ end")) (ert-deftest julia--test-indent-paren () "We should indent to line up with the text after an open paren." (julia--should-indent - " + " foobar(bar, baz)" - " + " foobar(bar, baz)")) @@ -186,31 +209,31 @@ foobar(bar, "We should indent to line up with the text after an open paren, even if there are additional spaces." (julia--should-indent - " + " foobar( bar, baz )" - " + " foobar( bar, baz )")) (ert-deftest julia--test-indent-paren-newline () "python-mode-like indentation." (julia--should-indent - " + " foobar( bar, baz)" - " + " foobar( bar, baz)") (julia--should-indent - " + " foobar( bar, baz )" - " + " foobar( bar, baz @@ -219,10 +242,10 @@ foobar( (ert-deftest julia--test-indent-equals () "We should increase indent on a trailing =." (julia--should-indent - " + " foo() = bar" - " + " foo() = bar")) @@ -244,12 +267,12 @@ qux")) (ert-deftest julia--test-indent-ignores-blank-lines () "Blank lines should not affect indentation of non-blank lines." (julia--should-indent - " + " if foo bar end" - " + " if foo bar @@ -258,11 +281,11 @@ end")) (ert-deftest julia--test-indent-comment-equal () "`=` at the end of comment should not increase indent level." (julia--should-indent - " + " # a = # b = c" - " + " # a = # b = c")) @@ -270,32 +293,32 @@ c")) (ert-deftest julia--test-indent-leading-paren () "`(` at the beginning of a line should not affect indentation." (julia--should-indent - " + " \(1)" - " + " \(1)")) (ert-deftest julia--test-top-level-following-paren-indent () "`At the top level, a previous line indented due to parens should not affect indentation." (julia--should-indent - "y1 = f(x, + "y1 = f(x, z) y2 = g(x)" - "y1 = f(x, + "y1 = f(x, z) y2 = g(x)")) (ert-deftest julia--test-indentation-of-multi-line-strings () "Indentation should only affect the first line of a multi-line string." - (julia--should-indent - " a = \"\"\" + (julia--should-indent + " a = \"\"\" description begin foo bar end \"\"\"" - "a = \"\"\" + "a = \"\"\" description begin foo @@ -442,6 +465,136 @@ end") (julia--should-font-lock string pos font-lock-string-face)) (julia--should-font-lock string (length string) font-lock-keyword-face))) +;;; Movement +(ert-deftest julia--test-beginning-of-defun-assn-1 () + "Point moves to beginning of single-line assignment function." + (julia--should-move-point + "f() = \"a + b\"" 'beginning-of-defun "a \\+" 1)) + +(ert-deftest julia--test-beginning-of-defun-assn-2 () + "Point moves to beginning of multi-line assignment function." + (julia--should-move-point + "f(x)= +x* +x" 'beginning-of-defun "\\*\nx" 1)) + +(ert-deftest julia--test-beginning-of-defun-assn-3 () + "Point moves to beginning of multi-line assignment function adjoining +another function." + (julia--should-move-point + "f( x +)::Int16 = x / 2 +f2(y)= +y*y" 'beginning-of-defun "2" 1)) + +(ert-deftest julia--test-beginning-of-defun-assn-4 () + "Point moves to beginning of 2nd multi-line assignment function adjoining +another function." + (julia--should-move-point + "f( x +)::Int16 = +x / +2 +f2(y) = +y*y" 'beginning-of-defun "\\*y" "f2")) + +(ert-deftest julia--test-beginning-of-defun-assn-5 () + "Point moves to beginning of 1st multi-line assignment function adjoining +another function with prefix arg." + (julia--should-move-point + "f( x +)::Int16 = +x / +2 +f2(y) = +y*y" 'beginning-of-defun "y\\*y" 1 nil 2)) + +(ert-deftest julia--test-beginning-of-macro () + "Point moves to beginning of macro." + (julia--should-move-point + "macro current_module() +return VERSION >= v\"0.7-\" :(@__MODULE__) : :(current_module()))) +end" 'beginning-of-defun "@" 1)) + +(ert-deftest julia--test-beginning-of-defun-1 () + "Point moves to beginning of defun in 'function's." + (julia--should-move-point + "function f(a, b) +a + b +end" 'beginning-of-defun "f(" 1)) + +(ert-deftest julia--test-beginning-of-defun-nested-1 () + "Point moves to beginning of nested function." + (julia--should-move-point + "function f(x) + +function fact(n) +if n == 0 +return 1 +else +return n * fact(n-1) +end +end + +return fact(x) +end" 'beginning-of-defun "fact(n" "function fact")) + +(ert-deftest julia--test-beginning-of-defun-nested-2 () + "Point moves to beginning of outermost function with prefix arg." + (julia--should-move-point + "function f(x) + +function fact(n) +if n == 0 +return 1 +else +return n * fact(n-1) +end +end + +return fact(x) +end" 'beginning-of-defun "n \\*" 1 nil 2)) + +(ert-deftest julia--test-beginning-of-defun-no-move () + "Point shouldn't move if there is no previous function." + (julia--should-move-point + "1 + 1 +f(x) = x + 1" 'beginning-of-defun "\\+" 4)) + +(ert-deftest julia--test-end-of-defun-assn-1 () + "Point should move to end of assignment function." + (julia--should-move-point + "f(x)::Int8 = +x *x" 'end-of-defun "(" "*x" 'end)) + +(ert-deftest julia--test-end-of-defun-nested-1 () + "Point should move to end of inner function when called from inner." + (julia--should-move-point + "function f(x) +function fact(n) +if n == 0 +return 1 +else +return n * fact(n-1) +end +end +return fact(x) +end" 'end-of-defun "n == 0" "end[ \n]+end\n" 'end)) + +(ert-deftest julia--test-end-of-defun-nested-2 () + "Point should move to end of outer function when called from inner with prefix." + (julia--should-move-point + "function f(x) +function fact(n) +if n == 0 +return 1 +else +return n * fact(n-1) +end +end +return fact(x) +end" 'end-of-defun "n == 0" "return fact(x)[ \n]+end" 'end 2)) + (defun julia--run-tests () (interactive) (if (featurep 'ert) diff --git a/julia-mode.el b/julia-mode.el index 4e5960c..ff4d7d8 100644 --- a/julia-mode.el +++ b/julia-mode.el @@ -373,16 +373,16 @@ Based on `python-syntax-stringify'." (2 ".") ; Don't highlight anything between. (3 "\""))))) ; Treat the last " in """ as a string delimiter. -(defun julia-in-comment () - "Return non-nil if point is inside a comment. +(defun julia-in-comment (&optional syntax-ppss) + "Return non-nil if point is inside a comment using SYNTAX-PPSS. Handles both single-line and multi-line comments." - (nth 4 (syntax-ppss))) + (nth 4 (or syntax-ppss (syntax-ppss)))) -(defun julia-in-string () - "Return non-nil if point is inside a string. +(defun julia-in-string (&optional syntax-ppss) + "Return non-nil if point is inside a string using SYNTAX-PPSS. Note this is Emacs' notion of what is highlighted as a string. As a result, it is true inside \"foo\", `foo` and 'f'." - (nth 3 (syntax-ppss))) + (nth 3 (or syntax-ppss (syntax-ppss)))) (defun julia-in-brackets () "Return non-nil if point is inside square brackets." @@ -637,6 +637,127 @@ meaning always increase indent on TAB and decrease on S-TAB." 'prog-mode 'fundamental-mode)) + +;;; Navigation +;; based off python.el +(defconst julia-beginning-of-defun-regex + (eval-when-compile (concat julia-function-regex "\\|" + julia-function-assignment-regex "\\|" + "\\_<macro\\_>")) + "Regex matching beginning of Julia function or macro.") + +(defun julia-syntax-context-type (&optional syntax-ppss) + "Return the context type using SYNTAX-PPSS. +TYPE can be `comment', `string' or `paren'." + (let ((ppss (or syntax-ppss (syntax-ppss)))) + (cond + ((nth 8 ppss) (if (nth 4 ppss) 'comment 'string)) + ((nth 1 ppss) 'paren)))) + +(defsubst julia-syntax-comment-or-string-p (&optional syntax-ppss) + "Return non-nil if SYNTAX-PPSS is inside string or comment." + (nth 8 (or syntax-ppss (syntax-ppss)))) + +(defun julia-looking-at-beginning-of-defun (&optional syntax-ppss) + "Check if point is at `beginning-of-defun' using SYNTAX-PPSS." + (and (not (julia-syntax-comment-or-string-p (or syntax-ppss (syntax-ppss)))) + (save-excursion + (beginning-of-line 1) + (looking-at julia-beginning-of-defun-regex)))) + +(defun julia--beginning-of-defun (&optional arg) + "Internal implementation of `julia-beginning-of-defun'. +With positive ARG search backwards, else search forwards." + (when (or (null arg) (= arg 0)) (setq arg 1)) + (let* ((re-search-fn (if (> arg 0) + #'re-search-backward + #'re-search-forward)) + (line-beg-pos (line-beginning-position)) + (line-content-start (+ line-beg-pos (current-indentation))) + (pos (point-marker)) + (beg-indentation + (and (> arg 0) + (save-excursion + (while (and (not (julia-looking-at-beginning-of-defun)) + ;; f(x) = ... function bodies may span multiple lines + (or (and (julia-indent-hanging) + (forward-line -1)) + ;; inside dangling parameter list + (and (eq 'paren (julia-syntax-context-type)) + (backward-up-list)) + (julia-last-open-block (point-min))))) + (or (and (julia-looking-at-beginning-of-defun) + (+ (current-indentation) julia-indent-offset)) + 0)))) + (found + (progn + (when (and (< arg 0) + (julia-looking-at-beginning-of-defun)) + (end-of-line 1)) + (while (and (funcall re-search-fn + julia-beginning-of-defun-regex nil t) + (or (julia-syntax-comment-or-string-p) + ;; handle nested defuns when moving backwards + ;; by checking matching indentation + (and (> arg 0) + (not (= (current-indentation) 0)) + (>= (current-indentation) beg-indentation))))) + (and (julia-looking-at-beginning-of-defun) + (or (not (= (line-number-at-pos pos) + (line-number-at-pos))) + (and (>= (point) line-beg-pos) + (<= (point) line-content-start) + (> pos line-content-start))))))) + (if found + (or (beginning-of-line 1) (point)) + (and (goto-char pos) nil)))) + +(defun julia-beginning-of-defun (&optional arg) + "Move point to `beginning-of-defun'. +With positive ARG search backwards else search forward. +ARG nil or 0 defaults to 1. When searching backwards, +nested defuns are handled depending on current point position. +Return non-nil (point) if point moved to `beginning-of-defun'." + (when (or (null arg) (= arg 0)) (setq arg 1)) + (let ((found)) + (while (and (not (= arg 0)) + (let ((keep-searching-p + (julia--beginning-of-defun arg))) + (when (and keep-searching-p (null found)) + (setq found t)) + keep-searching-p)) + (setq arg (if (> arg 0) (1- arg) (1+ arg)))) + found)) + +(defun julia-end-of-defun (&optional arg) + "Move point to the end of the current function. +Return nil if point is not in a function, otherwise point." + (interactive) + (let ((beg-defun-indent) + (beg-pos (point))) + (when (or (julia-looking-at-beginning-of-defun) + (julia-beginning-of-defun 1) + (julia-beginning-of-defun -1)) + (beginning-of-line) + (if (looking-at-p julia-function-assignment-regex) + ;; f(x) = ... + (progn + ;; skip any dangling lines + (while (and (forward-line) + (not (eobp)) + (or (julia-indent-hanging) + ;; dangling closing paren + (and (eq 'paren (julia-syntax-context-type)) + (search-forward ")")))))) + ;; otherwise skip forward to matching indentation (not in string/comment) + (setq beg-defun-indent (current-indentation)) + (while (and (not (eobp)) + (forward-line 1) + (or (julia-syntax-comment-or-string-p) + (> (current-indentation) beg-defun-indent))))) + (end-of-line) + (point)))) + ;;; IMENU (defvar julia-imenu-generic-expression ;; don't use syntax classes, screws egrep @@ -680,6 +801,8 @@ meaning always increase indent on TAB and decrease on S-TAB." (set (make-local-variable 'syntax-propertize-function) julia-syntax-propertize-function)) (set (make-local-variable 'indent-line-function) 'julia-indent-line) + (set (make-local-variable 'beginning-of-defun-function) #'julia-beginning-of-defun) + (set (make-local-variable 'end-of-defun-function) #'julia-end-of-defun) (setq indent-tabs-mode nil) (setq imenu-generic-expression julia-imenu-generic-expression) (imenu-add-to-menubar "Imenu"))