From 5e2a1c344cbd6906e37db29956f2793db766ab25 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 1 Sep 2026 21:29:33 +0200 Subject: [PATCH 1/2] Render hats and improve selection styling --- .../app-web-docs/src/docs/components/Code.css | 30 +++++- .../components/RecordedTestVisualizer.tsx | 102 +++++++++--------- .../docs/modifiers/inferPreviousMark.yml | 4 +- .../docs/modifiers/inferPreviousMark2.yml | 4 +- .../docs/modifiers/inferPreviousMark3.yml | 4 +- 5 files changed, 87 insertions(+), 57 deletions(-) diff --git a/packages/app-web-docs/src/docs/components/Code.css b/packages/app-web-docs/src/docs/components/Code.css index 9d85b134ef..1dc13f277d 100644 --- a/packages/app-web-docs/src/docs/components/Code.css +++ b/packages/app-web-docs/src/docs/components/Code.css @@ -72,6 +72,13 @@ } } +/* Code selection */ +.code-selection { + background-color: #264f78; +} + +/* Code cursor */ + .code-cursor-before, .code-cursor-after { position: relative; @@ -91,12 +98,12 @@ .code-cursor-before::before { left: -1px; - border-left: 2px solid white; + border-left: 2px solid #aeafad; } .code-cursor-after::after { right: -1px; - border-right: 2px solid white; + border-right: 2px solid #aeafad; } @keyframes code-cursor-blink { @@ -111,3 +118,22 @@ animation: none; } } + +/* Code hat */ + +.code-hat { + position: relative; +} + +.code-hat-default::before { + content: ""; + position: absolute; + top: 0.15em; + left: 50%; + width: 0.3em; + height: 0.3em; + border-radius: 50%; + background-color: #b9b6cd; + transform: translate(-50%, -100%); + pointer-events: none; +} diff --git a/packages/app-web-docs/src/docs/components/RecordedTestVisualizer.tsx b/packages/app-web-docs/src/docs/components/RecordedTestVisualizer.tsx index 870e91d302..cf582cabe2 100644 --- a/packages/app-web-docs/src/docs/components/RecordedTestVisualizer.tsx +++ b/packages/app-web-docs/src/docs/components/RecordedTestVisualizer.tsx @@ -4,12 +4,11 @@ import { createContext, useContext, useMemo, useState } from "react"; import type { DecorationItem } from "shiki"; import type { SelectionPlainObject, + SerializedMarks, TestCaseSnapshot, } from "@cursorless/lib-common"; -import { BorderStyle, plainObjectToSelection } from "@cursorless/lib-common"; +import { plainObjectToSelection, splitKey } from "@cursorless/lib-common"; import { Code } from "./Code"; -import { highlightColors } from "./highlightColors"; -import { highlightToDecoration } from "./highlightsToDecorations"; import type { RecordedTest } from "./types"; interface RecordedTestVisualizerContextValue { @@ -72,11 +71,13 @@ export function RecordedTestVisualizerOptions() { ); } -interface ModifierProps { +interface RecordedTestVisualizerProps { fixtureName: string; } -export function RecordedTestVisualizer({ fixtureName }: ModifierProps) { +export function RecordedTestVisualizer({ + fixtureName, +}: RecordedTestVisualizerProps): JSX.Element { const { fixtures, renderWhitespace } = useRecordedTestVisualizer(); const test = fixtures.get(fixtureName); @@ -121,12 +122,7 @@ export function RecordedTestVisualizer({ fixtureName }: ModifierProps) { ); } -function CodeState({ - renderWhitespace, - languageId, - link, - state, -}: { +interface CodeStateProps { renderWhitespace: boolean; languageId: string; link: { @@ -134,14 +130,27 @@ function CodeState({ url: string; }; state: TestCaseSnapshot; -}): JSX.Element { +} + +function CodeState({ + renderWhitespace, + languageId, + link, + state, +}: CodeStateProps): JSX.Element { + const decorations = useMemo( + () => [ + ...state.selections.map(toDecoration), + ...toMarkDecorations(state.marks), + ], + [state.selections, state.marks], + ); return ( {state.documentContents} @@ -151,50 +160,45 @@ function CodeState({ function toDecoration(plainSelection: SelectionPlainObject): DecorationItem { const selection = plainObjectToSelection(plainSelection); - if (selection.isEmpty) { - return { - start: selection.start, - end: selection.start, - properties: { - className: ["code-cursor-before"], - }, - }; - } - - const decoration = highlightToDecoration({ - range: selection, - style: { - backgroundColor: highlightColors.content.background, - borderColorSolid: highlightColors.content.borderSolid, - borderColorPorous: highlightColors.content.borderPorous, - borderStyle: { - top: BorderStyle.solid, - bottom: BorderStyle.solid, - left: BorderStyle.solid, - right: BorderStyle.solid, - }, - borderRadius: { - topLeft: true, - topRight: true, - bottomRight: true, - bottomLeft: true, - }, - }, - }); - - const className = selection.isReversed + const cursorClassName = selection.isReversed ? "code-cursor-before" : "code-cursor-after"; + const classNames = selection.isEmpty + ? [cursorClassName] + : [cursorClassName, "code-selection"]; + return { - ...decoration, + start: selection.start, + end: selection.end, + alwaysWrap: true, properties: { - ...decoration.properties, - className: [className], + className: classNames, }, }; } +function toMarkDecorations( + marks: SerializedMarks | undefined, +): DecorationItem[] { + if (marks == null) { + return []; + } + + return Object.entries(marks).map(([key, range]) => { + const { hatStyle } = splitKey(key); + + return { + start: range.start, + end: range.end, + alwaysWrap: true, + properties: { + className: ["code-hat", `code-hat-${hatStyle}`], + }, + }; + }); +} + function useRecordedTestVisualizer(): RecordedTestVisualizerContextValue { const value = useContext(RecordedTestVisualizerContext); if (value == null) { diff --git a/resources/fixtures/recorded/docs/modifiers/inferPreviousMark.yml b/resources/fixtures/recorded/docs/modifiers/inferPreviousMark.yml index f9a0c2dd60..3f57f4530e 100644 --- a/resources/fixtures/recorded/docs/modifiers/inferPreviousMark.yml +++ b/resources/fixtures/recorded/docs/modifiers/inferPreviousMark.yml @@ -21,7 +21,7 @@ command: usePrePhraseSnapshot: false initialState: documentContents: |- - // Hello world + const aaa = 0; selections: - anchor: {line: 0, character: 0} @@ -32,7 +32,7 @@ initialState: end: {line: 1, character: 9} finalState: documentContents: |- - // Hello world + const aaa = 0; selections: - anchor: {line: 1, character: 6} diff --git a/resources/fixtures/recorded/docs/modifiers/inferPreviousMark2.yml b/resources/fixtures/recorded/docs/modifiers/inferPreviousMark2.yml index 1f52251395..c1d6977daa 100644 --- a/resources/fixtures/recorded/docs/modifiers/inferPreviousMark2.yml +++ b/resources/fixtures/recorded/docs/modifiers/inferPreviousMark2.yml @@ -19,7 +19,7 @@ command: usePrePhraseSnapshot: false initialState: documentContents: |- - // Hello world + const aaa = 0; selections: - anchor: {line: 0, character: 0} @@ -30,7 +30,7 @@ initialState: end: {line: 1, character: 9} finalState: documentContents: |- - // Hello world + aaa selections: - anchor: {line: 0, character: 0} diff --git a/resources/fixtures/recorded/docs/modifiers/inferPreviousMark3.yml b/resources/fixtures/recorded/docs/modifiers/inferPreviousMark3.yml index a115509666..9036c50b44 100644 --- a/resources/fixtures/recorded/docs/modifiers/inferPreviousMark3.yml +++ b/resources/fixtures/recorded/docs/modifiers/inferPreviousMark3.yml @@ -19,7 +19,7 @@ command: usePrePhraseSnapshot: false initialState: documentContents: |- - // Hello world + const aaa = 0; selections: - anchor: {line: 0, character: 0} @@ -30,7 +30,7 @@ initialState: end: {line: 1, character: 9} finalState: documentContents: |- - // Hello world + const aaa = aaa; selections: - anchor: {line: 0, character: 0} From 17fa27c56037688dd36bd19b5c0279f4cb1216c3 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 1 Sep 2026 21:32:12 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76fdea072a..1cb2e2a313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2026 -- 2026-09-01: Visualize modifiers on the documentation page. ([#3328](https://github.com/cursorless-dev/cursorless/pull/3328)) +- 2026-09-01: Visualize modifiers on the documentation page. ([#3328](https://github.com/cursorless-dev/cursorless/pull/3328); [#3329](https://github.com/cursorless-dev/cursorless/pull/3329)) - 2026-08-30: Overhauled the user documentation with generated and grouped action, modifier, and scope reference pages, richer descriptions and examples, dedicated paired-delimiter and target guides, and a streamlined landing page. ([#3303](https://github.com/cursorless-dev/cursorless/pull/3303); [#3304](https://github.com/cursorless-dev/cursorless/pull/3304); [#3305](https://github.com/cursorless-dev/cursorless/pull/3305); [#3306](https://github.com/cursorless-dev/cursorless/pull/3306); [#3307](https://github.com/cursorless-dev/cursorless/pull/3307); [#3308](https://github.com/cursorless-dev/cursorless/pull/3308); [#3309](https://github.com/cursorless-dev/cursorless/pull/3309); [#3310](https://github.com/cursorless-dev/cursorless/pull/3310); [#3311](https://github.com/cursorless-dev/cursorless/pull/3311); [#3312](https://github.com/cursorless-dev/cursorless/pull/3312); [#3313](https://github.com/cursorless-dev/cursorless/pull/3313); [#3314](https://github.com/cursorless-dev/cursorless/pull/3314); [#3315](https://github.com/cursorless-dev/cursorless/pull/3315); [#3316](https://github.com/cursorless-dev/cursorless/pull/3316)) - 2026-05-03: Add support for if and for statements in Talon-script. [`#3274`](https://github.com/cursorless-dev/cursorless/pull/3274) - 2026-03-12: Added `userColor3` and `userColor4` as additional user-configurable hat colors. They are disabled by default until you enable and name them. ([#3206](https://github.com/cursorless-dev/cursorless/pull/3206))