0

The Qt framework has lots of camel case and pascal case keywords (e.g. textEdited or QWidget). To avoid pressing Shift constantly, I call add-global-abbrev to add a lowercase abbrev and use abbrev-mode to automatically expand them. I can then type in lowercase at all times.

The problem is that after an abbrev for a camel case word has been expanded once, if expanded again (as happens automatically in abbrev-mode), it switches to pascal case.

For example, if I write textEdited and no abbrev currently exists, I call C-x a g textedited to create an abbrev. The corresponding abbrev table then looks like:

;;-*-coding: utf-8;-*-
(define-abbrev-table 'global-abbrev-table
  '(
    ("textedited" "textEdited" nil :count 0)
   ))

With abbrev-mode on, the next time I write textedited the following happens. Lets say I forget this is a signal and not a method, so I write a left parenthesis, textedited(. It expands to textEdited(. That's the expansion I expect, but not the correct syntax. So, I delete the parenthesis and put a dot. The result is TextEdited.. The first character of the word is capitalized.

Why does deleting the left parenthesis and inserting the dot cause the t to capitalize? Is there a way to prevent that?

I have tried setting all values of dabbrev-case-replace and case-replace, but this does not correct the behavior. If I call write-abbrev-file, no other abbrevs are written except the global table. I take this to mean no other tables are defined which may be causing the expansion.

1
  • Does this or this help? If so, and if this is essentially a duplicate of one of those question, then please delete this one.
    – Drew
    Jan 2, 2021 at 4:27

1 Answer 1

1

Why does deleting the left parenthesis and inserting the dot cause the "t" to capitalize?

By default, abbrev expansion preserves case. The first expansion happens literally. The key to the expansion rule ("textedited" "textEdited" nil :count 0) is precisely "textedited". A parenthesis is considered a word-separator, thus "textEdited" is inserted. When the parenthesis is deleted and the dot inserted, a second expansion occurs because period is also a word-separator. In this expansion, however, "textEdited" is acted on and it has a capital letter.

(emacs) Dabbrev Customization states,

Normally, dynamic abbrev expansion preserves the case pattern of the dynamic abbrev you are expanding, by converting the expansion to that case pattern.

The capital letter indicates to the expansion function that the case should be a particular case pattern. It's not clear to me how Emacs defines these case patterns; does it differentiate camelCase from PascalCase (i.e. StudlyCaps)? It appears not. Hence, the "t" is capitalized.

Is there a way to prevent that?

Yes, several.

Using the case-fixed property keyword in the abbrev table definition:

;;-*-coding: utf-8;-*-
(define-abbrev-table 'global-abbrev-table
  '(
    ("textedited" "textEdited" nil :case-fixed t :count 0)
   ))

This works, but isn't practical for the described use case. The add-global-abbrev command cannot be used to define abbrev properties. You would need to manually edit the abbrev table definition any time an abbrev was added.

It turns out that an abbrev table is what's called an obarray. These can have properties themselves.

Another solution is to adjust the table property so that every entry is case-fixed:

(dolist (table abbrev-table-name-list)
    (abbrev-table-put (symbol-value table) :case-fixed t))

This works, but isn't transparent. The table definition given at the end-user level, that of the define-abbrev-table definition created with write-abbrev-file, won't show the case-fixed property. Further, this applies the case-fixed property to every abbrev in the table. Do you want every table entry to have this property?

There are also several options, dabbrev-case-replace, case-replace, dabbrev-case-fold-search, and case-fold-search, described in (emacs) Dabbrev Customization. On my reading, these should help, but they don't. That's either a bug or a misunderstanding by me.

So, it appears that case-fixed must be set on an individual abbrev to get the desired behavior. For those already created, just add case-fixed manually. Otherwise, here's a (potentially over-engineered) function to define abbrevs that allows control over the case-fixed property:

(defun my-add-table-abbrev (name expansion &optional fixed table interp)
  "Define abbrev with NAME and EXPANSION for last word(s) before point in TABLE.

FIXED sets case-fixed keyword property; default is nil.  TABLE defaults to 
`global-abbrev-table'.

Behaves similarly to `add-global-abbrev'.  The prefix argument
specifies the number of words before point that form the
expansion; or zero means the region is the expansion.  A negative
argument means to undefine the specified abbrev.  This command
uses the minibuffer to read the abbreviation.

Abbrevs are overwritten without prompt when called from Lisp.

\(fn NAME EXPANSION &optional FIXED TABLE)"
  ;; define behavior when called interactively
  (interactive
   (let* ((arg (prefix-numeric-value current-prefix-arg)) ; get numeric prefix
          ;; note negative prefix returns exp as nil
          (exp (and (>= arg 0) ; positive prefix grabs `arg` previous words
                    (buffer-substring-no-properties
                     (point)
                     (if (= arg 0) (mark) ; zero prefix grabs region
                       (save-excursion (forward-word (- arg)) (point))))))
          ;; when exp exists define name to create, otherwise name the one to destroy
          (name (read-string (format (if exp "Abbev name: "
                                       "Undefine abbrev: "))))  
          ;; when exp, ask user for expansion
          (expansion (and exp (read-string "Expansion: " exp))) 
          (table (symbol-value (intern-soft (completing-read
            "Abbrev table (global-abbrev-table): "
            abbrev-table-name-list nil t nil nil "global-abbrev-table"))))
          ;; don't ask for fixed case when deleting
          (fixed (and exp (y-or-n-p (format "Fix case? ")))))
     ;; send args to function, note last element indicates interactive
     (list name expansion fixed table t)))
  ;; end interactive behavior definition, start function definition 
  (let ((table (or table global-abbrev-table))  ; define default table
        (fixed (or fixed nil)))  ; define default case-fixed
    (set-text-properties 0 (length name) nil name)
    (set-text-properties 0 (length expansion) nil expansion)
    (if (or (null expansion)                     ; there is expansion to set,
            (not (abbrev-expansion name table))  ; the expansion is not already defined
            (not interp)                         ; and we're not calling from 
                                                 ; code (calling interactively)
            (y-or-n-p (format "%s expands to \"%s\"; redefine? "
                              name (abbrev-expansion name table))))
        (define-abbrev table name expansion nil :case-fixed fixed))))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.