Skip to content

Repository files navigation

soft-narrow

https://github.com/takeokunn/soft-narrow/actions/workflows/ci.yml/badge.svg

Emacs package to imitate narrow-to-region with more eye-candy.

Unlike narrow-to-region, which completely hides text outside the narrowed region, this package simply deemphasizes the text, makes it readonly, and makes it unreachable. This leads to a much more natural feeling, where the region stays static (instead of being brutally moved to a blank slate) and is clearly highlighted with respect to the rest of the buffer.

This package was inspired by fancy-narrow by Artur Malabarba, and reimplemented from scratch using modern Emacs 29.1+ APIs.

screenshot.png

Features

  • Minimal overhead — no function advising; buffer-local pre-command-hook and post-command-hook for boundary handling, installed only while narrowing is active
  • Stackable narrowing — true intersection semantics for progressive focusing
  • Region-scoped commands — run buffer-scanning commands (org export, mark-whole-buffer, …) scoped to the visible region via soft-narrow-execute / soft-narrow-with-restriction
  • Pure declarative approach — uses two buffer-local overlays carrying face, cursor-intangible, and read-only without modifying buffer text properties
  • Self-contained — single-file implementation with built-in org-mode integrations

Prerequisites

  • Emacs 29.1+

Installation

soft-narrow is not currently available from MELPA. Install it directly from the Git repository; the following leaf configuration is the recommended method.

Git URL with leaf (recommended)

(leaf soft-narrow
  :vc (:url "https://github.com/takeokunn/soft-narrow")
  :global-minor-mode t)

Local checkout with use-package

Clone this repository, add it to your load-path, and configure it with use-package:

(use-package soft-narrow
  :load-path "/path/to/soft-narrow"
  :config (soft-narrow-mode 1))

Manual

Clone this repository and add it to your load-path:

