branch: elpa/scala-mode commit c5ab65732c000baf08b50da96a839e7aa6620084 Merge: 34888c0 d4885ac Author: Heikki Vesalainen <heikki.vesalai...@iki.fi> Commit: Heikki Vesalainen <heikki.vesalai...@iki.fi>
Merge pull request #82 from IvanMalison/support_imenu Support imenu --- README.md | 52 +++++++++++++++++++-- scala-mode2-imenu.el | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++ scala-mode2-syntax.el | 91 ++++++++++++++++++++++++++++++++++++ scala-mode2.el | 13 ++++-- 4 files changed, 275 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index df0ec19..3a90d00 100644 --- a/README.md +++ b/README.md @@ -428,6 +428,37 @@ Very complex scala files may need the following in your emacs init (.emacs, etc) (setq max-specpdl-size 5000) ``` +## `beginning-of-defun` and `end-of-defun` + +scala-mode2 defines `scala-syntax:beginning-of-definition` and +`scala-syntax:end-of-definition` which move the cursor forward and +backward over class, trait, object, def, val, var, and type definitions. These +functions are assigned to the buffer local variables +`beginning-of-defun-function` and `end-of-defun-function` which makes +it so that the `beginning-of-defun` and `end-of-defun` functions behave +in a way that is appropriate to scala. These functions are not currently able to +support some of the more advanced scala definition types. +Multiple assignment to variables e.g. + +```scala +val a, b = (1, 2) +``` + +are among the assignment types that are not currently supported. In general +the only types of definition that these functions are likely to support +are ones that use only a single, simple (but possibly generic) identifier as +its identifer. + +## imenu + +scala-mode2 supports imenu, a library for accessing locations in documents that +is included in emacs 24. The custom variable `scala-imenu:should-flatten-index` +controls whether or not the imenu index will be hierarchical or completely flat. +The current iMenu implementation only goes one level deep i.e. nested classes are +not traversed. scala-mode2's imenu support depends heavily on the +`scala-syntax:end-of-definition` and `scala-syntax:beginning-of-definition` +functions, and as such, it shares their limitations. + ## Other features - highlights only properly formatted string and character constants - indenting a code line removes trailing whitespace @@ -447,16 +478,30 @@ The indenter thinks the second occurrence of `foo` is the body of the while. To work around this, terminate the while with a semicolon, or put a blank line after it. +As mentioned above, +`scala-syntax:end-of-definition` `scala-syntax:beginning-of-definition` +don't properly handle any defintions that don't have a simple, single +identifier. +Its likely that they will stumble when presented with some of the more advanced +and obscure scala definitions out there. + +There also seems to be a strange bug with scala-modes2's `end-of-defun` integration +where two functions are skipped instead of just one. `scala-syntax:end-of-definition` +does not have this problem, so if you find this bug bothering you a lot +you can bind whatever you normally bind to `end-of-defun` to +`scala-syntax:end-of-definition` in scala mode to alleviate the issue. + ## Future work - syntax-begin-function for reliably fontifying elements which span multiple lines -- beginning-of-defun, end-of-defun -- movement commands to move to previous or next definition (val, - var, def, class, trait, object) - highlight headings and annotations inside Scaladoc specially (use underline for headings) - highlight variables in string interpolation (scala 2.10) +- Improve `end-of-defun` and `beginning-of-defun`. In particular, + figure out why `end-of-defun` sometimes skips defintions + even though `scala-syntax:end-of-definition` does not and add + support for obscure types of val declarations. All suggestions and especially pull requests are welcomed in github https://github.com/hvesalai/scala-mode2 @@ -477,3 +522,4 @@ Contributors and valuable feedback: - Nic Ferrier - Tillmann Rendel - Jim Powers +- Ivan Malison diff --git a/scala-mode2-imenu.el b/scala-mode2-imenu.el new file mode 100644 index 0000000..5eec53a --- /dev/null +++ b/scala-mode2-imenu.el @@ -0,0 +1,126 @@ +;;; scala-mode-imenu.el - Major mode for editing scala +;;; Copyright (c) 2014 Heikki Vesalainen +;;; For information on the License, see the LICENSE file + +;;; Code: + +(require 'scala-mode2-syntax) + +;; Make lambdas proper clousures (only in this file) +(make-local-variable 'lexical-binding) +(setq lexical-binding t) + +(defcustom scala-imenu:should-flatten-index t + "Controls whether or not the imenu index is flattened or hierarchical.") +(defcustom scala-imenu:build-imenu-candidate + 'scala-imenu:default-build-imenu-candidate + "Controls whether or not the imenu index has definition type information.") +(defcustom scala-imenu:cleanup-hooks nil + "Functions that will be run after the construction of each imenu") + +(defun scala-imenu:flatten-list (incoming-list &optional predicate) + (when (not predicate) (setq predicate 'listp)) + (cl-mapcan (lambda (x) (if (funcall predicate x) + (scala-imenu:flatten-list x predicate) (list x))) incoming-list)) + +(defun scala-imenu:flatten-imenu-index (index) + (cl-mapcan (lambda (x) (if (listp (cdr x)) + (scala-imenu:flatten-imenu-index (cdr x)) + (list x))) index)) + +(defun scala-imenu:create-imenu-index () + (let ((imenu-index (cl-mapcar 'scala-imenu:build-imenu-candidates + (scala-imenu:create-index)))) + (dolist (cleanup-hook scala-imenu:cleanup-hooks) + (funcall cleanup-hook)) + (if scala-imenu:should-flatten-index + (scala-imenu:flatten-imenu-index imenu-index) + imenu-index))) + +(defun scala-imenu:build-imenu-candidates (member-info &optional parents) + (if (listp (car member-info)) + (let* ((current-member-info (car member-info)) + (child-member-infos (cdr member-info)) + (current-member-result + (scala-imenu:destructure-for-build-imenu-candidate + current-member-info parents)) + (current-member-name (car current-member-result))) + (if child-member-infos + (let ((current-member-members + (scala-imenu:build-child-members + (append parents `(,current-member-info)) + (cdr member-info)))) + `(,current-member-name . + ,(cons current-member-result current-member-members))) + current-member-result)) + (scala-imenu:destructure-for-build-imenu-candidate member-info parents))) + +(defun scala-imenu:build-child-members (parents child-members) + (cl-mapcar (lambda (child) (scala-imenu:build-imenu-candidates + child parents)) child-members)) + +(defun scala-imenu:destructure-for-build-imenu-candidate (member-info parents) + (cl-destructuring-bind (member-name definition-type marker) + member-info (funcall scala-imenu:build-imenu-candidate + member-name definition-type marker parents))) + + +(defun scala-imenu:default-build-imenu-candidate (member-name definition-type + marker parents) + (let* ((all-names + (append (cl-mapcar (lambda (parent) (car parent)) parents) + `(,member-name))) + (member-string (mapconcat 'identity all-names "."))) + `(,(format "(%s)%s" definition-type member-string) . ,marker))) + +(defun scala-imenu:create-index () + (let ((class nil) (index nil)) + (goto-char (point-max)) + (while (setq class (scala-imenu:parse-nested-from-end)) + (setq index (cons class index))) + index)) + +(defun scala-imenu:parse-nested-from-end () + (let ((last-point (point)) (class-name nil) (definition-type nil)) + (scala-syntax:beginning-of-definition) + ;; We're done if scala-syntax:beginning-of-definition has no effect. + (if (eq (point) last-point) nil + (progn (looking-at scala-syntax:all-definition-re) + (setq class-name (match-string-no-properties 2)) + (setq definition-type (match-string-no-properties 1))) + `(,`(,class-name ,definition-type ,(point-marker)) . + ,(scala-imenu:nested-members))))) + +(defun scala-imenu:parse-nested-from-beginning () + (scala-syntax:end-of-definition) + (scala-imenu:parse-nested-from-end)) + +(defun scala-imenu:nested-members () + (let ((start-point (point))) + (save-excursion (scala-syntax:end-of-definition) + ;; This gets us inside of the class definition + ;; It seems like there should be a better way + ;; to do this. + (backward-char) + (scala-imenu:get-nested-members start-point)))) + +(defvar scala-imenu:nested-definition-types '("class" "object" "trait")) + +(defun scala-imenu:get-nested-members (parent-start-point) + (scala-syntax:beginning-of-definition) + (if (< parent-start-point (point)) + (cons (scala-imenu:get-member-info-at-point) + (scala-imenu:get-nested-members parent-start-point)) + nil)) + +(defun scala-imenu:get-member-info-at-point () + (looking-at scala-syntax:all-definition-re) + (let* ((member-name (match-string-no-properties 2)) + (definition-type (match-string-no-properties 1))) + (if (member definition-type scala-imenu:nested-definition-types) + (save-excursion (scala-imenu:parse-nested-from-beginning)) + `(,member-name ,definition-type ,(point-marker))))) + + +(provide 'scala-mode2-imenu) +;;; scala-mode2-imenu.el ends here diff --git a/scala-mode2-syntax.el b/scala-mode2-syntax.el index 1eeb109..80cc8cc 100644 --- a/scala-mode2-syntax.el +++ b/scala-mode2-syntax.el @@ -899,4 +899,95 @@ not. A list must be either enclosed in parentheses or start with (when (looking-at scala-syntax:list-keywords-re) (goto-char (match-end 0)))))))) +;; Functions to help with finding the beginning and end of scala definitions. + +(defconst scala-syntax:modifiers-re + (regexp-opt '("override" "abstract" "final" "sealed" "implicit" "lazy" + "private" "protected" "case") 'words)) + +(defconst scala-syntax:whitespace-delimeted-modifiers-re + (concat "\\(?:" scala-syntax:modifiers-re "\\(?: *\\)" "\\)*")) + +(defconst scala-syntax:definition-words-re + (mapconcat 'regexp-quote '("class" "object" "trait" "val" "var" "def" "type") "\\|")) + +(defun scala-syntax:build-definition-re (words-re) + (concat " *" + scala-syntax:whitespace-delimeted-modifiers-re + words-re + "\\(?: *\\)" + "\\(?2:" + scala-syntax:id-re + "\\)")) + +(defconst scala-syntax:all-definition-re + (scala-syntax:build-definition-re + (concat "\\(?1:" scala-syntax:definition-words-re "\\)"))) + +;; Functions to help with beginning and end of definitions. + +(defun scala-syntax:backward-sexp-forcing () + (condition-case ex (backward-sexp) ('error (backward-char)))) + +(defun scala-syntax:forward-sexp-or-next-line () + (interactive) + (cond ((looking-at "\n") (next-line) (beginning-of-line)) + (t (forward-sexp)))) + +(defun scala-syntax:beginning-of-definition () + "This function may not work properly with certain types of scala definitions. +For example, no care has been taken to support multiple assignments to vals such as + +val a, b = (1, 2) +" + (interactive) + (let ((found-position + (save-excursion + (scala-syntax:backward-sexp-forcing) + (scala-syntax:movement-function-until-re scala-syntax:all-definition-re + 'scala-syntax:backward-sexp-forcing)))) + (when found-position (progn (goto-char found-position) (back-to-indentation))))) + +(defun scala-syntax:end-of-definition () + "This function may not work properly with certain types of scala definitions. +For example, no care has been taken to support multiple assignments to vals such as + +val a, b = (1, 2) +" + (interactive) + (re-search-forward scala-syntax:all-definition-re) + (scala-syntax:find-brace-equals-or-next) + (scala-syntax:handle-brace-equals-or-next)) + +(defun scala-syntax:find-brace-equals-or-next () + (scala-syntax:go-to-pos + (save-excursion + (scala-syntax:movement-function-until-cond-function + (lambda () (or (looking-at "[[:space:]]*[{=]") + (looking-at scala-syntax:all-definition-re))) + (lambda () (condition-case ex (scala-syntax:forward-sexp-or-next-line) ('error nil))))))) + +(defun scala-syntax:handle-brace-equals-or-next () + (cond ((looking-at "[[:space:]]*{") (forward-sexp)) + ((looking-at "[[:space:]]*=") (scala-syntax:forward-sexp-or-next-line) + (scala-syntax:handle-brace-equals-or-next)) + ((looking-at scala-syntax:all-definition-re) nil) + (t (scala-syntax:forward-sexp-or-next-line) + (scala-syntax:handle-brace-equals-or-next)))) + +(defun scala-syntax:movement-function-until-re (re movement-function) + (save-excursion + (scala-syntax:movement-function-until-cond-function + (lambda () (looking-at re)) movement-function))) + +(defun scala-syntax:movement-function-until-cond-function (cond-function movement-function) + (let ((last-point (point))) + (if (not (funcall cond-function)) + (progn (funcall movement-function) + (if (equal last-point (point)) nil + (scala-syntax:movement-function-until-cond-function + cond-function movement-function))) last-point))) + +(defun scala-syntax:go-to-pos (pos) (when pos (goto-char pos))) + (provide 'scala-mode2-syntax) diff --git a/scala-mode2.el b/scala-mode2.el index efe2a68..44ab399 100644 --- a/scala-mode2.el +++ b/scala-mode2.el @@ -12,6 +12,7 @@ (require 'scala-mode2-fontlock) (require 'scala-mode2-map) (require 'scala-mode2-sbt) +(require 'scala-mode2-imenu) ;; Tested only for emacs 24 (unless (<= 24 emacs-major-version) @@ -108,7 +109,10 @@ When started, runs `scala-mode-hook'. 'indent-line-function 'fixup-whitespace 'delete-indentation - 'indent-tabs-mode) + 'indent-tabs-mode + 'imenu-create-index-function + 'beginning-of-defun-function + 'end-of-defun-function) (add-hook 'syntax-propertize-extend-region-functions 'scala-syntax:propertize-extend-region) @@ -142,12 +146,13 @@ When started, runs `scala-mode-hook'. fixup-whitespace 'scala-indent:fixup-whitespace delete-indentation 'scala-indent:join-line indent-tabs-mode nil - ) + beginning-of-defun-function #'scala-syntax:beginning-of-definition + end-of-defun-function #'scala-syntax:end-of-definition + imenu-create-index-function #'scala-imenu:create-imenu-index) (use-local-map scala-mode-map) ;; add indent functionality to some characters (scala-mode-map:add-remove-indent-hook) - (scala-mode-map:add-self-insert-hooks) -) + (scala-mode-map:add-self-insert-hooks)) ;; Attach .scala files to the scala-mode ;;;###autoload