4

I'm completely new to elisp, and I want a function which cycles through 3 options:

  • Highlight the word.
  • Highlight the sentence.
  • Then highlight the paragraph.

What is the proper way to get the function to (setq setnewpoint t) and (setq cyclemarker 1) if I C-g mid-cycle?

(setq cyclemarker 1)
(setq setnewpoint t)

(defun highlight-cycle ()
  (interactive)
  (if setnewpoint
      (progn
        (forward-word)
        (backward-word)
        (point-to-register 1)
        (setq setnewpoint nil)))
  (cond ((= cyclemarker 1)
           (progn
             (register-to-point 1)
             (setq setnewpoint t)
             (setq cyclemarker 2)
             (keyboard-quit)))
        ((= cyclemarker 2)
           (progn
             (register-to-point 1)                   
             (mark-word)
             (setq cyclemarker 3)))
        ((= cyclemarker 3)
           (progn
             (register-to-point 1)
             (mark-end-of-sentence 1)
             (forward-sentence 1)
             (backward-sentence 1)
             (setq cyclemarker 4)
             ))
        ((= cyclemarker 4)
           (progn
             (register-to-point 1)
             (mark-paragraph)
             (cond ((/= (line-beginning-position) (point-min))
             (next-line)
             (beginning-of-line)))
             (setq cyclemarker 1)))
        ))
Drew
  • 75,699
  • 9
  • 109
  • 225
Seth Rothschild
  • 380
  • 1
  • 11
  • 2
    Check out [`unwind-protect`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Cleanups.html). – Dan Nov 11 '14 at 16:40
  • 1
    Seth, when you say "mid-cycle", do you mean between two separate executions of the function? If so, the function is not being interrupted at all, and you need to fix your title. – Malabarba Nov 11 '14 at 16:55
  • @Malabarba Yes, though I didn't know about unwind-protect. Maybe it would be possible to rewrite it so that it wouldn't leave the function without C-g, and then I could use unwind-protect to have the desired functionality. – Seth Rothschild Nov 11 '14 at 16:57
  • 2
    @SethRothschild I think the way you're doing it is the best one. You just need to advise keyboard-quit. I'm sure somebody will explain it if you fix the title to match the text. :) – Malabarba Nov 11 '14 at 17:01
  • 1
    @Malabarba Oh! I never would have figured that out myself. Should I write my own answer? – Seth Rothschild Nov 11 '14 at 17:06
  • @SethRothschild sure :) – Malabarba Nov 11 '14 at 17:09
  • 2
    FWIW - I would use `condition-case` with `quit`, and have the behavior be that when you use `C-g` (`quit`) during the command it resets the things you want to reset and then reinvokes the command. I would **not** advise `keyboard-quit` or in any other way try to have a `C-g` outside of the command affect the state for the command. – Drew Nov 11 '14 at 17:13
  • 1
    @Drew but the command is essentially instantaneous. How would he ever hit C-g in the middle of it? Maybe the cleanest way would be to check if region is active when the command starts, and reset the counter if it isn't. – Malabarba Nov 11 '14 at 17:20
  • 1
    @Malabarba: Oh, right. In that case, I would just let users cycle through the few states. If needed, I would add a "reset" state to the mix. It's not like there are 29 states to cycle through. Not a big deal, AFAICS. – Drew Nov 11 '14 at 17:40
  • 1
    In sum: If `C-g` is not for interrupting this command (it never needs to be interrupted; users have no opportunity to interrupt it) then `C-g` should be taken off the table. `C-g` is for interrupting the current command. This command should not be affecting the quit behavior of other commands, which are invoked after this one. That is an awful idea (IMHO). – Drew Nov 11 '14 at 17:53
  • 1
    @SethRothschild as a side note, you have a bunch of unnecessary progn in your code, and you shouldn't use that keyboard-quit. – Malabarba Nov 11 '14 at 18:28
  • @Malabarba Ah, thanks for that. I didn't know that you didn't need it with cond. I *did* need it yesterday when the function was written entirely with true or false if statements since I didn't yet know about cond. Those were darker times. – Seth Rothschild Nov 11 '14 at 18:40

