Compare commits

...

9 Commits

Author SHA1 Message Date
d68d7a7e00 Cut release v0.15.1 (#1452)
cut release v0.15.1
2024-02-20 08:10:26 +11:00
b135b97de6 Code mirror plugin lsp interface (#1444)
* better named dirs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* move some stuff around

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more logging

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* less logging

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add fs in

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* file reader

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* workspace

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of workspace folders

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of workspace folders

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup workspace folders

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup logs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-02-19 12:33:16 -08:00
de5885ce0b Enable/disable "start sketch", "edit sketch" and "extrude" appropriately (#1449)
* test that fails for when to enable extrude and sketch features

* add fix to make test pass

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-19 17:23:03 +11:00
ad7c544754 draft line snapshots (#1445)
* draft line snapshots

Make sure they don't get broken at some point, visual regression is only way to test these really

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-19 13:18:31 +11:00
4d77875bdc mouse listners should be reset outside of sketch (#1442)
* mouse listners should be reset outside of sketch (only orbit controls are needed) and also check mouse button

* tweak
2024-02-19 12:41:36 +11:00
3377923dcb fix flacky auto complete test (#1443) 2024-02-19 12:15:57 +11:00
c6005660c8 jsxify svgs (#1441) 2024-02-19 10:20:02 +11:00
66e62c6037 cancel execution on file change (#1440) 2024-02-19 09:23:18 +11:00
0a4a517bb4 try arm latest (#1439) 2024-02-17 22:12:39 -08:00
49 changed files with 706 additions and 228 deletions

View File

@ -79,7 +79,7 @@ jobs:
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-latest runs-on: macos-14
needs: playwright-ubuntu needs: playwright-ubuntu
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@ -68,7 +68,7 @@ test('Basic sketch', async ({ page }) => {
`const part001 = startSketchOn('-XZ')` `const part001 = startSketchOn('-XZ')`
) )
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
@ -367,6 +367,7 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.type(' |> startProfi') await page.keyboard.type(' |> startProfi')
// expect there be a single auto complete option that we can just hit enter on // expect there be a single auto complete option that we can just hit enter on
await expect(page.locator('.cm-completionLabel')).toBeVisible() await expect(page.locator('.cm-completionLabel')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accepting the auto complete, not a new line await page.keyboard.press('Enter') // accepting the auto complete, not a new line
await page.keyboard.type('([0,0], %)') await page.keyboard.type('([0,0], %)')
@ -576,7 +577,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.waitForTimeout(200) await page.waitForTimeout(200)
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(700) // wait for animation await page.waitForTimeout(700) // wait for animation
// hover again and check it works // hover again and check it works
@ -811,7 +812,7 @@ const part002 = startSketchOn('XY')
test('ProgramMemory can be serialised', async ({ page, context }) => { test('ProgramMemory can be serialised', async ({ page, context }) => {
const u = getUtils(page) const u = getUtils(page)
await context.addInitScript(async (token) => { await context.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`const part = startSketchOn('XY') `const part = startSketchOn('XY')
@ -847,3 +848,103 @@ test('ProgramMemory can be serialised', async ({ page, context }) => {
}) })
}) })
}) })
test('edit selections', async ({ page, context }) => {
const u = getUtils(page)
const selectionsSnippets = {
extrudeAndEditBlocked: '|> startProfileAt([10.81, 32.99], %)',
extrudeAndEditBlockedInFunction: '|> startProfileAt(pos, %)',
extrudeAndEditAllowed: '|> startProfileAt([15.72, 4.7], %)',
editOnly: '|> startProfileAt([15.79, -14.6], %)',
}
await context.addInitScript(
async ({
extrudeAndEditBlocked,
extrudeAndEditBlockedInFunction,
extrudeAndEditAllowed,
editOnly,
}: any) => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
${extrudeAndEditBlocked}
|> line([25.96, 2.93], %)
|> line([5.25, -5.72], %)
|> line([-2.01, -10.35], %)
|> line([-27.65, -2.78], %)
|> close(%)
|> extrude(5, %)
const part002 = startSketchOn('-XZ')
${extrudeAndEditAllowed}
|> line([10.32, 6.47], %)
|> line([9.71, -6.16], %)
|> line([-3.08, -9.86], %)
|> line([-12.02, -1.54], %)
|> close(%)
const part003 = startSketchOn('-XZ')
${editOnly}
|> line([27.55, -1.65], %)
|> line([4.95, -8], %)
|> line([-20.38, -10.12], %)
|> line([-15.79, 17.08], %)
fn yohey = (pos) => {
const part004 = startSketchOn('-XZ')
${extrudeAndEditBlockedInFunction}
|> line([27.55, -1.65], %)
|> line([4.95, -10.53], %)
|> line([-20.38, -8], %)
|> line([-15.79, 17.08], %)
return ''
}
yohey([15.79, -34.6])
`
)
},
selectionsSnippets
)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeVisible()
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeDisabled()
await page.getByText(selectionsSnippets.editOnly).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeDisabled()
await page
.getByText(selectionsSnippets.extrudeAndEditBlockedInFunction)
.click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeVisible()
// selecting an editable sketch but clicking "start sktech" should start a new sketch and not edit the existing one
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.click(700, 200)
// expect main content to contain `part005` i.e. started a new sketch
await expect(page.locator('.cm-content')).toHaveText(
/part005 = startSketchOn\('-XZ'\)/
)
})

View File

@ -419,3 +419,61 @@ test('extrude on each default plane should be stable', async ({
await runSnapshotsForOtherPlanes('YZ') await runSnapshotsForOtherPlanes('YZ')
await runSnapshotsForOtherPlanes('-YZ') await runSnapshotsForOtherPlanes('-YZ')
}) })
test('Draft segments should look right', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.89, -32.23]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.move(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 24.11
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.15.0", "version": "0.15.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.10.2", "@codemirror/autocomplete": "^6.10.2",

View File

@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "zoo-modeling-app", "productName": "zoo-modeling-app",
"version": "0.15.0" "version": "0.15.1"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -5,6 +5,8 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { kclManager } from 'lang/KclSingleton'
export const Toolbar = () => { export const Toolbar = () => {
const platform = usePlatform() const platform = usePlatform()
@ -13,14 +15,15 @@ export const Toolbar = () => {
const toolbarButtonsRef = useRef<HTMLUListElement>(null) const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const bgClassName = const bgClassName =
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80' 'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
const pathId = useMemo( const pathId = useMemo(() => {
() => if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
isCursorInSketchCommandRange( return false
}
return isCursorInSketchCommandRange(
engineCommandManager.artifactMap, engineCommandManager.artifactMap,
context.selectionRanges context.selectionRanges
),
[engineCommandManager.artifactMap, context.selectionRanges]
) )
}, [engineCommandManager.artifactMap, context.selectionRanges])
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) { function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current const span = toolbarButtonsRef.current
@ -50,7 +53,9 @@ export const Toolbar = () => {
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => send({ type: 'Enter sketch' })} onClick={() =>
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
}
icon={{ icon={{
icon: 'sketch', icon: 'sketch',
bgClassName, bgClassName,

View File

@ -332,6 +332,7 @@ class SceneEntities {
if (!draftSegment) { if (!draftSegment) {
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onDrag: (args) => { onDrag: (args) => {
if (args.event.which !== 1) return
this.onDragSegment({ this.onDragSegment({
...args, ...args,
sketchPathToNode, sketchPathToNode,
@ -339,6 +340,7 @@ class SceneEntities {
}, },
onMove: () => {}, onMove: () => {},
onClick: (args) => { onClick: (args) => {
if (args?.event.which !== 1) return
if (!args || !args.object) { if (!args || !args.object) {
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'Set selection', type: 'Set selection',
@ -396,6 +398,7 @@ class SceneEntities {
onDrag: () => {}, onDrag: () => {},
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.event.which !== 1) return
const { intersection2d } = args const { intersection2d } = args
if (!intersection2d) return if (!intersection2d) return
@ -792,6 +795,7 @@ class SceneEntities {
}, },
onClick: (args) => { onClick: (args) => {
if (!args || !args.object) return if (!args || !args.object) return
if (args.event.which !== 1) return
const { object, intersection } = args const { object, intersection } = args
const type = object?.userData?.type || '' const type = object?.userData?.type || ''
const posNorm = Number(intersection.normal?.z) > 0 const posNorm = Number(intersection.normal?.z) > 0

View File

@ -121,8 +121,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM15.6855 11.5L13.2101 14.8005L12.2071 13.7975L11.5 14.5046L12.9107 15.9153L13.6642 15.8617L16.4855 12.1L15.6855 11.5Z" d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM15.6855 11.5L13.2101 14.8005L12.2071 13.7975L11.5 14.5046L12.9107 15.9153L13.6642 15.8617L16.4855 12.1L15.6855 11.5Z"
fill="currentColor" fill="currentColor"
/> />
@ -137,8 +137,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
fill="currentColor" fill="currentColor"
/> />
@ -295,8 +295,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M14 10.5H6V9.5H14V10.5Z" d="M14 10.5H6V9.5H14V10.5Z"
fill="currentColor" fill="currentColor"
/> />
@ -343,8 +343,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M18 9.64741C17.1925 8.24871 16.0344 7.08457 14.6399 6.26971C13.2455 5.45486 11.6628 5.01742 10.0478 5.00051C8.4328 4.9836 6.84127 5.38779 5.43006 6.17326C4.01884 6.95873 2.83666 8.09837 2 9.47985L2.76881 9.94546C3.52456 8.69756 4.59243 7.66813 5.86718 6.95862C7.14193 6.2491 8.57955 5.88399 10.0384 5.89927C11.4972 5.91455 12.9269 6.30968 14.1865 7.04574C15.4461 7.7818 16.4922 8.83337 17.2216 10.0968L18 9.64741ZM15.2155 11.0953C14.6772 10.1628 13.9051 9.3867 12.9755 8.84347C12.0459 8.30023 10.9907 8.00861 9.91406 7.99733C8.8374 7.98606 7.77638 8.25552 6.83557 8.77917C5.89476 9.30281 5.10664 10.0626 4.54887 10.9836L5.34391 11.4651C5.81802 10.6822 6.48792 10.0364 7.28761 9.59132C8.0873 9.14622 8.98916 8.91718 9.90432 8.92676C10.8195 8.93635 11.7164 9.18423 12.5065 9.64598C13.2967 10.1077 13.953 10.7674 14.4106 11.56L15.2155 11.0953ZM10 14C10.8284 14 11.5 13.3284 11.5 12.5C11.5 11.6716 10.8284 11 10 11C9.17157 11 8.5 11.6716 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z" d="M18 9.64741C17.1925 8.24871 16.0344 7.08457 14.6399 6.26971C13.2455 5.45486 11.6628 5.01742 10.0478 5.00051C8.4328 4.9836 6.84127 5.38779 5.43006 6.17326C4.01884 6.95873 2.83666 8.09837 2 9.47985L2.76881 9.94546C3.52456 8.69756 4.59243 7.66813 5.86718 6.95862C7.14193 6.2491 8.57955 5.88399 10.0384 5.89927C11.4972 5.91455 12.9269 6.30968 14.1865 7.04574C15.4461 7.7818 16.4922 8.83337 17.2216 10.0968L18 9.64741ZM15.2155 11.0953C14.6772 10.1628 13.9051 9.3867 12.9755 8.84347C12.0459 8.30023 10.9907 8.00861 9.91406 7.99733C8.8374 7.98606 7.77638 8.25552 6.83557 8.77917C5.89476 9.30281 5.10664 10.0626 4.54887 10.9836L5.34391 11.4651C5.81802 10.6822 6.48792 10.0364 7.28761 9.59132C8.0873 9.14622 8.98916 8.91718 9.90432 8.92676C10.8195 8.93635 11.7164 9.18423 12.5065 9.64598C13.2967 10.1077 13.953 10.7674 14.4106 11.56L15.2155 11.0953ZM10 14C10.8284 14 11.5 13.3284 11.5 12.5C11.5 11.6716 10.8284 11 10 11C9.17157 11 8.5 11.6716 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
fill="currentColor" fill="currentColor"
/> />
@ -359,8 +359,8 @@ export const CustomIcon = ({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M4.35352 5.39647L14.253 15.296L14.9601 14.5889L5.06062 4.68936L4.35352 5.39647ZM12.5065 9.64599C11.9609 9.32713 11.3643 9.11025 10.746 9.00341L9.74058 7.99796C9.79835 7.99694 9.85618 7.99674 9.91406 7.99735C10.9907 8.00862 12.0459 8.30025 12.9755 8.84348C13.9051 9.38672 14.6772 10.1628 15.2155 11.0953L14.4106 11.56C13.953 10.7674 13.2967 10.1077 12.5065 9.64599ZM6.48788 8.98789L7.16295 9.66297C6.41824 10.1045 5.79317 10.7233 5.34391 11.4651L4.54887 10.9836C5.03646 10.1785 5.70009 9.49656 6.48788 8.98789ZM10.0384 5.89928C9.3134 5.89169 8.59366 5.97804 7.89655 6.15392L7.16867 5.42605C8.09637 5.13507 9.06776 4.99026 10.0478 5.00052C11.6628 5.01744 13.2455 5.45488 14.6399 6.26973C16.0344 7.08458 17.1925 8.24872 18 9.64742L17.2216 10.0968C16.4922 8.83338 15.4461 7.78181 14.1865 7.04575C12.9269 6.3097 11.4972 5.91456 10.0384 5.89928ZM5.00782 7.50783L4.36522 6.86524C3.42033 7.57557 2.61639 8.46208 2 9.47986L2.76881 9.94547C3.34775 8.98952 4.10986 8.16177 5.00782 7.50783ZM10 14C10.4142 14 10.7892 13.8321 11.0607 13.5607L8.93934 11.4394C8.66789 11.7108 8.5 12.0858 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z" d="M4.35352 5.39647L14.253 15.296L14.9601 14.5889L5.06062 4.68936L4.35352 5.39647ZM12.5065 9.64599C11.9609 9.32713 11.3643 9.11025 10.746 9.00341L9.74058 7.99796C9.79835 7.99694 9.85618 7.99674 9.91406 7.99735C10.9907 8.00862 12.0459 8.30025 12.9755 8.84348C13.9051 9.38672 14.6772 10.1628 15.2155 11.0953L14.4106 11.56C13.953 10.7674 13.2967 10.1077 12.5065 9.64599ZM6.48788 8.98789L7.16295 9.66297C6.41824 10.1045 5.79317 10.7233 5.34391 11.4651L4.54887 10.9836C5.03646 10.1785 5.70009 9.49656 6.48788 8.98789ZM10.0384 5.89928C9.3134 5.89169 8.59366 5.97804 7.89655 6.15392L7.16867 5.42605C8.09637 5.13507 9.06776 4.99026 10.0478 5.00052C11.6628 5.01744 13.2455 5.45488 14.6399 6.26973C16.0344 7.08458 17.1925 8.24872 18 9.64742L17.2216 10.0968C16.4922 8.83338 15.4461 7.78181 14.1865 7.04575C12.9269 6.3097 11.4972 5.91456 10.0384 5.89928ZM5.00782 7.50783L4.36522 6.86524C3.42033 7.57557 2.61639 8.46208 2 9.47986L2.76881 9.94547C3.34775 8.98952 4.10986 8.16177 5.00782 7.50783ZM10 14C10.4142 14 10.7892 13.8321 11.0607 13.5607L8.93934 11.4394C8.66789 11.7108 8.5 12.0858 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
fill="currentColor" fill="currentColor"
/> />

View File

@ -37,6 +37,7 @@ import { sceneInfra } from 'clientSideScene/sceneInfra'
import { getSketchQuaternion } from 'clientSideScene/sceneEntities' import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
import { startSketchOnDefault } from 'lang/modifyAst' import { startSketchOnDefault } from 'lang/modifyAst'
import { Program } from 'lang/wasm' import { Program } from 'lang/wasm'
import { isSingleCursorInPipe } from 'lang/queryAst'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -182,7 +183,10 @@ export const ModelingMachineProvider = ({
return canExtrudeSelection(selectionRanges) return canExtrudeSelection(selectionRanges)
}, },
'Selection is one face': ({ selectionRanges }) => { 'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
return false
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactMap, engineCommandManager.artifactMap,
selectionRanges selectionRanges
@ -199,6 +203,7 @@ export const ModelingMachineProvider = ({
await kclManager.executeAstMock(newAst, { updates: 'code' }) await kclManager.executeAstMock(newAst, { updates: 'code' })
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: () => {}, onClick: () => {},
onDrag: () => {},
}) })
}, },
'animate-to-face': async (_, { data: { plane, normal } }) => { 'animate-to-face': async (_, { data: { plane, normal } }) => {

View File

@ -3,9 +3,9 @@ import ReactCodeMirror, {
ViewUpdate, ViewUpdate,
keymap, keymap,
} from '@uiw/react-codemirror' } from '@uiw/react-codemirror'
import { FromServer, IntoServer } from 'editor/lsp/codec' import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Server from '../editor/lsp/server' import Server from '../editor/plugins/lsp/server'
import Client from '../editor/lsp/client' import Client from '../editor/plugins/lsp/client'
import { TEST } from 'env' import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
@ -15,8 +15,8 @@ import { useMemo, useRef } from 'react'
import { linter, lintGutter } from '@codemirror/lint' import { linter, lintGutter } from '@codemirror/lint'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections' import { processCodeMirrorRanges } from 'lib/selections'
import { LanguageServerClient } from 'editor/lsp' import { LanguageServerClient } from 'editor/plugins/lsp'
import kclLanguage from 'editor/lsp/language' import kclLanguage from 'editor/plugins/lsp/kcl/language'
import { EditorView, lineHighlightField } from 'editor/highlightextension' import { EditorView, lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors' import { kclErrToDiagnostic } from 'lang/errors'
@ -27,7 +27,9 @@ import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { sceneInfra } from 'clientSideScene/sceneInfra' import { sceneInfra } from 'clientSideScene/sceneInfra'
import { copilotBundle } from 'editor/copilot' import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { isTauri } from 'lib/isTauri'
import type * as LSP from 'vscode-languageserver-protocol'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -40,6 +42,15 @@ export const editorShortcutMeta = {
}, },
} }
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
// We only use workspace folders in Tauri since that is where we use more than
// one file.
if (isTauri()) {
return [{ uri: 'file://', name: 'ProjectRoot' }]
}
return []
}
export const TextEditor = ({ export const TextEditor = ({
theme, theme,
}: { }: {
@ -91,7 +102,7 @@ export const TextEditor = ({
}) })
} }
const lspClient = new LanguageServerClient({ client }) const lspClient = new LanguageServerClient({ client, name: 'kcl' })
return { lspClient } return { lspClient }
}, [setIsKclLspServerReady]) }, [setIsKclLspServerReady])
@ -107,7 +118,7 @@ export const TextEditor = ({
const lsp = kclLanguage({ const lsp = kclLanguage({
// When we have more than one file, we'll need to change this. // When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`, documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null, workspaceFolders: getWorkspaceFolders(),
client: kclLspClient, client: kclLspClient,
}) })
@ -128,7 +139,7 @@ export const TextEditor = ({
}) })
} }
const lspClient = new LanguageServerClient({ client }) const lspClient = new LanguageServerClient({ client, name: 'copilot' })
return { lspClient } return { lspClient }
}, [setIsCopilotLspServerReady]) }, [setIsCopilotLspServerReady])
@ -141,10 +152,10 @@ export const TextEditor = ({
let plugin = null let plugin = null
if (isCopilotLspServerReady && !TEST) { if (isCopilotLspServerReady && !TEST) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = copilotBundle({ const lsp = copilotPlugin({
// When we have more than one file, we'll need to change this. // When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`, documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: null, workspaceFolders: getWorkspaceFolders(),
client: copilotLspClient, client: copilotLspClient,
allowHTMLContent: true, allowHTMLContent: true,
}) })

View File

@ -65,6 +65,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
afterInitializedHooks: (() => Promise<void>)[] = [] afterInitializedHooks: (() => Promise<void>)[] = []
#fromServer: FromServer #fromServer: FromServer
private serverCapabilities: LSP.ServerCapabilities<any> = {} private serverCapabilities: LSP.ServerCapabilities<any> = {}
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
constructor(fromServer: FromServer, intoServer: IntoServer) { constructor(fromServer: FromServer, intoServer: IntoServer) {
super( super(
@ -167,9 +168,15 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
return this.serverCapabilities return this.serverCapabilities
} }
setNotifyFn(fn: (message: LSP.NotificationMessage) => void): void {
this.notifyFn = fn
}
async processNotifications(): Promise<void> { async processNotifications(): Promise<void> {
for await (const notification of this.#fromServer.notifications) { for await (const notification of this.#fromServer.notifications) {
await this.receiveAndSend(notification) if (this.notifyFn) {
this.notifyFn(notification)
}
} }
} }

View File

@ -11,30 +11,20 @@ import {
Annotation, Annotation,
EditorState, EditorState,
Extension, Extension,
Facet,
Prec, Prec,
StateEffect, StateEffect,
StateField, StateField,
Transaction, Transaction,
} from '@codemirror/state' } from '@codemirror/state'
import { completionStatus } from '@codemirror/autocomplete' import { completionStatus } from '@codemirror/autocomplete'
import { docPathFacet, offsetToPos, posToOffset } from 'editor/lsp/util' import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
import { LanguageServerPlugin } from 'editor/lsp/plugin' import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
import { LanguageServerOptions } from 'editor/lsp/plugin' import {
import { LanguageServerClient } from 'editor/lsp' LanguageServerPlugin,
documentUri,
// Create Facet for the current docPath languageId,
export const docPath = Facet.define<string, string>({ workspaceFolders,
combine(value: readonly string[]) { } from 'editor/plugins/lsp/plugin'
return value[value.length - 1]
},
})
export const relDocPath = Facet.define<string, string>({
combine(value: readonly string[]) {
return value[value.length - 1]
},
})
const ghostMark = Decoration.mark({ class: 'cm-ghostText' }) const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
@ -361,9 +351,9 @@ const completionRequester = (client: LanguageServerClient) => {
const pos = state.selection.main.head const pos = state.selection.main.head
const source = state.doc.toString() const source = state.doc.toString()
const path = state.facet(docPath) const dUri = state.facet(documentUri)
const relativePath = state.facet(relDocPath) const path = dUri.split('/').pop()!
const languageId = 'kcl' const relativePath = dUri.replace('file://', '')
// Set a new timeout to request completion // Set a new timeout to request completion
timeout = setTimeout(async () => { timeout = setTimeout(async () => {
@ -378,9 +368,9 @@ const completionRequester = (client: LanguageServerClient) => {
indentSize: 1, indentSize: 1,
insertSpaces: true, insertSpaces: true,
path, path,
uri: `file://${path}`, uri: dUri,
relativePath, relativePath,
languageId, languageId: state.facet(languageId),
position: offsetToPos(state.doc, pos), position: offsetToPos(state.doc, pos),
}, },
}) })
@ -483,21 +473,24 @@ const completionRequester = (client: LanguageServerClient) => {
}) })
} }
export function copilotServer(options: LanguageServerOptions) { export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: LanguageServerPlugin let plugin: LanguageServerPlugin | null = null
return ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
)
}
export const copilotBundle = (options: LanguageServerOptions): Extension => [ return [
docPath.of(options.documentUri.split('/').pop()!), documentUri.of(options.documentUri),
docPathFacet.of(options.documentUri.split('/').pop()!), languageId.of('kcl'),
relDocPath.of(options.documentUri.replace('file://', '')), workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
),
completionDecoration, completionDecoration,
Prec.highest(completionPlugin(options.client)), Prec.highest(completionPlugin(options.client)),
Prec.highest(viewCompletionPlugin(options.client)), Prec.highest(viewCompletionPlugin(options.client)),
completionRequester(options.client), completionRequester(options.client),
copilotServer(options),
] ]
}

View File

@ -1,7 +1,7 @@
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client' import Client from './client'
import { LanguageServerPlugin } from './plugin' import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
import { SemanticToken, deserializeTokens } from './semantic_tokens' import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
export interface CopilotGetCompletionsParams { export interface CopilotGetCompletionsParams {
doc: { doc: {
@ -90,26 +90,22 @@ interface LSPNotifyMap {
'textDocument/didOpen': LSP.DidOpenTextDocumentParams 'textDocument/didOpen': LSP.DidOpenTextDocumentParams
} }
// Server to client
interface LSPEventMap {
'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams
}
export type Notification = {
[key in keyof LSPEventMap]: {
jsonrpc: '2.0'
id?: null | undefined
method: key
params: LSPEventMap[key]
}
}[keyof LSPEventMap]
export interface LanguageServerClientOptions { export interface LanguageServerClientOptions {
client: Client client: Client
name: string
}
export interface LanguageServerOptions {
// We assume this is the main project directory, we are currently working in.
workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
} }
export class LanguageServerClient { export class LanguageServerClient {
private client: Client private client: Client
private name: string
public ready: boolean public ready: boolean
@ -124,6 +120,7 @@ export class LanguageServerClient {
constructor(options: LanguageServerClientOptions) { constructor(options: LanguageServerClientOptions) {
this.plugins = [] this.plugins = []
this.client = options.client this.client = options.client
this.name = options.name
this.ready = false this.ready = false
@ -133,11 +130,16 @@ export class LanguageServerClient {
async initialize() { async initialize() {
// Start the client in the background. // Start the client in the background.
this.client.setNotifyFn(this.processNotifications.bind(this))
this.client.start() this.client.start()
this.ready = true this.ready = true
} }
getName(): string {
return this.name
}
getServerCapabilities(): LSP.ServerCapabilities<any> { getServerCapabilities(): LSP.ServerCapabilities<any> {
return this.client.getServerCapabilities() return this.client.getServerCapabilities()
} }
@ -156,6 +158,11 @@ export class LanguageServerClient {
} }
async updateSemanticTokens(uri: string) { async updateSemanticTokens(uri: string) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) {
return
}
// Make sure we can only run, if we aren't already running. // Make sure we can only run, if we aren't already running.
if (!this.isUpdatingSemanticTokens) { if (!this.isUpdatingSemanticTokens) {
this.isUpdatingSemanticTokens = true this.isUpdatingSemanticTokens = true
@ -180,10 +187,18 @@ export class LanguageServerClient {
} }
async textDocumentHover(params: LSP.HoverParams) { async textDocumentHover(params: LSP.HoverParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.hoverProvider) {
return
}
return await this.request('textDocument/hover', params) return await this.request('textDocument/hover', params)
} }
async textDocumentCompletion(params: LSP.CompletionParams) { async textDocumentCompletion(params: LSP.CompletionParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.completionProvider) {
return
}
return await this.request('textDocument/completion', params) return await this.request('textDocument/completion', params)
} }
@ -234,11 +249,12 @@ export class LanguageServerClient {
async acceptCompletion(params: CopilotAcceptCompletionParams) { async acceptCompletion(params: CopilotAcceptCompletionParams) {
return await this.request('notifyAccepted', params) return await this.request('notifyAccepted', params)
} }
async rejectCompletions(params: CopilotRejectCompletionParams) { async rejectCompletions(params: CopilotRejectCompletionParams) {
return await this.request('notifyRejected', params) return await this.request('notifyRejected', params)
} }
private processNotification(notification: Notification) { private processNotifications(notification: LSP.NotificationMessage) {
for (const plugin of this.plugins) plugin.processNotification(notification) for (const plugin of this.plugins) plugin.processNotification(notification)
} }
} }

View File

@ -0,0 +1,75 @@
import { autocompletion } from '@codemirror/autocomplete'
import { Extension } from '@codemirror/state'
import { ViewPlugin, hoverTooltip, tooltips } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { offsetToPos } from 'editor/plugins/lsp/util'
import { LanguageServerOptions } from 'editor/plugins/lsp'
import {
LanguageServerPlugin,
documentUri,
languageId,
workspaceFolders,
} from 'editor/plugins/lsp/plugin'
export function kclPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null
return [
documentUri.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
null
),
tooltips({
position: 'absolute',
}),
autocompletion({
override: [
async (context) => {
if (plugin == null) return null
const { state, pos, explicit } = context
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
]
}

View File

@ -5,8 +5,8 @@ import {
defineLanguageFacet, defineLanguageFacet,
LanguageSupport, LanguageSupport,
} from '@codemirror/language' } from '@codemirror/language'
import { LanguageServerClient } from '.' import { LanguageServerClient } from 'editor/plugins/lsp'
import { kclPlugin } from './plugin' import { kclPlugin } from '.'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { parser as jsParser } from '@lezer/javascript' import { parser as jsParser } from '@lezer/javascript'
import { EditorState } from '@uiw/react-codemirror' import { EditorState } from '@uiw/react-codemirror'
@ -14,7 +14,7 @@ import { EditorState } from '@uiw/react-codemirror'
const data = defineLanguageFacet({}) const data = defineLanguageFacet({})
export interface LanguageOptions { export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[] | null workspaceFolders: LSP.WorkspaceFolder[]
documentUri: string documentUri: string
client: LanguageServerClient client: LanguageServerClient
} }

View File

@ -9,8 +9,8 @@ import {
NodeType, NodeType,
NodeSet, NodeSet,
} from '@lezer/common' } from '@lezer/common'
import { LanguageServerClient } from '.' import { LanguageServerClient } from 'editor/plugins/lsp'
import { posToOffset } from 'editor/lsp/util' import { posToOffset } from 'editor/plugins/lsp/util'
import { SemanticToken } from './semantic_tokens' import { SemanticToken } from './semantic_tokens'
import { DocInput } from '@codemirror/language' import { DocInput } from '@codemirror/language'
import { tags, styleTags } from '@lezer/highlight' import { tags, styleTags } from '@lezer/highlight'

View File

@ -1,13 +1,7 @@
import { autocompletion, completeFromList } from '@codemirror/autocomplete' import { completeFromList } from '@codemirror/autocomplete'
import { setDiagnostics } from '@codemirror/lint' import { setDiagnostics } from '@codemirror/lint'
import { Facet } from '@codemirror/state' import { Facet } from '@codemirror/state'
import { import { EditorView, Tooltip } from '@codemirror/view'
EditorView,
ViewPlugin,
Tooltip,
hoverTooltip,
tooltips,
} from '@codemirror/view'
import { import {
DiagnosticSeverity, DiagnosticSeverity,
CompletionItemKind, CompletionItemKind,
@ -23,9 +17,17 @@ import type {
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
import type { ViewUpdate, PluginValue } from '@codemirror/view' import type { ViewUpdate, PluginValue } from '@codemirror/view'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { LanguageServerClient, Notification } from '.' import { LanguageServerClient } from 'editor/plugins/lsp'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { offsetToPos, posToOffset } from 'editor/lsp/util' import { posToOffset } from 'editor/plugins/lsp/util'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const documentUri = Facet.define<string, string>({ combine: useLast })
export const languageId = Facet.define<string, string>({ combine: useLast })
export const workspaceFolders = Facet.define<
LSP.WorkspaceFolder[],
LSP.WorkspaceFolder[]
>({ combine: useLast })
const changesDelay = 500 const changesDelay = 500
@ -33,31 +35,22 @@ const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
) as Record<CompletionItemKind, string> ) as Record<CompletionItemKind, string>
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
const documentUri = Facet.define<string, string>({ combine: useLast })
const languageId = Facet.define<string, string>({ combine: useLast })
const client = Facet.define<LanguageServerClient, LanguageServerClient>({
combine: useLast,
})
export interface LanguageServerOptions {
workspaceFolders: LSP.WorkspaceFolder[] | null
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
}
export class LanguageServerPlugin implements PluginValue { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
private documentUri: string private documentUri: string
private languageId: string private languageId: string
private workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
constructor(private view: EditorView, private allowHTMLContent: boolean) { constructor(
this.client = this.view.state.facet(client) client: LanguageServerClient,
private view: EditorView,
private allowHTMLContent: boolean
) {
this.client = client
this.documentUri = this.view.state.facet(documentUri) this.documentUri = this.view.state.facet(documentUri)
this.languageId = this.view.state.facet(languageId) this.languageId = this.view.state.facet(languageId)
this.workspaceFolders = this.view.state.facet(workspaceFolders)
this.documentVersion = 0 this.documentVersion = 0
this.client.attachPlugin(this) this.client.attachPlugin(this)
@ -238,11 +231,28 @@ export class LanguageServerPlugin implements PluginValue {
return completeFromList(options)(context) return completeFromList(options)(context)
} }
processNotification(notification: Notification) { processNotification(notification: LSP.NotificationMessage) {
try { try {
switch (notification.method) { switch (notification.method) {
case 'textDocument/publishDiagnostics': case 'textDocument/publishDiagnostics':
this.processDiagnostics(notification.params) this.processDiagnostics(
notification.params as PublishDiagnosticsParams
)
break
case 'window/logMessage':
console.log(
'[lsp] [window/logMessage]',
this.client.getName(),
notification.params
)
break
case 'window/showMessage':
console.log(
'[lsp] [window/showMessage]',
this.client.getName(),
notification.params
)
break
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -284,65 +294,6 @@ export class LanguageServerPlugin implements PluginValue {
} }
} }
export function kclPlugin(options: LanguageServerOptions) {
let plugin: LanguageServerPlugin | null = null
return [
client.of(options.client),
documentUri.of(options.documentUri),
languageId.of('kcl'),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
null
),
tooltips({
position: 'absolute',
}),
autocompletion({
override: [
async (context) => {
if (plugin == null) return null
const { state, pos, explicit } = context
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
]
}
function formatContents( function formatContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string { ): string {

View File

@ -34,6 +34,8 @@ const ServerCapabilitiesProviders: IMethodServerCapabilityProviderDictionary = {
'textDocument/foldingRange': 'foldingRangeProvider', 'textDocument/foldingRange': 'foldingRangeProvider',
'textDocument/declaration': 'declarationProvider', 'textDocument/declaration': 'declarationProvider',
'textDocument/executeCommand': 'executeCommandProvider', 'textDocument/executeCommand': 'executeCommandProvider',
'textDocument/semanticTokens/full': 'semanticTokensProvider',
'textDocument/publishDiagnostics': 'diagnosticsProvider',
} }
function registerServerCapability( function registerServerCapability(

View File

@ -3,8 +3,9 @@ import init, {
InitOutput, InitOutput,
kcl_lsp_run, kcl_lsp_run,
ServerConfig, ServerConfig,
} from '../../wasm-lib/pkg/wasm_lib' } from 'wasm-lib/pkg/wasm_lib'
import { FromServer, IntoServer } from './codec' import { FromServer, IntoServer } from './codec'
import { fileSystemManager } from 'lang/std/fileSystemManager'
export default class Server { export default class Server {
readonly initOutput: InitOutput readonly initOutput: InitOutput
@ -31,7 +32,11 @@ export default class Server {
} }
async start(type_: 'kcl' | 'copilot', token?: string): Promise<void> { async start(type_: 'kcl' | 'copilot', token?: string): Promise<void> {
const config = new ServerConfig(this.#intoServer, this.#fromServer) const config = new ServerConfig(
this.#intoServer,
this.#fromServer,
fileSystemManager
)
if (type_ === 'copilot') { if (type_ === 'copilot') {
if (!token) { if (!token) {
throw new Error('auth token is required for copilot') throw new Error('auth token is required for copilot')

View File

@ -1,4 +1,4 @@
import { Facet, Text } from '@codemirror/state' import { Text } from '@codemirror/state'
export function posToOffset( export function posToOffset(
doc: Text, doc: Text,
@ -17,7 +17,3 @@ export function offsetToPos(doc: Text, offset: number) {
character: offset - line.from, character: offset - line.from,
} }
} }
export const docPathFacet = Facet.define<string, string>({
combine: (values) => values[values.length - 1],
})

View File

@ -228,7 +228,17 @@ class KclManager {
} }
} }
async executeAst(ast: Program = this._ast, updateCode = false) { private _cancelTokens: Map<number, boolean> = new Map()
async executeAst(
ast: Program = this._ast,
updateCode = false,
executionId?: number
) {
console.trace('executeAst')
const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
await this.ensureWasmInit() await this.ensureWasmInit()
this.isExecuting = true this.isExecuting = true
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
@ -236,6 +246,11 @@ class KclManager {
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })
this.isExecuting = false this.isExecuting = false
// Check the cancellation token for this execution before applying side effects
if (this._cancelTokens.get(currentExecutionId)) {
this._cancelTokens.delete(currentExecutionId)
return
}
this.logs = logs this.logs = logs
this.kclErrors = errors this.kclErrors = errors
this.programMemory = programMemory this.programMemory = programMemory
@ -248,6 +263,7 @@ class KclManager {
type: 'execution-done', type: 'execution-done',
data: null, data: null,
}) })
this._cancelTokens.delete(currentExecutionId)
} }
async executeAstMock( async executeAstMock(
ast: Program = this._ast, ast: Program = this._ast,
@ -295,7 +311,13 @@ class KclManager {
} }
) )
} }
async executeCode(code?: string) { async executeCode(code?: string, executionId?: number) {
const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
if (this._cancelTokens.get(currentExecutionId)) {
this._cancelTokens.delete(currentExecutionId)
return
}
await this.ensureWasmInit() await this.ensureWasmInit()
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
const result = await executeCode({ const result = await executeCode({
@ -304,6 +326,11 @@ class KclManager {
lastAst: this._ast, lastAst: this._ast,
force: false, force: false,
}) })
// Check the cancellation token for this execution before applying side effects
if (this._cancelTokens.get(currentExecutionId)) {
this._cancelTokens.delete(currentExecutionId)
return
}
if (!result.isChange) return if (!result.isChange) return
const { logs, errors, programMemory, ast } = result const { logs, errors, programMemory, ast } = result
this.logs = logs this.logs = logs
@ -311,6 +338,12 @@ class KclManager {
this.programMemory = programMemory this.programMemory = programMemory
this.ast = ast this.ast = ast
if (code) this.code = code if (code) this.code = code
this._cancelTokens.delete(currentExecutionId)
}
cancelAllExecutions() {
this._cancelTokens.forEach((_, key) => {
this._cancelTokens.set(key, true)
})
} }
setCode(code: string, shouldWriteFile = true) { setCode(code: string, shouldWriteFile = true) {
if (shouldWriteFile) { if (shouldWriteFile) {

View File

@ -1,5 +1,5 @@
import { ToolTip } from '../useStore' import { ToolTip } from '../useStore'
import { Selection } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { import {
BinaryExpression, BinaryExpression,
Program, Program,
@ -558,3 +558,24 @@ export function hasExtrudeSketchGroup({
const varValue = programMemory?.root[varName] const varValue = programMemory?.root[varName]
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup' return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
} }
export function isSingleCursorInPipe(
selectionRanges: Selections,
ast: Program
) {
if (selectionRanges.codeBasedSelections.length !== 1) return false
if (
doesPipeHaveCallExp({
ast,
selection: selectionRanges.codeBasedSelections[0],
calleeName: 'extrude',
})
)
return false
const selection = selectionRanges.codeBasedSelections[0]
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
const nodeTypes = pathToNode.map(([, type]) => type)
if (nodeTypes.includes('FunctionExpression')) return false
if (nodeTypes.includes('PipeExpression')) return true
return false
}

View File

@ -1,4 +1,8 @@
import { readBinaryFile, exists as tauriExists } from '@tauri-apps/api/fs' import {
readDir,
readBinaryFile,
exists as tauriExists,
} from '@tauri-apps/api/fs'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { join } from '@tauri-apps/api/path' import { join } from '@tauri-apps/api/path'
@ -53,6 +57,30 @@ class FileSystemManager {
return tauriExists(file) return tauriExists(file)
}) })
} }
getAllFiles(path: string): Promise<string[] | void> {
// Using local file system only works from Tauri.
if (!isTauri()) {
throw new Error(
'This function can only be called from a Tauri application'
)
}
return join(this.dir, path)
.catch((error) => {
throw new Error(`Error joining dir: ${error}`)
})
.then((p) => {
readDir(p, { recursive: true })
.catch((error) => {
throw new Error(`Error reading dir: ${error}`)
})
.then((files) => {
return files.map((file) => file.path)
})
})
}
} }
export const fileSystemManager = new FileSystemManager() export const fileSystemManager = new FileSystemManager()

View File

@ -9,7 +9,11 @@ import { SelectionRange } from '@uiw/react-codemirror'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { Program } from 'lang/wasm' import { Program } from 'lang/wasm'
import { doesPipeHaveCallExp, getNodeFromPath } from 'lang/queryAst' import {
doesPipeHaveCallExp,
getNodeFromPath,
isSingleCursorInPipe,
} from 'lang/queryAst'
import { CommandArgument } from './commandTypes' import { CommandArgument } from './commandTypes'
import { import {
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
@ -455,6 +459,7 @@ function resetAndSetEngineEntitySelectionCmds(
} }
export function isSketchPipe(selectionRanges: Selections) { export function isSketchPipe(selectionRanges: Selections) {
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) return false
return isCursorInSketchCommandRange( return isCursorInSketchCommandRange(
engineCommandManager.artifactMap, engineCommandManager.artifactMap,
selectionRanges selectionRanges

View File

@ -37,7 +37,10 @@ export type Events =
} }
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' const persistedToken =
localStorage?.getItem(TOKEN_PERSIST_KEY) ||
getCookie('__Secure-next-auth.session-token') ||
''
export const authMachine = createMachine<UserContext, Events>( export const authMachine = createMachine<UserContext, Events>(
{ {
@ -135,3 +138,23 @@ async function getUser(context: UserContext) {
return user return user
} }
function getCookie(cname: string): string {
if (isTauri()) {
return ''
}
let name = cname + '='
let decodedCookie = decodeURIComponent(document.cookie)
let ca = decodedCookie.split(';')
for (let i = 0; i < ca.length; i++) {
let c = ca[i]
while (c.charAt(0) === ' ') {
c = c.substring(1)
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length)
}
}
return ''
}

View File

@ -71,7 +71,12 @@ export type SetSelections =
} }
export type ModelingMachineEvent = export type ModelingMachineEvent =
| { type: 'Enter sketch' } | {
type: 'Enter sketch'
data?: {
forceNewSketch?: boolean
}
}
| { | {
type: 'Select default plane' type: 'Select default plane'
data: { plane: DefaultPlaneStr; normal: [number, number, number] } data: { plane: DefaultPlaneStr; normal: [number, number, number] }
@ -114,7 +119,7 @@ export type MoveDesc = { line: number; snippet: string }
export const modelingMachine = createMachine( export const modelingMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBDOFJYWVlQ2UlY3UrQsQFUQVy0Q00w3FLGVNTBtcQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iZCdKiWZpcQKEyHTKmTLrYrCGb5DRaGHKBGiYzKRoXZqeNodLoEB5PV6wD7sb5UQyRZisMbcQEIQRLGiSdRmfIqUxLCpw5QqcrQ4SGTTWUx5bGXPE3Ql3PjsZ4AVwwv1psXGCSEMjkkhkNFE-MNBxo4vEcM22wqOQxikMKwUktxrUk3nJ32IZEomFdnx+9BGdIBmoQdoUykkxmEyIhELMsjhobKoYUdsOIq2jo8zp9FK+LrdXwAkrcegEgl0BuF-X9AxrQIlDHspPzRKYbAsttYzfojCYZBYaEs5uCaCZuZmrm0c99877i4TkCQPn0oABbMCPfwANxenHIJEwquitYZwaspiT1U0c00qQKPZDGgOKVHcgk+SUGgn0uned-8+6RdlyCNcNwCL5UGebAAC9uHYA8j3+Ot+CMJFJFUMN9TtfI0RkBNxDtcoNHkSx+WMCoZG-bMC1nXMAOIbhYAVEg8H8CCoNgx4D38CBsCYz0wEQk94nrVDdgsJsTU-C91DhNJuQsCpRFHYiLwOKjrl-WjvnoohGOY1id2ePduN4-iKEE6s1XpESUJDGQlkkcUZE2RYnzUPY5M5cprBFDQEWEBzdg0qcaP-Es9NwJjnhY3B-AAQQAIW8fwAA0hPVU9RPszZJByMRG1Oc80jk8UNCcg1FFfVtajOJos00sKC10-SYtYpKUoATQymzGSsXLrDsZQ5EMA0DTwh9DnUUElMWNQoy-c4pWo31tKLCLWti-wyCgLoeqDbKDhyaQlCveQU2EUw5OUxEtnkdE0ROYQQrWtaWqigy4q6fB2D9Glj0y2yGxKGYVANE0Dn2UQ5IWbY5jbQxLujFRnqWp1GtW8LCUi6KtqYF58dwXjyEVTASFeMz4Is-bkIbbVthsIrrEsTDlDklNyuFKpTARaxkTEF6tKx7occ+tjIJguCD0wPRtpwKAhisgHerPBz+wtLQJGIjJ+QTDFw2OLZ-MCsR+TqnEGtCzHmo2j62rioyTMwGW5ewBWaayuyrEuvLZEccVFjMDEE2GkEth5qolmORxzeWjHcze23cdY2BcBIJh-HYVA0o9oGjAIqQHJHBQeeqMw5OyfttQNeZVC2QLKLRy3XuFhi7a21P08z7PuqVpDPeB59MjIhFxGFbI5MbGZdkcOQLyjZQckFpq5yTsWwAAR0VbjvqgX7c7644pAxY2ef1OQx4TWpWRyExR31ZQDVMZfrdX7HNtYphyel6g++EvrZAzFSEXUapwnxRgTKcIBpQFgnEyGoUQL8E6t1FvbfwzwwCrlQDufw5AP6PFgAfVWWh0JWGIsNTQo0dAPhFARCwixbDZBKGoYaSCZytwAEpgEEGAPgIRFRPCIYdOoUgDiGFOKkdIYhg4PnUP2dIqQ0S1yUAsNhf4bayi3tgDOAAZPAYBu6oEPH-QGfVUSCnqLYMQ1CiiBXDMiAi2o1CKPHE3ScLcNH3C0RnKmMBHjYG4uTcg3chF2SGtscEBoCKP0UCIhMxgyg3jrrE5Ei81GSHigAdxYuBCWnF4KYB4nxKmlB-B4AAGaoAIBAbgYB2i4C3KgD4kgYDsEEOxSWXFMCCAqagUJiQ0jZDyhoReJpahPVqHJJ85hNguX2MoWwlV0lZJyeLDiUtCmUwEmU3AlSCAvGeJBSQTAybsEqc8VcLS-DtLyRsnpuy+kmJVtlQZII7TIlTMRFyPM5Lgn7NkQco46g4UMMs7JHBty7mwPuTZxTtm9OqbU+pjTmmtMEI7aFB57mVP6YgfY-YDi2hMGicR+QvJKDZGIOQdhSiBTBasjFMKinmVKQig5RyTkkDOZBS5aLGVYt6bihA+K2T6jEPPI2Xk9gRkfqoNIcj2T0ohR1NKOy9k1NwHUvAKK6lopIAAI1gIIPg2LHn-X7nnYV0TZjDVfBfLYpVhoRgyE+Reshhq1CVQEFVqU1VVPZc8Y5pzzm8uuQao1JrBVPIOmEgiMwcjgiUK2fUWhSqLGkFsR+BFshRmfm4n8NEVnKuSv4TqfrEWauRU03VYbDWCD0KaoV8yM3yAqLYKBxE4SHDRHlbmyktiHGCvmlaCci3epLWWtlzxDmBs5dyi5Vy2nhvrY26NtM8VxosDhMeDl+Suq7W5CwCz5jV0JY3eq7itJju2vgLo5aNVaoadWxdggdpdFXea-+wZ9ig0sAcPIrYA7XQNE5Qdf6KhhgRF6m9u0DFTpnUGrlIaX1vu4VGz9pjv0EThtkfqVg1Dz2uojCM-6shyHPmiaDu9fr3qRdq59aLqNfA-QGTDLyCIghyPJcBew7ww1yoOWlKhEZWCoxuPeXxy0BsQ-O0NbSmMsZrGx2N6bjAmC2Isa8E0igOWlSelE-l7oGmg-jZ4hNiak3JsykpcGHkVsfTql9pnzPQss88QQWyLKKesjGgZasKpKGsMNBy1RoYPjmSCMejZ5iAYA7HdGVtR3goCM5jcFmyYUzhRZKT06OXBp5U5gmaXXMZY81lyg3nla+bxf58UgWRnaijmFnTAVpDChGY-E4cX0l6M1YYzApY+gVjiE2vU5gnx6hFJkdMJVws8wjHVk01hMLVB6-o-rRJvGZx2mBAJhSgkhLXQPPFCJyqjTMKOdQ3M2YPh5vGkGjgsiuIvQW1avWDFZyMZIQsuAOAEFGxIaQ3aITpn8pfB8JL0Il0uhzfUjhQXDvjjOd7-Xvu-fYP96krHnlhJctsE4NhFDKT9ioOE8MnKNgcHUbkpo1t9c+5gSQuAeUHkG+WEII2juWuW-j1suxhTQ7DGTmEGbSULL-XaPNL2R3I-WwzyQAA5bOAAFVAeB2CwAIPFCAEBAjwWMv4Fg6um3zHKjCMabYXKDg0HyBZvsI5ZEUKddJP2-tCoSfYzYWQVDnxNDbmhYI8pKIcoC3TCPpdI7zK7jHVIMM44bFhNkMIYQjNkKORsCZjiJM2OkBYunThYkR4lmcAAVHb-jAnPGCVnNn-ROdx+q8K4a5gFlKFNEt44menxsjcnqSwxEMxF9emX-Au3K-V6qXcLbKOGcm+RL7TImxxSDm04gNs4ZeZpBItbyjQ+tKKiJtnfixlfwAHlcD2arc0+K3gS+CAPzUwQx-2Bn8Vg39dIYoQVQWhiem+o4RGyzALRhguRkJPhqL+DM7+DlIkCUA9DDY8RgAwGkwpZkyapCqCCFQnTE4YiIzEQERXQQ46htjcjRZHAVCowR5tBkDYCrhcqtDdyG5oHdAPpX51I0F0FPCCBZyCAwGUBCoVCXiL4ArAKwhEERJWDMLfJRjqAvQcH0H4CMGcqaq17DaDBNqBQkbUqtgYQQIPgzwRh1DDRqb8g1ByG-acEMFZz+C8LFIMFki+iX70bNLyFcE8EOG5gCGPykKcgnDahpAIhwhPiRZ5AQiLx2ANaaB04faoBfZK7+Cq7q6a6kAWTGLv7HbCpja9piCHA5CIwmBwinDqziKKATbupVAuDnDM4YDwBRBxxQDY6N6CAg7SByCxIYSaA2JCDiQQjTI8x6gORIyIJD4EhgCNEf7MgkKVQ8zgjETXjmgpjlBTTnaSGWBS4WyXoFjjEZHcgMxe7Eq+5thdr9gLKVCHBRaOD8jpLCzbGWoHAnC6iLwphjyhZ1DxLJAHBjypDVAXi-HQYdL5KmTla2aVK3F9RhgMwZClzCg1CpC-LJDAhQjzBWLh4bGvZJYMpQpMqeasoPJglnhyIyrp4iLN6GBeTKRbpPgcwxYYjQY+p+r4mHQlxSBqCOIF52CjiEE6ZFR5SVSYRzSdh0kToMlKbx5GAlz9ihF5G9Gr7FCDggjVwUGthyLrH1GvTXqoYik+Yf4ijijoRQgJpVB5CnDXRqBsjyoVCqBzBb5iY-SSa9KMlewlw3zqaPwpgF7zAwwCiXTiKXFDFXF76FrJaG5FZEwlZWY4kgmoCOkNibAb7cjKS3jtglByS2izC+mtijSVCF5UGvQz6xGYAxl4r2AWC1SbA2DqBLAAGLGnB3Y1AlC7ColqlaT5lfbR5FnCqjRlCZAlDllVAQl8gfHzDpCp4p4LLRGo7M4XIHgdlijxqjzHDOKyCylZBlDtjiKlAuSmETny7xGJEEKznzCIinCyCNgZDQg3a2IIhsh-IUTuqKDnpoky5R7o4dnkQt7azMzghmAZ40IihJgigpjGjpgOiBmrQj5+KcDj7dyzmAG8yGguSVBcm9h5CPFhg8hZC-7DG5n76H567kwv4Fjn5vkLDxp6gLCEqSEAFbDoSXYqAxa3zYVPmR6QHZx8FjGilNF7BhzpDiKqB1CXTDR8g6hPj2D9paB+zmG0EKFQBKHMEkVSA8zi42DyD4aXmIDDThgmDqBzAzxTSUHMXUEWEyWMG2FMT2FbGcU6mOC6gIiYRjzcg0pdpzCCj2AQhRiBQXiLQuBAA */ /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBDOFJYWVlQ2UlY3UrQsQFUQVy0Q00w3FLGVNTBtcQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iZCdKiWZpcQKEyHTKmTLrYrCGb5DRaGHKBGiYzKRoXZqeNodLoEB5PV6wD7sb5UQyRZisMbcQEIQRLGiSdRmfIqUxLCpw5QqcrQ4SGTTWUx5bGXPE3Ql3PjsZ4AVwwv1psXGCSEMjkkhkNFE-MNBxo4vEcM22wqOQxikMKwUktxrUk3nJ32IZEomFdnx+9BGdIBmoQdoUykkxmEyIhELMsjhobKoYUdsOIq2jo8zp9FK+LrdXwAkrcegEgl0BuF-X9AxrQIlDHspPzRKYbAsttYzfojCYZBYaEs5uCaCZuZmrm0c99877i4TkCQPn0oABbMCPfwANxenHIJEwquitYZwaspiT1U0c00qQKPZDGgOKVHcgk+SUGgn0uned-8+6RdlyCNcNwCL5UGebAAC9uHYA8j3+Ot+CMJFJFUMN9TtfI0RkBNxDtcoNHkSx+WMCoZG-bMC1nXMAOIbhYAVEg8H8CCoNgx4D38CBsCYz0wEQk94nrVDdgsJsTU-C91DhNJuQsCpRFHYiLwOKjrl-WjvnoohGOY1id2ePduN4-iKEE6s1XpESUJDGQlkkcUZE2RYnzUPY5M5cprBFDQEWEBzdg0qcaP-Es9NwJjnhY3B-AAQQAIW8fwAA0hPVU9RPszZJByMRG1Oc80jk8UNCcg1FFfVtajOJos00sKC10-SYtYpKUoATQymzGSsXLrDsZQ5EMA0DTwh9DnUUElMWNQoy-c4pWo31tKLCLWti-wyCgLoeqDbKDhyaQlCveQU2EUw5OUxEtnkdE0ROYQQrWtaWqigy4q6fB2D9Glj0y2yGxKGYVANE0Dn2UQ5IWbY5jbQxLujFRnqWp1GtW8LCUi6KtqYF58dwXjyEVTASFeMz4Is-bkIbbVthsIrrEsTDlDklNyuFKpTARaxkTEF6tKx7occ+tjIJguCD0wPRtpwKAhisgHerPBz+wtLQJGIjJ+QTDFw2OLZ-MCsR+TqnEGtCzHmo2j62rioyTMwGW5ewBWaayuyrEuvLZEccVFjMDEE2GkEth5qolmORxzeWjHcze23cdY2BcBIJh-HYVA0o9oGjAIqQHJHBQeeqMw5OyfttQNeZVC2QLKLRy3XuFhi7a21P08z7PuqVpDPeB59MjIhFxGFbI5MbGZdkcOQLyjZQckFpq5yTsWwAAR0VbjvqgX7c7644pAxY2ef1OQx4TWpWRyExR31ZQDVMZfrdX7HNtYphyel6g++EvrZAzFSEXUapwnxRgTKcIBpQFgnEyGoUQL8E6t1FvbfwzwwCrlQDufw5AP6PFgAfVWWh0JWGIsNTQo0dAPhFARCwixbDZBKGoYaSCZytwAEpgEEGAPgIRFRPCIYdOoUgDiGFOKkdIYhg4PnUP2dIqQ0S1yUAsNhf4bayi3tgDOAAZPAYBu6oEPH-QGfVUSCnqLYMQ1CiiBXDMiAi2o1CKPHE3ScLcNH3C0RnKmMBHjYG4uTcg3chF2SGtscEBoCKP0UCIhMxgyg3jrrE5Ei81GSHigAdxYuBCWnF4KYB4nxKmlB-B4AAGaoAIBAbgYB2i4C3KgD4kgYDsEEOxSWXFMCCAqagUJiQ0jZDyhoReJpahPVqHJJ85hNguX2MoWwlV0lZJyeLDiUtCmUwEmU3AlSCAvGeJBSQTAybsEqc8VcLS-DtLyRsnpuy+kmJVtlQZII7TIlTMRFyPM5Lgn7NkQco46g4UMMs7JHBty7mwPuTZxTtm9OqbU+pjTmmtMEI7aFB57mVP6YgfY-YDi2hMGicR+QvJKDZGIOQdhSiBTBasjFMKinmVKQig5RyTkkDOZBS5aLGVYt6bihA+K2T6jEPPI2Xk9gRkfqoNIcj2T0ohR1NKOy9k1NwHUvAKK6lopIAAI1gIIPg2LHn-X7nnYV0TZjDVfBfLYpVhoRgyE+Reshhq1CVQEFVqU1VVPZc8Y5pzzm8uuQao1JrBVPIOmEgiMwcjgiUK2fUWhSqLGkFsR+BFshRmfm4n8NEVnKuSv4TqfrEWauRU03VYbDWCD0KaoV8yM3yAqLYKBxE4SHDRHlbmyktiHGCvmlaCci3epLWWtlzxDmBs5dyi5Vy2nhvrY26NtM8VxosDhMeDl+Suq7W5CwCz5jV0JY3eq7itJju2vgLo5aNVaoadWxdggdpdFXea-+wZ9ig0sAcPIrYA7XQNE5Qdf6KhhgRF6m9u0DFTpnUGrlIaX1vu4VGz9pjv0EThtkfqVg1Dz2uojCM-6shyHPmiaDu9fr3qRdq59aLqNfA-QGTDLyCIghyPJcBew7ww1yoOWlKhEZWCoxuPeXxy0BsQ-O0NbSmMsZrGx2N6bjAmC2Isa8E0igOWlSelE-l7oGmg-jZ4hNiak3JsykpcGHkVsfTql9pnzPQss88QQWyLKKesjGgZasKpKGsMNBy1RoYPjmSCMejZ5iAYA7HdGVtR3goCM5jcFmyYUzhRZKT06OXBp5U5gmaXXMZY81lyg3nla+bxf58UgWRnaijmFnTAVpDChGY-E4cX0l6M1YYzApY+gVjiE2vU5gnx6hFJkdMJVws8wjHVk01hMLVB6-o-rRJvGZx2mBAJhSgkhLXQPPFCJyqjTMKOdQ3M2YPh5vGkGjgsiuIvQW1avWDFZyMZIQsuAOAEFGxIaQ3aITpn8pfB8JL0Il0uhzfUjhQXDvjjOd7-Xvu-fYP96krHnlhJctsE4NhFDKT9ioOE8MnKNgcHUbkpo1t9c+5gSQuAeUHkG+WEII2juWuW-j1suxhTQ7DGTmEGbSULL-XaPNL2R3I-WwzyQAA5bOAAFVAeB2CwAIPFCAEBAjwWMv4Fg6um3zHKjCMabYXKDg0HyBZvsI5ZEUKdOnH3UBfaV-4VX6vNekAssYjDOO-Mml7WIQ4OREYmDhKcdW4jFATfdVUdJP2-tCoSfYzYWQVDnxNDbmhYI8pKIcoC3TCPpdI7zMnjHVIA-VZDFhNkMIYQjNkKORsCZjiJM2OkBYunThYkR4lmcAAVHb-jAnPGCVnNn-ROc1-XcK4a5gFlKFNEt447enxsjcnqSwxEMwD9eiP-Au3x+T6qXcLbKOGcm+RL7TImxxSDm04gNs4ZeZpBItbyjB+tKKiJtnfiYyX8AAeVwHsyrWaXim8CH0ED-xqUEEAPYBAMVjn2OxDFkCkBGRMGIhNHv1hAfFbG2Fcgg0JUZkQR-wLH8GZ38HKRIEoB6GGx4jAFoNJhSzJk1SFUEEKhOmJwxERmIgIiughx1DbG5GiyOAqFRjLzaDIGwFXC5VaG7kN3YO6AfQgLqVkPkKeEECzkEFoMoCFQqEvDwIyGAXwKKAoWkCsGYW+SjHUBek0IUPwCUM5U1Wn2G0GCbUChI2pVbAwggQIJIX-WGjU35BqAcN+y0MUKzn8F4WKUULJF9HAPo2aUcO0N0MSNzEMMflIU5BOG1DSARDhCfEizyAhEXjsAa00BcHOGZwwHgCiDjigGx1r0EBB2kDkFiQwk0BsSEHEghF4z1CWE2C0AdAPwJDABaPn2ZBIUqh5nBGImvHNBTHKCmnO2sMsClwtkvQLCmLQO5AZgz2JWzzbC7X7AWS2FbzHnkCWGe22Ne2QU8T2MtQOBOF1EXhTDHlCzqHiWSAODHlSGqAvGBOgw6XyVMnK1s0qWeL6jDAZgyFLmFBqFSF+WSGBChHmCsVL3uJlzzGvX5VhRZShNQBhLPDkRlSuIkEX0MC8mUi3SfA5hiwxGgx9T9VJMOhLikDUEcT7zsFHCEJ0yKjykqkwjmk7BZInTZKU0DyMBLn7DKPDwGOf2KEHBBGrkkMIKBOg1QylJ83nxFHFHQihATSqDyFOGujUDZHlQqFUDmA-zEx+kk16XZK9hLhvnU0fhTD73mBhgFEunEUcEenIXi2bivWS0NyKyJhKys081ZQeRdIbE2Df25GUlvHbBKDkltFmADNbFGkqH72kNeivzd0wATLxXsAsFqhGKqDhLhA01AyqCklSGb3IMLK0mLK+0rzLOFVGjKEyBKGrPUCWD5D+PmHSGbybwWRd1R2ZwuQPG7LFHjVHmOGcVkGVKyDKHbHEVKBcjCOnPlw9y9wIQXPmERFOFkEbFMMuhu1sQRDZD+QondUUHPRxPLzRw4G7PIiX21mZnBDMDbxoRFCTBFBTGNHTDGLbJoiPz8U4FP27gXKNlmDsRUBckqAFN7DyHeLDB5CyAxDkHSTgIAP1yQILFAM-NtSBxhBckdxcm7CKANHKjqDzPNzsB6LUSoOzn0MmOlNaL2DDnSHEVUDqGvOVNDjyjtHUxTUCwgtfJkMiKcKgBcJUPIqkB5nFxsHkHwxvMQGGnDBMHUDmBnimikNkskDSOiOzjiKYgSN2J4v1McF1AREwjHm5BpS7TmEFHsAhCjECgvEWhcCAA */
id: 'Modeling', id: 'Modeling',
tsTypes: {} as import('./modelingMachine.typegen').Typegen0, tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
@ -153,7 +158,7 @@ export const modelingMachine = createMachine(
'Enter sketch': [ 'Enter sketch': [
{ {
target: 'animating to existing sketch', target: 'animating to existing sketch',
cond: 'Selection is one face', cond: 'Selection is on face',
actions: ['set sketch metadata'], actions: ['set sketch metadata'],
}, },
'Sketch no face', 'Sketch no face',
@ -166,6 +171,8 @@ export const modelingMachine = createMachine(
internal: true, internal: true,
}, },
}, },
entry: 'reset client scene mouse handlers',
}, },
Sketch: { Sketch: {
@ -803,6 +810,7 @@ export const modelingMachine = createMachine(
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
onClick: async (args) => { onClick: async (args) => {
if (!args) return if (!args) return
if (args.event.which !== 1) return
const { intersection2d } = args const { intersection2d } = args
if (!intersection2d || !sketchPathToNode) return if (!intersection2d || !sketchPathToNode) return
const { modifiedAst } = addStartProfileAt( const { modifiedAst } = addStartProfileAt(
@ -818,6 +826,17 @@ export const modelingMachine = createMachine(
}, },
'add axis n grid': ({ sketchPathToNode }) => 'add axis n grid': ({ sketchPathToNode }) =>
sceneEntitiesManager.createSketchAxis(sketchPathToNode || []), sceneEntitiesManager.createSketchAxis(sketchPathToNode || []),
'reset client scene mouse handlers': () => {
// when not in sketch mode we don't need any mouse listeners
// (note the orbit controls are always active though)
sceneInfra.setCallbacks({
onClick: () => {},
onDrag: () => {},
onMouseEnter: () => {},
onMouseLeave: () => {},
onMove: () => {},
})
},
}, },
// end actions // end actions
} }

View File

@ -36,6 +36,7 @@ import { sep } from '@tauri-apps/api/path'
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig' import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { kclManager } from 'lang/KclSingleton'
// This route only opens in the Tauri desktop context for now, // This route only opens in the Tauri desktop context for now,
// as defined in Router.tsx, so we can use the Tauri APIs and types. // as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -55,6 +56,7 @@ const Home = () => {
// during the loading of the home page. This is wrapped // during the loading of the home page. This is wrapped
// in a single-use effect to avoid a potential infinite loop. // in a single-use effect to avoid a potential infinite loop.
useEffect(() => { useEffect(() => {
kclManager.cancelAllExecutions()
if (newDefaultDirectory) { if (newDefaultDirectory) {
sendToSettings({ sendToSettings({
type: 'Set Default Directory', type: 'Set Default Directory',

View File

@ -53,4 +53,38 @@ impl FileSystem for FileManager {
} }
}) })
} }
async fn get_all_files<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<Vec<std::path::PathBuf>, crate::errors::KclError> {
let mut files = vec![];
let mut stack = vec![path.as_ref().to_path_buf()];
while let Some(path) = stack.pop() {
if !path.is_dir() {
continue;
}
let mut read_dir = tokio::fs::read_dir(&path).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to read directory `{}`: {}", path.display(), e),
source_ranges: vec![source_range],
})
})?;
while let Ok(Some(entry)) = read_dir.next_entry().await {
let path = entry.path();
if path.is_dir() {
// Iterate over the directory.
stack.push(path);
} else {
files.push(path);
}
}
}
Ok(files)
}
} }

View File

@ -28,4 +28,11 @@ pub trait FileSystem: Clone {
path: P, path: P,
source_range: crate::executor::SourceRange, source_range: crate::executor::SourceRange,
) -> Result<bool, crate::errors::KclError>; ) -> Result<bool, crate::errors::KclError>;
/// Get all the files in a directory recursively.
async fn get_all_files<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<Vec<std::path::PathBuf>, crate::errors::KclError>;
} }

View File

@ -18,6 +18,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = exists, catch)] #[wasm_bindgen(method, js_name = exists, catch)]
fn exists(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>; fn exists(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = getAllFiles, catch)]
fn get_all_files(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>;
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -31,6 +34,9 @@ impl FileManager {
} }
} }
unsafe impl Send for FileManager {}
unsafe impl Sync for FileManager {}
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl FileSystem for FileManager { impl FileSystem for FileManager {
async fn read<P: AsRef<std::path::Path>>( async fn read<P: AsRef<std::path::Path>>(
@ -112,4 +118,53 @@ impl FileSystem for FileManager {
Ok(it_exists) Ok(it_exists)
} }
async fn get_all_files<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<Vec<std::path::PathBuf>, crate::errors::KclError> {
let promise = self
.manager
.get_all_files(
path.as_ref()
.to_str()
.ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: "Failed to convert path to string".to_string(),
source_ranges: vec![source_range],
})
})?
.to_string(),
)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from javascript: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let s = value.as_string().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get string from response from javascript: `{:?}`", value),
source_ranges: vec![source_range],
})
})?;
let files: Vec<String> = serde_json::from_str(&s).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to parse json from javascript: `{}` `{:?}`", s, e),
source_ranges: vec![source_range],
})
})?;
Ok(files.into_iter().map(|s| std::path::PathBuf::from(s)).collect())
}
} }

View File

@ -10,7 +10,7 @@ pub mod engine;
pub mod errors; pub mod errors;
pub mod executor; pub mod executor;
pub mod fs; pub mod fs;
pub mod lsp;
pub mod parser; pub mod parser;
pub mod server;
pub mod std; pub mod std;
pub mod token; pub mod token;

View File

@ -13,6 +13,8 @@ use tower_lsp::lsp_types::{
pub trait Backend { pub trait Backend {
fn client(&self) -> tower_lsp::Client; fn client(&self) -> tower_lsp::Client;
fn fs(&self) -> crate::fs::FileManager;
/// Get the current code map. /// Get the current code map.
fn current_code_map(&self) -> DashMap<String, String>; fn current_code_map(&self) -> DashMap<String, String>;

View File

@ -6,7 +6,7 @@ use std::{
sync::{Mutex, RwLock}, sync::{Mutex, RwLock},
}; };
use crate::server::copilot::types::CopilotCompletionResponse; use crate::lsp::copilot::types::CopilotCompletionResponse;
// if file changes, keep the cache. // if file changes, keep the cache.
// if line number is different for an existing file, clean. // if line number is different for an existing file, clean.

View File

@ -23,7 +23,7 @@ use tower_lsp::{
LanguageServer, LanguageServer,
}; };
use crate::server::{ use crate::lsp::{
backend::Backend as _, backend::Backend as _,
copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams}, copilot::types::{CopilotCompletionResponse, CopilotEditorInfo, CopilotLspCompletionParams, DocParams},
}; };
@ -42,6 +42,8 @@ impl Success {
pub struct Backend { pub struct Backend {
/// The client is used to send notifications and requests to the client. /// The client is used to send notifications and requests to the client.
pub client: tower_lsp::Client, pub client: tower_lsp::Client,
/// The file system client to use.
pub fs: crate::fs::FileManager,
/// Current code. /// Current code.
pub current_code_map: DashMap<String, String>, pub current_code_map: DashMap<String, String>,
/// The token is used to authenticate requests to the API server. /// The token is used to authenticate requests to the API server.
@ -54,11 +56,15 @@ pub struct Backend {
// Implement the shared backend trait for the language server. // Implement the shared backend trait for the language server.
#[async_trait::async_trait] #[async_trait::async_trait]
impl crate::server::backend::Backend for Backend { impl crate::lsp::backend::Backend for Backend {
fn client(&self) -> tower_lsp::Client { fn client(&self) -> tower_lsp::Client {
self.client.clone() self.client.clone()
} }
fn fs(&self) -> crate::fs::FileManager {
self.fs.clone()
}
fn current_code_map(&self) -> DashMap<String, String> { fn current_code_map(&self) -> DashMap<String, String> {
self.current_code_map.clone() self.current_code_map.clone()
} }
@ -125,15 +131,15 @@ impl Backend {
let pos = params.doc.position; let pos = params.doc.position;
let uri = params.doc.uri.to_string(); let uri = params.doc.uri.to_string();
let rope = ropey::Rope::from_str(&params.doc.source); let rope = ropey::Rope::from_str(&params.doc.source);
let offset = crate::server::util::position_to_offset(pos, &rope).unwrap_or_default(); let offset = crate::lsp::util::position_to_offset(pos, &rope).unwrap_or_default();
Ok(DocParams { Ok(DocParams {
uri: uri.to_string(), uri: uri.to_string(),
pos, pos,
language: params.doc.language_id.to_string(), language: params.doc.language_id.to_string(),
prefix: crate::server::util::get_text_before(offset, &rope).unwrap_or_default(), prefix: crate::lsp::util::get_text_before(offset, &rope).unwrap_or_default(),
suffix: crate::server::util::get_text_after(offset, &rope).unwrap_or_default(), suffix: crate::lsp::util::get_text_after(offset, &rope).unwrap_or_default(),
line_before: crate::server::util::get_line_before(pos, &rope).unwrap_or_default(), line_before: crate::lsp::util::get_line_before(pos, &rope).unwrap_or_default(),
rope, rope,
}) })
} }

View File

@ -29,7 +29,7 @@ use tower_lsp::{
Client, LanguageServer, Client, LanguageServer,
}; };
use crate::{ast::types::VariableKind, executor::SourceRange, parser::PIPE_OPERATOR, server::backend::Backend as _}; use crate::{ast::types::VariableKind, executor::SourceRange, lsp::backend::Backend as _, parser::PIPE_OPERATOR};
/// A subcommand for running the server. /// A subcommand for running the server.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -48,6 +48,8 @@ pub struct Server {
pub struct Backend { pub struct Backend {
/// The client for the backend. /// The client for the backend.
pub client: Client, pub client: Client,
/// The file system client to use.
pub fs: crate::fs::FileManager,
/// The stdlib completions for the language. /// The stdlib completions for the language.
pub stdlib_completions: HashMap<String, CompletionItem>, pub stdlib_completions: HashMap<String, CompletionItem>,
/// The stdlib signatures for the language. /// The stdlib signatures for the language.
@ -70,11 +72,15 @@ pub struct Backend {
// Implement the shared backend trait for the language server. // Implement the shared backend trait for the language server.
#[async_trait::async_trait] #[async_trait::async_trait]
impl crate::server::backend::Backend for Backend { impl crate::lsp::backend::Backend for Backend {
fn client(&self) -> Client { fn client(&self) -> Client {
self.client.clone() self.client.clone()
} }
fn fs(&self) -> crate::fs::FileManager {
self.fs.clone()
}
fn current_code_map(&self) -> DashMap<String, String> { fn current_code_map(&self) -> DashMap<String, String> {
self.current_code_map.clone() self.current_code_map.clone()
} }

View File

@ -2,5 +2,5 @@
mod backend; mod backend;
pub mod copilot; pub mod copilot;
pub mod lsp; pub mod kcl;
mod util; mod util;

View File

@ -129,16 +129,22 @@ pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
pub struct ServerConfig { pub struct ServerConfig {
into_server: js_sys::AsyncIterator, into_server: js_sys::AsyncIterator,
from_server: web_sys::WritableStream, from_server: web_sys::WritableStream,
fs: kcl_lib::fs::wasm::FileSystemManager,
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[wasm_bindgen] #[wasm_bindgen]
impl ServerConfig { impl ServerConfig {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(into_server: js_sys::AsyncIterator, from_server: web_sys::WritableStream) -> Self { pub fn new(
into_server: js_sys::AsyncIterator,
from_server: web_sys::WritableStream,
fs: kcl_lib::fs::wasm::FileSystemManager,
) -> Self {
Self { Self {
into_server, into_server,
from_server, from_server,
fs,
} }
} }
} }
@ -156,17 +162,19 @@ pub async fn kcl_lsp_run(config: ServerConfig) -> Result<(), JsValue> {
let ServerConfig { let ServerConfig {
into_server, into_server,
from_server, from_server,
fs,
} = config; } = config;
let stdlib = kcl_lib::std::StdLib::new(); let stdlib = kcl_lib::std::StdLib::new();
let stdlib_completions = kcl_lib::server::lsp::get_completions_from_stdlib(&stdlib).map_err(|e| e.to_string())?; let stdlib_completions = kcl_lib::lsp::kcl::get_completions_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
let stdlib_signatures = kcl_lib::server::lsp::get_signatures_from_stdlib(&stdlib).map_err(|e| e.to_string())?; let stdlib_signatures = kcl_lib::lsp::kcl::get_signatures_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
// We can unwrap here because we know the tokeniser is valid, since // We can unwrap here because we know the tokeniser is valid, since
// we have a test for it. // we have a test for it.
let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap(); let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap();
let (service, socket) = LspService::new(|client| kcl_lib::server::lsp::Backend { let (service, socket) = LspService::new(|client| kcl_lib::lsp::kcl::Backend {
client, client,
fs: kcl_lib::fs::FileManager::new(fs),
stdlib_completions, stdlib_completions,
stdlib_signatures, stdlib_signatures,
token_types, token_types,
@ -211,24 +219,24 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String) -> Result<(),
let ServerConfig { let ServerConfig {
into_server, into_server,
from_server, from_server,
fs,
} = config; } = config;
let (service, socket) = LspService::build(|client| kcl_lib::server::copilot::Backend { let (service, socket) = LspService::build(|client| kcl_lib::lsp::copilot::Backend {
client, client,
fs: kcl_lib::fs::FileManager::new(fs),
current_code_map: Default::default(), current_code_map: Default::default(),
editor_info: Arc::new(RwLock::new( editor_info: Arc::new(RwLock::new(kcl_lib::lsp::copilot::types::CopilotEditorInfo::default())),
kcl_lib::server::copilot::types::CopilotEditorInfo::default(), cache: kcl_lib::lsp::copilot::cache::CopilotCache::new(),
)),
cache: kcl_lib::server::copilot::cache::CopilotCache::new(),
token, token,
}) })
.custom_method("setEditorInfo", kcl_lib::server::copilot::Backend::set_editor_info) .custom_method("setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
.custom_method( .custom_method(
"getCompletions", "getCompletions",
kcl_lib::server::copilot::Backend::get_completions_cycling, kcl_lib::lsp::copilot::Backend::get_completions_cycling,
) )
.custom_method("notifyAccepted", kcl_lib::server::copilot::Backend::accept_completions) .custom_method("notifyAccepted", kcl_lib::lsp::copilot::Backend::accept_completions)
.custom_method("notifyRejected", kcl_lib::server::copilot::Backend::reject_completions) .custom_method("notifyRejected", kcl_lib::lsp::copilot::Backend::reject_completions)
.finish(); .finish();
let input = wasm_bindgen_futures::stream::JsStream::from(into_server); let input = wasm_bindgen_futures::stream::JsStream::from(into_server);