diff --git a/go-mode.el b/go-mode.el index cdebdc3c..5f754442 100644 --- a/go-mode.el +++ b/go-mode.el @@ -448,6 +448,21 @@ statements." ;; Match name+type pairs, such as "foo bar" in "var foo bar". (go--match-ident-type-pair 2 font-lock-type-face) + ;; Match type unions such as "int | string" in "interface { int | string }". + (go--match-type-union + (1 font-lock-type-face nil t) + (2 font-lock-type-face nil t)) + + ;; Match type params in instantiation such as "foo[int, string]". + (go--match-type-param-start + ;; Sub-matcher that matches individual type params in the list. + (go--fontify-type-param + nil + ;; Post-matcher that moves point back to "[" so next match can find + ;; nested param lists. + (go--fontify-type-param-post) + (1 font-lock-type-face nil t))) + ;; An anchored matcher for type switch case clauses. (go--match-type-switch-case (go--fontify-type-switch-case @@ -471,9 +486,9 @@ statements." (if go-fontify-function-calls ;; Function call/method name - `((,(concat "\\(" go-identifier-regexp "\\)[[:space:]]*(") 1 font-lock-function-name-face) - ;; Bracketed function call - (,(concat "[^[:word:][:multibyte:]](\\(" go-identifier-regexp "\\))[[:space:]]*(") 1 font-lock-function-name-face)) + `((go--match-func-name + (1 font-lock-function-name-face nil t) + (2 font-lock-function-name-face nil t))) ;; Method name `((,go-func-meth-regexp 2 font-lock-function-name-face))) @@ -491,7 +506,7 @@ statements." ("\\(!\\)[^=]" 1 font-lock-negation-char-face) ;; Composite literal type - (,(concat go-type-name-regexp "{") 1 font-lock-type-face) + (go--match-composite-literal 1 font-lock-type-face) ;; Map value type (go--match-map-value 1 font-lock-type-face) @@ -733,6 +748,16 @@ case keyword. It returns nil for the case line itself." "Return non-nil if point is inside a type switch statement." (go--in-paren-with-prefix-p ?{ ".(type)")) +(defun go--in-type-params-p () + "Return non-nil if point is inside a type param list." + (save-excursion + (and + (go-goto-opening-parenthesis) + (eq (char-after) ?\[) + (backward-word) + (skip-syntax-backward " ") + (member (thing-at-point 'word 'no-properties) '("type" "func"))))) + (defun go--fill-prefix () "Return fill prefix for following comment paragraph." (save-excursion @@ -1375,24 +1400,23 @@ declarations are also included." (let (found-match) (while (and (not found-match) - (re-search-forward (concat "\\(\\_<" go-identifier-regexp "\\)?(") end t)) + (search-forward "(" end t)) (when (not (go-in-string-or-comment-p)) (save-excursion - (goto-char (match-beginning 0)) - - (let ((name (match-string 1))) - (when name - ;; We are in a param list if "func" preceded the "(" (i.e. - ;; func literal), or if we are in an interface - ;; declaration, e.g. "interface { foo(i int) }". - (setq found-match (or (string= name "func") (go--in-interface-p)))) - - ;; Otherwise we are in a param list if our "(" is preceded - ;; by ") " or "func ". - (when (and (not found-match) (not (zerop (skip-syntax-backward " ")))) - (setq found-match (or - (eq (char-before) ?\)) - (looking-back "\\_ (go-paren-level) orig-level))) (forward-char)) - (when (and (looking-at-p ",") + (when (and (eq (char-after) ?,) (< (point) (1- end))) (forward-char) t))) @@ -1482,7 +1506,7 @@ comma, it stops at it. Return non-nil if comma was found." (goto-char (match-end 1)) (unless (member (match-string 1) go-constants) (setq found-match t))) - (setq done (not (go--search-next-comma end)))) + (setq done (not (go--search-next-comma end ?\))))) found-match)) (defun go--containing-decl () @@ -1594,6 +1618,81 @@ succeeds." found-match)) +(defconst go--type-union-re (concat go-type-name-regexp "\\s-*|\\||\\s-*" go-type-name-regexp)) + +(defun go--match-type-union (end) + "Search for type unions in interfaces and constraints." + (let (found-match) + (while (and + (not found-match) + ;; Look for "foo |" or "| foo" (i.e. a type name before or + ;; after a pipe). + (re-search-forward go--type-union-re end t)) + + (let ((match (match-string 1))) + ;; If we matched "foo |", move back one char so we can see the + ;; pipe again on the next iteration. + (if match + (backward-char) + (setq match (match-string 2))) + + (setq found-match (and + (not (member match go-mode-keywords)) + (or + (go--in-interface-p) + (go--in-type-params-p)))))) + found-match)) + + +(defvar go--fontify-type-param-beg nil) + +(defun go--match-type-param-start (end) + "Search for [ that starts a type instantiation param list." + (let (found-match) + (while (and + (not found-match) + (search-forward "[" end t)) + (when (not (go-in-string-or-comment-p)) + (setq go--fontify-type-param-beg (point)) + + ;; In general an index expression is ambiguous, but there are some + ;; things we can look for to be certain it is a type param list. + (setq found-match + (save-excursion + (or + ;; If we have more than one comma, or a composite literal "{" + ;; follows the param list. + (let ((commas 0)) + (save-excursion + (while (go--search-next-comma end ?\]) + (cl-incf commas)) + (or (> commas 0) + (eq (char-after (1+ (point))) ?{)))) + + ;; If we are preceded by a space and an identifer then we are + ;; in type name (e.g. preceded be "foo " in "var foo bar[int]"). + (and (not (zerop (skip-syntax-backward "^ "))) + (not (zerop (skip-syntax-backward " " (line-beginning-position)))) + (let ((word (thing-at-point 'word 'no-properties))) + (and word (not (member word go-mode-keywords)))))))))) + found-match)) + +(defun go--fontify-type-param (end) + "Advance through each type param in a type instantion param list." + (let (found-match done) + (while (and (not found-match) (not done)) + (skip-syntax-forward " ") + (setq found-match (and + (looking-at go-type-name-regexp) + (not (member (match-string 1) go-mode-keywords)))) + ;; Advance to next comma. We are done if there are no more commas. + (setq done (not (go--search-next-comma end ?\])))) + found-match)) + +(defun go--fontify-type-param-post () + "Move point back to opening bracket to allow matching nested param lists." + (goto-char go--fontify-type-param-beg)) + (defconst go--single-func-result-re (concat ")[[:space:]]+" go-type-name-regexp "\\(?:$\\|[[:space:]),]\\)")) (defun go--match-single-func-result (end) @@ -1631,6 +1730,56 @@ We are looking for the right-hand-side of the type alias" found-match)) +(defconst go--match-func-name-re + (concat "\\(?:^\\|[^)]\\)(\\(" go-identifier-regexp "\\))(\\|\\(" go-identifier-regexp "\\)[[(]")) + +(defun go--match-func-name (end) + "Search for func names in decls and invocations." + (let (found-match) + (while (and + (not found-match) + ;; Match "(foo)(" or "foo[" or "foo(". + (re-search-forward go--match-func-name-re end t)) + (if (eq (char-before) ?\() + ;; Followed directly by "(" is a match. + (setq found-match t) + ;; Followed by "[". We are a match if there is at least one + ;; comma between the "[" and "]", and if "]" is followed by + ;; "(". + (let ((commas 0)) + (while (go--search-next-comma end ?\]) + (cl-incf commas)) + (forward-char) + (setq found-match (and + ;; We found a comma (so we are sure these are type + ;; params), or we are at file level (so we are sure + ;; it is a func decl). + (or (> commas 0) (= 0 (go-paren-level))) + (eq (char-after) ?\()))))) + found-match)) + +(defconst go--match-composite-literal-re (concat go-type-name-regexp "[[{]")) + +(defun go--match-composite-literal (end) + "Search for composite literals." + (let (found-match) + (while (and + (not found-match) + ;; Match "foo{" or "foo[". + (re-search-forward go--match-composite-literal-re end t)) + + (setq found-match + (if (eq (char-before) ?\[) + ;; In "foo[" case, skip to closing "]". + (progn + (while (go--search-next-comma end ?\])) + ;; See if closing "]" is followed by "{". + (eq (char-after (1+ (point))) ?{)) + ;; The "foo{" case (definitely composite literal). + (eq (char-before) ?{)))) + + found-match)) + (defconst go--map-value-re (concat "\\_\\[\\(?:\\[[^]]*\\]\\)*[^]]*\\]" go-type-name-regexp)) diff --git a/test/go-font-lock-test.el b/test/go-font-lock-test.el index c943fe49..afed6289 100644 --- a/test/go-font-lock-test.el +++ b/test/go-font-lock-test.el @@ -45,6 +45,32 @@ QD// DQ( QKfuncK (VfV TintT) {} ")) +(ert-deftest go--fontify-generic-signature () + (go--should-fontify "KfuncK FfooF[a TintT](VaV TintT) { }") + (go--should-fontify "KfuncK FfooF[a TintT](TintT) { }")) + +(ert-deftest go--fontify-type-union () + (go--should-fontify "KfuncK FfooF[a TintT | TstringT | KstructK{} | *Tfoo.ZebraT](TintT) { }") + (go--should-fontify "KinterfaceK { TintT | Tfloat64T }")) + +(ert-deftest go--fontify-type-instantiation () + ;; ambiguous - leave it unfontified + (go--should-fontify "foo[int]") + + (go--should-fontify "KvarK VvV TfooT[TintT]") + (go--should-fontify "KvarK VvV TfooT[TbarT[TintT]]") + (go--should-fontify "foo[TintT, KstructK{}, *Tfoo.ZebraT]")) + +(ert-deftest go--fontify-func () + (go--should-fontify "KfuncK FfooF()") + (go--should-fontify "KfuncK FfooF[A TanyT]()") + (go--should-fontify "KfuncK (VfV TfooT) FfooF[A TanyT]()") + (go--should-fontify "FfooF(bar)") + (go--should-fontify "foo.FfooF(bar)") + (go--should-fontify "(FfooF)(foo)(foo)") + (go--should-fontify "{ foo[int](123) }") + (go--should-fontify "FfooF[TintT, TstringT](123)")) + (ert-deftest go--fontify-struct () (go--should-fontify "KstructK { i TintT }") (go--should-fontify "KstructK { a, b TintT }") @@ -98,6 +124,7 @@ KcaseK string: (go--should-fontify "TfooT{") (go--should-fontify "[]TfooT{") (go--should-fontify "Tfoo.ZarT{") + (go--should-fontify "Tfoo.ZarT[TintT]{") (go--should-fontify "[]Tfoo.ZarT{") (go--should-fontify "TfooT{CbarC:baz, CquxC: 123}")