Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 65

Crafting Interactive Commands with Buffer and Text Ops

Recommended resources

Introduction

In our last lesson, you built your first interactive commands to transform and wrap text within a selected region. You now have a solid foundation for the core pattern of text manipulation in Emacs: extracting, transforming, and replacing.

Today, we will assemble these pieces into a more sophisticated and practical command, truly fulfilling your goal of extending Emacs for your personal workflow. This lesson serves as a capstone project for the skills you've acquired, from basic functions and control flow to advanced buffer manipulation. We will combine iteration, searching, and text transformation to build a command that can process an entire buffer or a selected region.

Our main project will be to create a "bionic reading" command. This technique, which bolds the first part of each word, aims to guide the eye and increase reading speed—a potentially useful tool for any writer or editor. By building this, you'll learn a versatile pattern for creating powerful text-processing tools.


1. The Anatomy of a Buffer-Processing Command

Before we build our command, let's review the key functions that act as our building blocks. You've encountered most of these before, but seeing them together highlights the toolkit for this kind of task:

  • Navigation and Position:
    • goto-char: Moves the point to a specific character position.
    • point, point-min, point-max: Get the current, minimum (buffer start), and maximum (buffer end) character positions.
    • forward-word, forward-sentence: Move the point by logical text units.
  • Safe Execution Wrappers:
    • save-excursion: Executes code and restores the original point position afterward. You used this in the last lesson.
    • save-restriction: A more powerful version. It saves the current buffer's "narrowing" state. Narrowing temporarily makes Emacs treat a part of the buffer (like a region) as if it were the entire buffer. save-restriction ensures that after your code runs, the original view of the buffer is restored.
  • Searching and Analysis:
    • search-forward, search-backward: Find a string or regex from the current point.
    • bounds-of-thing-at-point: A very useful function that returns the start and end positions of the "thing" (e.g., a word, sentence, or symbol) under the point.
  • Modification:
    • insert, delete-region: The basic tools for adding and removing text.
    • put-text-property: Modifies the appearance of text without changing its content. This is how Emacs does syntax highlighting and formatting.
  • Iteration:
    • while: A loop that continues as long as a condition is true. This is perfect for iterating through a buffer until you reach the end.

These functions, when combined, allow you to programmatically move through a buffer, analyze its contents, and perform complex transformations.


2. Project Walkthrough: A "Bionic Reading" Command

We'll now walk through the creation of a bionic-reading command, following a live-coding session that demonstrates a common development process. This command will find every word in a region or buffer and make the first half of it bold.

The video below shows a developer building this exact functionality from scratch. We will break it down step-by-step.

emacs lisp coding, write a bionic reading command

To begin, watch this segment from Xah Lee's video 'emacs lisp coding, write a bionic reading command'. It establishes the basic structure of an interactive function designed to work on a region.

Watch from 02:07 to 04:54. Focus on how (interactive "r") is used to get the begin and end positions of the selected region.

Step 1: Processing a Single Word

The core logic of our command is to process one word. The steps are:

  1. Find the start and end positions of the word at the current point.
  2. Calculate how many characters make up the first half.
  3. Apply a "bold" text property to that first half.

The bounds-of-thing-at-point function is perfect for the first step. For the third, we'll use put-text-property.

emacs lisp coding, write a bionic reading command

Now, let's see how to implement the single-word logic. This part of the video covers finding word boundaries, calculating the length, and applying a visual property.

Watch from 07:43 to 16:14 and from 16:33 to 19:48. Don't worry about catching every detail of the code; focus on the process: Using bounds-of-thing-at-point to get the word's start and end. Doing simple arithmetic to find the midpoint. Using put-text-property to change the text's appearance. Notice how a red color is used for debugging before switching to bold.

The key function here is put-text-property. Its basic form is:
(put-text-property START END PROPERTY VALUE)
For example, to make text bold, you'd use face as the property and 'bold as the value: (put-text-property start end 'face 'bold).

