diff --git a/.github/workflows/e2e-local.yml b/.github/workflows/e2e-local.yml
index 090c0470..c656d61e 100644
--- a/.github/workflows/e2e-local.yml
+++ b/.github/workflows/e2e-local.yml
@@ -55,6 +55,7 @@ jobs:
[
home,
editor,
+ editor-find,
manual,
manual-examples,
manual-code,
diff --git a/docs/guide/reference/shortcuts.md b/docs/guide/reference/shortcuts.md
index 3e93922a..98a97dbb 100644
--- a/docs/guide/reference/shortcuts.md
+++ b/docs/guide/reference/shortcuts.md
@@ -27,6 +27,33 @@ On macOS, Command is the main shortcut modifier. Windows and Linux use Ctrl.
Evaluation needs a running Csound performance. A table plot needs a supported definition. When a browser or operating system takes a shortcut, use the corresponding page control instead.
+## Find and edit
+
+Click in an editor, then press Command+F on macOS or Ctrl+F on Windows and Linux. Find searches that file, even when several editors are open. The panel includes replace, regular expressions, case matching, and whole-word search. Read-only tool output supports Find without replace.
+
+Press Enter in the Find field for the next match, or Shift+Enter for the previous match. Escape closes the panel and returns focus to the editor. Replace all is one undo step. With focus outside an editor, the browser keeps its own Find shortcut.
+
+| Action | macOS | Windows / Linux |
+| --------------------------------------- | --------------------------- | ------------------------ |
+| Find and replace | Command+F | Ctrl+F |
+| Next match | Command+G or F3 | Ctrl+G or F3 |
+| Previous match | Command+Shift+G or Shift+F3 | Ctrl+Shift+G or Shift+F3 |
+| Select next occurrence | Command+D | Ctrl+D |
+| Select all occurrences of selected text | Command+Shift+L | Ctrl+Shift+L |
+| Go to line | Command+Option+G | Ctrl+Alt+G |
+
+The source editor also supports these commands:
+
+| Action | macOS | Windows / Linux |
+| ------------------------------------- | ------------------------------ | ------------------------------ |
+| Fold / unfold code at cursor | Command+Option+[ / ] | Ctrl+Shift+[ / ] |
+| Fold / unfold all code | Ctrl+Alt+[ / ] | Ctrl+Alt+[ / ] |
+| Open error list | Command+Shift+M | Ctrl+Shift+M |
+| Next error or warning | F8 | F8 |
+| Delete both brackets of an empty pair | Backspace between the brackets | Backspace between the brackets |
+
+Folding follows the file's language. Error shortcuts use messages already reported by Csound or the background checker.
+
## Plot windows and piano
Escape closes the focused table plot. With several plots, focus returns through the windows you used.
diff --git a/package-lock.json b/package-lock.json
index aa961594..1dd0958e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/language": "^6.13.1",
"@codemirror/lint": "^6.9.7",
+ "@codemirror/search": "^6.7.2",
"@codemirror/state": "^6.7.6",
"@codemirror/view": "^6.43.14",
"@csound/browser": "7.0.0-beta41",
diff --git a/package.json b/package.json
index 30158dd1..940903d7 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/language": "^6.13.1",
"@codemirror/lint": "^6.9.7",
+ "@codemirror/search": "^6.7.2",
"@codemirror/state": "^6.7.6",
"@codemirror/view": "^6.43.14",
"@csound/browser": "7.0.0-beta41",
diff --git a/puppeteer-tests/fixtures/editor-find.html b/puppeteer-tests/fixtures/editor-find.html
new file mode 100644
index 00000000..d3c92914
--- /dev/null
+++ b/puppeteer-tests/fixtures/editor-find.html
@@ -0,0 +1,41 @@
+
+
+
+
+ Editor Find check
+
+
+
+
+
+
+
diff --git a/puppeteer-tests/fixtures/editor-find.tsx b/puppeteer-tests/fixtures/editor-find.tsx
new file mode 100644
index 00000000..22a9c452
--- /dev/null
+++ b/puppeteer-tests/fixtures/editor-find.tsx
@@ -0,0 +1,159 @@
+import { useState } from "react";
+import { createRoot } from "react-dom/client";
+import { Provider } from "react-redux";
+import { configureStore } from "@reduxjs/toolkit";
+import { setDiagnostics } from "@codemirror/lint";
+import { ThemeProvider, createTheme } from "@mui/material/styles";
+import Editor, { openEditors } from "../../src/components/editor/editor";
+import { CodePane } from "../../src/components/score-tools/code-pane";
+import HotKeys from "../../src/components/hot-keys/hot-keys";
+import HotKeysReducer from "../../src/components/hot-keys/reducer";
+import dark from "../../src/styles/_theme-dracula";
+import light from "../../src/styles/_theme-github-light";
+
+// Real editor components and global shortcuts, without cloud reads or writes.
+const store = configureStore({
+ reducer: {
+ HotKeysReducer,
+ ProjectsReducer: () => ({ projects: { "find-fixture": {} } })
+ }
+});
+const lightMode = new URLSearchParams(location.search).has("light");
+const theme = createTheme({
+ ...(lightMode ? light : dark),
+ font: { regular: "system-ui, sans-serif", monospace: "monospace" }
+});
+document.body.style.background = theme.background;
+document.body.style.color = theme.textColor;
+history.replaceState(null, "", "/editor/find-fixture");
+
+(window as any).editorFindFixture = {
+ text: (id: string) => openEditors.get(id)?.state.doc.toString(),
+ cursorLine: (id: string) => {
+ const state = openEditors.get(id)!.state;
+ return state.doc.lineAt(state.selection.main.head).number;
+ },
+ selection: (id: string) => {
+ const state = openEditors.get(id)!.state;
+ return state.sliceDoc(
+ state.selection.main.from,
+ state.selection.main.to
+ );
+ },
+ select: (id: string, from: number, to = from) => {
+ openEditors
+ .get(id)!
+ .dispatch({ selection: { anchor: from, head: to } });
+ },
+ ranges: (id: string) =>
+ openEditors
+ .get(id)!
+ .state.selection.ranges.map(({ from, to }) => ({ from, to })),
+ diagnostics: () => {
+ const view = openEditors.get("first")!;
+ view.dispatch(
+ setDiagnostics(view.state, [
+ {
+ from: 2,
+ to: 11,
+ severity: "error",
+ message: "Example error"
+ },
+ {
+ from: 12,
+ to: 17,
+ severity: "warning",
+ message: "Example warning"
+ }
+ ])
+ );
+ },
+ lastFindPrevented: undefined
+};
+document.addEventListener("keydown", (event) => {
+ if (event.key.toLowerCase() === "f" && (event.metaKey || event.ctrlKey))
+ (window as any).editorFindFixture.lastFindPrevented =
+ event.defaultPrevented;
+});
+
+function Source({
+ id,
+ value,
+ filename = `${id}.csd`
+}: {
+ id: string;
+ value: string;
+ filename?: string;
+}) {
+ const [text, setText] = useState(value);
+ return (
+
+ );
+}
+
+function Fixture() {
+ const [source, setSource] = useState("i1 0 1\ni2 1 1");
+ return (
+
+
+
+
+
+
+
+ Tool output (read only)
+
+
+
+
+
+
+ );
+}
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+);
diff --git a/puppeteer-tests/tests/editor-find.js b/puppeteer-tests/tests/editor-find.js
new file mode 100644
index 00000000..68b590bf
--- /dev/null
+++ b/puppeteer-tests/tests/editor-find.js
@@ -0,0 +1,295 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import { mkdir } from "node:fs/promises";
+import puppeteer from "puppeteer";
+import { BROWSER_SETTINGS, target, targetName } from "../utils/config.js";
+
+for (const [platform, modifier, light] of [
+ ["MacIntel", "Meta", false],
+ ["Win32", "Control", true],
+ ["Linux x86_64", "Control", false]
+]) {
+ test(
+ `Find and replace in the focused editor on ${platform}`,
+ {
+ skip: targetName !== "local",
+ timeout: 45000
+ },
+ async () => {
+ const browser = await puppeteer.launch(BROWSER_SETTINGS);
+ const page = await browser.newPage();
+ page.setDefaultTimeout(10000);
+ try {
+ const errors = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+ await page.evaluateOnNewDocument((platform) => {
+ Object.defineProperty(navigator, "platform", {
+ get: () => platform
+ });
+ }, platform);
+ await page.goto(
+ `${target.baseUrl}/puppeteer-tests/fixtures/editor-find.html${light ? "?light" : ""}`
+ );
+ await page.waitForSelector(
+ '[data-editor="second"] .cm-content'
+ );
+ const pressFind = async () => {
+ await page.keyboard.down(modifier);
+ await page.keyboard.press("f");
+ await page.keyboard.up(modifier);
+ };
+ const second = '[data-editor="second"]';
+ await page.click(`${second} .cm-content`);
+ await pressFind();
+ assert.ok(
+ await page.$(`${second} .cm-search`),
+ "Find must open inside the focused editor"
+ );
+ assert.equal(
+ await page.$('[data-editor="first"] .cm-search'),
+ null
+ );
+ assert.equal(
+ await page.evaluate(
+ () => window.editorFindFixture.lastFindPrevented
+ ),
+ true
+ );
+ assert.equal(
+ await page.evaluate(() =>
+ document.activeElement?.getAttribute("name")
+ ),
+ "search"
+ );
+ await page.keyboard.type("alpha");
+ await page.click(`${second} input[name="case"]`);
+ await page.click(`${second} input[name="word"]`);
+ await page.waitForFunction(
+ () =>
+ document.querySelectorAll(
+ '[data-editor="second"] .cm-searchMatch'
+ ).length === 2
+ );
+ await page.click(`${second} button[name="next"]`);
+ assert.equal(
+ await page.evaluate(() =>
+ window.editorFindFixture.selection("second")
+ ),
+ "alpha"
+ );
+ const firstMatch = await page.evaluate(
+ () => window.editorFindFixture.ranges("second")[0].from
+ );
+ await page.keyboard.press("F3");
+ assert.notEqual(
+ await page.evaluate(
+ () => window.editorFindFixture.ranges("second")[0].from
+ ),
+ firstMatch
+ );
+ await page.keyboard.down("Shift");
+ await page.keyboard.press("F3");
+ await page.keyboard.up("Shift");
+ assert.equal(
+ await page.evaluate(
+ () => window.editorFindFixture.ranges("second")[0].from
+ ),
+ firstMatch
+ );
+ // Find again selects the existing query for replacement.
+ await pressFind();
+ await page.keyboard.type("tone(\\d+)");
+ await page.click(`${second} input[name="re"]`);
+ await page.type(`${second} input[name="replace"]`, "voice$1");
+ await page.click(`${second} button[name="replaceAll"]`);
+ await page.waitForFunction(() =>
+ window.editorFindFixture
+ .text("second")
+ .includes("voice12 voice34")
+ );
+ assert.equal(
+ await page.evaluate(() =>
+ window.editorFindFixture.text("first")
+ ),
+ "; untouched alpha\ninstr 1\n aSignal oscili 0.1, 440\nendin"
+ );
+ await page.click(`${second} input[name="search"]`);
+ await page.keyboard.press("Escape");
+ assert.equal(await page.$(`${second} .cm-search`), null);
+ assert.equal(
+ await page.evaluate(() =>
+ document.activeElement?.classList.contains("cm-content")
+ ),
+ true
+ );
+
+ // Repeated Find focuses the same panel; Escape and Undo retain normal editing.
+ await pressFind();
+ await pressFind();
+ assert.equal((await page.$$(`${second} .cm-search`)).length, 1);
+ await page.keyboard.press("Escape");
+ await page.keyboard.down(modifier);
+ await page.keyboard.press("z");
+ await page.keyboard.up(modifier);
+ await page.waitForFunction(() =>
+ window.editorFindFixture
+ .text("second")
+ .includes("tone12 tone34")
+ );
+
+ const press = async (keys, key) => {
+ for (const modifier of keys)
+ await page.keyboard.down(modifier);
+ await page.keyboard.press(
+ /^[a-z]$/.test(key) ? `Key${key.toUpperCase()}` : key
+ );
+ for (const modifier of [...keys].reverse())
+ await page.keyboard.up(modifier);
+ };
+ // Select repeated text without opening Find, then go to a line.
+ await page.evaluate(() =>
+ window.editorFindFixture.select("second", 2, 7)
+ );
+ await press([modifier], "d");
+ assert.equal(
+ await page.evaluate(
+ () => window.editorFindFixture.ranges("second").length
+ ),
+ 2
+ );
+ await page.evaluate(() =>
+ window.editorFindFixture.select("second", 2, 7)
+ );
+ await press([modifier, "Shift"], "l");
+ assert.equal(
+ await page.evaluate(
+ () => window.editorFindFixture.ranges("second").length
+ ),
+ 3
+ );
+ await press([modifier, "Alt"], "g");
+ await page.waitForSelector(`${second} .cm-goto-line input`);
+ await page.keyboard.type("3");
+ await page.keyboard.press("Enter");
+ await page.waitForFunction(
+ () => window.editorFindFixture.cursorLine("second") === 3
+ );
+
+ const first = '[data-editor="first"]';
+ await page.focus(`${first} .cm-content`);
+ await page.evaluate(() =>
+ window.editorFindFixture.select("first", 18)
+ );
+ const foldModifiers =
+ modifier === "Meta"
+ ? ["Meta", "Alt"]
+ : ["Control", "Shift"];
+ await press(foldModifiers, "BracketLeft");
+ await page.waitForSelector(`${first} .cm-foldPlaceholder`);
+ await press(foldModifiers, "BracketRight");
+ assert.equal(
+ await page.$(`${first} .cm-foldPlaceholder`),
+ null
+ );
+ await press(["Control", "Alt"], "BracketLeft");
+ assert.ok(await page.$(`${first} .cm-foldPlaceholder`));
+ await press(["Control", "Alt"], "BracketRight");
+ assert.equal(
+ await page.$(`${first} .cm-foldPlaceholder`),
+ null
+ );
+
+ await page.evaluate(() =>
+ window.editorFindFixture.select("first", 26)
+ );
+ const beforePair = await page.evaluate(() =>
+ window.editorFindFixture.text("first")
+ );
+ await page.keyboard.type("(");
+ assert.equal(
+ await page.evaluate(() =>
+ window.editorFindFixture.text("first")
+ ),
+ beforePair.slice(0, 26) + "()" + beforePair.slice(26)
+ );
+ await page.keyboard.press("Backspace");
+ assert.equal(
+ await page.evaluate(() =>
+ window.editorFindFixture.text("first")
+ ),
+ beforePair
+ );
+
+ await page.evaluate(() => {
+ window.editorFindFixture.select("first", 0);
+ window.editorFindFixture.diagnostics();
+ });
+ await page.keyboard.press("F8");
+ assert.equal(
+ await page.evaluate(() =>
+ window.editorFindFixture.selection("first")
+ ),
+ "untouched"
+ );
+ await page.keyboard.press("F8");
+ assert.equal(
+ await page.evaluate(() =>
+ window.editorFindFixture.selection("first")
+ ),
+ "alpha"
+ );
+ await press([modifier, "Shift"], "m");
+ await page.waitForSelector(`${first} .cm-panel-lint`);
+ assert.match(
+ await page.$eval(
+ `${first} .cm-panel-lint`,
+ (panel) => panel.textContent
+ ),
+ /Example error/
+ );
+ await page.keyboard.press("Escape");
+ assert.equal(await page.$(`${first} .cm-panel-lint`), null);
+
+ for (const id of ["tool-source", "tool-output"]) {
+ await page.focus(`[data-editor="${id}"] .cm-content`);
+ await pressFind();
+ assert.ok(await page.$(`[data-editor="${id}"] .cm-search`));
+ const replace = await page.$(
+ `[data-editor="${id}"] input[name="replace"]`
+ );
+ assert.equal(Boolean(replace), id === "tool-source");
+ await page.keyboard.press("Escape");
+ }
+ await page.focus('input[aria-label="Outside editor"]');
+ await pressFind();
+ assert.equal(
+ await page.evaluate(
+ () => window.editorFindFixture.lastFindPrevented
+ ),
+ false
+ );
+ assert.equal((await page.$$(".cm-search")).length, 0);
+ await page.keyboard.press("Escape");
+
+ await page.setViewport({ width: 360, height: 800 });
+ await page.focus(`${second} .cm-content`);
+ await pressFind();
+ assert.ok(
+ await page.$eval(
+ `${second} .cm-search`,
+ (panel) => panel.scrollWidth <= panel.clientWidth
+ )
+ );
+ assert.deepEqual(errors, []);
+ } finally {
+ const screenshots = new URL("../screenshots/", import.meta.url);
+ await mkdir(screenshots, { recursive: true });
+ await page.screenshot({
+ path: `${screenshots.pathname}editor-find-${platform.replaceAll(" ", "-")}.png`,
+ fullPage: true
+ });
+ await browser.close();
+ }
+ }
+ );
+}
diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx
index aa5bc51d..33623ac8 100644
--- a/src/components/editor/editor.tsx
+++ b/src/components/editor/editor.tsx
@@ -21,7 +21,7 @@ import {
clearCompilerDiagnostics
} from "./validation/messages";
import { editorDiagnostics } from "./validation/ranges";
-import { setDiagnostics } from "@codemirror/lint";
+import { lintKeymap, setDiagnostics } from "@codemirror/lint";
import { StateEffect } from "@codemirror/state";
import { projectSources } from "./validation/source";
import { csoundEditorLanguage } from "./csound-language";
@@ -38,7 +38,11 @@ import {
drawSelection,
dropCursor
} from "@codemirror/view";
-import { autocompletion, closeBrackets } from "@codemirror/autocomplete";
+import {
+ autocompletion,
+ closeBrackets,
+ closeBracketsKeymap
+} from "@codemirror/autocomplete";
import {
defaultKeymap,
history,
@@ -49,6 +53,7 @@ import {
bracketMatching,
defaultHighlightStyle,
foldGutter,
+ foldKeymap,
indentOnInput,
syntaxHighlighting
} from "@codemirror/language";
@@ -59,6 +64,7 @@ import { IDocument, IProject } from "../projects/types";
import * as projectActions from "../projects/actions";
import { lookupManualString } from "../project-editor/actions";
import { editorStyle } from "@styles/code-mirror-painter";
+import { editorSearch } from "./search";
import { useTheme } from "@emotion/react";
import { codeMirrorTheme } from "@styles/code-mirror-theme";
@@ -297,7 +303,9 @@ const CodeEditor = ({
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
languageCompartment.of(languageExtension),
+ editorSearch,
keymap.of([
+ ...closeBracketsKeymap,
...defaultKeymap.filter(
(keyb) =>
keyb &&
@@ -307,7 +315,9 @@ const CodeEditor = ({
"Ctrl-Enter"
].includes(keyb.key || "")
),
- ...historyKeymap
+ ...historyKeymap,
+ ...foldKeymap,
+ ...lintKeymap
]),
evalBlinkExtension,
bracketMatching(),
diff --git a/src/components/editor/search.ts b/src/components/editor/search.ts
new file mode 100644
index 00000000..b548b851
--- /dev/null
+++ b/src/components/editor/search.ts
@@ -0,0 +1,5 @@
+import { search, searchKeymap } from "@codemirror/search";
+import { keymap } from "@codemirror/view";
+
+// Bind to each view so Find follows focus, including tool inputs and outputs.
+export const editorSearch = [search({ top: true }), keymap.of(searchKeymap)];
diff --git a/src/components/hot-keys/default-bindings.ts b/src/components/hot-keys/default-bindings.ts
index e59ba784..69db6747 100644
--- a/src/components/hot-keys/default-bindings.ts
+++ b/src/components/hot-keys/default-bindings.ts
@@ -16,7 +16,6 @@ const defaultBindings: BindingsMap = {
stop_playback: isMac ? "opt+command+p" : "ctrl+shift+p",
// editor
doc_at_point: isMac ? "ctrl+." : "alt+.",
- // find_simple: isMac ? "command+f" : "ctrl+f",
undo: isMac ? "command+z" : "ctrl+z",
redo: isMac ? "shift+command+z" : "shift+ctrl+z",
eval: isMac ? "command+e" : "ctrl+e",
diff --git a/src/components/score-tools/code-pane.tsx b/src/components/score-tools/code-pane.tsx
index 68026082..a98be449 100644
--- a/src/components/score-tools/code-pane.tsx
+++ b/src/components/score-tools/code-pane.tsx
@@ -16,6 +16,7 @@ import {
} from "@codemirror/commands";
import { editorStyle } from "@styles/code-mirror-painter";
import { codeMirrorTheme } from "@styles/code-mirror-theme";
+import { editorSearch } from "../editor/search";
export function CodePane({
value,
@@ -47,6 +48,8 @@ export function CodePane({
config.current.of([]),
lineNumbers(),
drawSelection(),
+ EditorState.allowMultipleSelections.of(true),
+ editorSearch,
keymap.of([
...defaultKeymap,
...historyKeymap,
diff --git a/src/components/site-documents/keyboard-shortcuts.tsx b/src/components/site-documents/keyboard-shortcuts.tsx
index 2fffcf4f..f4f24833 100644
--- a/src/components/site-documents/keyboard-shortcuts.tsx
+++ b/src/components/site-documents/keyboard-shortcuts.tsx
@@ -8,6 +8,42 @@ type ShortcutRow = {
};
const shortcutRows: ShortcutRow[] = [
+ {
+ shortcut: isMac ? "command-f" : "ctrl-f",
+ action: "Find and replace in the focused editor"
+ },
+ {
+ shortcut: isMac ? "command-g / F3" : "ctrl-g / F3",
+ action: "Next match (add Shift for the previous match)"
+ },
+ {
+ shortcut: isMac ? "command-d" : "ctrl-d",
+ action: "Select next occurrence"
+ },
+ {
+ shortcut: isMac ? "command-shift-l" : "ctrl-shift-l",
+ action: "Select all occurrences of the selected text"
+ },
+ {
+ shortcut: isMac ? "command-opt-g" : "ctrl-alt-g",
+ action: "Go to line"
+ },
+ {
+ shortcut: isMac ? "command-opt-[" : "ctrl-shift-[",
+ action: "Fold code at cursor (use ] to unfold)"
+ },
+ {
+ shortcut: "ctrl-alt-[",
+ action: "Fold all code (use ] to unfold all)"
+ },
+ {
+ shortcut: isMac ? "command-shift-m" : "ctrl-shift-m",
+ action: "Open error list"
+ },
+ {
+ shortcut: "F8",
+ action: "Go to next error or warning"
+ },
{
shortcut: isMac ? "ctrl-command-n" : "ctrl-alt-n",
action: "New file"
diff --git a/src/styles/code-mirror-theme.ts b/src/styles/code-mirror-theme.ts
index 634a519c..37ff0ac8 100644
--- a/src/styles/code-mirror-theme.ts
+++ b/src/styles/code-mirror-theme.ts
@@ -62,6 +62,67 @@ export const codeMirrorTheme = (theme: Theme) =>
color: theme.textColor
},
".cm-panels-bottom": { borderTop: `1px solid ${theme.line}` },
+ ".cm-panels-top": { borderBottom: `1px solid ${theme.line}` },
+ ".cm-panel.cm-search, .cm-panel.cm-dialog, .cm-panel.cm-panel-lint":
+ {
+ fontFamily: theme.font.regular,
+ fontSize: "12px",
+ lineHeight: "1.6",
+ whiteSpace: "normal"
+ },
+ ".cm-panel.cm-search": { padding: "6px 32px 6px 10px" },
+ ".cm-search label": {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: "4px",
+ whiteSpace: "nowrap"
+ },
+ ".cm-search input[type=checkbox]": {
+ accentColor: theme.caretColor
+ },
+ ".cm-panel .cm-textfield, .cm-panel .cm-button": {
+ boxSizing: "border-box",
+ maxWidth: "100%",
+ minHeight: "28px",
+ border: `1px solid ${theme.line}`,
+ borderRadius: "4px",
+ font: "inherit",
+ color: theme.textColor,
+ backgroundImage: "none"
+ },
+ ".cm-panel .cm-textfield": {
+ backgroundColor: theme.background,
+ padding: "3px 6px"
+ },
+ ".cm-panel .cm-button": {
+ backgroundColor: theme.buttonBackground,
+ padding: "3px 8px",
+ cursor: "pointer"
+ },
+ ".cm-panel .cm-button:hover": {
+ backgroundColor: theme.buttonBackgroundHover
+ },
+ ".cm-panel input:focus-visible, .cm-panel button:focus-visible": {
+ outline: `1px solid ${theme.caretColor}`,
+ outlineOffset: "1px"
+ },
+ ".cm-panel.cm-search button[name=close]": {
+ top: "6px",
+ right: "4px",
+ width: "24px",
+ height: "28px",
+ color: theme.altTextColor,
+ fontSize: "18px",
+ cursor: "pointer"
+ },
+ ".cm-searchMatch": {
+ backgroundColor: alpha(theme.caretColor, 0.15),
+ outline: `1px solid ${alpha(theme.caretColor, 0.35)}`
+ },
+ ".cm-searchMatch-selected": {
+ backgroundColor: alpha(theme.caretColor, 0.3),
+ outline: `1px solid ${theme.caretColor}`
+ },
".cm-csound-synopsis:empty": { display: "none" },
// Keep the panel container in layout so CodeMirror can measure its
// scroll margins. display:none gives it a false top position of 0.