Compare commits
3 Commits
kcl-70
...
lsp-colors
Author | SHA1 | Date | |
---|---|---|---|
86ae7a989f | |||
334145f0be | |||
c24073b6ae |
@ -834,7 +834,13 @@ test('theme persists', async ({ page, context }) => {
|
||||
})
|
||||
|
||||
test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||
test('code color goober', async ({ page, context, scene, cmdBar }) => {
|
||||
test('code color goober', async ({
|
||||
page,
|
||||
context,
|
||||
scene,
|
||||
cmdBar,
|
||||
editor,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -879,6 +885,7 @@ sweepSketch = startSketchOn(XY)
|
||||
context,
|
||||
scene,
|
||||
cmdBar,
|
||||
editor,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
|
@ -41,6 +41,14 @@ interface LSPRequestMap {
|
||||
LSP.DefinitionParams,
|
||||
LSP.Definition | LSP.DefinitionLink[] | null,
|
||||
]
|
||||
'textDocument/documentColor': [
|
||||
LSP.DocumentColorParams,
|
||||
LSP.ColorInformation[] | null,
|
||||
]
|
||||
'textDocument/colorPresentation': [
|
||||
LSP.ColorPresentationParams,
|
||||
LSP.ColorPresentation[] | null,
|
||||
]
|
||||
}
|
||||
|
||||
// Client to server
|
||||
@ -229,6 +237,22 @@ export class LanguageServerClient {
|
||||
return await this.request('textDocument/definition', params)
|
||||
}
|
||||
|
||||
async textDocumentDocumentColor(params: LSP.DocumentColorParams) {
|
||||
const serverCapabilities = this.getServerCapabilities()
|
||||
if (!serverCapabilities.colorProvider) {
|
||||
return null
|
||||
}
|
||||
return await this.request('textDocument/documentColor', params)
|
||||
}
|
||||
|
||||
async textDocumentColorPresentation(params: LSP.ColorPresentationParams) {
|
||||
const serverCapabilities = this.getServerCapabilities()
|
||||
if (!serverCapabilities.colorProvider) {
|
||||
return null
|
||||
}
|
||||
return await this.request('textDocument/colorPresentation', params)
|
||||
}
|
||||
|
||||
attachPlugin(plugin: LanguageServerPlugin) {
|
||||
this.plugins.push(plugin)
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export {
|
||||
lspRenameEvent,
|
||||
lspSemanticTokensEvent,
|
||||
lspCodeActionEvent,
|
||||
lspColorUpdateEvent,
|
||||
} from './plugin/annotation'
|
||||
export {
|
||||
LanguageServerPlugin,
|
||||
|
@ -6,6 +6,7 @@ export enum LspAnnotation {
|
||||
Diagnostics = 'diagnostics',
|
||||
Rename = 'rename',
|
||||
CodeAction = 'code-action',
|
||||
ColorUpdate = 'color-update',
|
||||
}
|
||||
|
||||
const lspEvent = Annotation.define<LspAnnotation>()
|
||||
@ -14,3 +15,4 @@ export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
|
||||
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
|
||||
export const lspRenameEvent = lspEvent.of(LspAnnotation.Rename)
|
||||
export const lspCodeActionEvent = lspEvent.of(LspAnnotation.CodeAction)
|
||||
export const lspColorUpdateEvent = lspEvent.of(LspAnnotation.ColorUpdate)
|
||||
|
278
packages/codemirror-lsp-client/src/plugin/colors.ts
Normal file
278
packages/codemirror-lsp-client/src/plugin/colors.ts
Normal file
@ -0,0 +1,278 @@
|
||||
import {
|
||||
StateEffect,
|
||||
StateField,
|
||||
type Extension,
|
||||
type Range,
|
||||
} from '@codemirror/state'
|
||||
import {
|
||||
Decoration,
|
||||
type DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
WidgetType,
|
||||
type ViewUpdate,
|
||||
} from '@codemirror/view'
|
||||
|
||||
import type { LanguageServerPlugin } from './lsp'
|
||||
import { lspColorUpdateEvent } from './annotation'
|
||||
import { isArray } from '../lib/utils'
|
||||
import { offsetToPos, posToOffset, posToOffsetOrZero } from './util'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ---------- original helpers / widget / color utilities ---------- */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
interface PickerState {
|
||||
from: number
|
||||
to: number
|
||||
red: number
|
||||
green: number
|
||||
blue: number
|
||||
alpha: number
|
||||
}
|
||||
|
||||
export interface WidgetOptions extends PickerState {
|
||||
color: string
|
||||
}
|
||||
|
||||
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
|
||||
|
||||
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
|
||||
|
||||
function rgbaToHex(color: LSP.Color): string {
|
||||
return (
|
||||
'#' +
|
||||
[color.red, color.green, color.blue]
|
||||
.map((c) =>
|
||||
Math.round(c * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')
|
||||
)
|
||||
.join('')
|
||||
)
|
||||
}
|
||||
|
||||
function hexToRGBComponents(hex: string): number[] {
|
||||
const r = hex.slice(1, 3)
|
||||
const g = hex.slice(3, 5)
|
||||
const b = hex.slice(5, 7)
|
||||
return [parseInt(r, 16) / 255, parseInt(g, 16) / 255, parseInt(b, 16) / 255]
|
||||
}
|
||||
|
||||
async function discoverColorsViaLsp(
|
||||
view: EditorView,
|
||||
plugin: LanguageServerPlugin
|
||||
): Promise<WidgetOptions | Array<WidgetOptions> | null> {
|
||||
const responses = await plugin.requestDocumentColors()
|
||||
if (!responses) return null
|
||||
|
||||
const colors: Array<WidgetOptions> = []
|
||||
for (const color of responses) {
|
||||
if (!color.range || !color.color) continue
|
||||
|
||||
const { start, end } = color.range
|
||||
const from = posToOffset(view.state.doc, start)
|
||||
const to = posToOffset(view.state.doc, end)
|
||||
if (from == null || to == null) continue
|
||||
|
||||
colors.push({
|
||||
color: rgbaToHex(color.color),
|
||||
...color.color,
|
||||
from,
|
||||
to,
|
||||
})
|
||||
}
|
||||
return colors
|
||||
}
|
||||
|
||||
async function colorPickersDecorations(
|
||||
view: EditorView,
|
||||
plugin: LanguageServerPlugin
|
||||
): Promise<DecorationSet> {
|
||||
const widgets: Array<Range<Decoration>> = []
|
||||
const maybe = await discoverColorsViaLsp(view, plugin)
|
||||
if (!maybe) return Decoration.none
|
||||
|
||||
const optionsList = isArray(maybe) ? maybe : [maybe]
|
||||
for (const wo of optionsList) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(wo),
|
||||
side: 1,
|
||||
}).range(wo.from)
|
||||
)
|
||||
}
|
||||
return Decoration.set(widgets)
|
||||
}
|
||||
|
||||
export const wrapperClassName = 'cm-css-color-picker-wrapper'
|
||||
|
||||
class ColorPickerWidget extends WidgetType {
|
||||
private readonly state: PickerState
|
||||
private readonly color: string
|
||||
|
||||
constructor({ color, ...state }: WidgetOptions) {
|
||||
super()
|
||||
this.state = state
|
||||
this.color = color
|
||||
}
|
||||
|
||||
eq(other: ColorPickerWidget) {
|
||||
return (
|
||||
other.color === this.color &&
|
||||
other.state.from === this.state.from &&
|
||||
other.state.to === this.state.to &&
|
||||
other.state.alpha === this.state.alpha
|
||||
)
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const picker = document.createElement('input')
|
||||
pickerState.set(picker, this.state)
|
||||
picker.type = 'color'
|
||||
picker.value = this.color
|
||||
|
||||
const wrapper = document.createElement('span')
|
||||
wrapper.appendChild(picker)
|
||||
wrapper.className = wrapperClassName
|
||||
return wrapper
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const colorPickerTheme = EditorView.baseTheme({
|
||||
[`.${wrapperClassName}`]: {
|
||||
display: 'inline-block',
|
||||
outline: '1px solid #eee',
|
||||
marginRight: '0.6ch',
|
||||
height: '1em',
|
||||
width: '1em',
|
||||
transform: 'translateY(1px)',
|
||||
},
|
||||
[`.${wrapperClassName} input[type="color"]`]: {
|
||||
cursor: 'pointer',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
'&::-webkit-color-swatch-wrapper': { padding: 0 },
|
||||
'&::-webkit-color-swatch': { border: 'none' },
|
||||
'&::-moz-color-swatch': { border: 'none' },
|
||||
},
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ------------------- ✅ new state machinery -------------------- */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Effect that carries a fresh DecorationSet
|
||||
const setColorDecorations = StateEffect.define<DecorationSet>()
|
||||
|
||||
// Field that stores the current DecorationSet
|
||||
const colorDecorationsField = StateField.define<DecorationSet>({
|
||||
create: () => Decoration.none,
|
||||
update(value, tr) {
|
||||
value = value.map(tr.changes)
|
||||
for (const e of tr.effects) if (e.is(setColorDecorations)) value = e.value
|
||||
return value
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ------------------ original ViewPlugin, patched ---------------- */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const makeColorPicker = (plugin: ViewPlugin<LanguageServerPlugin>) =>
|
||||
ViewPlugin.fromClass(
|
||||
class ColorPickerViewPlugin {
|
||||
plugin: LanguageServerPlugin | null
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.plugin = view.plugin(plugin)
|
||||
if (!this.plugin) return
|
||||
|
||||
// initial async load → dispatch decorations
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
colorPickersDecorations(view, this.plugin).then((deco) => {
|
||||
view.dispatch({ effects: setColorDecorations.of(deco) })
|
||||
})
|
||||
}
|
||||
|
||||
async update(update: ViewUpdate) {
|
||||
if (!this.plugin) return
|
||||
if (!(update.docChanged || update.viewportChanged)) return
|
||||
|
||||
const deco = await colorPickersDecorations(update.view, this.plugin)
|
||||
update.view.dispatch({ effects: setColorDecorations.of(deco) })
|
||||
}
|
||||
},
|
||||
{
|
||||
eventHandlers: {
|
||||
change: (e: Event, view: EditorView) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
colorPickerChange(e, view, plugin)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* -------------------- unchanged event handler ------------------- */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
async function colorPickerChange(
|
||||
e: Event,
|
||||
view: EditorView,
|
||||
plugin: ViewPlugin<LanguageServerPlugin>
|
||||
): Promise<boolean> {
|
||||
const value = view.plugin(plugin)
|
||||
if (!value) return false
|
||||
|
||||
const target = e.target as HTMLInputElement
|
||||
if (
|
||||
target.nodeName !== 'INPUT' ||
|
||||
!target.parentElement?.classList.contains(wrapperClassName)
|
||||
)
|
||||
return false
|
||||
|
||||
const data = pickerState.get(target)!
|
||||
const converted = target.value + data.alpha
|
||||
const [red, green, blue] = hexToRGBComponents(converted)
|
||||
|
||||
const responses = await value.requestColorPresentation(
|
||||
{ red, green, blue, alpha: data.alpha },
|
||||
{
|
||||
start: offsetToPos(view.state.doc, data.from),
|
||||
end: offsetToPos(view.state.doc, data.to),
|
||||
}
|
||||
)
|
||||
if (!responses?.length) return false
|
||||
|
||||
for (const resp of responses) {
|
||||
const changes = resp.textEdit
|
||||
? {
|
||||
from: posToOffsetOrZero(view.state.doc, resp.textEdit.range.start),
|
||||
to: posToOffsetOrZero(view.state.doc, resp.textEdit.range.end),
|
||||
insert: resp.textEdit.newText,
|
||||
}
|
||||
: { from: data.from, to: data.to, insert: resp.label }
|
||||
|
||||
view.dispatch({ changes, annotations: [lspColorUpdateEvent] })
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ------------------------- public API --------------------------- */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export default function lspColorsExt(
|
||||
plugin: ViewPlugin<LanguageServerPlugin>
|
||||
): Extension {
|
||||
return [colorDecorationsField, makeColorPicker(plugin), colorPickerTheme]
|
||||
}
|
@ -48,6 +48,7 @@ import { isArray } from '../lib/utils'
|
||||
import lspGoToDefinitionExt from './go-to-definition'
|
||||
import lspRenameExt from './rename'
|
||||
import lspSignatureHelpExt from './signature-help'
|
||||
import lspColorsExt from './colors'
|
||||
|
||||
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
||||
export const docPathFacet = Facet.define<string, string>({
|
||||
@ -534,6 +535,37 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
})
|
||||
}
|
||||
|
||||
async requestDocumentColors() {
|
||||
if (
|
||||
!(this.client.getServerCapabilities().colorProvider && this.client.ready)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await this.client.textDocumentDocumentColor({
|
||||
textDocument: { uri: this.getDocUri() },
|
||||
})
|
||||
|
||||
if (!result) return
|
||||
return result
|
||||
}
|
||||
|
||||
async requestColorPresentation(color: LSP.Color, range: LSP.Range) {
|
||||
if (
|
||||
!(this.client.getServerCapabilities().colorProvider && this.client.ready)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await this.client.textDocumentColorPresentation({
|
||||
textDocument: { uri: this.getDocUri() },
|
||||
color,
|
||||
range,
|
||||
})
|
||||
if (!result) return
|
||||
return result
|
||||
}
|
||||
|
||||
async requestRename(
|
||||
view: EditorView,
|
||||
{ line, character }: { line: number; character: number }
|
||||
@ -1318,6 +1350,7 @@ export class LanguageServerPluginSpec
|
||||
return [
|
||||
linter(null),
|
||||
lspAutocompleteExt(plugin),
|
||||
lspColorsExt(plugin),
|
||||
lspFormatExt(plugin),
|
||||
lspGoToDefinitionExt(plugin),
|
||||
lspHoverExt(plugin),
|
||||
|
14
rust/Cargo.lock
generated
14
rust/Cargo.lock
generated
@ -535,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -963,7 +963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1746,7 +1746,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2987,7 +2987,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3306,7 +3306,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3900,7 +3900,7 @@ dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4753,7 +4753,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4220,8 +4220,8 @@ sketch001 = startSketchOn(XY)
|
||||
result,
|
||||
vec![tower_lsp::lsp_types::ColorInformation {
|
||||
range: tower_lsp::lsp_types::Range {
|
||||
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
|
||||
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
|
||||
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
|
||||
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
|
||||
},
|
||||
color: tower_lsp::lsp_types::Color {
|
||||
red: 1.0,
|
||||
@ -4272,8 +4272,8 @@ sketch001 = startSketchOn(XY)
|
||||
result,
|
||||
vec![tower_lsp::lsp_types::ColorInformation {
|
||||
range: tower_lsp::lsp_types::Range {
|
||||
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
|
||||
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
|
||||
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
|
||||
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
|
||||
},
|
||||
color: tower_lsp::lsp_types::Color {
|
||||
red: 1.0,
|
||||
@ -4291,8 +4291,8 @@ sketch001 = startSketchOn(XY)
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
},
|
||||
range: tower_lsp::lsp_types::Range {
|
||||
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
|
||||
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
|
||||
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
|
||||
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
|
||||
},
|
||||
color: tower_lsp::lsp_types::Color {
|
||||
red: 1.0,
|
||||
|
@ -438,8 +438,15 @@ impl Node<Program> {
|
||||
let add_color = |literal: &Node<Literal>| {
|
||||
// Check if the string is a color.
|
||||
if let Some(c) = literal.value.is_color() {
|
||||
let source_range = literal.as_source_range();
|
||||
// We subtract 1 from either side because of the "'s in the literal.
|
||||
let fixed_source_range = SourceRange::new(
|
||||
source_range.start() + 1,
|
||||
source_range.end() - 1,
|
||||
source_range.module_id(),
|
||||
);
|
||||
let color = ColorInformation {
|
||||
range: literal.as_source_range().to_lsp_range(code),
|
||||
range: fixed_source_range.to_lsp_range(code),
|
||||
color: tower_lsp::lsp_types::Color {
|
||||
red: c.r,
|
||||
green: c.g,
|
||||
@ -498,7 +505,11 @@ impl Node<Program> {
|
||||
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
|
||||
match node {
|
||||
crate::walk::Node::Literal(literal) => {
|
||||
if literal.start == pos_start && literal.end == pos_end && literal.value.is_color().is_some() {
|
||||
// Account for the quotes in the literal.
|
||||
if (literal.start + 1) == pos_start
|
||||
&& (literal.end - 1) == pos_end
|
||||
&& literal.value.is_color().is_some()
|
||||
{
|
||||
found.replace(true);
|
||||
return Ok(true);
|
||||
}
|
||||
|
@ -2729,6 +2729,17 @@ fn ty(i: &mut TokenSlice) -> PResult<Token> {
|
||||
keyword(i, "type")
|
||||
}
|
||||
|
||||
fn any_keyword(i: &mut TokenSlice) -> PResult<Token> {
|
||||
any.try_map(|token: Token| match token.token_type {
|
||||
TokenType::Keyword => Ok(token),
|
||||
_ => Err(CompilationError::fatal(
|
||||
token.as_source_range(),
|
||||
"expected some reserved keyword".to_owned(),
|
||||
)),
|
||||
})
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
fn keyword(i: &mut TokenSlice, expected: &str) -> PResult<Token> {
|
||||
any.try_map(|token: Token| match token.token_type {
|
||||
TokenType::Keyword if token.value == expected => Ok(token),
|
||||
@ -3143,12 +3154,14 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
|
||||
NonCode(Node<NonCodeNode>),
|
||||
LabeledArg(LabeledArg),
|
||||
UnlabeledArg(Expr),
|
||||
Keyword(Token),
|
||||
}
|
||||
let initial_unlabeled_arg = opt((expression, comma, opt(whitespace)).map(|(arg, _, _)| arg)).parse_next(i)?;
|
||||
let args: Vec<_> = repeat(
|
||||
0..,
|
||||
alt((
|
||||
terminated(non_code_node.map(ArgPlace::NonCode), whitespace),
|
||||
terminated(any_keyword.map(ArgPlace::Keyword), whitespace),
|
||||
terminated(labeled_argument, labeled_arg_separator).map(ArgPlace::LabeledArg),
|
||||
expression.map(ArgPlace::UnlabeledArg),
|
||||
)),
|
||||
@ -3164,6 +3177,18 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
|
||||
ArgPlace::LabeledArg(x) => {
|
||||
args.push(x);
|
||||
}
|
||||
ArgPlace::Keyword(kw) => {
|
||||
return Err(ErrMode::Cut(
|
||||
CompilationError::fatal(
|
||||
SourceRange::from(kw.clone()),
|
||||
format!(
|
||||
"`{}` is not the name of an argument (it's a reserved keyword)",
|
||||
kw.value
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
ArgPlace::UnlabeledArg(arg) => {
|
||||
let followed_by_equals = peek((opt(whitespace), equals)).parse_next(i).is_ok();
|
||||
if followed_by_equals {
|
||||
@ -5055,6 +5080,30 @@ bar = 1
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sensible_error_when_using_keyword_as_arg_label() {
|
||||
for (i, program) in ["pow(2, fn = 8)"].into_iter().enumerate() {
|
||||
let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
|
||||
let err = match fn_call_kw.parse(tokens.as_slice()) {
|
||||
Err(e) => e,
|
||||
Ok(ast) => {
|
||||
eprintln!("{ast:#?}");
|
||||
panic!("Expected this to error but it didn't");
|
||||
}
|
||||
};
|
||||
let cause = err.inner().cause.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
cause.message, "`fn` is not the name of an argument (it's a reserved keyword)",
|
||||
"failed test {i}: {program}"
|
||||
);
|
||||
assert_eq!(
|
||||
cause.source_range.start(),
|
||||
program.find("fn").unwrap(),
|
||||
"failed test {i}: {program}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sensible_error_when_missing_rhs_of_obj_property() {
|
||||
for (i, program) in ["{x = 1, y =}"].into_iter().enumerate() {
|
||||
|
@ -3092,3 +3092,24 @@ mod error_revolve_on_edge_get_edge {
|
||||
super::execute(TEST_NAME, true).await
|
||||
}
|
||||
}
|
||||
mod sketch_on_face_union {
|
||||
const TEST_NAME: &str = "sketch_on_face_union";
|
||||
|
||||
/// Test parsing KCL.
|
||||
#[test]
|
||||
fn parse() {
|
||||
super::parse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn unparse() {
|
||||
super::unparse(TEST_NAME).await
|
||||
}
|
||||
|
||||
/// Test that KCL is executed correctly.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_execute() {
|
||||
super::execute(TEST_NAME, true).await
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use super::{args::TyF64, DEFAULT_TOLERANCE};
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{types::RuntimeType, ExecState, KclValue, Solid},
|
||||
std::Args,
|
||||
std::{patterns::GeometryTrait, Args},
|
||||
};
|
||||
|
||||
/// Union two or more solids into a single solid.
|
||||
@ -123,7 +123,7 @@ pub(crate) async fn inner_union(
|
||||
let solid_out_id = exec_state.next_uuid();
|
||||
|
||||
let mut solid = solids[0].clone();
|
||||
solid.id = solid_out_id;
|
||||
solid.set_id(solid_out_id);
|
||||
let mut new_solids = vec![solid.clone()];
|
||||
|
||||
if args.ctx.no_engine_commands().await {
|
||||
@ -155,7 +155,7 @@ pub(crate) async fn inner_union(
|
||||
|
||||
// If we have more solids, set those as well.
|
||||
if !extra_solid_ids.is_empty() {
|
||||
solid.id = extra_solid_ids[0];
|
||||
solid.set_id(extra_solid_ids[0]);
|
||||
new_solids.push(solid.clone());
|
||||
}
|
||||
|
||||
@ -249,7 +249,7 @@ pub(crate) async fn inner_intersect(
|
||||
let solid_out_id = exec_state.next_uuid();
|
||||
|
||||
let mut solid = solids[0].clone();
|
||||
solid.id = solid_out_id;
|
||||
solid.set_id(solid_out_id);
|
||||
let mut new_solids = vec![solid.clone()];
|
||||
|
||||
if args.ctx.no_engine_commands().await {
|
||||
@ -281,7 +281,7 @@ pub(crate) async fn inner_intersect(
|
||||
|
||||
// If we have more solids, set those as well.
|
||||
if !extra_solid_ids.is_empty() {
|
||||
solid.id = extra_solid_ids[0];
|
||||
solid.set_id(extra_solid_ids[0]);
|
||||
new_solids.push(solid.clone());
|
||||
}
|
||||
|
||||
@ -385,7 +385,7 @@ pub(crate) async fn inner_subtract(
|
||||
let solid_out_id = exec_state.next_uuid();
|
||||
|
||||
let mut solid = solids[0].clone();
|
||||
solid.id = solid_out_id;
|
||||
solid.set_id(solid_out_id);
|
||||
let mut new_solids = vec![solid.clone()];
|
||||
|
||||
if args.ctx.no_engine_commands().await {
|
||||
@ -419,7 +419,7 @@ pub(crate) async fn inner_subtract(
|
||||
|
||||
// If we have more solids, set those as well.
|
||||
if !extra_solid_ids.is_empty() {
|
||||
solid.id = extra_solid_ids[0];
|
||||
solid.set_id(extra_solid_ids[0]);
|
||||
new_solids.push(solid.clone());
|
||||
}
|
||||
|
||||
|
@ -598,7 +598,7 @@ fn array_to_point2d(
|
||||
.map(|val| val.as_point2d().unwrap())
|
||||
}
|
||||
|
||||
trait GeometryTrait: Clone {
|
||||
pub trait GeometryTrait: Clone {
|
||||
type Set: Into<Vec<Self>> + Clone;
|
||||
fn id(&self) -> Uuid;
|
||||
fn original_id(&self) -> Uuid;
|
||||
@ -608,6 +608,7 @@ trait GeometryTrait: Clone {
|
||||
source_ranges: Vec<SourceRange>,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<[TyF64; 3], KclError>;
|
||||
#[allow(async_fn_in_trait)]
|
||||
async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
|
||||
}
|
||||
|
||||
@ -641,6 +642,8 @@ impl GeometryTrait for Solid {
|
||||
type Set = Vec<Solid>;
|
||||
fn set_id(&mut self, id: Uuid) {
|
||||
self.id = id;
|
||||
// We need this for in extrude.rs when you sketch on face.
|
||||
self.sketch.id = id;
|
||||
}
|
||||
|
||||
fn id(&self) -> Uuid {
|
||||
|
1060
rust/kcl-lib/tests/sketch_on_face_union/artifact_commands.snap
Normal file
1060
rust/kcl-lib/tests/sketch_on_face_union/artifact_commands.snap
Normal file
File diff suppressed because it is too large
Load Diff
3671
rust/kcl-lib/tests/sketch_on_face_union/ast.snap
Normal file
3671
rust/kcl-lib/tests/sketch_on_face_union/ast.snap
Normal file
File diff suppressed because it is too large
Load Diff
58
rust/kcl-lib/tests/sketch_on_face_union/input.kcl
Normal file
58
rust/kcl-lib/tests/sketch_on_face_union/input.kcl
Normal file
@ -0,0 +1,58 @@
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
// Define parameters
|
||||
trussSupportAngle = 15
|
||||
height = 120
|
||||
thickness = 4
|
||||
|
||||
sketch001 = startSketchOn(YZ)
|
||||
profile001 = startProfile(sketch001, at = [60, 0])
|
||||
|> xLine(length = -120, tag = $bottomFace)
|
||||
|> yLine(length = 12)
|
||||
|> angledLine(angle = 25, endAbsoluteX = 0, tag = $tag001)
|
||||
|> angledLine(angle = -25, endAbsoluteX = 60)
|
||||
|> close()
|
||||
|
||||
profile002 = startProfile(sketch001, at = [60-thickness, thickness])
|
||||
|> xLine(endAbsolute = thickness/2)
|
||||
|> yLine(endAbsolute = segEndY(tag001)-thickness) // update
|
||||
|> angledLine(endAbsoluteX = profileStartX(%), angle = -25)
|
||||
|> close(%)
|
||||
|
||||
profile003 = startProfile(sketch001, at = [-60+thickness, thickness])
|
||||
|> xLine(endAbsolute = -thickness/2)
|
||||
|> yLine(endAbsolute = segEndY(tag001)-thickness) // update
|
||||
|> angledLine(endAbsoluteX = profileStartX(%), angle = 205)
|
||||
|> close(%)
|
||||
|
||||
profile004 = subtract2d(profile001, tool = profile002)
|
||||
subtract2d(profile001, tool = profile003)
|
||||
|
||||
body001 = extrude(profile001, length = 2)
|
||||
|
||||
sketch002 = startSketchOn(offsetPlane(YZ, offset = .1))
|
||||
profile006 = startProfile(sketch002, at = [thickness/2-1, 14])
|
||||
|> angledLine(angle = 30, length = 25)
|
||||
|> angledLine(angle = -25, length = 5)
|
||||
|> angledLine(angle = 210, endAbsoluteX = profileStartX(%))
|
||||
|> close(%)
|
||||
|> extrude(%, length = 1.8)
|
||||
|
||||
profile007 = startProfile(sketch002, at = [-thickness/2+1, 14])
|
||||
|> angledLine(angle = 150, length = 25)
|
||||
|> angledLine(angle = 205, length = 5)
|
||||
|> angledLine(angle = -30, endAbsoluteX = profileStartX(%))
|
||||
|> close(%)
|
||||
|> extrude(%, length = 1.8)
|
||||
|
||||
newSketch = body001 + profile006 + profile007
|
||||
|
||||
leg001Sketch = startSketchOn(newSketch, face = bottomFace)
|
||||
legProfile001 = startProfile(leg001Sketch, at = [-60, 0])
|
||||
|> xLine(%, length = 4)
|
||||
|> yLine(%, length = 2)
|
||||
|> xLine(%, endAbsolute = profileStartX(%))
|
||||
|> close(%)
|
||||
|
||||
leg001 = extrude(legProfile001, length = 48)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = -90)
|
264
rust/kcl-lib/tests/sketch_on_face_union/ops.snap
Normal file
264
rust/kcl-lib/tests/sketch_on_face_union/ops.snap
Normal file
@ -0,0 +1,264 @@
|
||||
---
|
||||
source: kcl-lib/src/simulation_tests.rs
|
||||
description: Operations executed sketch_on_face_union.kcl
|
||||
---
|
||||
[
|
||||
{
|
||||
"labeledArgs": {},
|
||||
"name": "startSketchOn",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Plane",
|
||||
"artifact_id": "[uuid]"
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"labeledArgs": {
|
||||
"tool": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"name": "subtract2d",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"labeledArgs": {
|
||||
"tool": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"name": "subtract2d",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"labeledArgs": {
|
||||
"length": {
|
||||
"value": {
|
||||
"type": "Number",
|
||||
"value": 2.0,
|
||||
"ty": {
|
||||
"type": "Default",
|
||||
"len": {
|
||||
"type": "Inches"
|
||||
},
|
||||
"angle": {
|
||||
"type": "Degrees"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"name": "extrude",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"labeledArgs": {},
|
||||
"name": "startSketchOn",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Plane",
|
||||
"artifact_id": "[uuid]"
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KclStdLibCall",
|
||||
"name": "offsetPlane",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Plane",
|
||||
"artifact_id": "[uuid]"
|
||||
},
|
||||
"sourceRange": []
|
||||
},
|
||||
"labeledArgs": {
|
||||
"offset": {
|
||||
"value": {
|
||||
"type": "Number",
|
||||
"value": 0.1,
|
||||
"ty": {
|
||||
"type": "Default",
|
||||
"len": {
|
||||
"type": "Inches"
|
||||
},
|
||||
"angle": {
|
||||
"type": "Degrees"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
},
|
||||
{
|
||||
"labeledArgs": {
|
||||
"length": {
|
||||
"value": {
|
||||
"type": "Number",
|
||||
"value": 1.8,
|
||||
"ty": {
|
||||
"type": "Default",
|
||||
"len": {
|
||||
"type": "Inches"
|
||||
},
|
||||
"angle": {
|
||||
"type": "Degrees"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"name": "extrude",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"labeledArgs": {
|
||||
"length": {
|
||||
"value": {
|
||||
"type": "Number",
|
||||
"value": 1.8,
|
||||
"ty": {
|
||||
"type": "Default",
|
||||
"len": {
|
||||
"type": "Inches"
|
||||
},
|
||||
"angle": {
|
||||
"type": "Degrees"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"name": "extrude",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"labeledArgs": {
|
||||
"face": {
|
||||
"value": {
|
||||
"type": "TagIdentifier",
|
||||
"value": "bottomFace",
|
||||
"artifact_id": "[uuid]"
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"name": "startSketchOn",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Solid",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"labeledArgs": {
|
||||
"length": {
|
||||
"value": {
|
||||
"type": "Number",
|
||||
"value": 48.0,
|
||||
"ty": {
|
||||
"type": "Default",
|
||||
"len": {
|
||||
"type": "Inches"
|
||||
},
|
||||
"angle": {
|
||||
"type": "Degrees"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
},
|
||||
"name": "extrude",
|
||||
"sourceRange": [],
|
||||
"type": "StdLibCall",
|
||||
"unlabeledArg": {
|
||||
"value": {
|
||||
"type": "Sketch",
|
||||
"value": {
|
||||
"artifactId": "[uuid]"
|
||||
}
|
||||
},
|
||||
"sourceRange": []
|
||||
}
|
||||
}
|
||||
]
|
2639
rust/kcl-lib/tests/sketch_on_face_union/program_memory.snap
Normal file
2639
rust/kcl-lib/tests/sketch_on_face_union/program_memory.snap
Normal file
File diff suppressed because it is too large
Load Diff
BIN
rust/kcl-lib/tests/sketch_on_face_union/rendered_model.png
Normal file
BIN
rust/kcl-lib/tests/sketch_on_face_union/rendered_model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
62
rust/kcl-lib/tests/sketch_on_face_union/unparsed.snap
Normal file
62
rust/kcl-lib/tests/sketch_on_face_union/unparsed.snap
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
source: kcl-lib/src/simulation_tests.rs
|
||||
description: Result of unparsing sketch_on_face_union.kcl
|
||||
---
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
// Define parameters
|
||||
trussSupportAngle = 15
|
||||
height = 120
|
||||
thickness = 4
|
||||
|
||||
sketch001 = startSketchOn(YZ)
|
||||
profile001 = startProfile(sketch001, at = [60, 0])
|
||||
|> xLine(length = -120, tag = $bottomFace)
|
||||
|> yLine(length = 12)
|
||||
|> angledLine(angle = 25, endAbsoluteX = 0, tag = $tag001)
|
||||
|> angledLine(angle = -25, endAbsoluteX = 60)
|
||||
|> close()
|
||||
|
||||
profile002 = startProfile(sketch001, at = [60 - thickness, thickness])
|
||||
|> xLine(endAbsolute = thickness / 2)
|
||||
|> yLine(endAbsolute = segEndY(tag001) - thickness) // update
|
||||
|> angledLine(endAbsoluteX = profileStartX(%), angle = -25)
|
||||
|> close(%)
|
||||
|
||||
profile003 = startProfile(sketch001, at = [-60 + thickness, thickness])
|
||||
|> xLine(endAbsolute = -thickness / 2)
|
||||
|> yLine(endAbsolute = segEndY(tag001) - thickness) // update
|
||||
|> angledLine(endAbsoluteX = profileStartX(%), angle = 205)
|
||||
|> close(%)
|
||||
|
||||
profile004 = subtract2d(profile001, tool = profile002)
|
||||
subtract2d(profile001, tool = profile003)
|
||||
|
||||
body001 = extrude(profile001, length = 2)
|
||||
|
||||
sketch002 = startSketchOn(offsetPlane(YZ, offset = .1))
|
||||
profile006 = startProfile(sketch002, at = [thickness / 2 - 1, 14])
|
||||
|> angledLine(angle = 30, length = 25)
|
||||
|> angledLine(angle = -25, length = 5)
|
||||
|> angledLine(angle = 210, endAbsoluteX = profileStartX(%))
|
||||
|> close(%)
|
||||
|> extrude(%, length = 1.8)
|
||||
|
||||
profile007 = startProfile(sketch002, at = [-thickness / 2 + 1, 14])
|
||||
|> angledLine(angle = 150, length = 25)
|
||||
|> angledLine(angle = 205, length = 5)
|
||||
|> angledLine(angle = -30, endAbsoluteX = profileStartX(%))
|
||||
|> close(%)
|
||||
|> extrude(%, length = 1.8)
|
||||
|
||||
newSketch = body001 + profile006 + profile007
|
||||
|
||||
leg001Sketch = startSketchOn(newSketch, face = bottomFace)
|
||||
legProfile001 = startProfile(leg001Sketch, at = [-60, 0])
|
||||
|> xLine(%, length = 4)
|
||||
|> yLine(%, length = 2)
|
||||
|> xLine(%, endAbsolute = profileStartX(%))
|
||||
|> close(%)
|
||||
|
||||
leg001 = extrude(legProfile001, length = 48)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = -90)
|
@ -1,328 +0,0 @@
|
||||
import { language, syntaxTree } from '@codemirror/language'
|
||||
import type { Extension, Range, Text } from '@codemirror/state'
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view'
|
||||
import {
|
||||
Decoration,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
WidgetType,
|
||||
} from '@codemirror/view'
|
||||
import type { Tree } from '@lezer/common'
|
||||
import { NodeProp } from '@lezer/common'
|
||||
import { isArray } from '@src/lib/utils'
|
||||
|
||||
interface PickerState {
|
||||
from: number
|
||||
to: number
|
||||
alpha: string
|
||||
colorType: ColorType
|
||||
}
|
||||
|
||||
export interface WidgetOptions extends PickerState {
|
||||
color: string
|
||||
}
|
||||
|
||||
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
|
||||
|
||||
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
|
||||
|
||||
export enum ColorType {
|
||||
hex = 'HEX',
|
||||
}
|
||||
|
||||
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
|
||||
|
||||
function discoverColorsInKCL(
|
||||
syntaxTree: Tree,
|
||||
from: number,
|
||||
to: number,
|
||||
typeName: string,
|
||||
doc: Text,
|
||||
language?: string
|
||||
): WidgetOptions | Array<WidgetOptions> | null {
|
||||
switch (typeName) {
|
||||
case 'Program':
|
||||
case 'VariableDeclaration':
|
||||
case 'CallExpressionKw':
|
||||
case 'ObjectExpression':
|
||||
case 'ObjectProperty':
|
||||
case 'ArgumentList':
|
||||
case 'PipeExpression': {
|
||||
let innerTree = syntaxTree.resolveInner(from, 0).tree
|
||||
|
||||
if (!innerTree) {
|
||||
innerTree = syntaxTree.resolveInner(from, 1).tree
|
||||
if (!innerTree) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
|
||||
|
||||
if (overlayTree?.type.name !== 'Styles') {
|
||||
return null
|
||||
}
|
||||
|
||||
const ret: Array<WidgetOptions> = []
|
||||
overlayTree.iterate({
|
||||
from: 0,
|
||||
to: overlayTree.length,
|
||||
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
|
||||
const maybeWidgetOptions = discoverColorsInKCL(
|
||||
syntaxTree,
|
||||
// We add one because the tree doesn't include the
|
||||
// quotation mark from the style tag
|
||||
from + 1 + overlayFrom,
|
||||
from + 1 + overlayTo,
|
||||
type.name,
|
||||
doc,
|
||||
language
|
||||
)
|
||||
|
||||
if (maybeWidgetOptions) {
|
||||
if (isArray(maybeWidgetOptions)) {
|
||||
console.error('Unexpected nested overlays')
|
||||
ret.push(...maybeWidgetOptions)
|
||||
} else {
|
||||
ret.push(maybeWidgetOptions)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
case 'String': {
|
||||
const result = parseColorLiteral(doc.sliceString(from, to))
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
from,
|
||||
to,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function parseColorLiteral(colorLiteral: string): ColorData | null {
|
||||
const literal = colorLiteral.replace(/"/g, '')
|
||||
const match = hexRegex.exec(literal)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
const [color, alpha] = toFullHex(literal)
|
||||
|
||||
return {
|
||||
colorType: ColorType.hex,
|
||||
color,
|
||||
alpha,
|
||||
}
|
||||
}
|
||||
|
||||
function colorPickersDecorations(
|
||||
view: EditorView,
|
||||
discoverColors: typeof discoverColorsInKCL
|
||||
) {
|
||||
const widgets: Array<Range<Decoration>> = []
|
||||
|
||||
const st = syntaxTree(view.state)
|
||||
|
||||
for (const range of view.visibleRanges) {
|
||||
st.iterate({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
enter: ({ type, from, to }) => {
|
||||
const maybeWidgetOptions = discoverColors(
|
||||
st,
|
||||
from,
|
||||
to,
|
||||
type.name,
|
||||
view.state.doc,
|
||||
view.state.facet(language)?.name
|
||||
)
|
||||
|
||||
if (!maybeWidgetOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isArray(maybeWidgetOptions)) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(maybeWidgetOptions),
|
||||
side: 1,
|
||||
}).range(maybeWidgetOptions.from)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const wo of maybeWidgetOptions) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(wo),
|
||||
side: 1,
|
||||
}).range(wo.from)
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return Decoration.set(widgets)
|
||||
}
|
||||
|
||||
function toFullHex(color: string): string[] {
|
||||
if (color.length === 4) {
|
||||
// 3-char hex
|
||||
return [
|
||||
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||
'',
|
||||
]
|
||||
}
|
||||
|
||||
if (color.length === 5) {
|
||||
// 4-char hex (alpha)
|
||||
return [
|
||||
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
|
||||
color[4].repeat(2),
|
||||
]
|
||||
}
|
||||
|
||||
if (color.length === 9) {
|
||||
// 8-char hex (alpha)
|
||||
return [`#${color.slice(1, -2)}`, color.slice(-2)]
|
||||
}
|
||||
|
||||
return [color, '']
|
||||
}
|
||||
|
||||
export const wrapperClassName = 'cm-css-color-picker-wrapper'
|
||||
|
||||
class ColorPickerWidget extends WidgetType {
|
||||
private readonly state: PickerState
|
||||
private readonly color: string
|
||||
|
||||
constructor({ color, ...state }: WidgetOptions) {
|
||||
super()
|
||||
this.state = state
|
||||
this.color = color
|
||||
}
|
||||
|
||||
eq(other: ColorPickerWidget) {
|
||||
return (
|
||||
other.state.colorType === this.state.colorType &&
|
||||
other.color === this.color &&
|
||||
other.state.from === this.state.from &&
|
||||
other.state.to === this.state.to &&
|
||||
other.state.alpha === this.state.alpha
|
||||
)
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const picker = document.createElement('input')
|
||||
pickerState.set(picker, this.state)
|
||||
picker.type = 'color'
|
||||
picker.value = this.color
|
||||
|
||||
const wrapper = document.createElement('span')
|
||||
wrapper.appendChild(picker)
|
||||
wrapper.className = wrapperClassName
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const colorPickerTheme = EditorView.baseTheme({
|
||||
[`.${wrapperClassName}`]: {
|
||||
display: 'inline-block',
|
||||
outline: '1px solid #eee',
|
||||
marginRight: '0.6ch',
|
||||
height: '1em',
|
||||
width: '1em',
|
||||
transform: 'translateY(1px)',
|
||||
},
|
||||
[`.${wrapperClassName} input[type="color"]`]: {
|
||||
cursor: 'pointer',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
'&::-webkit-color-swatch-wrapper': {
|
||||
padding: 0,
|
||||
},
|
||||
'&::-webkit-color-swatch': {
|
||||
border: 'none',
|
||||
},
|
||||
'&::-moz-color-swatch': {
|
||||
border: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
interface IFactoryOptions {
|
||||
discoverColors: typeof discoverColorsInKCL
|
||||
}
|
||||
|
||||
export const makeColorPicker = (options: IFactoryOptions) =>
|
||||
ViewPlugin.fromClass(
|
||||
class ColorPickerViewPlugin {
|
||||
decorations: DecorationSet
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = colorPickersDecorations(view, options.discoverColors)
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = colorPickersDecorations(
|
||||
update.view,
|
||||
options.discoverColors
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
eventHandlers: {
|
||||
change: (e, view) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (
|
||||
target.nodeName !== 'INPUT' ||
|
||||
!target.parentElement ||
|
||||
!target.parentElement.classList.contains(wrapperClassName)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const data = pickerState.get(target)!
|
||||
|
||||
let converted = '"' + target.value + data.alpha + '"'
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
insert: converted,
|
||||
},
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const colorPicker: Extension = [
|
||||
makeColorPicker({ discoverColors: discoverColorsInKCL }),
|
||||
colorPickerTheme,
|
||||
]
|
@ -7,6 +7,7 @@ import type {
|
||||
} from '@kittycad/codemirror-lsp-client'
|
||||
import {
|
||||
lspCodeActionEvent,
|
||||
lspColorUpdateEvent,
|
||||
lspFormatCodeEvent,
|
||||
lspPlugin,
|
||||
lspRenameEvent,
|
||||
@ -88,6 +89,8 @@ export class KclPlugin implements PluginValue {
|
||||
isRelevant = true
|
||||
} else if (tr.annotation(lspCodeActionEvent.type)) {
|
||||
isRelevant = true
|
||||
} else if (tr.annotation(lspColorUpdateEvent.type)) {
|
||||
isRelevant = true
|
||||
}
|
||||
|
||||
// Don't make this an else.
|
||||
|
@ -8,7 +8,6 @@ import type {
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
|
||||
import { kclPlugin } from '@src/editor/plugins/lsp/kcl'
|
||||
import { colorPicker } from '@src/editor/plugins/lsp/kcl/colors'
|
||||
|
||||
export interface LanguageOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[]
|
||||
@ -22,7 +21,6 @@ export interface LanguageOptions {
|
||||
|
||||
export function kcl(options: LanguageOptions) {
|
||||
return new LanguageSupport(KclLanguage, [
|
||||
colorPicker,
|
||||
kclPlugin({
|
||||
documentUri: options.documentUri,
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
|
Reference in New Issue
Block a user