Step 2: Iterating Through the Buffer

With the logic for a single word complete, we now need to apply it to every word. A while loop is the natural choice. We can repeatedly move forward one word and apply our function until we reach the end of the buffer.

The forward-word function is ideal here. It moves the point to the end of the next word and, crucially, returns nil when it reaches the end of the buffer. This nil result will stop our while loop.

emacs lisp coding, write a bionic reading command

This next segment shows how to wrap the single-word logic in a loop to process the entire buffer. This is a fundamental pattern for batch processing text in Emacs.

Watch from 20:19 to 30:40. Pay attention to how goto-char (point-min) is used to start at the beginning, and how the (while (forward-word) ...) loop drives the process.

The pattern looks like this:

(goto-char (point-min)) ; Start at the beginning
(while (forward-word)
  ;; ... logic to process the word just before the new point position ...
  )

Step 3: Handling a Region with narrow-to-region

Our command now works on the whole buffer, but a good Emacs command should also respect the user's selection (the region). Instead of writing separate logic, we can use a powerful technique: temporarily "narrowing" the buffer to just the selected region.

The narrow-to-region function does exactly this. When you call (narrow-to-region begin end), all subsequent buffer operations (like point-min, point-max, and search-forward) will act as if the region is the entire buffer. To ensure we always return the buffer to its full view, we wrap this inside save-restriction.

emacs lisp coding, write a bionic reading command

Finally, let's make our command versatile. This clip shows how to adapt the buffer-wide command to operate only on a selected region using narrow-to-region.

Watch from 33:36 to 36:41. Focus on the (save-restriction (narrow-to-region begin end) ...) structure. Notice that the exact same while loop from the previous step can be reused inside this block without modification. This demonstrates the power of narrowing.

This save-restriction and narrow-to-region combination is a cornerstone of writing robust, flexible commands that can operate on either the entire buffer or just a part of it.


3. A Step-by-Step Development Process

The video showed a fluid, live-coding approach. Another way to build complex functions is to break the problem into smaller, well-defined helper functions and then compose them. This incremental approach can make your code easier to debug and understand.

The following article demonstrates this method by building a command to toggle comments in JavaScript test files. While the goal is different, the process of combining buffer navigation and text modification is the same.

An introduction to Emacs Lisp

Let's examine an article by Christian Johansen that showcases a more structured approach to building a command. You don't need to analyze all the code, but rather observe how the final functions combine buffer manipulation and text transformation.

Read the sections 'Disabling tests' and 'Enabling tests'. Notice how the buster-disable-test function combines navigation (buster-goto-beginning-of-test, forward-char) with transformation (insert "//"). Similarly, buster-enable-test combines searching (search-forward) with transformation (delete-region). This illustrates how a complex task is achieved by chaining together simpler operations.

This structured approach—building small, reliable pieces and then combining them—is an extremely effective strategy, especially as your Elisp projects grow in complexity.


4. Your Turn: Create a "Keyword Extractor" Command

Now it's time to synthesize these concepts into a practical command tailored to your work as a writer and researcher. You will create a command that scans a region of text for sentences containing a specific keyword and extracts them into a new buffer. This could be useful for pulling all sentences that mention a specific character, concept, or source from your notes.

Your Task:

Write an interactive command named my/extract-keyword-sentences.

Functionality:

  1. It should be an interactive command that prompts the user for a keyword (a string). Use (interactive "sKeyword: ").
  2. It should operate on the text within the active region. If no region is active, it should operate on the whole buffer.
  3. It should find or create a buffer named *keyword-extract* and clear its contents.
  4. It should iterate through all sentences in the source (region or buffer).
  5. For each sentence that contains the keyword, it should copy that sentence to the *keyword-extract* buffer, adding a newline after each.

