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.
- Minimal overhead — no function advising; buffer-local
pre-command-hookandpost-command-hookfor 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 viasoft-narrow-execute/soft-narrow-with-restriction - Pure declarative approach — uses two buffer-local overlays carrying
face,cursor-intangible, andread-onlywithout modifying buffer text properties - Self-contained — single-file implementation with built-in org-mode integrations
- Emacs 29.1+
soft-narrow is not currently available from MELPA. Install it directly from
the Git repository; the following leaf configuration is the recommended
method.
(leaf soft-narrow
:vc (:url "https://github.com/takeokunn/soft-narrow")
:global-minor-mode t)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))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.
The example/ directory contains sample files for interactive testing:
sample.el— Emacs Lisp file with multiple defuns (tryC-x n dto narrow to defun)sample.org— Org file with headings and blocks (tryC-x n sto narrow to subtree)
- Simply call
soft-narrow-to-regionto see it in action. To widen the region again afterwards usesoft-narrow-widen. - 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. - 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.
Key (with soft-narrow-mode) | Command | Description |
|---|---|---|
C-x n n | soft-narrow-to-region | Narrow to the active region |
C-x n w | soft-narrow-widen | Widen (pop one narrowing level) |
C-x n d | soft-narrow-to-defun | Narrow to the current defun |
C-x n p | soft-narrow-to-page | Narrow to the current page |
C-x n b | soft-narrow-org-to-block | Narrow to org block |
C-x n e | soft-narrow-org-to-element | Narrow to org element |
C-x n s | soft-narrow-org-to-subtree | Narrow to org subtree |
C-x n x | soft-narrow-execute | Run a command scoped to the soft-narrow region |
Use soft-narrow-active-p to check if soft-narrowing is active in the current buffer.
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 bufferNon-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.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:
orgexport (C-c C-e) exports the whole file, not the narrowed subtreemark-whole-buffer(C-x h) marks the whole buffercount-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 regionsoft-narrow-org-export-dispatch— org export scoped to the region (bind to taste, e.g. in place ofC-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).
soft-narrow-blocked-face– Face used to deemphasize unreachable textsoft-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.
soft-narrow uses Emacs 29.1+ native features:
- Two buffer-local overlays: The before-region and after-region overlays carry
face,cursor-intangible, andread-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.
Text not greyed out as expected:
- Check that
soft-narrow-active-preturnst - Verify overlays exist with
M-x describe-text-propertieson blocked text
Cursor still moving outside region:
- Verify you’re using Emacs 29.1+ for
cursor-intangiblesupport - Check
cursor-intangible-modeis enabled
Markers behaving unexpectedly:
- Markers automatically track insertions/deletions
- Use
soft-narrow-widento reset if state becomes corrupted
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 benchmarkThe 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.
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 NixGPL-3.0+. See LICENSE.
