mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 09:23:26 +00:00
feat: highlight selected SQL matches
This commit is contained in:
@@ -67,6 +67,7 @@ import {
|
||||
} from "@/lib/editorZoom";
|
||||
import { shortcutToCodeMirrorKey } from "@/lib/shortcutRegistry";
|
||||
import { trimmedSelectionLayer } from "@/lib/codemirrorTrimmedSelectionLayer";
|
||||
import { selectionMatchOccurrences } from "@/lib/codemirrorSelectionMatches";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
areSqlSemanticDiagnosticsEqual,
|
||||
@@ -1487,6 +1488,7 @@ onMounted(async () => {
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
trimmedSelectionLayer(),
|
||||
selectionMatchOccurrences(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { EditorState, Extension } from "@codemirror/state";
|
||||
import { highlightSelectionMatches, SearchCursor } from "@codemirror/search";
|
||||
import { EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
|
||||
|
||||
const MIN_SELECTION_MATCH_LENGTH = 2;
|
||||
const MAX_SELECTION_MATCH_LENGTH = 200;
|
||||
const MAX_SCROLLBAR_MARKS = 1200;
|
||||
|
||||
interface SelectionMatchMarker {
|
||||
from: number;
|
||||
to: number;
|
||||
lineNumber: number;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
function selectedMatchQuery(state: EditorState): string | null {
|
||||
const selection = state.selection.main;
|
||||
if (selection.empty) return null;
|
||||
if (selection.to - selection.from > MAX_SELECTION_MATCH_LENGTH) return null;
|
||||
|
||||
const text = state.sliceDoc(selection.from, selection.to);
|
||||
if (text.length < MIN_SELECTION_MATCH_LENGTH || !text.trim() || /[\r\n]/.test(text)) return null;
|
||||
return text;
|
||||
}
|
||||
|
||||
function buildSelectionMatchMarkers(state: EditorState): SelectionMatchMarker[] {
|
||||
const query = selectedMatchQuery(state);
|
||||
if (!query) return [];
|
||||
|
||||
const selection = state.selection.main;
|
||||
const markers: SelectionMatchMarker[] = [];
|
||||
const seenLines = new Set<number>();
|
||||
const cursor = new SearchCursor(state.doc, query);
|
||||
|
||||
for (let match = cursor.next(); !match.done; match = cursor.next()) {
|
||||
const { from, to } = match.value;
|
||||
if (from === to) continue;
|
||||
|
||||
const line = state.doc.lineAt(from);
|
||||
const selected = from === selection.from && to === selection.to;
|
||||
const lineKey = selected ? -line.number : line.number;
|
||||
if (seenLines.has(lineKey)) continue;
|
||||
seenLines.add(lineKey);
|
||||
|
||||
markers.push({ from, to, lineNumber: line.number, selected });
|
||||
if (markers.length >= MAX_SCROLLBAR_MARKS) break;
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
class SelectionMatchScrollbar {
|
||||
private readonly layer: HTMLElement;
|
||||
private signature = "";
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.layer = document.createElement("div");
|
||||
this.layer.className = "cm-selectionMatchScrollbarLayer";
|
||||
view.dom.appendChild(this.layer);
|
||||
this.render(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.selectionSet || update.geometryChanged) {
|
||||
this.render(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.layer.remove();
|
||||
}
|
||||
|
||||
private render(view: EditorView) {
|
||||
const markers = buildSelectionMatchMarkers(view.state);
|
||||
const lineCount = view.state.doc.lines;
|
||||
const signature = markers
|
||||
.map((marker) => `${marker.lineNumber}:${marker.from}:${marker.to}:${marker.selected}`)
|
||||
.join("|");
|
||||
if (signature === this.signature) return;
|
||||
this.signature = signature;
|
||||
this.layer.replaceChildren(
|
||||
...markers.map((marker) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = marker.selected
|
||||
? "cm-selectionMatchScrollbarMark cm-selectionMatchScrollbarMark-selected"
|
||||
: "cm-selectionMatchScrollbarMark";
|
||||
const ratio = lineCount <= 1 ? 0 : (marker.lineNumber - 1) / (lineCount - 1);
|
||||
el.style.top = `${Math.max(0, Math.min(1, ratio)) * 100}%`;
|
||||
return el;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const selectionMatchScrollbar = ViewPlugin.fromClass(SelectionMatchScrollbar);
|
||||
|
||||
export function selectionMatchOccurrences(): Extension {
|
||||
return [
|
||||
highlightSelectionMatches({
|
||||
minSelectionLength: MIN_SELECTION_MATCH_LENGTH,
|
||||
maxMatches: 500,
|
||||
}),
|
||||
selectionMatchScrollbar,
|
||||
EditorView.baseTheme({
|
||||
"&": {
|
||||
position: "relative",
|
||||
},
|
||||
".cm-selectionMatch": {
|
||||
backgroundColor: "color-mix(in oklab, var(--foreground) 8%, transparent)",
|
||||
outline: "1px solid color-mix(in oklab, var(--foreground) 10%, transparent)",
|
||||
},
|
||||
".cm-selectionMatch-main": {
|
||||
backgroundColor: "color-mix(in oklab, var(--primary) 14%, transparent)",
|
||||
outline: "1px solid color-mix(in oklab, var(--primary) 24%, transparent)",
|
||||
},
|
||||
".cm-selectionMatchScrollbarLayer": {
|
||||
bottom: "2px",
|
||||
pointerEvents: "none",
|
||||
position: "absolute",
|
||||
right: "2px",
|
||||
top: "2px",
|
||||
width: "5px",
|
||||
zIndex: "40",
|
||||
},
|
||||
".cm-selectionMatchScrollbarMark": {
|
||||
backgroundColor: "color-mix(in oklab, var(--primary) 48%, transparent)",
|
||||
borderRadius: "999px",
|
||||
height: "2px",
|
||||
position: "absolute",
|
||||
right: "0",
|
||||
transform: "translateY(-50%)",
|
||||
width: "3px",
|
||||
},
|
||||
".cm-selectionMatchScrollbarMark-selected": {
|
||||
backgroundColor: "color-mix(in oklab, var(--primary) 72%, transparent)",
|
||||
height: "3px",
|
||||
width: "4px",
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user