4

Consider the following line of Lisp code:

        (some-function 7 8 | 9) ;; some comment. note the extra indentation

The point is placed between '8' and '9'. If I perform (move-beginning-of-line), the point will be placed at the absolute beginning of the line, rather than at '('.

Same for move-end-of-line: I'd find it more desirable for it to place the point at ')' if I perform it once, and at the absolute end of the line if I perform it a second time. Some IDEs behave like that.

I tried to implement this but got stuck, my solution behaves particularly bad near the end of a buffer, and on the minibuffer as well. Is there a library that provides this functionality?

deprecated
  • 5,142
  • 3
  • 41
  • 62

4 Answers4

5

I don't know of any library, but it can be done in a few lines of Elisp.

For the beginning of line part, the bundled functions beginning-of-line-text and back-to-indentation (M-m) move to the beginning of the “interesting” part of the line. back-to-indentation ignores only whitespace whereas beginning-of-line-text skips over the fill prefix (in a programming language, this is typically the comment marker, if in a comment). See Smart home in Emacs for how to flip between the beginning of the actual and logical line.

For the end of line part, the following function implements what you're describing. The function end-of-line-code moves to the end of the line, except for trailing whitespace and an optional trailing comment. The function end-of-line-or-code does this, except that if the point was already at the target position, or if the line only contains whitespace and a comment, the point moves to the end of the actual line.

