feat(themes): let app themes set the code editor selection color (#42349)

* feat(themes): let app themes set the code editor selection color

* docs(themes): keep the editor selection translucent; rewrap
This commit is contained in:
Neil
2026-11-14 15:53:30 -08:00
committed by GitHub
parent c2c7e52c95
commit e8a2b7ddda
8 changed files with 60 additions and 11 deletions
+1
View File
@@ -64,6 +64,7 @@ Every key an app theme may set in `tokens` (see [plugin themes](plugin-themes.md
| `--chat-selection` | Selected text in chat; may be translucent, it paints over the chat | no | `--text-link` (else Orca's link blue) mixed into the chat background, up to 30% light and 40% dark, as deep as keeps selected text 4.5:1 (never under 15%) |
| `--chat-selection-foreground` | Text inside a chat selection | no | same as `--foreground` |
| `--text-link` | Links in Markdown previews, editors and agent chat | no | Orca's built-in color |
| `--editor-selection` | Selected text in the code editor; keep it translucent so find matches show through. An unfocused editor shows it at half its opacity | no | `--ring` at 30% opacity |
| `--diff-added` | Added-line hue: Monaco overview ruler and chat diff text | no | same as `--git-decoration-added` |
| `--diff-added-background` | Added lines in diffs | no | light: `--diff-added` at 13% opacity; dark: `--diff-added` at 16% opacity |
| `--diff-added-emphasis` | Added characters within a changed line | no | light: `--diff-added` at 15% opacity; dark: `--diff-added` at 20% opacity |
+9 -7
View File
@@ -33,13 +33,14 @@ file of at most 16 KiB, or 64 KiB for an icon theme.
`--primary`/`--primary-foreground`) must be opaque with at least 4.5:1 contrast; a pair that
fails is dropped and keeps Orca's built-in colors. A pair you set only half of is checked
against what the other half falls back to. Some checks drop only your token: `--text-link`
needs 4.5:1 on `--editor-surface`, each `--git-decoration-*` 3:1 on `--sidebar`,
`--chat-bold-foreground` 4.5:1 on the chat background and on `--chat-user-message`,
`--chat-muted-foreground` 4.5:1 on the chat background and on code blocks, and
needs 4.5:1 on `--editor-surface`, `--editor-selection` 4.5:1 under `--foreground`, each
`--git-decoration-*` 3:1 on `--sidebar`, `--chat-bold-foreground` 4.5:1 on the chat
background and on `--chat-user-message`, `--chat-muted-foreground` 4.5:1 on the chat
background and on code blocks, and
`--worktree-sidebar-active`, `--chat-user-message`, `--chat-inline-code` (against the chat link
color), `--chat-code-block` and the diff line and character tints 4.5:1 with the text drawn on
them. Those surfaces, `--chat-inline-code`, `--chat-selection` and `--host-badge` may be
translucent; they are checked as painted over the surface beneath.
them. Those surfaces, `--chat-inline-code`, `--chat-selection`, `--editor-selection` and
`--host-badge` may be translucent; they are checked as painted over the surface beneath.
- An omitted surface token paints exactly what Orca paints without a theme. The code editor's
diff view and links keep Monaco's own colors unless the theme sets a diff token or
`--text-link`. Setting only `--diff-added` and `--diff-removed` is enough; the line, gutter and
@@ -57,8 +58,9 @@ An app theme also colors the code editor: its background, text, line numbers, cu
selection follow the theme's tokens. Users can also pick any app theme, of either base, as just
their editor theme (Settings → Appearance → Light/Dark editor theme); the editor then takes its
colors from `--editor-surface` (else `--background`), `--foreground`, `--muted-foreground`,
`--accent`, `--ring`, `--popover`, `--border` and `tokenColors`, while the rest of Orca keeps the
app theme. If those tokens can't make a legible editor, it uses Monaco's default colors. To
`--accent`, `--ring`, `--editor-selection`, `--popover`, `--border` and `tokenColors`, while the
rest of Orca keeps the app theme. Keep `--editor-selection` translucent so find matches and
highlighted words show through it; without it the selection is `--ring` at 30%. If those tokens can't make a legible editor, it uses Monaco's default colors. To
color code itself, add `tokenColors` (Orca 1.4.245 and later) in the same shape as a VS Code theme's, so most VS Code theme lists paste in unchanged (`validate:plugin` flags entries without a `scope` and any setting other than these two):
```json
@@ -119,6 +119,11 @@ const CONTRAST_RULE_DEFINITIONS: readonly ContrastRule[] = [
['--chat-inline-code', '--chat-user-message']
].map((under) => chatPair('--chat-selection', '--chat-selection-foreground', undefined, under)),
{ ...textPair('--editor-surface', '--text-link'), drop: ['--text-link'] },
{
...textPair('--editor-selection', '--foreground'),
drop: ['--editor-selection'],
over: '--editor-surface'
},
chatPair('--chat-user-message', '--chat-link', ['--chat-user-message']),
...GIT_DECORATIONS.map((status): ContrastRule => ({
surface: '--sidebar',
@@ -0,0 +1,29 @@
import { expect, it } from 'vitest'
import type { ColorProbe } from '../../../app-shell/appearance/plugin-theme-tokens'
import { derivePluginMonacoTheme } from './plugin-monaco-theme'
const probe: ColorProbe = (value) => {
const hex = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(value.trim())
if (!hex) {
return null
}
const channel = (offset: number): number => Number.parseInt(hex[1].slice(offset, offset + 2), 16)
return [channel(0), channel(2), channel(4), hex[2] ? Number.parseInt(hex[2], 16) / 255 : 1]
}
it('paints a theme-chosen editor selection instead of the ring', () => {
const tokens: Record<string, string> = {
'--background': '#261d45',
'--foreground': '#ffffff',
'--ring': '#00ff6a',
'--editor-selection': '#ff2a6d66'
}
const theme = derivePluginMonacoTheme({
base: 'dark',
read: (token) => tokens[token] ?? '',
painted: tokens,
probe
})
expect(theme?.data.colors['editor.selectionBackground']).toBe('#ff2a6d66')
expect(theme?.data.colors['editor.inactiveSelectionBackground']).toBe('#ff2a6d33')
})
@@ -84,6 +84,9 @@ export function derivePluginMonacoTheme(input: {
const muted = color('--muted-foreground') ?? foreground
const accent = color('--accent')
const ring = color('--ring') ?? foreground
const themed = input.painted['--editor-selection'] ? color('--editor-selection') : null
// Why the ring by default: a theme's accent is a faint hover tint, too weak to read as a selection.
const selection: Rgba = themed ?? [ring[0], ring[1], ring[2], 0.3]
const popover = color('--popover')
const border = color('--border')
const colors: Record<string, string> = {
@@ -94,9 +97,13 @@ export function derivePluginMonacoTheme(input: {
'editorLineNumber.foreground': hex(muted),
'editorLineNumber.activeForeground': hex(foreground),
'editorCursor.foreground': hex(ring),
// Why from the ring: a theme's accent is a faint hover tint, too weak to read as a selection.
'editor.selectionBackground': hex([ring[0], ring[1], ring[2], 0.3]),
'editor.inactiveSelectionBackground': hex([ring[0], ring[1], ring[2], 0.15])
'editor.selectionBackground': hex(selection),
'editor.inactiveSelectionBackground': hex([
selection[0],
selection[1],
selection[2],
selection[3] / 2
])
}
if (accent) {
colors['editor.lineHighlightBackground'] = hex(accent)
@@ -68,6 +68,7 @@ export const PLUGIN_APP_THEME_TOKENS = [
'--chat-selection',
'--chat-selection-foreground',
'--text-link',
'--editor-selection',
'--diff-added',
'--diff-added-background',
'--diff-added-emphasis',
@@ -74,7 +74,9 @@ const PLUGIN_APP_THEME_CSS_FALLBACKS: Record<
light: { tint: '--text-link', percent: 30, into: '--chat-canvas' },
dark: { tint: '--text-link', percent: 40, into: '--chat-canvas' }
},
'--chat-selection-foreground': ['--foreground']
'--chat-selection-foreground': ['--foreground'],
// Monaco paints this one, not main.css (plugin-monaco-theme.ts).
'--editor-selection': { tint: '--ring', percent: 30 }
}
export function pluginAppThemeCssFallback(
@@ -73,6 +73,8 @@ const TOKEN_ROLES: Record<PluginAppThemeToken, string> = {
'--chat-selection': 'Selected text in chat; may be translucent, it paints over the chat',
'--chat-selection-foreground': 'Text inside a chat selection',
'--text-link': 'Links in Markdown previews, editors and agent chat',
'--editor-selection':
'Selected text in the code editor; keep it translucent so find matches show through. An unfocused editor shows it at half its opacity',
'--diff-added': 'Added-line hue: Monaco overview ruler and chat diff text',
'--diff-added-background': 'Added lines in diffs',
'--diff-added-emphasis': 'Added characters within a changed line',