diff --git a/package.json b/package.json index ca4a98199f..109e298952 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-rust": "^6.0.2", "@codemirror/language": "^6.13.1", + "@codemirror/search": "^6.7.2", "@codemirror/state": "^6.7.6", "@codemirror/view": "^6.43.13", "@firefox-devtools/react-contextmenu": "^5.2.4", diff --git a/src/components/shared/AssemblyView-codemirror.tsx b/src/components/shared/AssemblyView-codemirror.tsx index f0efaa4ec9..4b6e408c47 100644 --- a/src/components/shared/AssemblyView-codemirror.tsx +++ b/src/components/shared/AssemblyView-codemirror.tsx @@ -43,6 +43,7 @@ import { updateTimingsEffect, StringMarker, createHighlightedLineExtension, + codeViewerExtension, } from 'firefox-profiler/utils/codemirror-shared'; // An "effect" is like a redux action. This effect is used to replace the value @@ -209,8 +210,7 @@ export class AssemblyViewEditor { instructionAddressGutter, highlightedLineConf.of(createHighlightedLineExtension(highlightedLine)), syntaxHighlighting(classHighlighter), - EditorState.readOnly.of(true), - EditorView.editable.of(false), + codeViewerExtension, ], }); const lineTimings = addressTimingsToLineTimings( diff --git a/src/components/shared/CodeView.css b/src/components/shared/CodeView.css index a87062fe88..8b9bd4484f 100644 --- a/src/components/shared/CodeView.css +++ b/src/components/shared/CodeView.css @@ -14,6 +14,18 @@ --codeview-linenum-foreground-color: ##aaa; --codeview-nonzero-background-color: #edf6ff; --codeview-highlight-background-color: #fffbcc; + --codeview-textfield-border-color: var(--grey-90-a20); + --codeview-textfield-hover-border-color: var(--grey-90-a30); + --codeview-search-match-background-color: color-mix( + in srgb, + var(--yellow-50) 40%, + transparent + ); + --codeview-search-selected-match-background-color: color-mix( + in srgb, + var(--orange-50) 50%, + transparent + ); display: flex; min-height: 0; @@ -50,6 +62,18 @@ --codeview-linenum-foreground-color: var(--grey-30); --codeview-nonzero-background-color: var(--grey-80); --codeview-highlight-background-color: var(--yellow-90); + --codeview-textfield-border-color: var(--grey-10-a20); + --codeview-textfield-hover-border-color: var(--grey-10-a40); + --codeview-search-match-background-color: color-mix( + in srgb, + var(--yellow-60) 35%, + transparent + ); + --codeview-search-selected-match-background-color: color-mix( + in srgb, + var(--orange-60) 60%, + transparent + ); --theme-highlight-keyword: var(--magenta-65); --theme-highlight-type: var(--purple-40); --theme-highlight-variable: var(--blue-40); @@ -174,6 +198,130 @@ word-spacing: normal; } +/** + * CodeMirror's search panel and search matches. CodeMirror's base theme gives + * the panel tiny fonts, cramped spacing and gradient buttons. Its colors are + * always the light ones, because we don't tell CodeMirror about our dark mode; + * we use our own color variables instead. The base theme's selectors are + * scoped by generated class names. The ".codeView .cm-editor" prefix makes + * these rules more specific than the base theme's. + */ +.codeView .cm-editor { + .cm-panels { + background-color: var(--codeview-header-background-color); + color: var(--codeview-foreground-color); + } + + .cm-panels-top { + border-bottom: 1px solid var(--codeview-header-border-color); + } + + .cm-textfield { + border: 1px solid var(--codeview-textfield-border-color); + border-radius: 2px; + background-color: var(--base-background-color); + color: var(--base-foreground-color); + font-size: inherit; + } + + .cm-textfield:hover { + border-color: var(--codeview-textfield-hover-border-color); + } + + /* The same focus ring as .photon-input, which CodeMirror's text field can't + use because CodeMirror creates the element. */ + .cm-textfield:focus-visible { + border-color: var(--focus-border-color); + box-shadow: + 0 0 0 1px var(--focus-border-color), + 0 0 0 4px var(--focus-shadow-color); + outline: 0; + } + + .cm-button { + /* No border, so that the focus ring sits on the button's edge. The extra + 1px of padding makes up for the text field's border. */ + padding: calc(0.2em + 1px) 1em; + border: none; + border-radius: 2px; + background-color: var(--clickable-background-color); + background-image: none; + color: var(--clickable-foreground-color); + font-size: inherit; + } + + .cm-button:hover { + background-color: var(--clickable-hover-background-color); + } + + /* CodeMirror's base theme has a gradient for pressed buttons, even when the + pointer has left the button. */ + .cm-button:active { + background-image: none; + } + + .cm-button:active:hover { + background-color: var(--clickable-active-background-color); + } + + .cm-searchMatch { + background-color: var(--codeview-search-match-background-color); + } + + .cm-searchMatch-selected { + background-color: var(--codeview-search-selected-match-background-color); + } + + .cm-panel.cm-search { + display: flex; + flex-wrap: wrap; + align-items: center; + padding: 4px; + font-size: 12px; + row-gap: 4px; + + input, + .cm-button, + label { + margin: 0 2px; + } + + input[type='checkbox'] { + margin-inline-end: 4px; + vertical-align: -2px; + } + + label { + font-size: inherit; + } + + /* CodeMirror positions the close button absolutely at the top. Put it at + the end of the row instead, so that it's centered with the other + controls. */ + [name='close'] { + position: static; + display: flex; + width: 20px; + height: 20px; + align-items: center; + justify-content: center; + border-radius: 2px; + margin-left: auto; + color: inherit; + font-size: 16px; + line-height: 1; + } + + [name='close']:hover { + background-color: var(--clickable-ghost-hover-background-color); + } + + [name='close']:active:hover { + background-color: var(--clickable-ghost-active-background-color); + } + } +} + /** * Syntax highlighting uses the Lezer's classHighlighter. From its documentation: * diff --git a/src/components/shared/SourceView-codemirror.ts b/src/components/shared/SourceView-codemirror.ts index ae88c8d4ff..2c4db953c0 100644 --- a/src/components/shared/SourceView-codemirror.ts +++ b/src/components/shared/SourceView-codemirror.ts @@ -32,6 +32,7 @@ import { timingsExtension, updateTimingsEffect, createHighlightedLineExtension, + codeViewerExtension, } from 'firefox-profiler/utils/codemirror-shared'; // This "compartment" allows us to swap the syntax highlighting language when @@ -84,15 +85,6 @@ function _languageExtForPath(path: string | null): LanguageSupport | [] { return javascript(); } -// Adjustments to make a CodeMirror editor work as a non-editable code viewer. -const codeViewerExtension = [ - EditorState.readOnly.of(true), - EditorView.editable.of(false), - // Allow tabbing to the view (to an element *inside* the scroller so that the - // up / down keys trigger scrolling), and take focus on mousedown. - EditorView.contentAttributes.of({ tabindex: '0' }), -]; - // Creates a lineNumbers extension that displays line numbers offset by startLine - 1. function _lineNumbersForStartLine(startLine: number) { if (startLine <= 1) { diff --git a/src/test/components/__snapshots__/BottomBox.test.tsx.snap b/src/test/components/__snapshots__/BottomBox.test.tsx.snap index d48ed6cf9b..c71ad6aad9 100644 --- a/src/test/components/__snapshots__/BottomBox.test.tsx.snap +++ b/src/test/components/__snapshots__/BottomBox.test.tsx.snap @@ -11,6 +11,7 @@ exports[`BottomBox should show the assembly view when pressing the toggle button role="textbox" spellcheck="false" style="tab-size: 4;" + tabindex="0" translate="no" writingsuggestions="false" > diff --git a/src/utils/codemirror-shared.ts b/src/utils/codemirror-shared.ts index b19b24b15a..083d292146 100644 --- a/src/utils/codemirror-shared.ts +++ b/src/utils/codemirror-shared.ts @@ -8,9 +8,15 @@ import { GutterMarker, gutter, gutterLineClass, + keymap, } from '@codemirror/view'; -import type { EditorState } from '@codemirror/state'; -import { StateField, StateEffect, RangeSet } from '@codemirror/state'; +import { + EditorState, + StateField, + StateEffect, + RangeSet, +} from '@codemirror/state'; +import { search, searchKeymap } from '@codemirror/search'; import type { LineTimings } from 'firefox-profiler/types'; @@ -193,4 +199,20 @@ export const timingsExtension = [ nonZeroLineDecorationHighlighter, ]; +// Adjustments to make a CodeMirror editor work as a non-editable code viewer. +export const codeViewerExtension = [ + EditorState.readOnly.of(true), + EditorView.editable.of(false), + // Allow tabbing to the view (to an element *inside* the scroller so that the + // up / down keys trigger scrolling), and take focus on mousedown. + EditorView.contentAttributes.of({ tabindex: '0' }), + // CodeMirror only renders the lines in and near the viewport, so the + // browser's find-in-page can't find text outside of it. CodeMirror's own + // search panel searches the whole document. It opens with Ctrl/Cmd+F while + // the editor has focus. The replace controls are hidden because the editor + // is read-only. + search({ top: true }), + keymap.of(searchKeymap), +]; + export { createHighlightedLineExtension }; diff --git a/yarn.lock b/yarn.lock index fbe5a24e53..b240cac0ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1866,6 +1866,15 @@ "@codemirror/view" "^6.0.0" crelt "^1.0.5" +"@codemirror/search@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.7.2.tgz#93a49eb21c026de473c4fc3a047b8fd67784b0f0" + integrity sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.37.0" + crelt "^1.0.5" + "@codemirror/state@^6.0.0", "@codemirror/state@^6.7.0", "@codemirror/state@^6.7.6": version "6.7.6" resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.7.6.tgz#48af761fae3947745f17fa143cc5b5084225388d" @@ -1878,10 +1887,10 @@ resolved "https://registry.yarnpkg.com/@codemirror/streamparser/-/streamparser-6.0.0.tgz#cc48a8f67e9001222d69938fabab157d68ce59d6" integrity sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w== -"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.43.13": - version "6.43.13" - resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.43.13.tgz#a2d0286bf9e6051cb89076fd1c8dd58b907008d0" - integrity sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A== +"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.37.0", "@codemirror/view@^6.43.13": + version "6.43.15" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.43.15.tgz#3da2515b3919e199b68520a5b6e559c2a4cfbe9e" + integrity sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw== dependencies: "@codemirror/state" "^6.7.0" crelt "^1.0.6"