Skip to content

Commit 57a36db

Browse files
authored
Merge branch 'main' into worker-lsp-defaults
2 parents a1ee4b3 + d79fdcf commit 57a36db

15 files changed

Lines changed: 1498 additions & 274 deletions

.github/workflows/add-pr-labels.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ jobs:
1414
pull-requests: write
1515

1616
steps:
17-
- uses: actions/labeler@v6
17+
- uses: actions/labeler@v7

src/cm/baseExtensions.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
closeBracketsKeymap,
55
completionKeymap,
66
} from "@codemirror/autocomplete";
7-
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
7+
import { history } from "@codemirror/commands";
88
import {
99
bracketMatching,
1010
defaultHighlightStyle,
@@ -79,8 +79,6 @@ export default function createBaseExtensions(
7979
keymap.of([
8080
...(autoCloseBrackets ? closeBracketsKeymap : []),
8181
...completionKeymap,
82-
...defaultKeymap,
83-
...historyKeymap,
8482
]),
8583
);
8684
extensions.push(

src/cm/colorChip.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { LSPPlugin } from "@codemirror/lsp-client";
2+
import type { Range } from "@codemirror/state";
3+
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
4+
import pickColor from "dialogs/color";
5+
import color from "utils/color";
6+
import type { Color as LspColor } from "vscode-languageserver-types";
7+
8+
export const COLOR_CHIP_CLASS = "cm-color-chip";
9+
export const COLOR_CHIP_SOURCE_ATTR = "data-color-source";
10+
11+
export type ColorChipSource = "regex" | "lsp";
12+
13+
export interface ColorChipPayload {
14+
from: number;
15+
to: number;
16+
css: string;
17+
pickerSeed?: string;
18+
source: ColorChipSource;
19+
}
20+
21+
const chipState = new WeakMap<HTMLElement, ColorChipPayload>();
22+
23+
export function getColorChipPayload(
24+
el: HTMLElement | null | undefined,
25+
): ColorChipPayload | undefined {
26+
if (!el) return undefined;
27+
return chipState.get(el);
28+
}
29+
30+
export function findColorChip(target: EventTarget | null): HTMLElement | null {
31+
if (!(target instanceof Element)) return null;
32+
return target.closest(`.${COLOR_CHIP_CLASS}`) as HTMLElement | null;
33+
}
34+
35+
export class ColorChipWidget extends WidgetType {
36+
constructor(readonly payload: ColorChipPayload) {
37+
super();
38+
}
39+
40+
eq(other: ColorChipWidget): boolean {
41+
return (
42+
other.payload.from === this.payload.from &&
43+
other.payload.to === this.payload.to &&
44+
other.payload.css === this.payload.css &&
45+
other.payload.source === this.payload.source &&
46+
(other.payload.pickerSeed || "") === (this.payload.pickerSeed || "")
47+
);
48+
}
49+
50+
toDOM(): HTMLElement {
51+
const el = document.createElement("span");
52+
el.className = COLOR_CHIP_CLASS;
53+
el.setAttribute(COLOR_CHIP_SOURCE_ATTR, this.payload.source);
54+
el.style.display = "inline-block";
55+
el.style.width = "0.9em";
56+
el.style.height = "0.9em";
57+
el.style.borderRadius = "2px";
58+
el.style.verticalAlign = "middle";
59+
el.style.margin = "0 2px";
60+
el.style.boxSizing = "border-box";
61+
el.style.border = "1px solid rgba(0,0,0,0.2)";
62+
el.style.backgroundColor = this.payload.css;
63+
el.style.cursor = "pointer";
64+
el.style.userSelect = "none";
65+
el.title = this.payload.css;
66+
el.dataset["color"] = this.payload.pickerSeed || this.payload.css;
67+
el.dataset["colorraw"] = this.payload.css;
68+
chipState.set(el, this.payload);
69+
return el;
70+
}
71+
72+
ignoreEvent(): boolean {
73+
return false;
74+
}
75+
}
76+
77+
export function colorChipDecoration(
78+
payload: ColorChipPayload,
79+
): Range<Decoration> {
80+
return Decoration.widget({
81+
widget: new ColorChipWidget(payload),
82+
side: -1,
83+
}).range(payload.from);
84+
}
85+
86+
export function isViewEditable(view: EditorView): boolean {
87+
const readOnly = view.contentDOM.ariaReadOnly === "true";
88+
const editable = view.contentDOM.contentEditable === "true";
89+
return !readOnly && editable;
90+
}
91+
92+
export function hasLspColorProvider(view: EditorView): boolean {
93+
const lsp = LSPPlugin.get(view) as {
94+
client?: {
95+
connected?: boolean;
96+
serverCapabilities?: { colorProvider?: boolean | object } | null;
97+
};
98+
} | null;
99+
return !!(
100+
lsp?.client?.connected && lsp.client.serverCapabilities?.colorProvider
101+
);
102+
}
103+
104+
export async function openColorPicker(seed: string): Promise<string | null> {
105+
try {
106+
const picked = await pickColor(seed || "#ffffff");
107+
return picked || null;
108+
} catch {
109+
return null;
110+
}
111+
}
112+
113+
export function clamp01(n: number): number {
114+
if (!Number.isFinite(n)) return 0;
115+
return Math.min(1, Math.max(0, n));
116+
}
117+
118+
function channelToByte(n: number): number {
119+
return Math.round(clamp01(n) * 255);
120+
}
121+
122+
export function lspColorToCss(c: LspColor): string {
123+
const r = channelToByte(c.red);
124+
const g = channelToByte(c.green);
125+
const b = channelToByte(c.blue);
126+
const a = clamp01(c.alpha ?? 1);
127+
if (a < 1) {
128+
const alpha = Number(a.toFixed(3));
129+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
130+
}
131+
return `rgb(${r}, ${g}, ${b})`;
132+
}
133+
134+
export function lspColorToHex(c: LspColor): string {
135+
const r = channelToByte(c.red).toString(16).padStart(2, "0");
136+
const g = channelToByte(c.green).toString(16).padStart(2, "0");
137+
const b = channelToByte(c.blue).toString(16).padStart(2, "0");
138+
const a = clamp01(c.alpha ?? 1);
139+
if (a < 1) {
140+
const aa = channelToByte(a).toString(16).padStart(2, "0");
141+
return `#${r}${g}${b}${aa}`;
142+
}
143+
return `#${r}${g}${b}`;
144+
}
145+
146+
export function cssToLspColor(css: string): LspColor | null {
147+
if (!css || typeof css !== "string") return null;
148+
try {
149+
const { r, g, b, a } = color(css.trim()).rgb;
150+
return {
151+
red: clamp01(r / 255),
152+
green: clamp01(g / 255),
153+
blue: clamp01(b / 255),
154+
alpha: clamp01(a),
155+
};
156+
} catch {
157+
return null;
158+
}
159+
}
160+
161+
export function detectColorFormat(
162+
text: string,
163+
): "hex" | "rgb" | "hsl" | "other" {
164+
const t = text.trim().toLowerCase();
165+
if (t.startsWith("#")) return "hex";
166+
if (t.startsWith("rgb")) return "rgb";
167+
if (t.startsWith("hsl")) return "hsl";
168+
return "other";
169+
}
170+
171+
export const colorChipTheme = EditorView.baseTheme({
172+
[`.${COLOR_CHIP_CLASS}`]: {
173+
userSelect: "none",
174+
},
175+
});

0 commit comments

Comments
 (0)