4 Answers4

4

This is tangential to the elisp related question but you might find the behaviour of expand-region what you are looking for. You may also find it's implementation interesting from an idiomatic approach to the problem.

Basically the main expander wraps the per-word, per-sentence and per-paragraph hooks in a save-excursion loop and then uses the "best" results going forward.

stsquad
  • 4,626
  • 28
  • 45
2

The variable last-command contains the last command executed. If the last command was keyboard-quit, it will contain the symbol keyboard-quit. You can test this command in your function:

(defvar highlight-something-counter 0)

(defun highlight-something ()
  (interactive)
  (if (eql last-command 'keyboard-quit)
    (setq highlight-something-counter 0))
  (message "Highlight counter is now %d, last command was %s"
           highlight-something-counter last-command)
  (incf highlight-something-counter))

Note however that this will only work when the user pressed C-g immediately before invoking your command, so you will probably want to reset the counter on any command other than yours:

(defvar highlight-something-counter 0)

(defun highlight-something ()
  (interactive)
  (unless (eql last-command 'highlight-something)
    (setq highlight-something-counter 0))
  (message "Highlight counter is now %d, last command was %s"
           highlight-something-counter last-command)
  (incf highlight-something-counter))

If you really want only C-g to reset your counter and allow intervening commands, then you will need to use a different solution — advising keyboard-quit, as suggested by Malabarba, might be workable.

jch
  • 5,680
  • 22
  • 39
1

You shouldn't sweat about keyboard interrupts for such an undemanding task as you described. But the way to do it is though unwind-protect:

(require 'cl)

(defun lispy--mark (bnd)
  "Mark BND.  BND is a cons of beginning and end positions."
  (destructuring-bind (beg . end) bnd
    (goto-char beg)
    (push-mark-command t t)
    (goto-char end)))

(defun highlight-cycle ()
  (interactive)
  (unwind-protect
       (progn
         (sit-for 2)
         (let ((bnd (if (region-active-p)
                        (cons (region-beginning)
                              (region-end))
                      (bounds-of-thing-at-point 'word))))
           (if (region-active-p)
               (cond ((equal bnd (bounds-of-thing-at-point 'word))
                      (lispy--mark (bounds-of-thing-at-point 'sentence)))
                     ((equal bnd (bounds-of-thing-at-point 'page))
                      (deactivate-mark t)
                      (lispy--mark (get 'highlight-cycle 'word)))
                     (t
                      (lispy--mark (bounds-of-thing-at-point 'page))))
             (put 'highlight-cycle 'word bnd)
             (lispy--mark bnd))))
    (error "Have some patience, please")))

(global-set-key (kbd "M-m") 'highlight-cycle)

highlight-cycle will cycle between word, sentence and page, but it will wait for 2 seconds before doing anything. If the user interrupts with C-g, it will complain. You can put something other than complaint there, of course.

abo-abo
  • 13,943
  • 1
  • 29
  • 43
  • Really? You want the command to *always end by raising an error*? Doesn't sound too good, to me. – Drew Nov 11 '14 at 17:15
  • 1
    It's just for example. I could change it to `user-error` instead. `message` is too weak - it's overwritten. – abo-abo Nov 11 '14 at 17:20
1

If I don't invoke keyboard-quit mid code above, advising keyboard-quit directly answers the question.

(defadvice keyboard-quit (before highlight-cycle-counter)
"reset counter for highlight-cycle"
     (progn
         (setq setnewpoint t)
         (setq cyclemarker 1)
      )
)

(ad-activate 'keyboard-quit)

This might not be the right answer to the problem, but it is sufficient for now.

Seth Rothschild
  • 380
  • 1
  • 11
  • 1
    There are only 3 highlight states to cycle among, plus an unhighlight state, AFAICT. Why bother fiddling with `C-g` - that only makes things more, not less, complex for users. Why not just let them cycle to the reset state? And if you really need a way for users to explicitly reset other than by cycling, let `C-u` do that. Advising `keyboard-quit` is not a great idea, IMHO. – Drew Nov 11 '14 at 17:44