(add-to-list 'load-path "/path/to/soft-narrow")
(require 'soft-narrow)

Or download soft-narrow.el, open it in Emacs, and use M-x package-install-from-buffer.

Usage

The example/ directory contains sample files for interactive testing:

  • sample.el — Emacs Lisp file with multiple defuns (try C-x n d to narrow to defun)
  • sample.org — Org file with headings and blocks (try C-x n s to narrow to subtree)
  1. Simply call soft-narrow-to-region to see it in action. To widen the region again afterwards use soft-narrow-widen.
  2. If you activate the global minor mode (soft-narrow-mode), then the standard narrowing keys (C-x n n, C-x n w, etc) will make use of soft-narrow. Disabling the mode automatically widens all narrowed buffers.
  3. You can narrow to multiple regions in sequence. Each successive narrow creates an intersection of all narrowed regions, allowing you to progressively focus on specific parts of your buffer.

Commands

Key (with soft-narrow-mode)CommandDescription
C-x n nsoft-narrow-to-regionNarrow to the active region
C-x n wsoft-narrow-widenWiden (pop one narrowing level)
C-x n dsoft-narrow-to-defunNarrow to the current defun
C-x n psoft-narrow-to-pageNarrow to the current page
C-x n bsoft-narrow-org-to-blockNarrow to org block
C-x n esoft-narrow-org-to-elementNarrow to org element
C-x n ssoft-narrow-org-to-subtreeNarrow to org subtree
C-x n xsoft-narrow-executeRun a command scoped to the soft-narrow region

Use soft-narrow-active-p to check if soft-narrowing is active in the current buffer.

Stackable Narrowing

soft-narrow supports stackable narrowing with true intersection semantics:

;; Example 1: Overlapping regions
(soft-narrow-to-region 1 100)   ; Narrow to lines 1-100
(soft-narrow-to-region 50 150)  ; Further narrow to lines 50-150
;; Visible region: 50-100 (intersection)

;; Example 2: Widening
(soft-narrow-widen)  ; Returns to 1-100
(soft-narrow-widen)  ; Returns to full buffer

Non-overlapping regions produce an empty intersection:

(soft-narrow-to-region 100 200)  ; Lines 100-200
(soft-narrow-to-region 300 400)  ; Lines 300-400 (no overlap)
;; Result: intersection is empty, so the entire buffer is blocked.
;; Use soft-narrow-widen to pop back and restore the previous region.

Scoping Commands to the Region

soft-narrow keeps point-min / point-max pointed at the whole buffer so the surrounding text stays visible. The trade-off is that commands which scan the accessible buffer do **not** see the narrowing and operate on the entire buffer, for example:

  • org export (C-c C-e) exports the whole file, not the narrowed subtree
  • mark-whole-buffer (C-x h) marks the whole buffer
  • count-words, sort-lines, fill-region, org-babel-execute-buffer, etc.

This is inherent: Emacs ties the accessible portion of a buffer to the displayed portion, so a purely visual narrowing cannot automatically restrict these commands. Real narrow-to-region scopes them precisely because it hides the surrounding text — the exact thing soft-narrow avoids.

To scope such an operation to the visible region on demand, soft-narrow applies a genuine narrow-to-region only for the duration of that operation:

  • C-x n x (soft-narrow-execute) — read a command and run it scoped to the region
  • soft-narrow-org-export-dispatch — org export scoped to the region (bind to taste, e.g. in place of C-c C-e)
  • soft-narrow-with-restriction — a macro for use in your own code/config:
;; Any buffer-scanning operation, scoped to the soft-narrow region:
(soft-narrow-with-restriction
  (org-export-as 'html))

;; Bind org export to run scoped while soft-narrowing:
(with-eval-after-load 'org
  (define-key org-mode-map (kbd "C-c C-e")
    (lambda () (interactive)
      (if (soft-narrow-active-p)
          (soft-narrow-org-export-dispatch)
        (org-export-dispatch)))))

Use soft-narrow-region-bounds to get the current visible region as a (START . END) cons (nil when not narrowed).

Customization

  • soft-narrow-blocked-face – Face used to deemphasize unreachable text
  • soft-narrow-lighter – Mode-line lighter while narrowing is active (default: " *")

Note: this is designed for user interaction. For use within Lisp code, the standard narrow-to-region is preferable, because soft-narrow is susceptible to inhibit-read-only and some corner cases.

Technical Details

How It Works

soft-narrow uses Emacs 29.1+ native features:

  • Two buffer-local overlays: The before-region and after-region overlays carry face, cursor-intangible, and read-only, providing visual deemphasis, cursor restriction, and edit protection without snapshotting, removing, or restoring text properties owned by other packages.
  • Insertion protection: Overlay modification hooks reject insertions in blocked regions while still respecting inhibit-read-only.
  • Zero-width intersections: Disjoint or touching stacked regions produce an explicit zero-width intersection and block the entire buffer until that stack frame is popped.
  • Buffer independence: Overlay and marker state is local to each base or indirect buffer, so narrowing one buffer does not alter its siblings.
  • Stackable state: True intersection of multiple narrowed regions via marker-based LIFO stack

Emacs handles visual deemphasis and cursor restriction natively. While narrowing is active, buffer-local hooks suppress movement across the boundary, clamp point back into the active region, refresh marker-derived bounds after edits, and clean up state before a major-mode change. The final widen removes every hook and overlay and releases all stack markers, even if cursor-mode cleanup signals an error.

Troubleshooting

Text not greyed out as expected:

  • Check that soft-narrow-active-p returns t
  • Verify overlays exist with M-x describe-text-properties on blocked text

Cursor still moving outside region:

  • Verify you’re using Emacs 29.1+ for cursor-intangible support
  • Check cursor-intangible-mode is enabled

Markers behaving unexpectedly:

  • Markers automatically track insertions/deletions
  • Use soft-narrow-widen to reset if state becomes corrupted

Benchmark

make benchmark compares soft-narrow directly with fancy-narrow at the pinned commit c9b3363752c09045b8ce7a2635afae42d2ae63c7. The first invocation fetches that commit into ~/.cache/soft-narrow (override with BENCHMARK_CACHE); this explicit target is the only network-dependent part and is not run by the normal test or CI targets.

make benchmark

# A smaller exploratory run, or custom repeat counts:
BENCHMARK_BUFFER_BYTES=1048576 \
BENCHMARK_WARMUPS=2 \
BENCHMARK_SAMPLES=10 \
BENCHMARK_EDIT_ITERATIONS=1000 \
make benchmark

The default run uses a 10 MiB fundamental-mode buffer in non-displayed batch Emacs. For each implementation it discards three warmups and reports the minimum, median, mean, and nearest-rank p95 of 15 samples. It measures initial setup, an insert/delete pair at the center of the active region (2,000 pairs per sample), and widen separately. Buffer creation and narrowing needed to prepare the edit and widen cases are outside their timed intervals; garbage collection runs immediately before each timed interval.

Times are microseconds per operation. The soft/fancy median ratio is below 1 when soft-narrow is faster for that scenario. These results characterize only the printed Emacs build, machine, buffer mode and workload; they are not a universal performance or “fastest” claim. Run the benchmark on representative major modes and files before drawing conclusions for a particular workflow.

Development

make compile       # Byte-compile soft-narrow.el
make test          # Run ERT tests
make lint          # Run checkdoc
make package-lint  # Run package-lint
make benchmark     # Compare with pinned fancy-narrow (network on first run)
nix flake check    # Run all checks via Nix

License

GPL-3.0+. See LICENSE.

About

A gentle narrow-to-region for Emacs with visual deemphasis

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages