Compare commits
1 Commits
kcl-71
...
lsp-colors
Author | SHA1 | Date | |
---|---|---|---|
86ae7a989f |
28
.github/workflows/e2e-tests.yml
vendored
28
.github/workflows/e2e-tests.yml
vendored
@ -40,7 +40,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Download Wasm cache
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: ${{ github.event_name != 'schedule' && steps.filter.outputs.rust == 'false' }}
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
@ -52,7 +52,7 @@ jobs:
|
||||
branch: main
|
||||
path: rust/kcl-wasm-lib/pkg
|
||||
|
||||
- name: Build Wasm condition
|
||||
- name: Build WASM condition
|
||||
id: wasm
|
||||
run: |
|
||||
set -euox pipefail
|
||||
@ -70,7 +70,7 @@ jobs:
|
||||
run: |
|
||||
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
|
||||
|
||||
- name: Install Rust
|
||||
- name: Install rust
|
||||
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
@ -81,7 +81,7 @@ jobs:
|
||||
with:
|
||||
tool: wasm-pack
|
||||
|
||||
- name: Use Rust cache
|
||||
- name: Rust Cache
|
||||
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@ -117,7 +117,7 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
name: prepared-wasm
|
||||
|
||||
- name: Copy prepared Wasm
|
||||
- name: Copy prepared wasm
|
||||
run: |
|
||||
ls -R prepared-wasm
|
||||
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
|
||||
@ -133,17 +133,20 @@ jobs:
|
||||
id: deps-install
|
||||
run: npm install
|
||||
|
||||
- name: Cache browsers
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install browsers
|
||||
- name: Install Playwright Browsers
|
||||
run: npm run playwright install --with-deps
|
||||
|
||||
- name: Capture snapshots
|
||||
- name: build web
|
||||
run: npm run tronb:vite:dev
|
||||
|
||||
- name: Run ubuntu/chrome snapshots
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
shell: bash
|
||||
@ -167,7 +170,7 @@ jobs:
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
- name: Check diff
|
||||
- name: Check for changes
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
shell: bash
|
||||
id: git-check
|
||||
@ -178,8 +181,9 @@ jobs:
|
||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit changes
|
||||
if: ${{ steps.git-check.outputs.modified == 'true' }}
|
||||
- name: Commit changes, if any
|
||||
# TODO: find a more reliable way to detect visual changes
|
||||
if: ${{ false && steps.git-check.outputs.modified == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
||||
@ -189,7 +193,7 @@ jobs:
|
||||
git fetch origin
|
||||
echo ${{ github.head_ref }}
|
||||
git checkout ${{ github.head_ref }}
|
||||
git commit --message "Update snapshots" || true
|
||||
git commit -m "A snapshot a day keeps the bugs away! 📷🐛" || true
|
||||
git push
|
||||
git push origin ${{ github.head_ref }}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -251786,7 +251786,7 @@
|
||||
}
|
||||
},
|
||||
"required": false,
|
||||
"description": "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to trajectoryCurve.",
|
||||
"description": "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to sketchPlane.",
|
||||
"labelRequired": true
|
||||
},
|
||||
{
|
||||
@ -256720,7 +256720,7 @@
|
||||
false
|
||||
],
|
||||
[
|
||||
"// Create a spring by sweeping around a helix path.\n\n// Create a helix around the Z axis.\nhelixPath = helix(\n angleStart = 0,\n ccw = true,\n revolutions = 4,\n length = 10,\n radius = 5,\n axis = Z,\n)\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn(YZ)\n |> circle(center = [0, 0], radius = 1)\n |> sweep(path = helixPath, relativeTo = \"sketchPlane\")",
|
||||
"// Create a spring by sweeping around a helix path.\n\n// Create a helix around the Z axis.\nhelixPath = helix(\n angleStart = 0,\n ccw = true,\n revolutions = 4,\n length = 10,\n radius = 5,\n axis = Z,\n)\n\n// Create a spring by sweeping around the helix path.\nspringSketch = startSketchOn(YZ)\n |> circle(center = [0, 0], radius = 1)\n |> sweep(path = helixPath)",
|
||||
false
|
||||
],
|
||||
[
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,3 +1,4 @@
|
||||
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
|
||||
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
||||
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
|
||||
@ -8,7 +9,6 @@ import {
|
||||
settingsToToml,
|
||||
} from '@e2e/playwright/test-utils'
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
// Make the user avatar image always 404
|
||||
@ -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(
|
||||
@ -873,56 +879,13 @@ sweepSketch = startSketchOn(XY)
|
||||
mask: lowerRightMasks(page),
|
||||
})
|
||||
})
|
||||
test('code color goober works with single quotes', async ({
|
||||
page,
|
||||
context,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Create a pipe using a sweep.
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
sweepSketch = startSketchOn(XY)
|
||||
|> startProfile(at = [2, 0])
|
||||
|> arc(angleStart = 0, angleEnd = 360, radius = 2)
|
||||
|> sweep(path = sweepPath)
|
||||
|> appearance(
|
||||
color = '#bb00ff',
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await expect(page, 'expect small color widget').toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: lowerRightMasks(page),
|
||||
})
|
||||
})
|
||||
|
||||
test('code color goober opening window', async ({
|
||||
page,
|
||||
context,
|
||||
scene,
|
||||
cmdBar,
|
||||
editor,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 132 KiB |
@ -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),
|
||||
|
20
rust/Cargo.lock
generated
20
rust/Cargo.lock
generated
@ -1815,7 +1815,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-bumper"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@ -1826,7 +1826,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-derive-docs"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -1845,7 +1845,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-directory-test-macro"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1854,7 +1854,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-language-server"
|
||||
version = "0.2.71"
|
||||
version = "0.2.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@ -1875,7 +1875,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-language-server-release"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@ -1895,7 +1895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.71"
|
||||
version = "0.2.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1971,7 +1971,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-python-bindings"
|
||||
version = "0.3.71"
|
||||
version = "0.3.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kcl-lib",
|
||||
@ -1986,7 +1986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper 0.14.32",
|
||||
@ -1999,7 +1999,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-to-core"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -2013,7 +2013,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-wasm-lib"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
[package]
|
||||
name = "kcl-bumper"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/KittyCAD/modeling-api"
|
||||
rust-version = "1.76"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-directory-test-macro"
|
||||
description = "A tool for generating tests from a directory of kcl files"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kcl-language-server-release"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
edition = "2021"
|
||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||
publish = false
|
||||
|
@ -2,7 +2,7 @@
|
||||
name = "kcl-language-server"
|
||||
description = "A language server for KCL."
|
||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||
version = "0.2.71"
|
||||
version = "0.2.70"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.71"
|
||||
version = "0.2.70"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -112,7 +112,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
/// // Create a spring by sweeping around the helix path.
|
||||
/// springSketch = startSketchOn(YZ)
|
||||
/// |> circle( center = [0, 0], radius = 1)
|
||||
/// |> sweep(path = helixPath, relativeTo = "sketchPlane")
|
||||
/// |> sweep(path = helixPath)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
@ -167,7 +167,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
path = { docs = "The path to sweep the sketch along" },
|
||||
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
|
||||
tolerance = { docs = "Tolerance for this operation" },
|
||||
relative_to = { docs = "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to trajectoryCurve."},
|
||||
relative_to = { docs = "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to sketchPlane."},
|
||||
tag_start = { docs = "A named tag for the face at the start of the sweep, i.e. the original sketch" },
|
||||
tag_end = { docs = "A named tag for the face at the end of the sweep" },
|
||||
},
|
||||
@ -191,13 +191,14 @@ async fn inner_sweep(
|
||||
};
|
||||
let relative_to = match relative_to.as_deref() {
|
||||
Some("sketchPlane") => RelativeTo::SketchPlane,
|
||||
Some("trajectoryCurve") | None => RelativeTo::TrajectoryCurve,
|
||||
Some("trajectoryCurve") => RelativeTo::TrajectoryCurve,
|
||||
Some(_) => {
|
||||
return Err(KclError::Syntax(crate::errors::KclErrorDetails {
|
||||
source_ranges: vec![args.source_range],
|
||||
message: "If you provide relativeTo, it must either be 'sketchPlane' or 'trajectoryCurve'".to_owned(),
|
||||
}))
|
||||
}
|
||||
None => RelativeTo::default(),
|
||||
};
|
||||
|
||||
let mut solids = Vec::new();
|
||||
|
@ -83,7 +83,7 @@ export END = 'end'
|
||||
/// // Create a spring by sweeping around the helix path.
|
||||
/// springSketch = startSketchOn(YZ)
|
||||
/// |> circle( center = [0, 0], radius = 0.5)
|
||||
/// |> sweep(path = helixPath, relativeTo = "sketchPlane")
|
||||
/// |> sweep(path = helixPath)
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
@ -104,7 +104,7 @@ export END = 'end'
|
||||
/// // Create a spring by sweeping around the helix path.
|
||||
/// springSketch = startSketchOn(XY)
|
||||
/// |> circle( center = [0, 0], radius = 0.5 )
|
||||
/// |> sweep(path = helixPath, relativeTo = "sketchPlane")
|
||||
/// |> sweep(path = helixPath)
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
@ -124,7 +124,7 @@ export END = 'end'
|
||||
/// // Create a spring by sweeping around the helix path.
|
||||
/// springSketch = startSketchOn(XY)
|
||||
/// |> circle( center = [0, 0], radius = 1 )
|
||||
/// |> sweep(path = helixPath, relativeTo = "sketchPlane")
|
||||
/// |> sweep(path = helixPath)
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
@ -413,7 +413,7 @@ export fn offsetPlane(
|
||||
/// // Create a spring by sweeping around the helix path.
|
||||
/// sweepedSpring = clone(springSketch)
|
||||
/// |> translate(x=100)
|
||||
/// |> sweep(path = helixPath, relativeTo = "sketchPlane")
|
||||
/// |> sweep(path = helixPath)
|
||||
/// ```
|
||||
///
|
||||
/// ```kcl
|
||||
|
@ -5122,7 +5122,7 @@ description: Artifact commands bench.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -5134,7 +5134,7 @@ description: Artifact commands bench.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -906,7 +906,7 @@ description: Artifact commands cold-plate.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -5576,7 +5576,7 @@ description: Artifact commands cpu-cooler.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -6111,7 +6111,7 @@ description: Artifact commands cpu-cooler.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -9469,7 +9469,7 @@ description: Artifact commands cpu-cooler.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -9601,7 +9601,7 @@ description: Artifact commands cpu-cooler.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -10120,7 +10120,7 @@ description: Artifact commands cpu-cooler.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -10252,7 +10252,7 @@ description: Artifact commands cpu-cooler.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1598,7 +1598,7 @@ description: Artifact commands exhaust-manifold.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -1610,7 +1610,7 @@ description: Artifact commands exhaust-manifold.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -1622,7 +1622,7 @@ description: Artifact commands exhaust-manifold.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -1634,7 +1634,7 @@ description: Artifact commands exhaust-manifold.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -4491,7 +4491,7 @@ description: Artifact commands utility-sink.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -418,7 +418,7 @@ description: Artifact commands subtract_regression03.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -395,7 +395,7 @@ description: Artifact commands subtract_regression05.kcl
|
||||
"trajectory": "[uuid]",
|
||||
"sectional": false,
|
||||
"tolerance": 0.0000001,
|
||||
"relative_to": "trajectory_curve"
|
||||
"relative_to": "sketch_plane"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kcl-python-bindings"
|
||||
version = "0.3.71"
|
||||
version = "0.3.70"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/kittycad/modeling-app"
|
||||
exclude = ["tests/*", "files/*", "venv/*"]
|
||||
|
@ -227,31 +227,6 @@ async fn new_context_state(current_file: Option<std::path::PathBuf>) -> Result<(
|
||||
Ok((ctx, state))
|
||||
}
|
||||
|
||||
/// Parse the kcl code from a file path.
|
||||
#[pyfunction]
|
||||
async fn parse(path: String) -> PyResult<()> {
|
||||
tokio()
|
||||
.spawn(async move {
|
||||
let (code, path) = get_code_and_file_path(&path)
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
let _program = kcl_lib::Program::parse_no_errs(&code)
|
||||
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
|
||||
}
|
||||
|
||||
/// Parse the kcl code.
|
||||
#[pyfunction]
|
||||
fn parse_code(code: String) -> PyResult<()> {
|
||||
let _program = kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the kcl code from a file path.
|
||||
#[pyfunction]
|
||||
async fn execute(path: String) -> PyResult<()> {
|
||||
@ -559,8 +534,6 @@ fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<Discovered>()?;
|
||||
|
||||
// Add our functions to the module.
|
||||
m.add_function(wrap_pyfunction!(parse, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(parse_code, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute_code, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute_and_snapshot, m)?)?;
|
||||
|
@ -39,33 +39,6 @@ async def test_kcl_execute():
|
||||
await kcl.execute(lego_file)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_parse_with_exception():
|
||||
# Read from a file.
|
||||
try:
|
||||
await kcl.parse(os.path.join(files_dir, "parse_file_error"))
|
||||
except Exception as e:
|
||||
assert e is not None
|
||||
assert len(str(e)) > 0
|
||||
assert "lksjndflsskjfnak;jfna##" in str(e)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_parse():
|
||||
# Read from a file.
|
||||
await kcl.parse(lego_file)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_parse_code():
|
||||
# Read from a file.
|
||||
with open(lego_file, "r") as f:
|
||||
code = str(f.read())
|
||||
assert code is not None
|
||||
assert len(code) > 0
|
||||
kcl.parse_code(code)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_code():
|
||||
# Read from a file.
|
||||
@ -124,7 +97,9 @@ async def test_kcl_execute_and_snapshot():
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_and_snapshot_dir():
|
||||
# Read from a file.
|
||||
image_bytes = await kcl.execute_and_snapshot(car_wheel_dir, kcl.ImageFormat.Jpeg)
|
||||
image_bytes = await kcl.execute_and_snapshot(
|
||||
car_wheel_dir, kcl.ImageFormat.Jpeg
|
||||
)
|
||||
assert image_bytes is not None
|
||||
assert len(image_bytes) > 0
|
||||
|
||||
@ -154,12 +129,10 @@ def test_kcl_format():
|
||||
assert formatted_code is not None
|
||||
assert len(formatted_code) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_format_dir():
|
||||
await kcl.format_dir(car_wheel_dir)
|
||||
|
||||
|
||||
def test_kcl_lint():
|
||||
# Read from a file.
|
||||
with open(os.path.join(files_dir, "box_with_linter_errors.kcl"), "r") as f:
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
description = "A test server for KCL"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-to-core"
|
||||
description = "Utility methods to convert kcl to engine core executable tests"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kcl-wasm-lib"
|
||||
version = "0.1.71"
|
||||
version = "0.1.70"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
rust-version = "1.83"
|
||||
|
@ -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, '').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,
|
||||
|
@ -212,12 +212,6 @@ code {
|
||||
z-index: 99999999999 !important;
|
||||
}
|
||||
|
||||
.cm-rename-popup input {
|
||||
/* use black text on white background in both light and dark mode */
|
||||
color: black !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
|
Reference in New Issue
Block a user