mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
feat(editor): Rainbow brackets and semantic highlighting (rainbow variables) (#59)
Two editor toggles under Settings › Editor › Color Scheme › Language Defaults (off by default, also actions `editor.toggleRainbowBrackets` / `editor.toggleSemanticHighlighting`):
- Semantic highlighting (IDEA's rainbow variables): locals and parameters coloured by a name hash from LSP semantic tokens; fields, statics, constants, library symbols untouched.
- Rainbow brackets (VS Code's bracket pair colorization): (), [], {} by depth, unmatched in the error colour, strings/comments skipped, visible ranges only with 4 KB checkpoints.
Palettes are theme tokens (`--syn-rainbow-1..5`, `--syn-bracket-1..3`, light/dark/high-contrast), editable in the Color Scheme tree, and mapped from IntelliJ `.icls` RAINBOW_COLOR1..5.
App verification on macOS found the colours were overridden by the syntax highlighter's span nested inside the marks; the rules now cover descendants. Verified both toggles on/off live in a TS file.
Fixes #56
https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
This commit is contained in:
1 parent
2e0c74e31d
commit
beab7992f9
17 files changed
+679
-9
No files matched your search
@@ -289,6 +289,11 @@ pub struct EditorSettings {
|
||||
pub color_scheme: String,
|
||||
/// Color Scheme edits: scheme id → attribute id → override.
|
||||
pub color_overrides: BTreeMap<String, BTreeMap<String, TextAttributesOverride>>,
|
||||
/// Color Scheme › Language Defaults › Semantic highlighting: local variables and
|
||||
/// parameters get a colour derived from their name (IDEA's rainbow identifiers).
|
||||
pub semantic_highlighting: bool,
|
||||
/// Color Scheme › Language Defaults › Rainbow brackets: `()`, `[]`, `{}` coloured by depth.
|
||||
pub rainbow_brackets: bool,
|
||||
pub ligatures: bool,
|
||||
/// Fallback font for glyphs the main font lacks; empty = none.
|
||||
pub fallback_font: String,
|
||||
@@ -329,6 +334,8 @@ impl Default for EditorSettings {
|
||||
format_on_save: false,
|
||||
color_scheme: "theme-default".into(),
|
||||
color_overrides: BTreeMap::new(),
|
||||
semantic_highlighting: false,
|
||||
rainbow_brackets: false,
|
||||
ligatures: false,
|
||||
fallback_font: String::new(),
|
||||
font_weight: 400,
|
||||
|
||||
+11
-1
@@ -195,6 +195,7 @@ The full table is `UI_KEYS` in [`scripts/theme/intellij.ts`](../../scripts/theme
|
||||
| `--syn-annotation`, `--syn-decorator` | `DEFAULT_METADATA` |
|
||||
| `--syn-punct`, `--syn-escape` | `DEFAULT_OPERATION_SIGN`, `DEFAULT_VALID_STRING_ESCAPE` |
|
||||
| `--syn-tag`, `--syn-attribute`, `--syn-todo`, `--syn-invalid` | `DEFAULT_TAG`, `DEFAULT_ATTRIBUTE`, `TODO_DEFAULT_ATTRIBUTES`, `BAD_CHARACTER` |
|
||||
| `--syn-rainbow-1` … `--syn-rainbow-5` (Semantic highlighting) | `RAINBOW_COLOR1` … `RAINBOW_COLOR5` |
|
||||
| `--wave-error`, `--wave-warning` | `ERRORS_ATTRIBUTES`, `WARNING_ATTRIBUTES` (effect colour) |
|
||||
| `--bg-matched-brace`, `--bg-identifier-under-caret` | `MATCHED_BRACE_ATTRIBUTES`, `IDENTIFIER_UNDER_CARET_ATTRIBUTES` |
|
||||
| `--bg-text-search-result`, `--bg-search-match` | `TEXT_SEARCH_RESULT_ATTRIBUTES`, `SEARCH_RESULT_ATTRIBUTES` |
|
||||
@@ -238,6 +239,7 @@ from `type`, then map:
|
||||
| `--fg-whitespace` | `editorWhitespace.foreground` |
|
||||
| `--bg-text-search-result`, `--bg-text-search-result-current` | `editor.findMatchHighlightBackground`, `editor.findMatchBackground` |
|
||||
| `--bg-matched-brace` | `editorBracketMatch.background` |
|
||||
| `--syn-bracket-1` … `--syn-bracket-3` (Rainbow brackets) | `editorBracketHighlight.foreground1` … `foreground3` |
|
||||
| `--bg-identifier-under-caret`, `-write` | `editor.wordHighlightBackground`, `editor.wordHighlightStrongBackground` |
|
||||
| `--wave-error`, `--wave-warning` | `editorError.foreground`, `editorWarning.foreground` |
|
||||
| `--gutter-modified`, `--gutter-added`, `--gutter-deleted` | `editorGutter.modifiedBackground`, `.addedBackground`, `.deletedBackground` |
|
||||
@@ -292,7 +294,7 @@ a token.
|
||||
|
||||
<!-- BEGIN GENERATED TOKEN REFERENCE: pnpm gen:theme-docs -->
|
||||
|
||||
194 themeable tokens: 112 in the UI theme file, 82 in the editor colour scheme file.
|
||||
202 themeable tokens: 112 in the UI theme file, 90 in the editor colour scheme file.
|
||||
|
||||
### Surfaces (UI theme)
|
||||
|
||||
@@ -401,6 +403,14 @@ a token.
|
||||
| `--syn-markup-code` | `#6aab73` | `#067d17` |
|
||||
| `--syn-invalid` | `#f75464` | `#f50000` |
|
||||
| `--syn-inline-value` | `#868a91` | `#8c8c8c` |
|
||||
| `--syn-rainbow-1` | `#6fafbd` | `#9b3b6a` |
|
||||
| `--syn-rainbow-2` | `#d79b6e` | `#114d77` |
|
||||
| `--syn-rainbow-3` | `#9fbf6b` | `#9c6a2c` |
|
||||
| `--syn-rainbow-4` | `#c98bc6` | `#005910` |
|
||||
| `--syn-rainbow-5` | `#8e9cf0` | `#bc5150` |
|
||||
| `--syn-bracket-1` | `#e8c25b` | `#0431fa` |
|
||||
| `--syn-bracket-2` | `#d78bd6` | `#319331` |
|
||||
| `--syn-bracket-3` | `#4ea7f0` | `#7b3814` |
|
||||
|
||||
### Editor (editor scheme)
|
||||
|
||||
|
||||
@@ -135,6 +135,11 @@ export const ICLS_ATTRIBUTES: Record<string, Array<[Part, string]>> = {
|
||||
DEFAULT_ATTRIBUTE: [["FOREGROUND", "--syn-attribute"]],
|
||||
TODO_DEFAULT_ATTRIBUTES: [["FOREGROUND", "--syn-todo"]],
|
||||
BAD_CHARACTER: [["FOREGROUND", "--syn-invalid"]],
|
||||
RAINBOW_COLOR1: [["FOREGROUND", "--syn-rainbow-1"]],
|
||||
RAINBOW_COLOR2: [["FOREGROUND", "--syn-rainbow-2"]],
|
||||
RAINBOW_COLOR3: [["FOREGROUND", "--syn-rainbow-3"]],
|
||||
RAINBOW_COLOR4: [["FOREGROUND", "--syn-rainbow-4"]],
|
||||
RAINBOW_COLOR5: [["FOREGROUND", "--syn-rainbow-5"]],
|
||||
ERRORS_ATTRIBUTES: [["EFFECT_COLOR", "--wave-error"]],
|
||||
WARNING_ATTRIBUTES: [["EFFECT_COLOR", "--wave-warning"]],
|
||||
NOT_USED_ELEMENT_ATTRIBUTES: [["FOREGROUND", "--fg-unused"]],
|
||||
|
||||
@@ -2016,6 +2016,13 @@ export type EditorSettings_Deserialize = {
|
||||
colorScheme?: string,
|
||||
/** Color Scheme edits: scheme id → attribute id → override. */
|
||||
colorOverrides?: { [key in string]: { [key in string]: TextAttributesOverride_Deserialize } },
|
||||
/**
|
||||
* Color Scheme › Language Defaults › Semantic highlighting: local variables and
|
||||
* parameters get a colour derived from their name (IDEA's rainbow identifiers).
|
||||
*/
|
||||
semanticHighlighting?: boolean,
|
||||
/** Color Scheme › Language Defaults › Rainbow brackets: `()`, `[]`, `{}` coloured by depth. */
|
||||
rainbowBrackets?: boolean,
|
||||
ligatures?: boolean,
|
||||
/** Fallback font for glyphs the main font lacks; empty = none. */
|
||||
fallbackFont?: string,
|
||||
@@ -2056,6 +2063,13 @@ export type EditorSettings_Serialize = {
|
||||
colorScheme: string,
|
||||
/** Color Scheme edits: scheme id → attribute id → override. */
|
||||
colorOverrides: { [key in string]: { [key in string]: TextAttributesOverride_Serialize } },
|
||||
/**
|
||||
* Color Scheme › Language Defaults › Semantic highlighting: local variables and
|
||||
* parameters get a colour derived from their name (IDEA's rainbow identifiers).
|
||||
*/
|
||||
semanticHighlighting: boolean,
|
||||
/** Color Scheme › Language Defaults › Rainbow brackets: `()`, `[]`, `{}` coloured by depth. */
|
||||
rainbowBrackets: boolean,
|
||||
ligatures: boolean,
|
||||
/** Fallback font for glyphs the main font lacks; empty = none. */
|
||||
fallbackFont: string,
|
||||
|
||||
@@ -40,6 +40,9 @@ import { useDocMeta, useOutline } from "./model/document";
|
||||
import { fontZoom, setFontZoom } from "./cm/prefs";
|
||||
import { useEditorRegistry } from "@/editor/api";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { editorPrefs } from "@/platform/editorSettings";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { toggleReadOnly } from "./fileAttributes";
|
||||
import { openNavBarPopup } from "./view/NavBarPopup";
|
||||
import {
|
||||
@@ -298,6 +301,26 @@ function addNextOccurrence(view: EditorView) {
|
||||
if (nextOccurrence(view) === "notFound") void executeAction("editor.showHint", { text: "Not found, press again to search from the top" });
|
||||
}
|
||||
|
||||
/** Settings › Editor › Color Scheme switches that are also toggle actions. */
|
||||
type SchemeSwitch = "semanticHighlighting" | "rainbowBrackets";
|
||||
|
||||
export async function toggleSchemeSwitch(key: SchemeSwitch, on = !editorPrefs()[key]): Promise<void> {
|
||||
try {
|
||||
useProject.setState({ settings: await call(commands.settingsSet("global", `editor.${key}`, JSON.stringify(on))) });
|
||||
} catch (e) {
|
||||
notifications().notify({ level: "error", title: "Cannot change the setting", body: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
const schemeSwitch = (id: string, title: string, key: SchemeSwitch, keywords: string[]): Action => ({
|
||||
id,
|
||||
title,
|
||||
category: "Editor",
|
||||
keywords,
|
||||
toggled: () => editorPrefs()[key],
|
||||
run: () => toggleSchemeSwitch(key),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
export function registerEditorActions(): () => void {
|
||||
@@ -316,6 +339,16 @@ export function registerEditorActions(): () => void {
|
||||
return pane?.active ? { paneId: pane.id, tabId: pane.active } : null;
|
||||
};
|
||||
return registerActions([
|
||||
schemeSwitch("editor.toggleSemanticHighlighting", "Semantic Highlighting", "semanticHighlighting", [
|
||||
"rainbow variables",
|
||||
"rainbow identifiers",
|
||||
"color scheme",
|
||||
]),
|
||||
schemeSwitch("editor.toggleRainbowBrackets", "Rainbow Brackets", "rainbowBrackets", [
|
||||
"bracket pair colorization",
|
||||
"rainbow parentheses",
|
||||
"color scheme",
|
||||
]),
|
||||
text("code.commentLine", "Comment with Line Comment", (v) => commentLine(v)),
|
||||
text("code.commentBlock", "Comment with Block Comment", (v) => toggleBlockComment(v)),
|
||||
text("code.duplicateLine", "Duplicate Line or Selection", (v) => duplicateLineOrSelection(v)),
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { EditorState, type Extension } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { ensureSyntaxTree } from "@codemirror/language";
|
||||
import { languages } from "@codemirror/language-data";
|
||||
import { BracketScanner, bracketClass } from "./rainbowBrackets";
|
||||
import { heavyExtension } from "./setup";
|
||||
import type { EditorSettings } from "@/platform/ipc";
|
||||
|
||||
const cStyle = EditorState.languageData.of(() => [
|
||||
{ commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
|
||||
]);
|
||||
|
||||
/** `char@depth` of every coloured bracket (`!` for unmatched). */
|
||||
function marks(doc: string, ext: Extension = [], from = 0, to = doc.length, scanner = new BracketScanner()) {
|
||||
const state = EditorState.create({ doc, extensions: ext });
|
||||
ensureSyntaxTree(state, doc.length, 5000);
|
||||
return scanner.marks(state, from, to).map((m) => `${doc[m.from]}${m.depth < 0 ? "!" : m.depth}`);
|
||||
}
|
||||
|
||||
const javascript = () => languages.find((l) => l.name === "JavaScript")!.load();
|
||||
|
||||
describe("rainbow brackets", () => {
|
||||
it("colours brackets by nesting depth, cycling through the palette", () => {
|
||||
expect(marks("f(a[b{c}d]e)")).toEqual(["(0", "[1", "{2", "}2", "]1", ")0"]);
|
||||
expect([0, 1, 2, 3, 4].map(bracketClass)).toEqual([
|
||||
"cm-rainbow-bracket-1",
|
||||
"cm-rainbow-bracket-2",
|
||||
"cm-rainbow-bracket-3",
|
||||
"cm-rainbow-bracket-1",
|
||||
"cm-rainbow-bracket-2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks unmatched and mismatched brackets", () => {
|
||||
expect(marks("a)b")).toEqual([")!"]);
|
||||
expect(marks("(]")).toEqual(["(!", "]!"]);
|
||||
expect(marks("x(y")).toEqual(["(!"]);
|
||||
expect(marks("((a)")).toEqual(["(!", "(1", ")1"]);
|
||||
expect(bracketClass(-1)).toBe("cm-rainbow-bracket-unmatched");
|
||||
});
|
||||
|
||||
it("skips strings and comments lexically when there is no syntax tree", () => {
|
||||
const doc = "f(\"(\", '[', `{\n)`) // (\n/* ] */ g()";
|
||||
expect(marks(doc, cStyle)).toEqual(["(0", ")0", "(0", ")0"]);
|
||||
// Primes and lifetimes are not quotes.
|
||||
expect(marks("f x' = (x', y)", cStyle)).toEqual(["(0", ")0"]);
|
||||
});
|
||||
|
||||
it("skips strings, comments and regexps with the syntax tree", async () => {
|
||||
const doc = 'f("(", /[(]/, `{`) // )\n/* ] */ g([1, {a: 2}])';
|
||||
expect(marks(doc, await javascript())).toEqual(["(0", ")0", "(0", "[1", "{2", "}2", "]1", ")0"]);
|
||||
});
|
||||
|
||||
it("knows the depth at a viewport far into the document and after edits", () => {
|
||||
const body = "x(y)\n".repeat(5000);
|
||||
const doc = `{\n${body}[z]\n}`;
|
||||
const scanner = new BracketScanner();
|
||||
const at = doc.indexOf("[z]");
|
||||
expect(marks(doc, cStyle, at, doc.length, scanner)).toEqual(["[1", "]1", "}0"]);
|
||||
// Checkpoints make the rescan start near the viewport; an edit before it invalidates them.
|
||||
let state = EditorState.create({ doc, extensions: cStyle });
|
||||
const tr = state.update({ changes: { from: 100, insert: "(" } });
|
||||
scanner.invalidate(100);
|
||||
state = tr.state;
|
||||
const from = at + 1;
|
||||
expect(
|
||||
scanner
|
||||
.marks(state, from, state.doc.length)
|
||||
.map((m) => `${state.doc.sliceString(m.from, m.from + 1)}${m.depth < 0 ? "!" : m.depth}`),
|
||||
).toEqual(["[2", "]2", "}!"]);
|
||||
});
|
||||
|
||||
it("is installed by the Rainbow brackets setting", () => {
|
||||
const render = (rainbowBrackets: boolean) => {
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: "f(a[b])",
|
||||
extensions: [cStyle, heavyExtension(7, { rainbowBrackets } as unknown as EditorSettings)],
|
||||
}),
|
||||
parent: document.body,
|
||||
});
|
||||
const classes = [...view.contentDOM.querySelectorAll("[class*=cm-rainbow-bracket]")].map((e) => e.className);
|
||||
view.destroy();
|
||||
return classes;
|
||||
};
|
||||
expect(render(false)).toEqual([]);
|
||||
expect(render(true)).toEqual([
|
||||
"cm-rainbow-bracket-1",
|
||||
"cm-rainbow-bracket-2",
|
||||
"cm-rainbow-bracket-2",
|
||||
"cm-rainbow-bracket-1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Rainbow brackets (VS Code's bracket pair colorization): `()`, `[]`, `{}`
|
||||
* coloured by nesting depth (`--syn-bracket-<n>`, cycling), unmatched ones in
|
||||
* the error colour. Strings and comments are skipped: with the syntax tree
|
||||
* when the language has one, else lexically with the language's comment
|
||||
* tokens and quotes.
|
||||
*
|
||||
* Only the viewport is decorated. The nesting state at its start comes from
|
||||
* checkpoints saved every few KB while scanning, so scrolling and typing
|
||||
* rescan from the nearest checkpoint before the change, not from the top.
|
||||
*/
|
||||
import { Prec, type EditorState, type Extension, type Text } from "@codemirror/state";
|
||||
import { Decoration, ViewPlugin, type DecorationSet, type EditorView, type ViewUpdate } from "@codemirror/view";
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
|
||||
type Tree = ReturnType<typeof syntaxTree>;
|
||||
type NodeType = Tree["type"];
|
||||
|
||||
/** Number of `--syn-bracket-<n>` colours the depth cycles through. */
|
||||
export const BRACKET_COLORS = 3;
|
||||
|
||||
const CHUNK = 4096;
|
||||
/** How far past the viewport to look for the closers of visible openers. */
|
||||
const LOOKAHEAD = 64_000;
|
||||
/** Longest comment / quote token the lexical scanner needs to see past a chunk end. */
|
||||
const TOKEN_SLACK = 8;
|
||||
|
||||
/** Closer char code of each opener char code: `(` `[` `{`. */
|
||||
const CLOSER: Record<number, number> = { 40: 41, 91: 93, 123: 125 };
|
||||
|
||||
/** An open bracket; the stack is a persistent list so checkpoints can share it. */
|
||||
interface Opener {
|
||||
closer: number;
|
||||
pos: number;
|
||||
depth: number;
|
||||
prev: Opener | null;
|
||||
}
|
||||
|
||||
/** Lexical mode: code, line comment, block comment, string (quote in `quote`). */
|
||||
type Mode = 0 | 1 | 2 | 3;
|
||||
|
||||
interface ScanState {
|
||||
pos: number;
|
||||
stack: Opener | null;
|
||||
mode: Mode;
|
||||
quote: string;
|
||||
}
|
||||
|
||||
export interface BracketMark {
|
||||
from: number;
|
||||
/** 0-based depth, or -1 for an unmatched bracket. */
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface CommentTokens {
|
||||
line?: string;
|
||||
block?: { open: string; close: string };
|
||||
}
|
||||
|
||||
const SKIP_NODE = /string|comment|char|rune|regexp|textblock|docstring|heredoc/i;
|
||||
const skipCache = new WeakMap<NodeType, boolean>();
|
||||
function skipsNode(type: NodeType): boolean {
|
||||
let v = skipCache.get(type);
|
||||
if (v === undefined) skipCache.set(type, (v = !type.isTop && SKIP_NODE.test(type.name)));
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bracket scanner of one editor: keeps the checkpoints between rebuilds and
|
||||
* drops the ones after an edit (`invalidate`).
|
||||
*/
|
||||
export class BracketScanner {
|
||||
private checkpoints: ScanState[] = [];
|
||||
private tree: Tree | null = null;
|
||||
|
||||
/** Forget checkpoints after `pos` (first changed position of the old document). */
|
||||
invalidate(pos: number) {
|
||||
const keep = pos - TOKEN_SLACK;
|
||||
let n = this.checkpoints.length;
|
||||
while (n > 0 && this.checkpoints[n - 1]!.pos > keep) n--;
|
||||
this.checkpoints.length = n;
|
||||
}
|
||||
|
||||
/** Brackets in `[from, to)` with their depth; unmatched ones get depth -1. */
|
||||
marks(state: EditorState, from: number, to: number): BracketMark[] {
|
||||
const tree = treeOf(state);
|
||||
if (tree !== this.tree) {
|
||||
// A grown tree may reinterpret what was past its old end.
|
||||
if (this.tree && tree) this.invalidate(Math.min(this.tree.length, tree.length));
|
||||
else this.checkpoints.length = 0;
|
||||
this.tree = tree;
|
||||
}
|
||||
const doc = state.doc;
|
||||
to = Math.min(to, doc.length);
|
||||
const limit = tree ? Math.min(doc.length, tree.length) : doc.length;
|
||||
const comments = tree ? {} : (state.languageDataAt<CommentTokens>("commentTokens", 0)[0] ?? {});
|
||||
const out: BracketMark[] = [];
|
||||
const s = this.start(from);
|
||||
const visit = (pos: number, depth: number) => {
|
||||
if (pos >= from && pos < to) out.push({ from: pos, depth });
|
||||
};
|
||||
this.scan(doc, tree, comments, s, Math.min(to, limit), visit);
|
||||
// Visible openers whose closer never comes are unmatched; give up (leave them coloured) past the lookahead.
|
||||
let open = s.stack;
|
||||
let shallowest = Infinity;
|
||||
for (let o = open; o && o.pos >= from; o = o.prev) shallowest = o.depth;
|
||||
if (shallowest !== Infinity) {
|
||||
const end = Math.min(limit, to + LOOKAHEAD);
|
||||
this.scan(doc, tree, comments, s, end, () => {}, shallowest);
|
||||
open = s.stack;
|
||||
if (s.pos >= doc.length && (open?.depth ?? -1) >= shallowest) {
|
||||
const unmatched = new Set<number>();
|
||||
for (let o = open; o && o.depth >= shallowest; o = o.prev) unmatched.add(o.pos);
|
||||
for (const m of out) if (unmatched.has(m.from)) m.depth = -1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private start(from: number): ScanState {
|
||||
const cps = this.checkpoints;
|
||||
let lo = 0;
|
||||
let hi = cps.length - 1;
|
||||
let best = -1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (cps[mid]!.pos <= from) {
|
||||
best = mid;
|
||||
lo = mid + 1;
|
||||
} else hi = mid - 1;
|
||||
}
|
||||
return best < 0 ? { pos: 0, stack: null, mode: 0, quote: "" } : { ...cps[best]! };
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances `s` to `end`, calling `visit` for every bracket. With `untilBelow`,
|
||||
* stops once the stack is shallower than that depth (lookahead).
|
||||
*/
|
||||
private scan(
|
||||
doc: Text,
|
||||
tree: Tree | null,
|
||||
comments: CommentTokens,
|
||||
s: ScanState,
|
||||
end: number,
|
||||
visit: (pos: number, depth: number) => void,
|
||||
untilBelow = -1,
|
||||
) {
|
||||
const cps = this.checkpoints;
|
||||
const lineFirst = comments.line ? comments.line.charCodeAt(0) : -1;
|
||||
const blockFirst = comments.block ? comments.block.open.charCodeAt(0) : -1;
|
||||
while (s.pos < end) {
|
||||
const chunkEnd = Math.min(end, (Math.floor(s.pos / CHUNK) + 1) * CHUNK);
|
||||
const base = s.pos;
|
||||
const text = doc.sliceString(base, Math.min(doc.length, chunkEnd + TOKEN_SLACK));
|
||||
const stop = chunkEnd - base;
|
||||
const skips: number[] = [];
|
||||
if (tree)
|
||||
tree.iterate({
|
||||
from: base,
|
||||
to: chunkEnd,
|
||||
enter: (n) => {
|
||||
if (!skipsNode(n.type)) return;
|
||||
skips.push(n.from - base, n.to - base);
|
||||
return false;
|
||||
},
|
||||
});
|
||||
let k = 0;
|
||||
let i = 0;
|
||||
while (i < stop) {
|
||||
if (tree) {
|
||||
while (k < skips.length && skips[k + 1]! <= i) k += 2;
|
||||
if (k < skips.length && skips[k]! <= i) {
|
||||
i = skips[k + 1]!;
|
||||
continue;
|
||||
}
|
||||
} else if (s.mode !== 0) {
|
||||
i = lexSkip(text, i, stop, s, comments);
|
||||
continue;
|
||||
}
|
||||
const ch = text.charCodeAt(i);
|
||||
if (!tree && (ch === 34 || ch === 39 || ch === 96 || ch === lineFirst || ch === blockFirst)) {
|
||||
const next = lexEnter(text, i, s, comments);
|
||||
if (next !== i) {
|
||||
i = next;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (ch === 40 || ch === 91 || ch === 123) {
|
||||
const depth = s.stack ? s.stack.depth + 1 : 0;
|
||||
visit(base + i, depth);
|
||||
s.stack = { closer: CLOSER[ch]!, pos: base + i, depth, prev: s.stack };
|
||||
} else if (ch === 41 || ch === 93 || ch === 125) {
|
||||
if (s.stack && s.stack.closer === ch) {
|
||||
visit(base + i, s.stack.depth);
|
||||
s.stack = s.stack.prev;
|
||||
if ((s.stack?.depth ?? -1) < untilBelow) {
|
||||
s.pos = base + i + 1;
|
||||
return;
|
||||
}
|
||||
} else visit(base + i, -1);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
s.pos = base + i;
|
||||
const last = cps[cps.length - 1];
|
||||
if (s.pos >= (last ? last.pos : 0) + CHUNK && s.pos <= doc.length) cps.push({ ...s });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The syntax tree when the editor has a language, else null (lexical scanning). */
|
||||
function treeOf(state: EditorState): Tree | null {
|
||||
const tree = syntaxTree(state);
|
||||
return tree.length || state.doc.length === 0 ? tree : null;
|
||||
}
|
||||
|
||||
/** In code at `i`: enters a comment / string and returns the index after its opening token, else `i`. */
|
||||
function lexEnter(text: string, i: number, s: ScanState, c: CommentTokens): number {
|
||||
const ch = text[i]!;
|
||||
if (c.line && text.startsWith(c.line, i)) {
|
||||
s.mode = 1;
|
||||
return i + c.line.length;
|
||||
}
|
||||
if (c.block && text.startsWith(c.block.open, i)) {
|
||||
s.mode = 2;
|
||||
return i + c.block.open.length;
|
||||
}
|
||||
if (ch === '"' || ch === "`" || (ch === "'" && quoteStartsString(text, i))) {
|
||||
s.mode = 3;
|
||||
s.quote = ch;
|
||||
return i + 1;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/** `'` starts a string unless it ends a word (Haskell's `x'`) or has no partner on the line (Rust lifetimes). */
|
||||
function quoteStartsString(text: string, i: number): boolean {
|
||||
if (i > 0 && /[\w]/.test(text[i - 1]!)) return false;
|
||||
const nl = text.indexOf("\n", i + 1);
|
||||
const close = text.indexOf("'", i + 1);
|
||||
return close >= 0 && (nl < 0 || close < nl);
|
||||
}
|
||||
|
||||
/** Inside a comment / string at `i`: the index after its end (back in code), else `stop` (still inside). */
|
||||
function lexSkip(text: string, i: number, stop: number, s: ScanState, c: CommentTokens): number {
|
||||
if (s.mode === 1 || s.mode === 2) {
|
||||
const close = s.mode === 1 ? "\n" : (c.block?.close ?? "\n");
|
||||
const at = text.indexOf(close, i);
|
||||
if (at < 0 || at >= stop) return stop;
|
||||
s.mode = 0;
|
||||
return at + close.length;
|
||||
}
|
||||
for (; i < stop; i++) {
|
||||
const ch = text[i];
|
||||
if (ch === "\\") i++;
|
||||
else if (ch === s.quote || (ch === "\n" && s.quote !== "`")) {
|
||||
s.mode = 0;
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/** Class of a bracket at `depth` (-1: unmatched); the palette repeats every `BRACKET_COLORS` levels. */
|
||||
export function bracketClass(depth: number): string {
|
||||
return depth < 0 ? "cm-rainbow-bracket-unmatched" : `cm-rainbow-bracket-${(depth % BRACKET_COLORS) + 1}`;
|
||||
}
|
||||
|
||||
const markCache = new Map<string, Decoration>();
|
||||
function markFor(depth: number): Decoration {
|
||||
const cls = bracketClass(depth);
|
||||
let m = markCache.get(cls);
|
||||
if (!m) markCache.set(cls, (m = Decoration.mark({ class: cls })));
|
||||
return m;
|
||||
}
|
||||
|
||||
function build(view: EditorView, scanner: BracketScanner): DecorationSet {
|
||||
const { from, to } = view.viewport;
|
||||
return Decoration.set(
|
||||
scanner.marks(view.state, from, to).map((m) => markFor(m.depth).range(m.from, m.from + 1)),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const plugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
scanner = new BracketScanner();
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = build(view, this.scanner);
|
||||
}
|
||||
update(u: ViewUpdate) {
|
||||
if (u.docChanged) {
|
||||
let first = Infinity;
|
||||
u.changes.iterChangedRanges((fromA) => (first = Math.min(first, fromA)));
|
||||
this.scanner.invalidate(first);
|
||||
}
|
||||
if (u.docChanged || u.viewportChanged || syntaxTree(u.startState) !== syntaxTree(u.state))
|
||||
this.decorations = build(u.view, this.scanner);
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
|
||||
/** Rainbow brackets for one editor (Settings › Editor › Color Scheme › Rainbow brackets). */
|
||||
export function rainbowBrackets(): Extension {
|
||||
// Above the syntax highlighter so the bracket colour is the innermost span.
|
||||
return Prec.high(plugin);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import { changeLane, overviewLane } from "./lanes";
|
||||
import { foldingExtension } from "./folding";
|
||||
import { syntaxHighlightField } from "./syntaxHighlight";
|
||||
import { indentGuides } from "./indentGuides";
|
||||
import { rainbowBrackets } from "./rainbowBrackets";
|
||||
import { stickyMaxLines, stickyScroll } from "./stickyScroll";
|
||||
import { findExtension } from "./find";
|
||||
import { codeLensExtension } from "./codeLens";
|
||||
@@ -204,6 +205,7 @@ export function heavyExtension(docLength: number, settings?: EditorSettings): Ex
|
||||
return [
|
||||
prefs.codeEditing.highlightBraces ? bracketMatching({ renderMatch }) : [],
|
||||
prefs.appearance.indentGuides ? indentGuides : [],
|
||||
prefs.rainbowBrackets ? rainbowBrackets() : [],
|
||||
prefs.codeEditing.highlightSelection ? highlightSelectionMatches({ minSelectionLength: 2, maxMatches: 500 }) : [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* ones are read through `--editor-*` locals with fallbacks to the base tokens.
|
||||
*/
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { BRACKET_COLORS } from "./rainbowBrackets";
|
||||
|
||||
export const openideaTheme = EditorView.theme({
|
||||
"&": {
|
||||
@@ -158,6 +159,19 @@ export const openideaTheme = EditorView.theme({
|
||||
backgroundPosition: "6px 0",
|
||||
},
|
||||
|
||||
// Rainbow brackets (cm/rainbowBrackets.ts); `*` too: the syntax highlighter's span sits inside and would win.
|
||||
...Object.fromEntries(
|
||||
Array.from({ length: BRACKET_COLORS }, (_, i) => [
|
||||
`.cm-rainbow-bracket-${i + 1}, .cm-rainbow-bracket-${i + 1} *`,
|
||||
{
|
||||
color: `var(--syn-bracket-${i + 1})`,
|
||||
fontWeight: `var(--syn-bracket-${i + 1}-bold, inherit)`,
|
||||
fontStyle: `var(--syn-bracket-${i + 1}-italic, inherit)`,
|
||||
},
|
||||
]),
|
||||
),
|
||||
".cm-rainbow-bracket-unmatched, .cm-rainbow-bracket-unmatched *": { color: "var(--syn-invalid)" },
|
||||
|
||||
// Whitespace rendering
|
||||
".cm-highlightSpace": {
|
||||
backgroundImage: "radial-gradient(circle at 50% 55%, var(--fg-whitespace, var(--fg-faint)) 11%, transparent 12%)",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Text } from "@codemirror/state";
|
||||
import type { CodeAction } from "@/bindings";
|
||||
import { semanticRanges } from "./semantic";
|
||||
import { RAINBOW_COLORS, rainbowIndex, semanticRanges } from "./semantic";
|
||||
import { lineErrorsAwayFromCaret, sortCodeActions } from "./intentions";
|
||||
import { splitSignature } from "./signature";
|
||||
import { dedupeLocations, usagesStatusText } from "./navigation";
|
||||
@@ -28,6 +28,66 @@ describe("semanticRanges", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("semantic highlighting (rainbow identifiers)", () => {
|
||||
// fn f(total: u32, self) { let count = total; static S; obj.field; count }
|
||||
const doc = Text.of(["fn f(total, self) { let count = total; S; obj.field; count + total }"]);
|
||||
const tokens = {
|
||||
tokenTypes: ["variable", "parameter", "property", "selfParameter"],
|
||||
tokenModifiers: ["static", "declaration"],
|
||||
data: [
|
||||
[0, 5, 5, 1, 0b10], // total (parameter declaration)
|
||||
[0, 12, 4, 3, 0], // self
|
||||
[0, 24, 5, 0, 0b10], // count
|
||||
[0, 32, 5, 1, 0], // total
|
||||
[0, 39, 1, 0, 0b01], // S (static)
|
||||
[0, 42, 3, 0, 0], // obj
|
||||
[0, 46, 5, 2, 0], // field
|
||||
[0, 53, 5, 0, 0], // count
|
||||
[0, 61, 5, 1, 0], // total
|
||||
].flat(),
|
||||
};
|
||||
const rainbow = (r: { className: string }) => /cm-sem-rainbow-(\d)/.exec(r.className)?.[1] ?? null;
|
||||
|
||||
it("hashes names to a stable palette index", () => {
|
||||
expect(rainbowIndex("count")).toBe(rainbowIndex("count"));
|
||||
const all = ["a", "b", "count", "total", "index", "value", "name", "item", "result", "i", "j", "k"].map(rainbowIndex);
|
||||
for (const n of all) expect(n >= 1 && n <= RAINBOW_COLORS).toBe(true);
|
||||
expect(new Set(all).size).toBeGreaterThan(2);
|
||||
});
|
||||
|
||||
it("colours locals and parameters by name, only when enabled", () => {
|
||||
expect(semanticRanges(doc, tokens).some((r) => rainbow(r))).toBe(false);
|
||||
const ranges = semanticRanges(doc, tokens, true);
|
||||
const byText = new Map<string, Set<string | null>>();
|
||||
for (const r of ranges) {
|
||||
const name = doc.sliceString(r.from, r.to);
|
||||
byText.set(name, (byText.get(name) ?? new Set()).add(rainbow(r)));
|
||||
}
|
||||
expect(byText.get("total")).toEqual(new Set([String(rainbowIndex("total"))]));
|
||||
expect(byText.get("count")).toEqual(new Set([String(rainbowIndex("count"))]));
|
||||
expect(byText.get("obj")).toEqual(new Set([String(rainbowIndex("obj"))]));
|
||||
// Parameters keep their own class too; self, statics and fields get no rainbow colour.
|
||||
expect(ranges.find((r) => r.from === 5)!.className).toBe(`cm-sem-parameter cm-sem-declaration cm-sem-rainbow-${rainbowIndex("total")}`);
|
||||
expect(byText.get("self")).toEqual(new Set([null]));
|
||||
expect(byText.get("S")).toEqual(new Set([null]));
|
||||
expect(byText.get("field")).toEqual(new Set([null]));
|
||||
});
|
||||
|
||||
it("leaves module-level variables plain when the server marks locals", () => {
|
||||
const ts = Text.of(["let top = 1; function f(p) { let x = top + p; }"]);
|
||||
const r = semanticRanges(
|
||||
ts,
|
||||
{
|
||||
tokenTypes: ["variable", "parameter"],
|
||||
tokenModifiers: ["declaration", "local"],
|
||||
data: [0, 4, 3, 0, 0b01, 0, 24, 1, 1, 0b11, 0, 33, 1, 0, 0b11, 0, 37, 3, 0, 0, 0, 43, 1, 1, 0b10],
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(r.filter((x) => x.className.includes("rainbow")).map((x) => ts.sliceString(x.from, x.to))).toEqual(["p", "x", "p"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortCodeActions", () => {
|
||||
const a = (title: string, kind: string | null, isPreferred = false, disabledReason: string | null = null): CodeAction => ({
|
||||
title,
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
* `cm-sem-*` classes (styled with the --syn-* tokens) layered over the Lezer
|
||||
* highlighting. Refreshed after edits (debounced); in between, existing marks
|
||||
* are mapped through the changes.
|
||||
*
|
||||
* Semantic highlighting (Color Scheme › Language Defaults, IDEA's rainbow
|
||||
* identifiers): local variables and parameters additionally get
|
||||
* `cm-sem-rainbow-<n>`, `n` derived from the name, so one name keeps one
|
||||
* colour (`--syn-rainbow-<n>`) throughout the file.
|
||||
*/
|
||||
import { Prec, StateEffect, StateField, type Extension, type Text } from "@codemirror/state";
|
||||
import { Decoration, EditorView, ViewPlugin, type DecorationSet, type ViewUpdate } from "@codemirror/view";
|
||||
import { commands, type SemanticTokens } from "@/platform/ipc";
|
||||
import { editorPrefs } from "@/platform/editorSettings";
|
||||
import { debounce, docOf, isUnavailable, lspRequest } from "./lsp";
|
||||
import { onLspRefresh, serverBackoff } from "./availability";
|
||||
|
||||
@@ -50,6 +56,23 @@ const MODIFIER_CLASS: Record<string, string> = {
|
||||
defaultLibrary: "builtin",
|
||||
};
|
||||
|
||||
/** Number of `--syn-rainbow-<n>` colours (IDEA's five semantic highlighting colours). */
|
||||
export const RAINBOW_COLORS = 5;
|
||||
|
||||
/** Stable 1-based palette index of an identifier (FNV-1a over its UTF-16 units). */
|
||||
export function rainbowIndex(name: string): number {
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
h ^= name.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return ((h >>> 0) % RAINBOW_COLORS) + 1;
|
||||
}
|
||||
|
||||
/** Locals and parameters only: no fields, statics, constants, library or `self` names. */
|
||||
const RAINBOW_TYPES = new Set(["variable", "parameter"]);
|
||||
const NOT_LOCAL = new Set(["static", "constant", "defaultLibrary", "global", "classScope", "fileScope"]);
|
||||
|
||||
export interface SemanticRange {
|
||||
from: number;
|
||||
to: number;
|
||||
@@ -57,9 +80,11 @@ export interface SemanticRange {
|
||||
}
|
||||
|
||||
/** Decodes absolute `[line, col, len, type, modifiers]` quintuples into styled ranges. */
|
||||
export function semanticRanges(doc: Text, tokens: SemanticTokens): SemanticRange[] {
|
||||
export function semanticRanges(doc: Text, tokens: SemanticTokens, rainbow = false): SemanticRange[] {
|
||||
const out: SemanticRange[] = [];
|
||||
const { data, tokenTypes, tokenModifiers } = tokens;
|
||||
// Servers with a `local` modifier (TypeScript) tell module-level variables apart.
|
||||
const marksLocals = tokenModifiers.includes("local");
|
||||
for (let i = 0; i + 4 < data.length; i += 5) {
|
||||
const line = data[i]!,
|
||||
col = data[i + 1]!,
|
||||
@@ -77,11 +102,19 @@ export function semanticRanges(doc: Text, tokens: SemanticTokens): SemanticRange
|
||||
if (constant && (type === "variable" || type === "property")) base = "constant";
|
||||
if (base && base !== "variable") classes.push(`cm-sem-${base}`);
|
||||
for (const n of names) if (MODIFIER_CLASS[n]) classes.push(`cm-sem-${MODIFIER_CLASS[n]}`);
|
||||
if (!classes.length) continue;
|
||||
const local =
|
||||
rainbow &&
|
||||
!constant &&
|
||||
RAINBOW_TYPES.has(type) &&
|
||||
!names.some((n) => NOT_LOCAL.has(n)) &&
|
||||
(type === "parameter" || !marksLocals || names.includes("local"));
|
||||
if (!classes.length && !local) continue;
|
||||
const l = doc.line(line + 1);
|
||||
const from = l.from + Math.min(col, l.length);
|
||||
const to = Math.min(from + len, l.to);
|
||||
if (from < to) out.push({ from, to, className: classes.join(" ") });
|
||||
if (from >= to) continue;
|
||||
if (local) classes.push(`cm-sem-rainbow-${rainbowIndex(doc.sliceString(from, to))}`);
|
||||
out.push({ from, to, className: classes.join(" ") });
|
||||
}
|
||||
return out.sort((a, b) => a.from - b.from || a.to - b.to);
|
||||
}
|
||||
@@ -107,12 +140,33 @@ const semanticPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
private refresh = debounce(() => void this.run(), 600);
|
||||
private offReady: () => void;
|
||||
private rainbow = editorPrefs().semanticHighlighting;
|
||||
/** Last answer, re-decorated without a request when Semantic highlighting is toggled. */
|
||||
private last: { doc: Text; tokens: SemanticTokens | null } | null = null;
|
||||
private destroyed = false;
|
||||
constructor(private view: EditorView) {
|
||||
this.refresh();
|
||||
this.offReady = onLspRefresh("semanticTokens", () => this.refresh());
|
||||
}
|
||||
update(u: ViewUpdate) {
|
||||
if (u.docChanged) this.refresh();
|
||||
const rainbow = editorPrefs().semanticHighlighting;
|
||||
if (rainbow === this.rainbow) return;
|
||||
this.rainbow = rainbow;
|
||||
const last = this.last;
|
||||
// Settings arrive as a compartment reconfiguration; no dispatching inside an update.
|
||||
if (last && last.doc === u.state.doc)
|
||||
queueMicrotask(() => {
|
||||
if (!this.destroyed && this.view.state.doc === last.doc) this.decorate(last.doc, last.tokens);
|
||||
});
|
||||
else this.refresh();
|
||||
}
|
||||
decorate(doc: Text, tokens: SemanticTokens | null) {
|
||||
this.last = { doc, tokens };
|
||||
const ranges = tokens ? semanticRanges(doc, tokens, this.rainbow) : [];
|
||||
this.view.dispatch({
|
||||
effects: setSemantic.of(Decoration.set(ranges.map((r) => markFor(r.className).range(r.from, r.to)), true)),
|
||||
});
|
||||
}
|
||||
async run() {
|
||||
const view = this.view;
|
||||
@@ -122,15 +176,13 @@ const semanticPlugin = ViewPlugin.fromClass(
|
||||
try {
|
||||
const tokens = await lspRequest(() => commands.lspSemanticTokens(doc.doc));
|
||||
if (view.state.doc !== state.doc) return this.refresh();
|
||||
const ranges = tokens ? semanticRanges(state.doc, tokens) : [];
|
||||
view.dispatch({
|
||||
effects: setSemantic.of(Decoration.set(ranges.map((r) => markFor(r.className).range(r.from, r.to)), true)),
|
||||
});
|
||||
this.decorate(state.doc, tokens);
|
||||
} catch (e) {
|
||||
if (isUnavailable(e)) serverBackoff.mark(doc.doc);
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
this.refresh.cancel();
|
||||
this.offReady();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Editor-side styling for LSP features (CodeMirror theme; tokens only). */
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { RAINBOW_COLORS } from "./semantic";
|
||||
|
||||
const tooltipSurface = {
|
||||
backgroundColor: "var(--bg-popup)",
|
||||
@@ -350,6 +351,18 @@ export const lspTheme = EditorView.theme({
|
||||
".cm-sem-static": { fontStyle: "italic" },
|
||||
".cm-sem-mutable": { textDecoration: "underline", textDecorationColor: "var(--fg-faint)", textUnderlineOffset: "3px" },
|
||||
".cm-sem-deprecated": { textDecoration: "line-through" },
|
||||
// Semantic highlighting: after the base classes so a parameter's colour gives way, and over the
|
||||
// syntax highlighter's span inside.
|
||||
...Object.fromEntries(
|
||||
Array.from({ length: RAINBOW_COLORS }, (_, i) => [
|
||||
`.cm-sem-rainbow-${i + 1}, .cm-sem-rainbow-${i + 1} *`,
|
||||
{
|
||||
color: `var(--syn-rainbow-${i + 1})`,
|
||||
fontWeight: `var(--syn-rainbow-${i + 1}-bold, inherit)`,
|
||||
fontStyle: `var(--syn-rainbow-${i + 1}-italic, inherit)`,
|
||||
},
|
||||
]),
|
||||
),
|
||||
|
||||
...markdown,
|
||||
});
|
||||
@@ -354,9 +354,27 @@ export const COLOR_SCHEME_PAGE = editorPage({
|
||||
"background",
|
||||
"bold",
|
||||
"italic",
|
||||
"rainbow",
|
||||
...SCHEMES.map((s) => s.name),
|
||||
],
|
||||
sections: [
|
||||
{ items: [{ t: "custom", label: "Scheme", keywords: ATTRIBUTES.map((a) => a.name), component: ColorSchemePage }] },
|
||||
{
|
||||
title: "Language Defaults",
|
||||
items: [
|
||||
{
|
||||
t: "check",
|
||||
key: "editor.semanticHighlighting",
|
||||
label: "Semantic highlighting",
|
||||
hint: "Local variables and parameters get a colour derived from their name (needs a language server).",
|
||||
},
|
||||
{
|
||||
t: "check",
|
||||
key: "editor.rainbowBrackets",
|
||||
label: "Rainbow brackets",
|
||||
hint: "Color (), [] and {} by nesting depth; unmatched brackets use the error color.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -94,6 +94,8 @@ export const ATTRIBUTES: readonly AttributeDef[] = [
|
||||
syn("operator", "Braces and Operators", [LD], "punct"),
|
||||
syn("markupTag", "Markup › Tag", [LD], "tag"),
|
||||
syn("markupAttribute", "Markup › Attribute", [LD], "attribute"),
|
||||
...[1, 2, 3, 4, 5].map((n) => syn(`semantic${n}`, `Semantic highlighting › Color ${n}`, [LD], `rainbow-${n}`)),
|
||||
...[1, 2, 3].map((n) => syn(`bracket${n}`, `Rainbow brackets › Level ${n}`, [LD], `bracket-${n}`)),
|
||||
|
||||
syn("pySelf", "Self parameter", ["Python"], "self"),
|
||||
syn("pyKeywordArg", "Keyword argument", ["Python"], "keyword-arg"),
|
||||
|
||||
@@ -52,6 +52,10 @@ export function lineCommentStyleFor(prefs: EditorExtras, language: string | unde
|
||||
|
||||
export const EDITOR_EXTRA_DEFAULTS = {
|
||||
colorOverrides: {} as Record<string, Record<string, TextAttributesOverride>>,
|
||||
/** Color Scheme › Language Defaults › Semantic highlighting (rainbow locals / parameters). */
|
||||
semanticHighlighting: false,
|
||||
/** Color Scheme › Language Defaults › Rainbow brackets. */
|
||||
rainbowBrackets: false,
|
||||
ligatures: false,
|
||||
fallbackFont: "",
|
||||
/** Typography Settings › Main / Bold font weight (CSS weights, IDEA's Regular / Bold). */
|
||||
|
||||
@@ -115,6 +115,14 @@ html {
|
||||
--syn-markup-code: #6aab73;
|
||||
--syn-invalid: #f75464;
|
||||
--syn-inline-value: #868a91;
|
||||
--syn-rainbow-1: #6fafbd;
|
||||
--syn-rainbow-2: #d79b6e;
|
||||
--syn-rainbow-3: #9fbf6b;
|
||||
--syn-rainbow-4: #c98bc6;
|
||||
--syn-rainbow-5: #8e9cf0;
|
||||
--syn-bracket-1: #e8c25b;
|
||||
--syn-bracket-2: #d78bd6;
|
||||
--syn-bracket-3: #4ea7f0;
|
||||
|
||||
/* editor (scheme colours) */
|
||||
--fg-editor: #bcbec4;
|
||||
@@ -449,6 +457,14 @@ html {
|
||||
--syn-markup-code: #067d17;
|
||||
--syn-invalid: #f50000;
|
||||
--syn-inline-value: #8c8c8c;
|
||||
--syn-rainbow-1: #9b3b6a;
|
||||
--syn-rainbow-2: #114d77;
|
||||
--syn-rainbow-3: #9c6a2c;
|
||||
--syn-rainbow-4: #005910;
|
||||
--syn-rainbow-5: #bc5150;
|
||||
--syn-bracket-1: #0431fa;
|
||||
--syn-bracket-2: #319331;
|
||||
--syn-bracket-3: #7b3814;
|
||||
|
||||
/* editor */
|
||||
--fg-editor: #080808;
|
||||
@@ -670,6 +686,14 @@ html {
|
||||
--syn-markup-code: #8ccc94;
|
||||
--syn-invalid: #ff8a95;
|
||||
--syn-inline-value: #a5a8ae;
|
||||
--syn-rainbow-1: #8fd0de;
|
||||
--syn-rainbow-2: #f0b88a;
|
||||
--syn-rainbow-3: #bedc8a;
|
||||
--syn-rainbow-4: #e8a8e4;
|
||||
--syn-rainbow-5: #b0bcff;
|
||||
--syn-bracket-1: #ffd700;
|
||||
--syn-bracket-2: #ee90ee;
|
||||
--syn-bracket-3: #87cefa;
|
||||
|
||||
--fg-editor: #e3e4e8;
|
||||
--caret: #ffffff;
|
||||
|
||||
@@ -35,6 +35,14 @@
|
||||
"--syn-markup-code": "#9ccc7a",
|
||||
"--syn-invalid": "#ff6b7a",
|
||||
"--syn-inline-value": "#6b7a8f",
|
||||
"--syn-rainbow-1": "#7fd1c4",
|
||||
"--syn-rainbow-2": "#e0a46e",
|
||||
"--syn-rainbow-3": "#b9a3f0",
|
||||
"--syn-rainbow-4": "#9ccc7a",
|
||||
"--syn-rainbow-5": "#e58fa3",
|
||||
"--syn-bracket-1": "#f2d07a",
|
||||
"--syn-bracket-2": "#d7a0e0",
|
||||
"--syn-bracket-3": "#6fc3df",
|
||||
|
||||
// Editor
|
||||
"--fg-editor": "#d9dfe8",
|
||||
|
||||
Reference in new issue
Block a user