mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
fix(editor): keep selected text visible over highlighted ranges (#1454)
The selection is drawn in a layer behind the text, so any range with its own background (find matches, warnings in light schemes, usages, semantic backgrounds) hid it: selected text there kept its yellow or other highlight. Selected text is now also marked in the innermost span with the selection colour, in text editors, notebook cells and the VCS viewers.
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
selectLineStart,
|
||||
} from "@codemirror/commands";
|
||||
import { markdownImages } from "./markdownImages";
|
||||
import { selectionMarks } from "./selectionMarks";
|
||||
import type { EditorExtras } from "@/platform/editorSettings";
|
||||
import { openideaHighlightStyle } from "./highlight";
|
||||
import {
|
||||
@@ -504,6 +505,7 @@ export function caretExtension(prefs: EditorExtras): Extension {
|
||||
// Before drawSelection: its measure request must run before the cursor layer redraws.
|
||||
a.smoothCaretMovement ? caretGlide : [],
|
||||
drawSelection({ cursorBlinkRate: a.caretBlinking ? Math.max(100, Math.min(5000, a.caretBlinkPeriod)) * 2 : 0 }),
|
||||
selectionMarks,
|
||||
classes.length ? EditorView.editorAttributes.of({ class: classes.join(" ") }) : [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { EditorSelection, EditorState, Prec, type Extension } from "@codemirror/state";
|
||||
import { Decoration, EditorView } from "@codemirror/view";
|
||||
import { selectionMarks } from "./selectionMarks";
|
||||
|
||||
const views: EditorView[] = [];
|
||||
afterEach(() => views.splice(0).forEach((v) => v.destroy()));
|
||||
|
||||
function view(doc: string, selection: EditorSelection, extensions: Extension) {
|
||||
extensions = [EditorState.allowMultipleSelections.of(true), extensions];
|
||||
const parent = document.createElement("div");
|
||||
document.body.appendChild(parent);
|
||||
const v = new EditorView({ state: EditorState.create({ doc, selection, extensions }), parent });
|
||||
views.push(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
/** A background mark as features add it (lint range, find match, usage, semantic bg…). */
|
||||
const backgroundMark = (cls: string, from: number, to: number, prec: (e: Extension) => Extension = (e) => e) =>
|
||||
prec(EditorView.decorations.of(Decoration.set(Decoration.mark({ class: cls }).range(from, to))));
|
||||
|
||||
const DOC = "return request.app.state.credits";
|
||||
|
||||
describe("selectionMarks", () => {
|
||||
it.each([
|
||||
["default", (e: Extension) => e],
|
||||
["high (Lezer highlighter)", Prec.high],
|
||||
["lowest", Prec.lowest],
|
||||
])("paints selected text inside a %s-precedence background mark", (_, prec) => {
|
||||
// The reporter's case: a light-scheme warning background over `request.app.state.credits`.
|
||||
const v = view(DOC, EditorSelection.single(7, DOC.length), [
|
||||
backgroundMark("cm-lintRange-warning", 7, DOC.length, prec),
|
||||
selectionMarks,
|
||||
]);
|
||||
const selected = [...v.contentDOM.querySelectorAll(".cm-openidea-selected")];
|
||||
expect(selected.map((e) => e.textContent).join("")).toBe(DOC.slice(7));
|
||||
for (const el of selected) expect(el.closest(".cm-lintRange-warning")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("covers every range of a multi-selection, partially overlapping marks too", () => {
|
||||
const v = view(DOC, EditorSelection.create([EditorSelection.range(0, 3), EditorSelection.range(10, 20)], 1), [
|
||||
backgroundMark("cm-openidea-findMatch", 15, 24),
|
||||
selectionMarks,
|
||||
]);
|
||||
const text = [...v.contentDOM.querySelectorAll(".cm-openidea-selected")].map((e) => e.textContent).join("|");
|
||||
expect(text.replace(/\|/g, "")).toBe(DOC.slice(0, 3) + DOC.slice(10, 20));
|
||||
const inMatch = [...v.contentDOM.querySelectorAll(".cm-openidea-findMatch .cm-openidea-selected")];
|
||||
expect(inMatch.map((e) => e.textContent).join("")).toBe(DOC.slice(15, 20));
|
||||
});
|
||||
|
||||
it("adds nothing for an empty selection and clears when the selection collapses", () => {
|
||||
const v = view(DOC, EditorSelection.single(0, 6), [selectionMarks]);
|
||||
expect(v.contentDOM.querySelector(".cm-openidea-selected")).not.toBeNull();
|
||||
v.dispatch({ selection: EditorSelection.cursor(3) });
|
||||
expect(v.contentDOM.querySelector(".cm-openidea-selected")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Prec, RangeSetBuilder, type Extension } from "@codemirror/state";
|
||||
import { Decoration, EditorView, ViewPlugin, type DecorationSet, type ViewUpdate } from "@codemirror/view";
|
||||
|
||||
/**
|
||||
* `drawSelection` paints the selection in a layer behind the text, so any mark with its own
|
||||
* background (light-scheme warnings, find matches, usages, semantic backgrounds…) hides it.
|
||||
* This repaints selected text in the innermost span: higher-precedence marks nest closer to the
|
||||
* text, and the only other `Prec.highest` marks (LSP semantic tokens) set colour, not background.
|
||||
*/
|
||||
const selectedMark = Decoration.mark({ class: "cm-openidea-selected" });
|
||||
|
||||
function selectedRanges(view: EditorView): DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const ranges = view.state.selection.ranges.filter((r) => !r.empty);
|
||||
if (!ranges.length) return Decoration.none;
|
||||
for (const { from: vFrom, to: vTo } of view.visibleRanges)
|
||||
for (const r of ranges) {
|
||||
const from = Math.max(r.from, vFrom);
|
||||
const to = Math.min(r.to, vTo);
|
||||
if (from < to) builder.add(from, to, selectedMark);
|
||||
}
|
||||
return builder.finish();
|
||||
}
|
||||
|
||||
const plugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = selectedRanges(view);
|
||||
}
|
||||
update(u: ViewUpdate) {
|
||||
if (u.selectionSet || u.docChanged || u.viewportChanged) this.decorations = selectedRanges(u.view);
|
||||
}
|
||||
},
|
||||
{ decorations: (p) => p.decorations },
|
||||
);
|
||||
|
||||
const theme = EditorView.baseTheme({
|
||||
".cm-openidea-selected": {
|
||||
backgroundColor: "var(--editor-selection-inactive, var(--bg-editor-selection-inactive))",
|
||||
},
|
||||
"&.cm-focused .cm-openidea-selected": {
|
||||
backgroundColor: "var(--editor-selection, var(--bg-editor-selection))",
|
||||
},
|
||||
});
|
||||
|
||||
export const selectionMarks: Extension = [Prec.highest(plugin), theme];
|
||||
@@ -12,6 +12,7 @@ import { defaultKeymap, indentWithTab } from "@codemirror/commands";
|
||||
import { bracketMatching, indentOnInput, syntaxHighlighting } from "@codemirror/language";
|
||||
import { openideaTheme } from "@/editor/core/cm/theme";
|
||||
import { openideaHighlightStyle } from "@/editor/core/cm/highlight";
|
||||
import { selectionMarks } from "@/editor/core/cm/selectionMarks";
|
||||
import { findLanguageDescription, loadSupport } from "@/editor/core/cm/languages";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import type { NotebookSession } from "./session";
|
||||
@@ -43,6 +44,7 @@ export function CellEditor({
|
||||
syntaxHighlighting(openideaHighlightStyle),
|
||||
lineNumbers(),
|
||||
drawSelection(),
|
||||
selectionMarks,
|
||||
highlightActiveLine(),
|
||||
highlightSpecialChars(),
|
||||
bracketMatching(),
|
||||
|
||||
@@ -270,9 +270,10 @@ export const viewerTheme = EditorView.theme(
|
||||
"&.cm-openidea-ligatures .cm-scroller": { fontVariantLigatures: "normal" },
|
||||
".cm-content": { caretColor: "var(--caret, var(--fg-strong))", WebkitUserSelect: "text", userSelect: "text" },
|
||||
...caretStyles,
|
||||
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, ::selection": {
|
||||
backgroundColor: "var(--bg-selected-strong)",
|
||||
},
|
||||
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, ::selection, .cm-openidea-selected, &.cm-focused .cm-openidea-selected":
|
||||
{
|
||||
backgroundColor: "var(--bg-selected-strong)",
|
||||
},
|
||||
".cm-activeLine": { backgroundColor: "var(--bg-current-line)" },
|
||||
".cm-gutters": {
|
||||
backgroundColor: "var(--bg-app)",
|
||||
|
||||
Reference in New Issue
Block a user