(Recently) Software Engineer @ Google Silicon
(Also recently) EE/CS @ The Cooper Union
On 6/5/2021, 10:42:41 AM
Return to blog
(In Emacs,) I was copying (killing/yanking) Lisp code from a Lisp-mode buffer into a Fundamental-mode buffer and noticed that the code was still syntax-highlighted. Moreover, when editing the code, the text remained colored but new text was not appopriately colored.
I was able to find the answer to this pretty quickly, and I think it's an excellent example of how extensible Emacs is.
To summarize the kill/yank behavior to the unaware, Emacs has two ways of deleting text. One is a true "delete," which removes the text and forgets about it. The second way is "killing" text, which removes the text and remembers it, so you can "yank" it back at some later time.1 There is also the option to save text without deleting so that it can be yanked back later. These form the common text-editing operations commonly known as delete, cut, paste, and copy, respectively.
The only issue I have with this is that the default shortcuts are not very consistent. An article "Emacs Delete vs. Kill" goes into some of the different delete/kill combinations you could possibly want (e.g., kill to next word, delete backwards a paragraph). Unlike ViM, which has fixed movement modifiers, the Emacs defaults are just there to start you off with what is probably most useful (with the ability to configure shortcuts later as needed). Some of the useful defaults I've found:
delete-char(delete next character)
backword-kill-word(cut previous word)
mark-word kill-region(cut next word)
In general, I don't use
delete-region (there isn't a default keybinding to it anyways), because the kill commands are more convenient and you can yank back the killed text. Killing/yanking also works with the system clipboard, so it works with outside applications as well (more on this later). When yanking back text, you can choose an older kill with M-y (
The question is, where does the killed or copied data get stored? Paraphrasing the above answer, it gets stored in a linked list called the
kill-ring. It is a linked list so that you can yank back older kills.
The equivalent of
kill-ring in ViM are "registers," described in this article2. This makes ViM equally capable when it comes to plugins in this regard as Emacs, but you're limited to using Vimscript as opposed to Elisp. I'm sure there are ways to get the system clipboard in other applications as well, but this probably involves some obscure library function. For example, JetBrains editors have a ton of functionality, but plugin developers may have to search deep to find some obscure API like
com.intellij.openapi.ide.CopyPasteManager. In some less-featured plugin environments, it may be impossible to fetch or modify the clipboard/kill ring.
Compare those with the Emacs kill ring, which is a global linked list variable.
Not a specialized data container like ViM registers, and not buried deep in some API that may limit your functionality. This is the epitome of a hackable editor.5 Moreover, you can easily find it using the help commands (C-h v kill-ring, or in the notes for
kill-region) -- I probably would've looked here next if I couldn't find the answer online.
In other words, you can view and modify the kill ring very easily, both because it is so accessible and using a common linked list format. Which brings us back to the original question -- why does killing copy text style? We can look at the kill ring with C-h v kill-ring to see what is actually being stored.
Killing regular text will do what you might expect. For example, if I kill some parts of the previous sentence, the kill ring may look something like:
("expect" "lar text will do what y" "Killing regular")
But if I go into a Lisp buffer (that is syntax-highlighted) and kill the following text:
(define (compose f g) ;; returns the composition (f o g). Assumes f, g take one argument (lambda (x) (f (g x))))
then the following entry is prepended to the kill ring:
#("(define (compose f g) ;; returns the composition (f o g). Assumes f, g take one argument (lambda (x) (f (g x)))) " 0 1 (fontified t) 1 7 (face font-lock-keyword-face fontified t) 7 9 (fontified t) 9 16 (face font-lock-function-name-face fontified t) 16 24 (fontified t) 24 27 (face font-lock-comment-delimiter-face fontified t) 27 91 (face font-lock-comment-face fontified t) 91 94 (fontified t) 94 100 (face font-lock-keyword-face fontified t) 100 117 (fontified t))
It becomes pretty clear then that the text properties get killed and yanked along with the text.6 From the above link, we learn that text properties are buffer metadata and not part of the buffer, and thus will not get saved like the rest of the buffer. Reloading the non-syntax highlighted file (e.g., with C-x C-v RET) will remove the syntax highlighting. Or, as the like suggests, we can implement a new command that removes the text properties from the selected region:
(defun remove-display-text-property (start end) "Remote all text properties from START to END. This is useful when copying stuff with a display property set from elsewhere." (interactive "r") (set-text-properties start end nil))
Ok, I lied a little. We inspected the kill ring variable to see why syntax highlighting is being killed/yanked, but you're not actually supposed to modify the kill ring directly, for the reasons specified in the descripion of
List of killed text sequences. Since the kill ring is supposed to interact nicely with cut-and-paste facilities offered by window systems, use of this variable should interact nicely with ‘interprogram-cut-function’ and ‘interprogram-paste-function’. The functions ‘kill-new’, ‘kill-append’, and ‘current-kill’ are supposed to implement this interaction; you may want to use them instead of manipulating the kill ring directly.
There is a little bit of an interface to this data structure because of the complexities related to copy/paste to/from other applications7. But I would argue that having the kill ring as a transparent variable gives you a window into how the kill/yank procedures operate. With this window, it is easier to extend and debug this functionality.
1. These quoted terms might sound strange, but they're Emacs jargon.
2. Also, ViM's terminology differs slightly from Emacs. In ViM, "yank" (y command) means copy, "delete" (d) means cut, and "paste" (p) means paste. I think this terminology makes more sense.
3. These nested footnotes appear out-of-order because they are implemented in a very simple way.
4. The correct term for a member of the Church of Emacs, according to according to St. Ignucius himself.3
5. Of coure, then it's up to you not to break the editor, but it's all in the spirit of hacking. But Emacsites4, with their dwim ways, strongly defend their claims to advanced and accessible configurability.
6. I assume all rich-text clipboard material serialize their data in a similar way, but I haven't looked into it. But I imagine this Lisp representation to be fairly organic amongst clipboard rich-text serialization methods.
7. I appreciate this feature, since I've never had a good experience copying text from ViM to another application.
© Copyright 2023 Jonathan Lam