(defun end-of-line-code ()
  (interactive "^")
  (save-match-data
    (let* ((bolpos (progn (beginning-of-line) (point)))
           (eolpos (progn (end-of-line) (point))))
      (if (comment-search-backward bolpos t)
          (search-backward-regexp comment-start-skip bolpos 'noerror))
      (skip-syntax-backward " " bolpos))))

(defun end-of-line-or-code ()
  (interactive "^")
  (let ((here (point)))
    (end-of-line-code)
    (if (or (= here (point))
        (bolp))
        (end-of-line))))
Community
  • 1
  • 1
Gilles 'SO- stop being evil'
  • 104,111
  • 38
  • 209
  • 254
  • Could I ask you when why you use `let*` in the first and just `let` in the second function? – PascalVKooten Jan 10 '13 at 15:39
  • @Dualinity `let*` allows dependencies in definitions, e.g. `(let* ((foo 3) (bar (+ foo 2))) …)` binds `bar` to 5. This isn't actually needed here, `let` would have worked. I do need to enforce that `(progn (beginning of line) (point))` is executed before `(progn (end-of-line) (point))`; in Emacs Lisp and even in Common Lisp this is guaranteed even for `let`, but I prefer to write `let*` as an indication for the maintainer that the order matters. – Gilles 'SO- stop being evil' Jan 10 '13 at 16:45
  • I figured that it wasn't neccessary, but I do believe that these little indications will make maintaining code a lot easier in the long run! – PascalVKooten Jan 10 '13 at 16:50
2

Some suggestions that almost do what you ask:

In lisp code, you can sort-of do what you want, with the sexp movement commands. To get to the beginning of the expression from somewhere in the middle, use backward-up-list, which is bound to M-C-u. In your example, that would bring you to the open parenthesis. To move backwards over individual elements in the list, use backward-sexp, bound to M-C-b; forward-sexp moves the other way, and is bound to M-C-f. From the beginning of an sexp, you can skip to the next with M-C-n; reverse with M-C-p.

None of these commands are actually looking at the physical line you are on, so they'll go back or forward over multiple lines.

Other options include Ace Jump mode, which is a very slick way to quickly navigate to the beginning of any word visible on the screen. That might eliminate your need to use line-specific commands. For quick movement within a line, I usually use M-f and M-b to jump over words. Holding the M key down while tapping on b or f is quick enough that I end up using that by default most of the time.

Edit:

Forgot one other nice command - back-to-indentation, bound to M-m. This will back you up to the first non-whitespace character in a line. You could advice this to behave normally on the first call, and then to back up to the beginning of the line on the second call:

(defadvice back-to-indentation (around back-to-back)
  (if (eq last-command this-command)
      (beginning-of-line)
    ad-do-it))

(ad-activate 'back-to-indentation)
Tyler
  • 9,872
  • 2
  • 33
  • 57
  • Thanks for the tips! But I'm afraid I already use both sexp-based (via paredit) and word-based movement commands. – deprecated Jan 09 '13 at 18:29
  • An advised `back-to-indentation` might help then, I edited my answer. – Tyler Jan 09 '13 at 19:01
  • It works great! I must say yours is a very elegant implementation. Moving to the end looks like a harder task though - see my comments at Dualinity's answer. – deprecated Jan 09 '13 at 20:31
1

I just wrote these two functions that have the behavior you are looking for.

(defun move-beginning-indent ()
  (interactive)
  (if (eq last-command this-command)
      (beginning-of-line)
    (back-to-indentation))
)


(defun move-end-indent ()
  (interactive)
  (if (eq last-command this-command)
      (end-of-line)
    (end-of-line)
    (search-backward-regexp "\\s)" nil t)   ; searches backwards for a 
    (forward-char 1))                       ; closed delimiter such as ) or ]
)

(global-set-key [f7] 'move-beginning-indent)          
(global-set-key [f8] 'move-end-indent)  

Just try them out, they should behave exactly the way you'd want them to.

PascalVKooten
  • 20,643
  • 17
  • 103
  • 160
  • Hey, thank you! While `move-end-indent` surely covers correctly the vast majority of cases, it can stop at spurious parentheses contained at the comments. Likewise, if one took the approach of doing the work left-to-right and stopping at the first encountered semicolon, false positives (e.g. the string ";") could ruin the operation as well. – deprecated Jan 09 '13 at 20:25
  • It occurs to me that one could leverage the parsing / string propertizing that is already done for us: one would scan the line left-to-right, and stop at the first character rendered as a comment. – deprecated Jan 09 '13 at 20:28
  • I agree on the comment part. I realized it when posting (the comment characters). – PascalVKooten Jan 09 '13 at 20:28
  • Well, on the meanwhile two have been provided! – deprecated Jan 09 '13 at 20:45
1

I use this:

(defun beginning-of-line-or-text (arg)
  "Move to BOL, or if already there, to the first non-whitespace character."
  (interactive "p")
  (if (bolp)
      (beginning-of-line-text arg)
    (move-beginning-of-line arg)))
(put 'beginning-of-line-or-text 'CUA 'move)
;; <home> is still bound to move-beginning-of-line
(global-set-key (kbd "C-a") 'beginning-of-line-or-text)

(defun end-of-code-or-line ()
  "Move to EOL. If already there, to EOL sans comments.
    That is, the end of the code, ignoring any trailing comment
    or whitespace.  Note this does not handle 2 character
    comment starters like // or /*.  Such will not be skipped."
  (interactive)
  (if (not (eolp))
      (end-of-line)
    (skip-chars-backward " \t")
    (let ((pt (point))
          (lbp (line-beginning-position))
          (comment-start-re (concat (if comment-start
                                        (regexp-quote
                                         (replace-regexp-in-string
                                          "[[:space:]]*" "" comment-start))
                                      "[^[:space:]][[:space:]]*$")
                                    "\\|\\s<"))
          (comment-stop-re "\\s>")
          (lim))
      (when (re-search-backward comment-start-re lbp t)
        (setq lim (point))
        (if (re-search-forward comment-stop-re (1- pt) t)
            (goto-char pt)
          (goto-char lim)               ; test here ->
          (while (looking-back comment-start-re (1- (point)))
            (backward-char))
          (skip-chars-backward " \t"))))))
(put 'end-of-code-or-line 'CUA 'move)
;; <end> is still bound to end-of-visual-line
(global-set-key (kbd "C-e") 'end-of-code-or-line)
jpkotta
  • 9,237
  • 3
  • 29
  • 34
  • 1
    Your implementation works perfectly but I'm picking Gilles' answer as the accepted as it is the most concise – deprecated Jan 09 '13 at 20:47