mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 09:08:32 +02:00
Add CodeMirror extension to display find match count in the editor. (#390)
This commit is contained in:
@@ -434,11 +434,23 @@
|
|||||||
|
|
||||||
input {
|
input {
|
||||||
@apply bg-surface border-border-subtle focus:border-border-focus;
|
@apply bg-surface border-border-subtle focus:border-border-focus;
|
||||||
@apply border outline-none cursor-text;
|
@apply border outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
input.cm-textfield {
|
||||||
@apply focus-within:text-text;
|
@apply cursor-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-search label {
|
||||||
|
@apply inline-flex items-center h-6 px-1.5 rounded-sm border border-border-subtle cursor-default text-text-subtle text-xs;
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(:checked) {
|
||||||
|
@apply text-primary border-border;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the "All" button */
|
/* Hide the "All" button */
|
||||||
@@ -446,4 +458,31 @@
|
|||||||
button[name="select"] {
|
button[name="select"] {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Replace next/prev button text with chevron icons */
|
||||||
|
|
||||||
|
.cm-search button[name="next"],
|
||||||
|
.cm-search button[name="prev"] {
|
||||||
|
@apply text-[0px] w-7 h-6 inline-flex items-center justify-center border border-border-subtle mr-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-search button[name="prev"]::after,
|
||||||
|
.cm-search button[name="next"]::after {
|
||||||
|
@apply block w-3.5 h-3.5 bg-text;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-search button[name="prev"]::after {
|
||||||
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E");
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-search button[name="next"]::after {
|
||||||
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E");
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-search-match-count {
|
||||||
|
@apply text-text-subtle text-xs font-mono whitespace-nowrap px-1.5 py-0.5 self-center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import type { TwigCompletionOption } from './twig/completion';
|
|||||||
import { twig } from './twig/extension';
|
import { twig } from './twig/extension';
|
||||||
import { pathParametersPlugin } from './twig/pathParameters';
|
import { pathParametersPlugin } from './twig/pathParameters';
|
||||||
import { url } from './url/extension';
|
import { url } from './url/extension';
|
||||||
|
import { searchMatchCount } from './searchMatchCount';
|
||||||
|
|
||||||
export const syntaxHighlightStyle = HighlightStyle.define([
|
export const syntaxHighlightStyle = HighlightStyle.define([
|
||||||
{
|
{
|
||||||
@@ -256,6 +257,7 @@ export const readonlyExtensions = [
|
|||||||
|
|
||||||
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||||
search({ top: true }),
|
search({ top: true }),
|
||||||
|
searchMatchCount(),
|
||||||
hideGutter
|
hideGutter
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
|
|||||||
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