Hints:

  • You'll need a structure like (let ((begin ...) (end ...)) ...). Use (use-region-p) to check if a region is active. If it is, get the boundaries from (region-beginning) and (region-end). If not, use (point-min) and (point-max).
  • Use save-restriction and narrow-to-region to limit your search space.
  • Use with-current-buffer (get-buffer-create "*keyword-extract*") to perform operations (like insert and delete-region) on your target buffer.
  • Inside a while loop, you can use (re-search-forward (concat "\\b" (regexp-quote keyword) "\\b") nil t) to find the next occurrence of the keyword. The \\b ensures you match whole words. regexp-quote makes the user's input safe for use in a regex.
  • After finding the keyword, use (bounds-of-thing-at-point 'sentence) to get the start and end of the surrounding sentence.
  • Extract the sentence text with buffer-substring.
  • You'll need to carefully manage the point as you search and extract to avoid an infinite loop. After processing a sentence, make sure to move the point past it before the next iteration of the while loop.

This is a challenging exercise that brings together everything we've covered. Take your time, use the *Messages* buffer and Edebug to inspect your variables, and break the problem down into smaller parts.

Click here to see a possible solution.
(defun my/extract-keyword-sentences (keyword)
  "Extract sentences containing KEYWORD from region or buffer.
The sentences are placed in the `*keyword-extract*` buffer."
  (interactive "sKeyword: ")
  ;; 1. Determine the boundaries for the search.
  (let ((begin (if (use-region-p) (region-beginning) (point-min)))
        (end (if (use-region-p) (region-end) (point-max)))
        (search-keyword (concat "\\b" (regexp-quote keyword) "\\b")))

    ;; 2. Prepare the destination buffer.
    (with-current-buffer (get-buffer-create "*keyword-extract*")
      ;; Clear the buffer before adding new content.
      (delete-region (point-min) (point-max)))

    ;; 3. Operate on the source buffer safely.
    (with-current-buffer (current-buffer)
      (save-excursion
        (save-restriction
          ;; 4. Narrow to the specified region/buffer.
          (narrow-to-region begin end)
          (goto-char (point-min))

          ;; 5. Loop through, finding all occurrences of the keyword.
          (while (re-search-forward search-keyword nil t)
            ;; We found the keyword. Now find the sentence boundaries.
            (let* ((sentence-bounds (bounds-of-thing-at-point 'sentence))
                   (sentence-start (car sentence-bounds))
                   (sentence-end (cdr sentence-bounds))
                   (sentence-text (buffer-substring-no-properties sentence-start sentence-end)))
              
              ;; 6. Insert the found sentence into our extract buffer.
              (with-current-buffer (get-buffer-create "*keyword-extract*")
                (goto-char (point-max)) ; Go to the end to append.
                (insert sentence-text "\n\n"))

              ;; 7. Move point to the end of the sentence to continue the search.
              (goto-char sentence-end)))))))
    
    ;; 8. Display the results.
    (display-buffer "*keyword-extract*")))

Conclusion

Congratulations! You have now reached the pinnacle of creating custom interactive commands in Elisp. You've gone from simple functions to building a sophisticated tool that combines buffer navigation, conditional logic, iteration, and text transformation.

Key Takeaways:

  • Complex commands are built by combining simple, powerful functions like search-forward, bounds-of-thing-at-point, and insert.
  • The while loop is the primary tool for iterating through a buffer to perform batch operations.
  • The (save-restriction (narrow-to-region ...)) pattern is essential for writing flexible commands that can operate on either a region or an entire buffer without code duplication.
  • Using helper forms like with-current-buffer and save-excursion keeps your code clean and prevents unintended side effects.

Preview of the Next Lesson

You've written several powerful functions, but they currently live in your *scratch* buffer or a temporary file. To make them a permanent part of your Emacs environment, you need to organize them properly. In our next lesson, you will learn how to create your own personal Elisp file, and how to use provide and require to load it into Emacs, turning your collection of scripts into a true, reusable extension.