検索と置換
CodeMirror 6 での @codemirror/search を使った検索と置換: 検索パネル、キーバインディング、マッチのハイライト、正規表現検索。
@codemirror/search
@codemirror/search パッケージは、CodeMirror 6 の検索・置換機能を提供します。検索パネル UI、キーボードショートカット、マッチのハイライト、正規表現のサポートが含まれています。
検索のセットアップ
検索を有効にするには、search() Extension と searchKeymap キーバインディングを追加します。
import { search, searchKeymap } from "@codemirror/search";
import { keymap } from "@codemirror/view";
const extensions = [
search(),
keymap.of(searchKeymap),
];searchKeymap
searchKeymap は以下のデフォルトバインディングを提供します。
Ctrl-F(Windows/Linux)/Cmd-F(macOS)-- 検索パネルを開くCtrl-H(Windows/Linux)/Cmd-Option-F(macOS)-- 置換付きで検索パネルを開くF3/Ctrl-G-- 次のマッチを検索Shift-F3/Ctrl-Shift-G-- 前のマッチを検索Alt-G-- 行へ移動
highlightSelectionMatches()
@codemirror/search のこの Extension は、現在選択されているテキストの出現箇所をドキュメント全体でハイライトします。検索パネルとは独立して動作します。
import { highlightSelectionMatches } from "@codemirror/search";
const extensions = [highlightSelectionMatches()];選択の最小長やスタイルを設定できます。
highlightSelectionMatches({
minSelectionLength: 2,
highlightWordAroundCursor: true,
})highlightWordAroundCursor が true の場合、テキストを選択せずにカーソルを単語内に置くだけで、その単語のすべての出現箇所がハイライトされます。
プログラムによる検索コマンド
このパッケージは、コードから検索を制御するためのコマンドをエクスポートしています。
import {
openSearchPanel,
closeSearchPanel,
findNext,
findPrevious,
replaceNext,
replaceAll,
selectMatches,
} from "@codemirror/search";openSearchPanel と closeSearchPanel
検索パネルをプログラムで開閉するコマンドです。
import { openSearchPanel, closeSearchPanel } from "@codemirror/search";
// Open the search panel
openSearchPanel(view);
// Close the search panel
closeSearchPanel(view);findNext と findPrevious
次または前のマッチに選択を移動します。
import { findNext, findPrevious } from "@codemirror/search";
findNext(view);
findPrevious(view);replaceNext と replaceAll
現在のマッチまたはすべてのマッチを置換します。
import { replaceNext, replaceAll } from "@codemirror/search";
replaceNext(view);
replaceAll(view);selectMatches
すべてのマッチを同時に選択し、複数選択を作成します。
import { selectMatches } from "@codemirror/search";
selectMatches(view);検索の設定
search() 関数は設定オブジェクトを受け取ります。
import { search } from "@codemirror/search";
const extensions = [
search({
top: true, // Show the search panel at the top of the editor (default: false)
}),
];SearchQuery
SearchQuery と setSearchQuery エフェクトを使って、検索の初期状態をプログラムで設定できます。
import { SearchQuery, setSearchQuery } from "@codemirror/search";
const query = new SearchQuery({
search: "hello",
caseSensitive: false,
regexp: false,
replace: "world",
});
view.dispatch({
effects: setSearchQuery.of(query),
});正規表現検索
検索パネルには正規表現モードのトグルボタンがあります。有効にすると、検索文字列が正規表現として解釈されます。
SearchQuery を使ってプログラムで正規表現検索を設定することもできます。
import { SearchQuery, setSearchQuery } from "@codemirror/search";
const query = new SearchQuery({
search: "\\bfunction\\s+\\w+",
regexp: true,
});
view.dispatch({
effects: setSearchQuery.of(query),
});正規表現検索は JavaScript の正規表現構文をサポートしています。キャプチャグループは置換文字列内で $1、$2 などで参照できます。
const query = new SearchQuery({
search: "(\\w+)\\s*=\\s*(\\w+)",
regexp: true,
replace: "$2 = $1", // Swap the two sides of the assignment
});検索パネルのスタイリング
検索パネルはテーマでスタイルを設定できる CSS クラスでレンダリングされます。
import { EditorView } from "@codemirror/view";
const searchTheme = EditorView.theme({
".cm-panels": {
backgroundColor: "#f5f5f5",
borderBottom: "1px solid #ddd",
},
".cm-searchMatch": {
backgroundColor: "#ffd54f80",
},
".cm-searchMatch-selected": {
backgroundColor: "#ff980080",
},
});.cm-panels-- 検索パネルのコンテナ.cm-searchMatch-- ドキュメント内のハイライトされた検索マッチ.cm-searchMatch-selected-- 現在選択されているマッチ
検索の完全なセットアップ
検索、選択ハイライト、キーバインディングを含む完全な例です。
import { EditorState } from "@codemirror/state";
import { EditorView, keymap } from "@codemirror/view";
import {
search,
searchKeymap,
highlightSelectionMatches,
} from "@codemirror/search";
const state = EditorState.create({
doc: "Search for text in this editor.",
extensions: [
search({ top: true }),
highlightSelectionMatches(),
keymap.of(searchKeymap),
],
});
const view = new EditorView({
state,
parent: document.getElementById("editor"),
});以下のエディタで Ctrl-F(macOS では Cmd-F)を押して検索パネルを開いてみてください。テキストには検索できる繰り返しの単語が含まれています:
カスタムハイライトレイヤー
検索ロジックが CodeMirror の外部に存在する場合があります。例えば、エディタペインとプレビューペインで検索バーを共有する場合や、カスタムマッチャー(ファジー検索、AST を考慮した検索など)が独立して動作し、ドキュメントのオフセットを返す場合です。そのような場合、@codemirror/search は不要で、代わりに事前計算済みの範囲を受け取る薄いデコレーションレイヤーが欲しくなります。
パターンは StateField<DecorationSet> と単一の StateEffect の組み合わせで、1 つのトランザクションでハイライトセット全体を置換します。フィールドの update() 関数は 2 つの独立した処理を担います。
編集を通じたマッピング — ユーザーが文字を入力すると、
tr.changesによって既存のデコレーションが新しい位置に再マップされ、検索ディスパッチの間もハイライトがテキストに追従し続けます。エフェクト発生時の置換 — オーケストレーターが新しい結果を持つと、
setSearchHighlightsEffectに範囲の完全なリストを乗せてディスパッチし、フィールドはセット全体を一括で置換します。
エフェクトとマークデコレーションの定義
import { StateEffect } from "@codemirror/state";
import { Decoration } from "@codemirror/view";
export interface SearchHighlightRange {
from: number;
to: number;
active: boolean; // true => the navigation target (one per dispatch)
}
// Single-shot replace: the whole set is swapped each time.
// No incremental "add one hit" effect — the caller owns the canonical list.
export const setSearchHighlightsEffect =
StateEffect.define<SearchHighlightRange[]>();
// Two-tier marks: active hit gets a distinct class for styling.
const findMatchDeco = Decoration.mark({ class: "find-match" });
const findMatchActiveDeco = Decoration.mark({ class: "find-match-active" });デコレーションセットを防御的に構築する
Decoration.set は入力が from 位置でソートされていることを要求し、長さゼロや反転した範囲を拒否します。呼び出し側の不備で不変条件を違反しないよう、防御的にソートとフィルタリングを行います。
import { Decoration } from "@codemirror/view";
import type { DecorationSet } from "@codemirror/view";
function buildDecorations(
ranges: readonly SearchHighlightRange[],
docLength: number,
): DecorationSet {
if (ranges.length === 0) return Decoration.none;
const sorted = ranges
// Drop zero-length ranges (Decoration.mark requires from < to) and
// out-of-bounds ranges (defensive guard for upstream regex edge cases).
.filter((r) => r.from < r.to && r.from >= 0 && r.to <= docLength)
.slice()
.sort((a, b) => a.from - b.from || a.to - b.to);
if (sorted.length === 0) return Decoration.none;
return Decoration.set(
sorted.map((r) =>
(r.active ? findMatchActiveDeco : findMatchDeco).range(r.from, r.to),
),
);
}ステートフィールド
update() 関数は、エフェクトを確認する前に既存のデコレーションを tr.changes でマップします。この順序が重要です。エフェクトが新しい範囲を持つ場合、編集が適用された後のドキュメント長に対して検証されます。
import { StateField } from "@codemirror/state";
import { Decoration, EditorView } from "@codemirror/view";
import type { DecorationSet } from "@codemirror/view";
export const searchHighlightField = StateField.define<DecorationSet>({
create: () => Decoration.none,
update(value, tr) {
let next = value;
// Step 1: remap through edits so marks stay glued to text between dispatches.
// Without this, a character insertion before a highlight would leave the mark
// pointing at the wrong offset until the next search dispatch.
if (tr.docChanged) {
next = next.map(tr.changes);
}
// Step 2: if new ranges arrived, replace the entire set.
for (const e of tr.effects) {
if (e.is(setSearchHighlightsEffect)) {
next = buildDecorations(e.value, tr.state.doc.length);
}
}
return next;
},
// Wire the DecorationSet into the view's decoration layer.
provide: (field) => EditorView.decorations.from(field),
});CodeMirror 外部から範囲をプッシュする
フィールドを Extension として登録し、view.dispatch で範囲をプッシュします。
import { EditorView, basicSetup } from "codemirror";
const view = new EditorView({
doc: "The quick brown fox jumps over the lazy dog.",
extensions: [basicSetup, searchHighlightField],
parent: document.getElementById("editor"),
});
// Called by your external search orchestrator whenever hits change.
function applyHighlights(ranges: SearchHighlightRange[]) {
view.dispatch({
effects: setSearchHighlightsEffect.of(ranges),
});
}
// Example: highlight "fox" (basic) and "dog" (active / navigation target).
applyHighlights([
{ from: 16, to: 19, active: false }, // "fox"
{ from: 40, to: 43, active: true }, // "dog" — the active navigation hit
]);
// Clear all highlights by dispatching an empty array.
applyHighlights([]);2 段階のための CSS
マーククラスにスタイルが必要です。エディタのスタイルシートか EditorView.theme に追加します。
import { EditorView } from "@codemirror/view";
const highlightTheme = EditorView.theme({
".find-match": {
backgroundColor: "#ffd54f80",
borderRadius: "2px",
},
".find-match-active": {
backgroundColor: "#ff980080",
outline: "1px solid #e65100",
borderRadius: "2px",
},
});@codemirror/search を使わない理由
@codemirror/search は CM 内部に独自の検索状態を持ち、独自のクエリを実行します。カスタムマッチャーを使う場合や、複数のペインをまたいで単一の検索を行う必要がある場合は、このような薄いカスタムフィールドが適切です。このフィールドは範囲を保持してリマップするだけで、何が一致するかを決めるオーケストレーターが唯一の信頼できる情報源であり続けます。