mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-28 11:37:04 +02:00
Add CodeMirror extension to display find match count in the editor. (#390)
This commit is contained in:
115
src-web/components/core/Editor/searchMatchCount.ts
Normal file
115
src-web/components/core/Editor/searchMatchCount.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { getSearchQuery, searchPanelOpen } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
||||
|
||||
/**
|
||||
* A CodeMirror extension that displays the total number of search matches
|
||||
* inside the built-in search panel.
|
||||
*/
|
||||
export function searchMatchCount(): Extension {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
private countEl: HTMLElement | null = null;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
this.updateCount();
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
// Recompute when doc changes, search state changes, or selection moves
|
||||
const query = getSearchQuery(update.state);
|
||||
const prevQuery = getSearchQuery(update.startState);
|
||||
const open = searchPanelOpen(update.state);
|
||||
const prevOpen = searchPanelOpen(update.startState);
|
||||
|
||||
if (update.docChanged || update.selectionSet || !query.eq(prevQuery) || open !== prevOpen) {
|
||||
this.updateCount();
|
||||
}
|
||||
}
|
||||
|
||||
private updateCount() {
|
||||
const state = this.view.state;
|
||||
const open = searchPanelOpen(state);
|
||||
const query = getSearchQuery(state);
|
||||
|
||||
if (!open) {
|
||||
this.removeCountEl();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureCountEl();
|
||||
|
||||
if (!query.search) {
|
||||
if (this.countEl) {
|
||||
this.countEl.textContent = '0/0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = state.selection.main;
|
||||
let count = 0;
|
||||
let currentIndex = 0;
|
||||
const MAX_COUNT = 9999;
|
||||
const cursor = query.getCursor(state);
|
||||
while (!cursor.next().done) {
|
||||
count++;
|
||||
if (cursor.value.from <= selection.from && cursor.value.to >= selection.to) {
|
||||
currentIndex = count;
|
||||
}
|
||||
if (count > MAX_COUNT) break;
|
||||
}
|
||||
|
||||
if (this.countEl) {
|
||||
if (count > MAX_COUNT) {
|
||||
this.countEl.textContent = `${MAX_COUNT}+`;
|
||||
} else if (count === 0) {
|
||||
this.countEl.textContent = '0/0';
|
||||
} else if (currentIndex > 0) {
|
||||
this.countEl.textContent = `${currentIndex}/${count}`;
|
||||
} else {
|
||||
this.countEl.textContent = `0/${count}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ensureCountEl() {
|
||||
// Find the search panel in the editor DOM
|
||||
const panel = this.view.dom.querySelector('.cm-search');
|
||||
if (!panel) {
|
||||
this.countEl = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.countEl && this.countEl.parentElement === panel) {
|
||||
return; // Already attached
|
||||
}
|
||||
|
||||
this.countEl = document.createElement('span');
|
||||
this.countEl.className = 'cm-search-match-count';
|
||||
|
||||
// Reorder: insert prev button, then next button, then count after the search input
|
||||
const searchInput = panel.querySelector('input');
|
||||
const prevBtn = panel.querySelector('button[name="prev"]');
|
||||
const nextBtn = panel.querySelector('button[name="next"]');
|
||||
if (searchInput && searchInput.parentElement === panel) {
|
||||
searchInput.after(this.countEl);
|
||||
if (prevBtn) this.countEl.after(prevBtn);
|
||||
if (nextBtn && prevBtn) prevBtn.after(nextBtn);
|
||||
} else {
|
||||
panel.prepend(this.countEl);
|
||||
}
|
||||
}
|
||||
|
||||
private removeCountEl() {
|
||||
if (this.countEl) {
|
||||
this.countEl.remove();
|
||||
this.countEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.removeCountEl();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user