branch: elpa/scala-mode commit 6c257f60de7988b73bb350add884f5b3109ab0cb Author: Heikki Vesalainen <heikkivesalai...@yahoo.com> Commit: Heikki Vesalainen <heikkivesalai...@yahoo.com>
Enhanced support for comments and multi-line strings, including - indenting of comments - paragraph movement - filling See README.md for details --- README.md | 169 ++++++++++++++++++++++++++++++++++++++++-------- scala-mode-indent.el | 48 +++++++++++++- scala-mode-map.el | 7 +- scala-mode-paragraph.el | 121 ++++++++++++++++++++++++++++++++++ scala-mode.el | 57 +++------------- 5 files changed, 320 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index dc38997..92f7193 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # scala-mode2 — A new scala-mode for emacs This is a new scala major mode for emacs 24. It is a complete rewrite -based on scala language specification 2.9. +based on the *Scala Language Specification 2.9*. The mode intends to provide the basic emacs support, including - indenting of code, comments and multi-line strings @@ -18,14 +18,14 @@ standard emacs motions work ofcourse. The mode has been developed on 24.2 and uses features not available in emacs prior to version 24. -2. Download the files to a local directory, you can use the *git* -command. This will create a directory called scala-mode2. +2. Download the files to a local directory. You can use the *git clone* +command, this will create a new directory called scala-mode2. ``` -> git clone git://github.com/hvesalai/scala-mode2.git +git clone git://github.com/hvesalai/scala-mode2.git ``` -3. Include the following in your '.emacs' file. If you have been -using the old scala-mode, make sure it is no longer in load-path. +3. Include the following in your `.emacs` file. If you have been +using the old scala-mode, make sure it is no longer in *load-path*. ``` (add-to-list 'load-path "/path/to/scala-mode2/") (require 'scala-mode) @@ -34,12 +34,9 @@ using the old scala-mode, make sure it is no longer in load-path. 4. That's it. Next you can start emacs and take a look at the customization menu for scala-mode (use **M-x** *customize-mode* when in scala-mode or use **M-x** *customize-variable* to customize one -variable). - -Free emacs tip: if you are using emacs from a text terminal with dark -background and you are having trouble with colors, try setting the -customization variable *frame-background-mode* to *dark* (use **M-x** -*customize-variable*). +variable). Also be sure to check the customization tips on various +keyboard commands and general emacs parameters which cannot be +modified from the scala-mode customization menu. ## Indenting modes @@ -63,7 +60,7 @@ val x = List(1, 2, 3) ``` The *operators* and *eager* modes will indent the second row in the -following code, as the first line ends with opchar. +following code, as the first line ends with an operator character. ``` val x = 20 + @@ -72,18 +69,18 @@ val x = 20 + The *reluctant* mode (default) will not indent the line in either case. However, all three modes will indent the second line in these -examples as specified by the *Scala Language Specification*, section -1.2. +examples as it is clear that the first line cannot terminate a statement +(see the *Scala Language Specification 2.9*, section 1.2). ``` val x = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9). - map (x => x + 1) // last token of previous line can not terminate a statement + map (x => x + 1) // last token of previous line cannot terminate a statement val y = (List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) map (x => x + 1)) // inside 'newlines disabled' region ``` -You can use empty lines in *eager* mode to stop it from indenting a +You can use empty lines in the *eager* mode to stop it from indenting a line. For example ``` @@ -95,8 +92,8 @@ val y = foo("bar") ("zot", "kala") // a tuple ``` -However, in all three modes pressing **tab** key repeatedly on a line -will toggle between the modes. +However, in all three modes pressing the **tab** key repeatedly on a +line will toggle between the modes. ### Value expressions (scala-indent:indent-value-expression) @@ -189,29 +186,144 @@ val xs = for (i <- 1 to 10) yield i ``` +## Indenting multi-line comments + +The start of a multi-line comment is indented to the same level with +code. + +If a multi-line comment begins with `/**` it is considered to be a +scaladoc comment. Scaladoc comments are indented acording to the +scaladoc style guide. + +``` +/** This is a scaladoc comment. + * 2nd line. + */ +``` + +Other multi-line comments are indented under the first asterix. + +``` +/**** + * Supercalifragilistic- + * expialidocious! + */ + +/* + A comment + */ +``` + +## Filling (i.e. word wrap) + +Paragraph *filling* (emacs jargon for word wrap) is supported for +comments and multi-line strings. Auto-fill is not supported yet. + +To re-fill a paragraph, use the *fill-paragraph* command ( **M-q** +). As always, the column at which to wrap is controlled by the +*fill-column* variable, which you set it with the *set-fill-column* +command. To set the default, you use the *customize-variable* command +or a mode-hook. + ## Motion -Basic emacs motion will work as expected. The *forward-sexp* and -*backward-sexp* ( **M-C-f**, **M-C-b** ) motion commands will move over -reserved words, literals, ids and lists. +Basic emacs motion will work as expected. + +Text paragraph motion (i.e. *forward-paragraph*, *backward-paragraph*) +works inside comments and multi-line strings, and it respect scaladoc's +wiki-style markup. + +The commands *forward-sexp* and *backward-sexp* ( **M-C-f**, **M-C-b** +) motion commands will move over reserved words, literals, ids and +lists. + +## Keymap and other commands + +For the sake of customizability, scala-mode does not alter the default +emacs keymap beyond what is absolutely necessary. However, you can customize +the mode through the scala-mode-hook. Here are some suggested modification +you may want to try. Just copy-paste it to your `.emacs` file. + +```lisp +(add-hook 'scala-mode-hook '(lambda () + + ;; Bind the 'newline-and-indent' command to RET (aka 'enter'). This + ;; is normally also available as C-j. The 'newline-and-indent' + ;; command has the following functionality: 1) it removes trailing + ;; whitespace from the current line, 2) it create a new line, and 3) + ;; indents it. An alternative is the + ;; 'reindent-then-newline-and-indent' command. + (local-set-key (kbd "RET") 'newline-and-indent) + + ;; Bind the 'join-line' command to C-M-j. This command is normally + ;; bound to M-^ which is hard to access, especially on some European + ;; keyboards. The 'join-line' command has the effect or joining the + ;; current line with the previous while fixing whitespace at the + ;; joint. + (local-set-key (kbd "C-M-j") 'join-line) + + ;; Bind the backtab (shift tab) to + ;; 'scala-indent:indent-with-reluctant-strategy command. This is usefull + ;; when using the 'eager' mode by default and you want to "outdent" a + ;; code line as a new statement. + (local-set-key (kbd "<backtab>") 'scala-indent:indent-with-reluctant-strategy) + + ;; Add the scalaindent:indent-scaladoc-asterisk + ;; post-self-insert-hook which indents the multi-line comment + ;; asterisk to its correct place when you type it. It also adds a + ;; space after the asterisk if the parameter to the function is + ;; t. If you don't want the space, replace t with nil. + (add-hook 'post-self-insert-hook + '(lambda () (scala-mode:indent-scaladoc-asterisk t))) + + ;; and other bindings here +)) +``` + +## Whitespace + +Emacs has a very nice minor mode for highlighting bad whitespace and +removing any unwanted whitespace when you save a file. To use it, add +this to your .emacs file. + +```lisp +(add-hook 'scala-mode-hook '(lambda () + (require 'whitespace) + + ;; clean-up whitespace at save + (make-local-variable 'before-save-hook) + (add-hook 'before-save-hook 'whitespace-cleanup) + + ;; turn on highlight. To configure what is highlighted, customize + ;; the *whitespace-style* variable. A sane set of things to + ;; highlight is: face, tabs, trailing + (whitespace-mode) +)) +``` + +Since scala is indented with two spaces, scala-mode2 does not use tabs +at all for indents by default. If you want to turn on tabs mode, you +can do so in the mode hook (set *indent-tabs-mode* to t). ## Code highlighting Highlighting code is still a work in progress. Feedback on how it should work is welcomed as issues to this github project. +Free emacs tip: if you are using emacs from a text terminal with dark +background and you are having trouble with colors, try setting the +customization variable *frame-background-mode* to *dark* (use **M-x** +*customize-variable*). + ## Other features -- supports multi-line strings both in highlight and movement - highlights only properly formatted string and character constants -- fills scaladoc comments properly ( **M-q** ) - indenting a code line removes trailing whitespace ## Future work -- indent scaladoc left margin correctly (currently scaladoc is not - indented at all) -- fill row comments after code properly (currently produces error) -- indent and fill multi-line strings with margin correctly +- beginning-of-defun, end-of-defun +- indent case, etc after they are typed (use first space as self-insert-hook) +- indent multi-line strings with margin correctly - movement commands to move to previous or next definition (val, var, def, class, trait, object) - highlight headings and annotations inside scaladoc specially (use @@ -229,3 +341,4 @@ Contributors and valuable feedback: - Ray Racine - Eiríkr Åsheim (aka Erik Osheim) - Seth Tisue +- Gary Pamparà \ No newline at end of file diff --git a/scala-mode-indent.el b/scala-mode-indent.el index 72f4a7b..47772d7 100644 --- a/scala-mode-indent.el +++ b/scala-mode-indent.el @@ -668,6 +668,14 @@ anchored at 'anchor'." ; (message "normal at %d" (current-column)) 0))) +(defun scala-indent:goto-line-comment-anchor (&optional pos) + "Goto and return the position relative to which a line comment +will be indented. This will be the start of the line-comment on +previous line, if any." + (if (and (looking-at "\\s *//") + (forward-comment -1)) + (point))) + ;;; ;;; Indentation engine ;;; @@ -701,7 +709,8 @@ nothing was applied." point 'point'. Returns the new column, or nil if the indent cannot be determined." (or (scala-indent:apply-indent-rules - `((scala-indent:goto-open-parentheses-anchor scala-indent:resolve-open-parentheses-step) + `((scala-indent:goto-line-comment-anchor 0) + (scala-indent:goto-open-parentheses-anchor scala-indent:resolve-open-parentheses-step) (scala-indent:goto-for-enumerators-anchor scala-indent:resolve-list-step) (scala-indent:goto-forms-align-anchor scala-indent:resolve-forms-align-step) (scala-indent:goto-list-anchor scala-indent:resolve-list-step) @@ -739,10 +748,43 @@ strings" (let ((state (save-excursion (syntax-ppss (line-beginning-position))))) (if (not (nth 8 state)) ;; 8 = start pos of comment or string, nil if none (scala-indent:indent-code-line strategy) - (scala-indent:indent-line-to (current-indentation)) - nil))) + (if (integerp (nth 4 state)) ;; 4 = nesting level of comment for scaladoc + (scala-indent:indent-line-to (scala-indent:scaladoc-indent (nth 8 state))) + (scala-indent:indent-line-to (current-indentation)))))) (defun scala-indent:indent-with-reluctant-strategy () (interactive) (scala-indent:indent-line scala-indent:reluctant-strategy)) +(defun scala-indent:scaladoc-indent (comment-start-pos) + "Calculate indent for a multi-line comment. Scaladoc +lines (starting with /**) are indented under the second +aseterix. Other multi-line comment rows are indented undet the +first asterisk. + +Note: start line is indented as code since the start of the +comment is outside the comment region. " + (save-excursion + (goto-char comment-start-pos) + (when (looking-at "/\\*+") + (goto-char + (if (= (- (match-end 0) (match-beginning 0)) 3) + (- (match-end 0) 1) + (+ (match-beginning 0) 1))) + (current-column)))) + +(defun scala-mode:indent-scaladoc-asterisk (&optional insert-space-p) + "This function is meant to be used with post-self-insert-hook. + +Indents the line if position is right after an asterisk in a +multi-line comment block and there is only whitespace before the asterisk. + +If insert-space is true, also adds a space after the asterisk if +the asterisk is the last character on the line." + (let ((state (syntax-ppss))) + (when (and (integerp (nth 4 state)) + (looking-back "^\\s *\\*" (line-beginning-position))) + (when (and insert-space-p + (looking-at "\\s *$")) + (insert " ")) + (scala-indent:indent-line-to (scala-indent:scaladoc-indent (nth 8 state)))))) diff --git a/scala-mode-map.el b/scala-mode-map.el index 2108e11..efba50b 100644 --- a/scala-mode-map.el +++ b/scala-mode-map.el @@ -4,6 +4,8 @@ (provide 'scala-mode-map) +(require 'scala-mode-indent) + (defmacro scala-mode-map:define-keys (key-map key-funcs) (cons 'progn (mapcar #'(lambda (key-func) @@ -27,10 +29,7 @@ (scala-mode-map:define-keys keymap ( - ([backtab] 'scala-indent:indent-with-reluctant-strategy) - ([(control c)(control r)] 'scala-indent:rotate-run-on-strategy) - ([(control c)(control c)] 'comment-region) - ;; ("}" 'scala-electric-brace) +;; ([(control c)(control r)] 'scala-indent:rotate-run-on-strategy) )) (setq scala-mode-map keymap))) diff --git a/scala-mode-paragraph.el b/scala-mode-paragraph.el new file mode 100644 index 0000000..c5679bf --- /dev/null +++ b/scala-mode-paragraph.el @@ -0,0 +1,121 @@ +;;; scala-mode-paragraph.el - Major mode for editing scala, paragraph +;;; detection and fill +;;; Copyright (c) 2012 Heikki Vesalainen For information on the License, +;;; see the LICENSE file + +;;; Based on Scala Language Specification (SLS) Version 2.9 + +;;; Provides paragraph navigation and fill for scaladocs and +;;; multi-line strings. + +(provide 'scala-mode-paragraph) + +(defconst scala-paragraph:paragraph-line-start-re + (concat "\\(?:\\s *" ; whitespace + "\\(?://+\\|\\*\\|/\\*+" ; comment start + "\\||\\)?" ; multi-line margin | + "\\s *\\)")) ; whitespace + +(defconst scala-paragraph:scaladoc-list-start-re + (concat "\\(?:-" ; unordered liststs + "\\|[1IiAa]\\." ; ordered lists + "\\)\\s *")) + +(defconst scala-paragraph:fill-first-line-re + (concat "\\s *\\(//+\\|\\*\\||\\)?\\s *" + "\\(?:" scala-paragraph:scaladoc-list-start-re "\\)?")) + +(defconst scala-paragraph:paragraph-start-re + (concat scala-paragraph:paragraph-line-start-re + "\\(?:$" ; empty line + "\\|=[^=\n]+=[ ]*$" ; heading 1 + "\\|==[^=\n]+==[ ]*$" ; heading 2 + "\\|===[^=\n]+===[ ]*$" ; heading 3 + "\\|====+[^=\n]+====+[ ]*$" ; heading 4-n + "\\|" + scala-paragraph:scaladoc-list-start-re + "\\|{{{" ; code block start + "\\|}}}" ; code block end + "\\|@[a-zA-Z]+\\>" ; annotations + "\\)" + "\\|\\(?:\\s *\\*/\\)" ; end of comment + )) + +(defconst scala-paragraph:paragraph-separate-re + (concat scala-paragraph:paragraph-line-start-re + "\\(?:$" + "\\|=[^=\n]+=[ ]*$" ; heading 1 + "\\|==[^=\n]+==[ ]*$" ; heading 2 + "\\|===[^=\n]+===[ ]*$" ; heading 3 + "\\|====+[^=\n]+====+[ ]*$" ; heading 4-n + "\\|@[a-zA-Z]+\\>" ; annotations + "\\|{{{" ; code block start + "\\|}}}" ; code block end + "\\)" + "\\|\\(?:\\s *\\*/\\)" ; end of comment + )) + +(defun scala-paragraph:fill-function () + (let (fill) + (save-restriction + (save-excursion + (widen) + (beginning-of-line) + (cond ((looking-at "\\s */?\\*+\\s *") + (setq fill (replace-regexp-in-string + "/\\*+" + (lambda (str) (if (= (length str) 3) " *" " *")) + (match-string-no-properties 0))) + (goto-char (match-end 0)) + (when (looking-at scala-paragraph:scaladoc-list-start-re) + (setq fill + (concat fill (make-string (- (match-end 0) + (match-beginning 0)) ?\s))))) + ((or (re-search-forward "\"\"\"|" (line-end-position) t) + (and (eq (nth 3 (syntax-ppss)) t) + (re-search-forward "^\\s *|" (line-end-position) t))) + (setq fill (concat (make-string (- (current-column) 1) ?\s) "|")) + (setq fill (concat fill (make-string (skip-syntax-forward " ") ?\s))) + (when (looking-at scala-paragraph:scaladoc-list-start-re) + (setq fill + (concat fill (make-string (- (match-end 0) + (match-beginning 0)) ?\s)))))))) + fill)) + +(defun scala-paragraph:fill-paragraph (&rest args) + ;; move to inside multi-line comment or multi-line stirng, if outside + (when (looking-at "\\s *\\(?:/\\**\\|\"\"\"\\)\\s *") + (goto-char (match-end 0))) + (let ((state (syntax-ppss)) + (fill-paragraph-function + ;; Avoid infinite recursion, set fill-paragraph-function to + ;; nil if it is 'scala-paragraph:fill-paragraph + (unless (eq fill-paragraph-function 'scala-paragraph:fill-paragraph) + fill-paragraph-function))) + (cond ((integerp (nth 4 state)) + ;; mask multi-line comments and fill + (save-restriction + (narrow-to-region (nth 8 state) + (save-excursion (goto-char (nth 8 state)) + (if (forward-comment 1) + (point) + (point-max)))) + (fill-paragraph)) + t) + ((eq (nth 4 state) t) + ;; let normal fill-function handle this + nil) + ((eq (nth 3 state) t) + ;; mask multi-line strings and fill. + (save-restriction + (narrow-to-region (nth 8 state) + (save-excursion (goto-char (nth 8 state)) + (or (ignore-errors + (forward-sexp) + (point)) + (point-max)))) + (fill-paragraph)) + t) + ;; TODO: fill lists + ;; the rest should not be filled (code, etc) + (t t)))) diff --git a/scala-mode.el b/scala-mode.el index 605209d..716ae05 100644 --- a/scala-mode.el +++ b/scala-mode.el @@ -8,6 +8,7 @@ (require 'scala-mode-constants) (require 'scala-mode-syntax) +(require 'scala-mode-paragraph) (require 'scala-mode-fontlock) (require 'scala-mode-indent) (require 'scala-mode-map) @@ -26,28 +27,6 @@ (defmacro scala-mode:make-local-variables (&rest quoted-names) (cons 'progn (mapcar #'(lambda (quoted-name) `(make-local-variable ,quoted-name)) quoted-names))) -(defconst scala-mode:comment-line-start - (concat "[ \t]*" ; whitespace - "\\(//+\\|\\**\\)" ; comment start - "[ \t]*")) ; whitespace - -(defconst scala-mode:paragraph-start - (concat scala-mode:comment-line-start - "\\($" ; empty line - "\\|=[^=\n]+=[ ]*$" ; heading 1 - "\\|==[^=\n]+==[ ]*$" ; heading 2 - "\\|===[^=\n]+===[ ]*$" ; heading 3 - "\\|====+[^=\n]+====+[ ]*$" ; heading 4-n - "\\|-" ; unordered liststs - "\\|[1IiAa]\\." ; ordered lists -; "\\|{{{" ; code block start -; "\\|}}}" ; code block end - "\\|@[a-zA-Z]+\\>" ; annotations - "\\)" - )) - -(defconst scala-mode:paragraph-separate - (concat scala-mode:comment-line-start "$")) (defun scala-mode:forward-sexp-function (&optional count) (unless count (setq count 1)) @@ -57,24 +36,6 @@ (dotimes (n count) (scala-syntax:forward-sexp)))) -;; (defun scala-mode () -;; "Major mode for editing scala code. - -;; When started, runs `scala-mode-hook'. - -;; \\{scala-mode-map}" -;; (interactive) -;; (kill-all-local-variables) -;; (set-syntax-table scala-mode-syntax-table) - -;; (scala-mode:make-local-variables -;; 'require-final-newline -;; 'comment-start -;; 'comment-end -;; 'comment-start-line -;; 'comment-column -;; 'comment-multi-line) - ;;;###autoload (define-derived-mode scala-mode prog-mode "Scala" "Major mode for editing scala code. @@ -82,17 +43,20 @@ When started, runs `scala-mode-hook'. \\{scala-mode-map}" - :syntax-table scala-syntax:syntax-table + :syntax-table scala-syntax:syntax-table ; :group ; :abbrev (scala-mode:make-local-variables 'scala-mode:debug-messages + 'post-self-insert-hook 'syntax-propertize-function 'font-lock-defaults 'paragraph-start 'paragraph-separate 'fill-paragraph-function + 'adaptive-fill-function + 'adaptive-fill-first-line-regexp 'comment-start 'comment-end 'comment-start-skip @@ -113,21 +77,20 @@ When started, runs `scala-mode-hook'. font-lock-defaults '((scala-font-lock:keywords) nil) - ;; TODO: paragraph-start, paragraphs-separate, paragraph-ignore-fill-prefix ;; TODO: beginning-of-defun-function, end-of-defun-function ;; comments - paragraph-start scala-mode:paragraph-start - paragraph-separate scala-mode:paragraph-separate - fill-paragraph-function 'c-fill-paragraph + paragraph-start scala-paragraph:paragraph-start-re + paragraph-separate scala-paragraph:paragraph-separate-re + fill-paragraph-function 'scala-paragraph:fill-paragraph + adaptive-fill-function 'scala-paragraph:fill-function + adaptive-fill-first-line-regexp scala-paragraph:fill-first-line-re comment-start "// " comment-end "" comment-start-skip "\\(//+\\|/\\*+\\)[ \t]*" comment-column 0 comment-multi-line t - ;; TODO: comment-indent-function - ;; TODO: forward-sexp-function forward-sexp-function 'scala-mode:forward-sexp-function indent-line-function 'scala-indent:indent-line indent-tabs-mode nil