Compare commits
53 Commits
remove-the
...
v0.23.1
Author | SHA1 | Date | |
---|---|---|---|
0e8d0083c4 | |||
4f4167b247 | |||
fbc2e9d02c | |||
33b15e818b | |||
6cebb84ae0 | |||
85403e47e4 | |||
0dfee64e3b | |||
6370d45f94 | |||
fb3e922180 | |||
1257ec0327 | |||
08e9fe2e52 | |||
7cec1d45fe | |||
93710bc8f2 | |||
87e7e9447f | |||
8be113d284 | |||
7cfc927d5c | |||
c0f04d5f86 | |||
3dbc701f26 | |||
16e7ae38e3 | |||
24c7260327 | |||
72cfc4a471 | |||
2d128ed32e | |||
cd6749ba02 | |||
7243405e1b | |||
c8da057ec2 | |||
220fe5b2b8 | |||
4e6429de49 | |||
5391a65b18 | |||
592628917a | |||
4c6e8633f7 | |||
c5150468a2 | |||
39126dbff1 | |||
f86a69f12a | |||
de354ee5d3 | |||
dfef7338ee | |||
ee08948f54 | |||
832f6b65e2 | |||
68efd77c5d | |||
8f138109dd | |||
8972f53256 | |||
0c5b13ade5 | |||
446f92a53a | |||
2256e3bc09 | |||
9e2876edc6 | |||
a138af1ec8 | |||
684c585a48 | |||
500be20649 | |||
5fbbe2fa8c | |||
5f5ecc5afe | |||
3dafc31cad | |||
9c230bc678 | |||
1fad6966b6 | |||
c7efb4c006 |
17
.github/workflows/ci.yml
vendored
@ -138,6 +138,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
|
||||||
- name: Copy updated .json files
|
- name: Copy updated .json files
|
||||||
if: github.event_name == 'schedule'
|
if: github.event_name == 'schedule'
|
||||||
@ -238,12 +239,8 @@ jobs:
|
|||||||
shell: cmd
|
shell: cmd
|
||||||
|
|
||||||
- name: Build the app (debug)
|
- name: Build the app (debug)
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
if: ${{ env.BUILD_RELEASE == 'false' }}
|
if: ${{ env.BUILD_RELEASE == 'false' }}
|
||||||
with:
|
run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||||
includeRelease: false
|
|
||||||
includeDebug: true
|
|
||||||
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
|
||||||
|
|
||||||
- name: Build for Mac TestFlight (nightly)
|
- name: Build for Mac TestFlight (nightly)
|
||||||
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
|
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
|
||||||
@ -336,7 +333,6 @@ jobs:
|
|||||||
# specific and we want to overwrite it with the this new build after and
|
# specific and we want to overwrite it with the this new build after and
|
||||||
# not upload the apple store build to the public bucket
|
# not upload the apple store build to the public bucket
|
||||||
- name: Build the app (release) and sign
|
- name: Build the app (release) and sign
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||||
env:
|
env:
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
@ -348,8 +344,7 @@ jobs:
|
|||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
|
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
|
||||||
with:
|
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||||
args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: matrix.os != 'ubuntu-latest'
|
if: matrix.os != 'ubuntu-latest'
|
||||||
@ -367,7 +362,7 @@ jobs:
|
|||||||
export VITE_KC_API_BASE_URL
|
export VITE_KC_API_BASE_URL
|
||||||
xvfb-run yarn test:e2e:tauri
|
xvfb-run yarn test:e2e:tauri
|
||||||
env:
|
env:
|
||||||
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/app"
|
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
|
||||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
|
||||||
- name: Run e2e tests (windows only)
|
- name: Run e2e tests (windows only)
|
||||||
@ -376,13 +371,15 @@ jobs:
|
|||||||
cargo install tauri-driver --force
|
cargo install tauri-driver --force
|
||||||
yarn wdio run wdio.conf.ts
|
yarn wdio run wdio.conf.ts
|
||||||
env:
|
env:
|
||||||
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\app.exe"
|
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe"
|
||||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
|
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
|
||||||
E2E_TAURI_ENABLED: true
|
E2E_TAURI_ENABLED: true
|
||||||
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
|
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
|
||||||
|
|
||||||
publish-apps-release:
|
publish-apps-release:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
|
||||||
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
|
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
|
||||||
|
2
.gitignore
vendored
@ -56,3 +56,5 @@ src-tauri/gen
|
|||||||
|
|
||||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||||
Mac_App_Distribution.provisionprofile
|
Mac_App_Distribution.provisionprofile
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Ignore artifacts:
|
# Ignore artifacts:
|
||||||
build
|
build
|
||||||
|
dist
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
# Ignore Rust projects:
|
# Ignore Rust projects:
|
||||||
@ -9,5 +10,6 @@ src/wasm-lib/pkg
|
|||||||
src/wasm-lib/kcl/bindings
|
src/wasm-lib/kcl/bindings
|
||||||
e2e/playwright/export-snapshots
|
e2e/playwright/export-snapshots
|
||||||
|
|
||||||
|
|
||||||
# XState generated files
|
# XState generated files
|
||||||
src/machines/**.typegen.ts
|
src/machines/**.typegen.ts
|
||||||
|
7
.vscode/settings.json
vendored
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"cSpell.words": [
|
|
||||||
"geos"
|
|
||||||
],
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.insertSpaces": true,
|
|
||||||
}
|
|
26
README.md
@ -124,36 +124,20 @@ Before you submit a contribution PR to this repo, please ensure that:
|
|||||||
|
|
||||||
## Release a new version
|
## Release a new version
|
||||||
|
|
||||||
1. Bump the versions in the .json files by creating a `Cut release v{x}.{y}.{z}` PR, committing the changes from
|
1. Bump the versions by running `./make-realease.sh` while on a fresh pull of main
|
||||||
|
|
||||||
```bash
|
That will create the branch with the updated json files for you.
|
||||||
VERSION=x.y.z yarn run bump-jsons
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively you can try the experimental `make-release.sh` bash script that will create the branch with the updated json files for you.
|
|
||||||
run `./make-release.sh` for a patch update
|
run `./make-release.sh` for a patch update
|
||||||
run `./make-release.sh "minor"` for minor
|
run `./make-release.sh "minor"` for minor
|
||||||
run `./make-release.sh "major"` for major
|
run `./make-release.sh "major"` for major
|
||||||
|
|
||||||
The PR may serve as a place to discuss the human-readable changelog and extra QA. A quick way of getting PR's merged since the last bump is to [use this PR filter](https://github.com/KittyCAD/modeling-app/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Amerged+), open up the browser console and paste in the following
|
After it runs you should just need to push the push the branch and open a PR (it will suggest a changelog for you too, delete any that are not user facing)
|
||||||
|
|
||||||
```typescript
|
The PR may serve as a place to discuss the human-readable changelog and extra QA.
|
||||||
console.log(
|
|
||||||
'- ' +
|
|
||||||
Array.from(
|
|
||||||
document.querySelectorAll('[data-hovercard-type="pull_request"]')
|
|
||||||
).map((a) => `[${a.innerText}](${a.href})`).join(`
|
|
||||||
- `)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
grab the md list and delete any that are older than the last bump
|
|
||||||
|
|
||||||
2. Merge the PR
|
2. Merge the PR
|
||||||
|
|
||||||
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
|
3. Profit (A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions if the PR was correctly named)
|
||||||
|
|
||||||
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
|
|
||||||
|
|
||||||
## Fuzzing the parser
|
## Fuzzing the parser
|
||||||
|
|
||||||
|
@ -91,8 +91,9 @@ const part001 = startSketchOn('-XZ')
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.waitForCmdReceive('extrude')
|
await u.waitForCmdReceive('extrude')
|
||||||
@ -330,7 +331,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
|
|||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// wait for execution done
|
// wait for execution done
|
||||||
@ -386,8 +387,8 @@ test('Draft segments should look right', async ({ page, context }) => {
|
|||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@ -443,7 +444,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
|
|||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
@ -490,7 +491,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
|||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
@ -589,7 +590,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
|||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
@ -689,7 +690,7 @@ const part002 = startSketchOn(part001, 'seg01')
|
|||||||
}, KCL_DEFAULT_LENGTH)
|
}, KCL_DEFAULT_LENGTH)
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
@ -739,7 +740,7 @@ test('Zoom to fit on load - solid 2d', async ({ page, context }) => {
|
|||||||
}, KCL_DEFAULT_LENGTH)
|
}, KCL_DEFAULT_LENGTH)
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
@ -776,7 +777,7 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
|
|||||||
}, KCL_DEFAULT_LENGTH)
|
}, KCL_DEFAULT_LENGTH)
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
@ -795,3 +796,83 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
|
|||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Grid visibility', () => {
|
||||||
|
test('Grid turned off', async ({ page }) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
const stream = page.getByTestId('stream')
|
||||||
|
const mask = [
|
||||||
|
page.locator('#app-header'),
|
||||||
|
page.locator('#sidebar-top-ribbon'),
|
||||||
|
page.locator('#sidebar-bottom-ribbon'),
|
||||||
|
]
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
// wait for execution done
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-message-type="execution-done"]')
|
||||||
|
).toHaveCount(2)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
// TODO: Find a way to truly know that the objects have finished
|
||||||
|
// rendering, because an execution-done message is not sufficient.
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
await expect(stream).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
mask,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Grid turned on', async ({ page }) => {
|
||||||
|
await page.addInitScript(
|
||||||
|
async ({ settingsKey, settings }) => {
|
||||||
|
localStorage.setItem(settingsKey, settings)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
settingsKey: TEST_SETTINGS_KEY,
|
||||||
|
settings: TOML.stringify({
|
||||||
|
settings: {
|
||||||
|
...TEST_SETTINGS,
|
||||||
|
modeling: {
|
||||||
|
...TEST_SETTINGS.modeling,
|
||||||
|
showScaleGrid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const u = await getUtils(page)
|
||||||
|
const stream = page.getByTestId('stream')
|
||||||
|
const mask = [
|
||||||
|
page.locator('#app-header'),
|
||||||
|
page.locator('#sidebar-top-ribbon'),
|
||||||
|
page.locator('#sidebar-bottom-ribbon'),
|
||||||
|
]
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await u.openDebugPanel()
|
||||||
|
// wait for execution done
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-message-type="execution-done"]')
|
||||||
|
).toHaveCount(2)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
// TODO: Find a way to truly know that the objects have finished
|
||||||
|
// rendering, because an execution-done message is not sufficient.
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
|
||||||
|
await expect(stream).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
mask,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@ -207,6 +207,23 @@ export const getMovementUtils = (opts: any) => {
|
|||||||
return { toSU, click00r }
|
return { toSU, click00r }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForAuthAndLsp(page: Page) {
|
||||||
|
const waitForLspPromise = page.waitForEvent('console', async (message) => {
|
||||||
|
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
|
||||||
|
// but that doesn't seem to make it to the console for macos/safari :(
|
||||||
|
if (message.text().includes('start kcl lsp')) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto('/')
|
||||||
|
await waitForPageLoad(page)
|
||||||
|
|
||||||
|
return waitForLspPromise
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUtils(page: Page) {
|
export async function getUtils(page: Page) {
|
||||||
// Chrome devtools protocol session only works in Chromium
|
// Chrome devtools protocol session only works in Chromium
|
||||||
const browserType = page.context().browser()?.browserType().name()
|
const browserType = page.context().browser()?.browserType().name()
|
||||||
@ -214,7 +231,8 @@ export async function getUtils(page: Page) {
|
|||||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||||
|
waitForPageLoad: () => waitForPageLoad(page),
|
||||||
removeCurrentCode: () => removeCurrentCode(page),
|
removeCurrentCode: () => removeCurrentCode(page),
|
||||||
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||||
updateCamPosition: async (xyz: [number, number, number]) => {
|
updateCamPosition: async (xyz: [number, number, number]) => {
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
// comment
|
|
||||||
|
|
||||||
const hi = 5 + 4
|
|
81
package.json
@ -1,50 +1,43 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.22.6",
|
"version": "0.23.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.16.0",
|
"@codemirror/autocomplete": "^6.17.0",
|
||||||
|
"@codemirror/commands": "^6.6.0",
|
||||||
|
"@codemirror/language": "^6.10.2",
|
||||||
|
"@codemirror/lint": "^6.8.1",
|
||||||
|
"@codemirror/search": "^6.5.6",
|
||||||
|
"@codemirror/state": "^6.4.1",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
|
"@csstools/postcss-oklab-function": "^3.0.16",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.19",
|
"@headlessui/react": "^1.7.19",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.67",
|
"@kittycad/lib": "^0.0.69",
|
||||||
"@lezer/javascript": "^1.4.9",
|
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
|
||||||
"@react-hook/resize-observer": "^2.0.1",
|
"@react-hook/resize-observer": "^2.0.1",
|
||||||
"@replit/codemirror-interact": "^6.3.1",
|
"@replit/codemirror-interact": "^6.3.1",
|
||||||
"@tauri-apps/api": "2.0.0-beta.12",
|
"@tauri-apps/api": "^2.0.0-beta.14",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-dialog": "^2.0.0-beta.6",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0-beta.3",
|
"@tauri-apps/plugin-fs": "^2.0.0-beta.6",
|
||||||
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-http": "^2.0.0-beta.7",
|
||||||
"@tauri-apps/plugin-os": "^2.0.0-beta.3",
|
"@tauri-apps/plugin-os": "^2.0.0-beta.6",
|
||||||
"@tauri-apps/plugin-process": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-process": "^2.0.0-beta.6",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
|
"@tauri-apps/plugin-shell": "^2.0.0-beta.7",
|
||||||
"@tauri-apps/plugin-updater": "^2.0.0-beta.3",
|
"@tauri-apps/plugin-updater": "^2.0.0-beta.6",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
|
||||||
"@testing-library/react": "^15.0.2",
|
|
||||||
"@testing-library/user-event": "^14.5.2",
|
|
||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@tweenjs/tween.js": "^23.1.1",
|
"@tweenjs/tween.js": "^23.1.1",
|
||||||
"@types/node": "^18.19.31",
|
|
||||||
"@types/react": "^18.3.2",
|
|
||||||
"@types/react-dom": "^18.2.25",
|
|
||||||
"@uiw/react-codemirror": "^4.21.25",
|
|
||||||
"@xstate/inspect": "^0.8.0",
|
"@xstate/inspect": "^0.8.0",
|
||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.2.0",
|
"codemirror": "^6.0.1",
|
||||||
"debounce-promise": "^3.1.2",
|
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
|
||||||
"formik": "^2.4.6",
|
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html2canvas-pro": "^1.4.3",
|
"html2canvas-pro": "^1.5.2",
|
||||||
"http-server": "^14.1.1",
|
|
||||||
"json-rpc-2.0": "^1.6.0",
|
"json-rpc-2.0": "^1.6.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"node-fetch": "^3.3.2",
|
|
||||||
"re-resizable": "^6.9.11",
|
"re-resizable": "^6.9.11",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -55,20 +48,15 @@
|
|||||||
"react-modal-promise": "^1.0.2",
|
"react-modal-promise": "^1.0.2",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"sketch-helpers": "^0.0.4",
|
"sketch-helpers": "^0.0.4",
|
||||||
"swr": "^2.2.5",
|
"three": "^0.166.1",
|
||||||
"three": "^0.164.1",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vitest": "^1.6.0",
|
|
||||||
"vscode-jsonrpc": "^8.2.1",
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"wasm-pack": "^0.12.1",
|
"vscode-uri": "^3.0.8",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"ws": "^8.17.0",
|
"xstate": "^4.38.2"
|
||||||
"xstate": "^4.38.2",
|
|
||||||
"zustand": "^4.5.2"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@ -85,8 +73,8 @@
|
|||||||
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
||||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
|
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
||||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
|
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
@ -121,13 +109,16 @@
|
|||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.24.3",
|
"@babel/preset-env": "^7.24.3",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.45.1",
|
||||||
"@tauri-apps/cli": "^2.0.0-beta.13",
|
"@tauri-apps/cli": "==2.0.0-beta.13",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@types/debounce-promise": "^3.1.9",
|
"@testing-library/react": "^15.0.2",
|
||||||
"@types/mocha": "^10.0.6",
|
"@types/mocha": "^10.0.6",
|
||||||
|
"@types/node": "^18.19.31",
|
||||||
"@types/pixelmatch": "^5.2.6",
|
"@types/pixelmatch": "^5.2.6",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
|
"@types/react": "^18.3.2",
|
||||||
|
"@types/react-dom": "^18.2.25",
|
||||||
"@types/react-modal": "^3.16.3",
|
"@types/react-modal": "^3.16.3",
|
||||||
"@types/three": "^0.163.0",
|
"@types/three": "^0.163.0",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
@ -147,21 +138,27 @@
|
|||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
|
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||||
"happy-dom": "^14.3.10",
|
"happy-dom": "^14.3.10",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"pixelmatch": "^5.3.0",
|
"pixelmatch": "^5.3.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.8",
|
||||||
"setimmediate": "^1.0.5",
|
"setimmediate": "^1.0.5",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"vite": "^5.2.9",
|
"vite": "^5.2.9",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-package-version": "^1.1.0",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
"vitest": "^1.6.0",
|
||||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||||
"wait-on": "^7.2.0",
|
"wait-on": "^7.2.0",
|
||||||
|
"wasm-pack": "^0.13.0",
|
||||||
|
"ws": "^8.17.0",
|
||||||
"yarn": "^1.22.22"
|
"yarn": "^1.22.22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
packages/codemirror-lsp-client/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
*.d.ts
|
||||||
|
*.js
|
35
packages/codemirror-lsp-client/package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@kittycad/codemirror-lsp-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "An LSP client for the codemirror editor.",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"repository": "https://github.com/KittyCAD/modeling-app",
|
||||||
|
"author": "Zoo Engineering Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": false,
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.16.3",
|
||||||
|
"@codemirror/language": "^6.10.2",
|
||||||
|
"@codemirror/state": "^6.4.1",
|
||||||
|
"@lezer/highlight": "^1.2.0",
|
||||||
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
|
"json-rpc-2.0": "^1.7.0",
|
||||||
|
"typescript": "^5.5.2",
|
||||||
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
|
"vscode-uri": "^3.0.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.9",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import * as vsrpc from 'vscode-jsonrpc'
|
import * as vsrpc from 'vscode-jsonrpc'
|
||||||
|
|
||||||
|
import { Codec } from '.'
|
||||||
import Bytes from './bytes'
|
import Bytes from './bytes'
|
||||||
import PromiseMap from './map'
|
|
||||||
import Queue from './queue'
|
import Queue from './queue'
|
||||||
import Tracer from '../tracer'
|
import Tracer from './tracer'
|
||||||
import { Codec } from '../codec'
|
import PromiseMap from './map'
|
||||||
|
|
||||||
export default class StreamDemuxer extends Queue<Uint8Array> {
|
export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||||
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
|
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
|
||||||
@ -15,9 +15,12 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
|||||||
new Queue<vsrpc.RequestMessage>()
|
new Queue<vsrpc.RequestMessage>()
|
||||||
|
|
||||||
readonly #start: Promise<void>
|
readonly #start: Promise<void>
|
||||||
|
private trace: boolean = false
|
||||||
|
|
||||||
constructor() {
|
constructor(trace?: boolean) {
|
||||||
super()
|
super()
|
||||||
|
this.trace = trace || false
|
||||||
|
|
||||||
this.#start = this.start()
|
this.#start = this.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +67,10 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
|||||||
contentLength = null
|
contentLength = null
|
||||||
|
|
||||||
const message = JSON.parse(delimited) as vsrpc.Message
|
const message = JSON.parse(delimited) as vsrpc.Message
|
||||||
Tracer.server(message)
|
|
||||||
|
if (this.trace) {
|
||||||
|
Tracer.server(message)
|
||||||
|
}
|
||||||
|
|
||||||
// demux the message stream
|
// demux the message stream
|
||||||
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
||||||
@ -85,7 +91,9 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
|||||||
|
|
||||||
add(bytes: Uint8Array): void {
|
add(bytes: Uint8Array): void {
|
||||||
const message = Codec.decode(bytes) as vsrpc.Message
|
const message = Codec.decode(bytes) as vsrpc.Message
|
||||||
Tracer.server(message)
|
if (this.trace) {
|
||||||
|
Tracer.server(message)
|
||||||
|
}
|
||||||
|
|
||||||
// demux the message stream
|
// demux the message stream
|
||||||
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
@ -1,12 +1,16 @@
|
|||||||
import * as jsrpc from 'json-rpc-2.0'
|
import * as jsrpc from 'json-rpc-2.0'
|
||||||
import * as vsrpc from 'vscode-jsonrpc'
|
import * as vsrpc from 'vscode-jsonrpc'
|
||||||
|
|
||||||
import Bytes from './codec/bytes'
|
import Bytes from './bytes'
|
||||||
import StreamDemuxer from './codec/demuxer'
|
import StreamDemuxer from './demuxer'
|
||||||
import Headers from './codec/headers'
|
import Headers from './headers'
|
||||||
import Queue from './codec/queue'
|
import Queue from './queue'
|
||||||
import Tracer from './tracer'
|
import Tracer from './tracer'
|
||||||
import { LspWorkerEventType, LspWorker } from './types'
|
|
||||||
|
export enum LspWorkerEventType {
|
||||||
|
Init = 'init',
|
||||||
|
Call = 'call',
|
||||||
|
}
|
||||||
|
|
||||||
export const encoder = new TextEncoder()
|
export const encoder = new TextEncoder()
|
||||||
export const decoder = new TextDecoder()
|
export const decoder = new TextDecoder()
|
||||||
@ -33,16 +37,24 @@ export class IntoServer
|
|||||||
implements AsyncGenerator<Uint8Array, never, void>
|
implements AsyncGenerator<Uint8Array, never, void>
|
||||||
{
|
{
|
||||||
private worker: Worker | null = null
|
private worker: Worker | null = null
|
||||||
private type_: LspWorker | null = null
|
private type_: String | null = null
|
||||||
constructor(type_?: LspWorker, worker?: Worker) {
|
|
||||||
|
private trace: boolean = false
|
||||||
|
|
||||||
|
constructor(type_?: String, worker?: Worker, trace?: boolean) {
|
||||||
super()
|
super()
|
||||||
if (worker && type_) {
|
if (worker && type_) {
|
||||||
this.worker = worker
|
this.worker = worker
|
||||||
this.type_ = type_
|
this.type_ = type_
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.trace = trace || false
|
||||||
}
|
}
|
||||||
enqueue(item: Uint8Array): void {
|
enqueue(item: Uint8Array): void {
|
||||||
Tracer.client(Headers.remove(decoder.decode(item)))
|
if (this.trace) {
|
||||||
|
Tracer.client(Headers.remove(decoder.decode(item)))
|
||||||
|
}
|
||||||
|
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
worker: this.type_,
|
worker: this.type_,
|
||||||
@ -71,7 +83,7 @@ export namespace FromServer {
|
|||||||
// Calls private method .start() which can throw.
|
// Calls private method .start() which can throw.
|
||||||
// This is an odd one of the bunch but try/catch seems most suitable here.
|
// This is an odd one of the bunch but try/catch seems most suitable here.
|
||||||
try {
|
try {
|
||||||
return new StreamDemuxer()
|
return new StreamDemuxer(false)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
13
packages/codemirror-lsp-client/src/client/codec/tracer.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Message } from 'vscode-languageserver-protocol'
|
||||||
|
|
||||||
|
export default class Tracer {
|
||||||
|
static client(message: string): void {
|
||||||
|
console.log('lsp client message', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
static server(input: string | Message): void {
|
||||||
|
const message: string =
|
||||||
|
typeof input === 'string' ? input : JSON.stringify(input)
|
||||||
|
console.log('lsp server message', message)
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,8 @@
|
|||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
import Client from './client'
|
|
||||||
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
|
import { FromServer, IntoServer } from './codec'
|
||||||
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
|
import Client from './jsonrpc'
|
||||||
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
|
import { LanguageServerPlugin } from '../plugin/lsp'
|
||||||
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
|
|
||||||
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
|
||||||
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
|
||||||
import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams'
|
|
||||||
import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams'
|
|
||||||
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
|
||||||
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
|
||||||
import { LspWorker } from './types'
|
|
||||||
|
|
||||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||||
|
|
||||||
@ -31,12 +23,6 @@ interface LSPRequestMap {
|
|||||||
LSP.TextEdit[] | null
|
LSP.TextEdit[] | null
|
||||||
]
|
]
|
||||||
'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]]
|
'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]]
|
||||||
'copilot/getCompletions': [
|
|
||||||
CopilotLspCompletionParams,
|
|
||||||
CopilotCompletionResponse
|
|
||||||
]
|
|
||||||
'kcl/updateUnits': [UpdateUnitsParams, UpdateUnitsResponse | null]
|
|
||||||
'kcl/updateCanExecute': [UpdateCanExecuteParams, UpdateCanExecuteResponse]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client to server
|
// Client to server
|
||||||
@ -49,21 +35,13 @@ interface LSPNotifyMap {
|
|||||||
'workspace/didCreateFiles': LSP.CreateFilesParams
|
'workspace/didCreateFiles': LSP.CreateFilesParams
|
||||||
'workspace/didRenameFiles': LSP.RenameFilesParams
|
'workspace/didRenameFiles': LSP.RenameFilesParams
|
||||||
'workspace/didDeleteFiles': LSP.DeleteFilesParams
|
'workspace/didDeleteFiles': LSP.DeleteFilesParams
|
||||||
'copilot/notifyAccepted': CopilotAcceptCompletionParams
|
|
||||||
'copilot/notifyRejected': CopilotRejectCompletionParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LanguageServerClientOptions {
|
export interface LanguageServerClientOptions {
|
||||||
client: Client
|
name: string
|
||||||
name: LspWorker
|
fromServer: FromServer
|
||||||
}
|
intoServer: IntoServer
|
||||||
|
initializedCallback: () => void
|
||||||
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 {
|
||||||
@ -76,18 +54,18 @@ export class LanguageServerClient {
|
|||||||
|
|
||||||
public initializePromise: Promise<void>
|
public initializePromise: Promise<void>
|
||||||
|
|
||||||
private isUpdatingSemanticTokens: boolean = false
|
|
||||||
private semanticTokens: SemanticToken[] = []
|
|
||||||
private queuedUids: string[] = []
|
|
||||||
|
|
||||||
constructor(options: LanguageServerClientOptions) {
|
constructor(options: LanguageServerClientOptions) {
|
||||||
this.plugins = []
|
|
||||||
this.client = options.client
|
|
||||||
this.name = options.name
|
this.name = options.name
|
||||||
|
this.plugins = []
|
||||||
|
|
||||||
|
this.client = new Client(
|
||||||
|
options.fromServer,
|
||||||
|
options.intoServer,
|
||||||
|
options.initializedCallback
|
||||||
|
)
|
||||||
|
|
||||||
this.ready = false
|
this.ready = false
|
||||||
|
|
||||||
this.queuedUids = []
|
|
||||||
this.initializePromise = this.initialize()
|
this.initializePromise = this.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,19 +89,10 @@ export class LanguageServerClient {
|
|||||||
|
|
||||||
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
|
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
|
||||||
this.notify('textDocument/didOpen', params)
|
this.notify('textDocument/didOpen', params)
|
||||||
|
|
||||||
// Update the facet of the plugins to the correct value.
|
|
||||||
for (const plugin of this.plugins) {
|
|
||||||
plugin.documentUri = params.textDocument.uri
|
|
||||||
plugin.languageId = params.textDocument.languageId
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateSemanticTokens(params.textDocument.uri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
|
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
|
||||||
this.notify('textDocument/didChange', params)
|
this.notify('textDocument/didChange', params)
|
||||||
this.updateSemanticTokens(params.textDocument.uri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) {
|
textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) {
|
||||||
@ -134,18 +103,9 @@ export class LanguageServerClient {
|
|||||||
added: LSP.WorkspaceFolder[],
|
added: LSP.WorkspaceFolder[],
|
||||||
removed: LSP.WorkspaceFolder[]
|
removed: LSP.WorkspaceFolder[]
|
||||||
) {
|
) {
|
||||||
// Add all the current workspace folders in the plugin to removed.
|
|
||||||
for (const plugin of this.plugins) {
|
|
||||||
removed.push(...plugin.workspaceFolders)
|
|
||||||
}
|
|
||||||
this.notify('workspace/didChangeWorkspaceFolders', {
|
this.notify('workspace/didChangeWorkspaceFolders', {
|
||||||
event: { added, removed },
|
event: { added, removed },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add all the new workspace folders to the plugins.
|
|
||||||
for (const plugin of this.plugins) {
|
|
||||||
plugin.workspaceFolders = added
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaceDidCreateFiles(params: LSP.CreateFilesParams) {
|
workspaceDidCreateFiles(params: LSP.CreateFilesParams) {
|
||||||
@ -160,33 +120,13 @@ export class LanguageServerClient {
|
|||||||
this.notify('workspace/didDeleteFiles', params)
|
this.notify('workspace/didDeleteFiles', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSemanticTokens(uri: string) {
|
async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) {
|
||||||
const serverCapabilities = this.getServerCapabilities()
|
const serverCapabilities = this.getServerCapabilities()
|
||||||
if (!serverCapabilities.semanticTokensProvider) {
|
if (!serverCapabilities.semanticTokensProvider) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we can only run, if we aren't already running.
|
return this.request('textDocument/semanticTokens/full', params)
|
||||||
if (!this.isUpdatingSemanticTokens) {
|
|
||||||
this.isUpdatingSemanticTokens = true
|
|
||||||
|
|
||||||
const result = await this.request('textDocument/semanticTokens/full', {
|
|
||||||
textDocument: {
|
|
||||||
uri,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.semanticTokens = await deserializeTokens(
|
|
||||||
result.data,
|
|
||||||
this.getServerCapabilities().semanticTokensProvider
|
|
||||||
)
|
|
||||||
|
|
||||||
this.isUpdatingSemanticTokens = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSemanticTokens(): SemanticToken[] {
|
|
||||||
return this.semanticTokens
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async textDocumentHover(params: LSP.HoverParams) {
|
async textDocumentHover(params: LSP.HoverParams) {
|
||||||
@ -239,6 +179,10 @@ export class LanguageServerClient {
|
|||||||
return this.client.request(method, params) as Promise<LSPRequestMap[K][1]>
|
return this.client.request(method, params) as Promise<LSPRequestMap[K][1]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestCustom<P, R>(method: string, params: P): Promise<R> {
|
||||||
|
return this.client.request(method, params) as Promise<R>
|
||||||
|
}
|
||||||
|
|
||||||
private notify<K extends keyof LSPNotifyMap>(
|
private notify<K extends keyof LSPNotifyMap>(
|
||||||
method: K,
|
method: K,
|
||||||
params: LSPNotifyMap[K]
|
params: LSPNotifyMap[K]
|
||||||
@ -246,44 +190,8 @@ export class LanguageServerClient {
|
|||||||
return this.client.notify(method, params)
|
return this.client.notify(method, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletion(params: CopilotLspCompletionParams) {
|
notifyCustom<P>(method: string, params: P): void {
|
||||||
const response = await this.request('copilot/getCompletions', params)
|
return this.client.notify(method, params)
|
||||||
//
|
|
||||||
this.queuedUids = [...response.completions.map((c) => c.uuid)]
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async accept(uuid: string) {
|
|
||||||
const badUids = this.queuedUids.filter((u) => u !== uuid)
|
|
||||||
this.queuedUids = []
|
|
||||||
this.acceptCompletion({ uuid })
|
|
||||||
this.rejectCompletions({ uuids: badUids })
|
|
||||||
}
|
|
||||||
|
|
||||||
async reject() {
|
|
||||||
const badUids = this.queuedUids
|
|
||||||
this.queuedUids = []
|
|
||||||
this.rejectCompletions({ uuids: badUids })
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptCompletion(params: CopilotAcceptCompletionParams) {
|
|
||||||
this.notify('copilot/notifyAccepted', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectCompletions(params: CopilotRejectCompletionParams) {
|
|
||||||
this.notify('copilot/notifyRejected', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateUnits(
|
|
||||||
params: UpdateUnitsParams
|
|
||||||
): Promise<UpdateUnitsResponse | null> {
|
|
||||||
return await this.request('kcl/updateUnits', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCanExecute(
|
|
||||||
params: UpdateCanExecuteParams
|
|
||||||
): Promise<UpdateCanExecuteResponse> {
|
|
||||||
return await this.request('kcl/updateCanExecute', params)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private processNotifications(notification: LSP.NotificationMessage) {
|
private processNotifications(notification: LSP.NotificationMessage) {
|
@ -6,7 +6,6 @@ import {
|
|||||||
unregisterServerCapability,
|
unregisterServerCapability,
|
||||||
} from './server-capability-registration'
|
} from './server-capability-registration'
|
||||||
import { Codec, FromServer, IntoServer } from './codec'
|
import { Codec, FromServer, IntoServer } from './codec'
|
||||||
import { err } from 'lib/trap'
|
|
||||||
|
|
||||||
const client_capabilities: LSP.ClientCapabilities = {
|
const client_capabilities: LSP.ClientCapabilities = {
|
||||||
textDocument: {
|
textDocument: {
|
||||||
@ -67,8 +66,13 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
#fromServer: FromServer
|
#fromServer: FromServer
|
||||||
private serverCapabilities: LSP.ServerCapabilities<any> = {}
|
private serverCapabilities: LSP.ServerCapabilities<any> = {}
|
||||||
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
|
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
|
||||||
|
private initializedCallback: () => void
|
||||||
|
|
||||||
constructor(fromServer: FromServer, intoServer: IntoServer) {
|
constructor(
|
||||||
|
fromServer: FromServer,
|
||||||
|
intoServer: IntoServer,
|
||||||
|
initializedCallback: () => void
|
||||||
|
) {
|
||||||
super(
|
super(
|
||||||
new jsrpc.JSONRPCServer(),
|
new jsrpc.JSONRPCServer(),
|
||||||
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
|
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
|
||||||
@ -82,6 +86,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.#fromServer = fromServer
|
this.#fromServer = fromServer
|
||||||
|
this.initializedCallback = initializedCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
@ -124,7 +129,9 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
this.serverCapabilities,
|
this.serverCapabilities,
|
||||||
capabilityRegistration
|
capabilityRegistration
|
||||||
)
|
)
|
||||||
if (err(caps)) return (this.serverCapabilities = {})
|
if (caps instanceof Error) {
|
||||||
|
return (this.serverCapabilities = {})
|
||||||
|
}
|
||||||
this.serverCapabilities = caps
|
this.serverCapabilities = caps
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -139,7 +146,9 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
this.serverCapabilities,
|
this.serverCapabilities,
|
||||||
capabilityUnregistration
|
capabilityUnregistration
|
||||||
)
|
)
|
||||||
if (err(caps)) return (this.serverCapabilities = {})
|
if (caps instanceof Error) {
|
||||||
|
return (this.serverCapabilities = {})
|
||||||
|
}
|
||||||
this.serverCapabilities = caps
|
this.serverCapabilities = caps
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -151,7 +160,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
{
|
{
|
||||||
processId: null,
|
processId: null,
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
name: 'kcl-language-client',
|
name: 'codemirror-lsp-client',
|
||||||
},
|
},
|
||||||
capabilities: client_capabilities,
|
capabilities: client_capabilities,
|
||||||
rootUri: null,
|
rootUri: null,
|
||||||
@ -163,6 +172,8 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
|||||||
// notify "initialized": client --> server
|
// notify "initialized": client --> server
|
||||||
this.notify(LSP.InitializedNotification.type.method, {})
|
this.notify(LSP.InitializedNotification.type.method, {})
|
||||||
|
|
||||||
|
this.initializedCallback()
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.afterInitializedHooks.map((f: () => Promise<void>) => f())
|
this.afterInitializedHooks.map((f: () => Promise<void>) => f())
|
||||||
)
|
)
|
@ -42,8 +42,9 @@ function registerServerCapability(
|
|||||||
serverCapabilities: ServerCapabilities,
|
serverCapabilities: ServerCapabilities,
|
||||||
registration: Registration
|
registration: Registration
|
||||||
): ServerCapabilities | Error {
|
): ServerCapabilities | Error {
|
||||||
const serverCapabilitiesCopy =
|
const serverCapabilitiesCopy = JSON.parse(
|
||||||
serverCapabilities as IFlexibleServerCapabilities
|
JSON.stringify(serverCapabilities)
|
||||||
|
) as IFlexibleServerCapabilities
|
||||||
const { method, registerOptions } = registration
|
const { method, registerOptions } = registration
|
||||||
const providerName = ServerCapabilitiesProviders[method]
|
const providerName = ServerCapabilitiesProviders[method]
|
||||||
|
|
||||||
@ -51,7 +52,10 @@ function registerServerCapability(
|
|||||||
if (!registerOptions) {
|
if (!registerOptions) {
|
||||||
serverCapabilitiesCopy[providerName] = true
|
serverCapabilitiesCopy[providerName] = true
|
||||||
} else {
|
} else {
|
||||||
serverCapabilitiesCopy[providerName] = Object.assign({}, registerOptions)
|
serverCapabilitiesCopy[providerName] = Object.assign(
|
||||||
|
{},
|
||||||
|
JSON.parse(JSON.stringify(registerOptions))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new Error('Could not register server capability.')
|
return new Error('Could not register server capability.')
|
||||||
@ -64,8 +68,9 @@ function unregisterServerCapability(
|
|||||||
serverCapabilities: ServerCapabilities,
|
serverCapabilities: ServerCapabilities,
|
||||||
unregistration: Unregistration
|
unregistration: Unregistration
|
||||||
): ServerCapabilities {
|
): ServerCapabilities {
|
||||||
const serverCapabilitiesCopy =
|
const serverCapabilitiesCopy = JSON.parse(
|
||||||
serverCapabilities as IFlexibleServerCapabilities
|
JSON.stringify(serverCapabilities)
|
||||||
|
) as IFlexibleServerCapabilities
|
||||||
const { method } = unregistration
|
const { method } = unregistration
|
||||||
const providerName = ServerCapabilitiesProviders[method]
|
const providerName = ServerCapabilitiesProviders[method]
|
||||||
|
|
57
packages/codemirror-lsp-client/src/index.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { foldService } from '@codemirror/language'
|
||||||
|
import { Extension, EditorState } from '@codemirror/state'
|
||||||
|
import { ViewPlugin } from '@codemirror/view'
|
||||||
|
|
||||||
|
import {
|
||||||
|
docPathFacet,
|
||||||
|
LanguageServerPlugin,
|
||||||
|
LanguageServerPluginSpec,
|
||||||
|
languageId,
|
||||||
|
workspaceFolders,
|
||||||
|
LanguageServerOptions,
|
||||||
|
} from './plugin/lsp'
|
||||||
|
|
||||||
|
export type { LanguageServerClientOptions } from './client'
|
||||||
|
export { LanguageServerClient } from './client'
|
||||||
|
export {
|
||||||
|
Codec,
|
||||||
|
FromServer,
|
||||||
|
IntoServer,
|
||||||
|
LspWorkerEventType,
|
||||||
|
} from './client/codec'
|
||||||
|
export type { LanguageServerOptions } from './plugin/lsp'
|
||||||
|
export {
|
||||||
|
LanguageServerPlugin,
|
||||||
|
LanguageServerPluginSpec,
|
||||||
|
docPathFacet,
|
||||||
|
languageId,
|
||||||
|
workspaceFolders,
|
||||||
|
lspSemanticTokensEvent,
|
||||||
|
lspDiagnosticsEvent,
|
||||||
|
lspFormatCodeEvent,
|
||||||
|
} from './plugin/lsp'
|
||||||
|
export { posToOffset, offsetToPos } from './plugin/util'
|
||||||
|
|
||||||
|
export function lspPlugin(options: LanguageServerOptions): Extension {
|
||||||
|
let plugin: LanguageServerPlugin | null = null
|
||||||
|
const viewPlugin = ViewPlugin.define(
|
||||||
|
(view) => (plugin = new LanguageServerPlugin(options, view)),
|
||||||
|
new LanguageServerPluginSpec()
|
||||||
|
)
|
||||||
|
|
||||||
|
let ext = [
|
||||||
|
docPathFacet.of(options.documentUri),
|
||||||
|
languageId.of('kcl'),
|
||||||
|
workspaceFolders.of(options.workspaceFolders),
|
||||||
|
viewPlugin,
|
||||||
|
foldService.of((state: EditorState, lineStart: number, lineEnd: number) => {
|
||||||
|
if (plugin == null) return null
|
||||||
|
// Get the folding ranges from the language server.
|
||||||
|
// Since this is async we directly need to update the folding ranges after.
|
||||||
|
const range = plugin?.foldingRange(lineStart, lineEnd)
|
||||||
|
return range
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
return ext
|
||||||
|
}
|
112
packages/codemirror-lsp-client/src/plugin/autocomplete.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
acceptCompletion,
|
||||||
|
autocompletion,
|
||||||
|
clearSnippet,
|
||||||
|
closeCompletion,
|
||||||
|
hasNextSnippetField,
|
||||||
|
moveCompletionSelection,
|
||||||
|
nextSnippetField,
|
||||||
|
prevSnippetField,
|
||||||
|
startCompletion,
|
||||||
|
} from '@codemirror/autocomplete'
|
||||||
|
import { Prec, Extension } from '@codemirror/state'
|
||||||
|
import { EditorView, keymap, KeyBinding, ViewPlugin } from '@codemirror/view'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CompletionItemKind,
|
||||||
|
CompletionTriggerKind,
|
||||||
|
} from 'vscode-languageserver-protocol'
|
||||||
|
|
||||||
|
import { LanguageServerPlugin } from './lsp'
|
||||||
|
import { offsetToPos } from './util'
|
||||||
|
import { syntaxTree } from '@codemirror/language'
|
||||||
|
|
||||||
|
export const CompletionItemKindMap = Object.fromEntries(
|
||||||
|
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
||||||
|
) as Record<CompletionItemKind, string>
|
||||||
|
|
||||||
|
const lspAutocompleteKeymap: readonly KeyBinding[] = [
|
||||||
|
{ key: 'Ctrl-Space', run: startCompletion },
|
||||||
|
{
|
||||||
|
key: 'Escape',
|
||||||
|
run: (view: EditorView): boolean => {
|
||||||
|
if (clearSnippet(view)) return true
|
||||||
|
|
||||||
|
return closeCompletion(view)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'ArrowDown', run: moveCompletionSelection(true) },
|
||||||
|
{ key: 'ArrowUp', run: moveCompletionSelection(false) },
|
||||||
|
{ key: 'PageDown', run: moveCompletionSelection(true, 'page') },
|
||||||
|
{ key: 'PageUp', run: moveCompletionSelection(false, 'page') },
|
||||||
|
{ key: 'Enter', run: acceptCompletion },
|
||||||
|
{
|
||||||
|
key: 'Tab',
|
||||||
|
run: (view: EditorView): boolean => {
|
||||||
|
if (hasNextSnippetField(view.state)) {
|
||||||
|
const result = nextSnippetField(view)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return acceptCompletion(view)
|
||||||
|
},
|
||||||
|
shift: prevSnippetField,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const lspAutocompleteKeymapExt = Prec.highest(keymap.of(lspAutocompleteKeymap))
|
||||||
|
|
||||||
|
export default function lspAutocompleteExt(
|
||||||
|
plugin: ViewPlugin<LanguageServerPlugin>
|
||||||
|
): Extension {
|
||||||
|
return [
|
||||||
|
lspAutocompleteKeymapExt,
|
||||||
|
autocompletion({
|
||||||
|
defaultKeymap: false,
|
||||||
|
override: [
|
||||||
|
async (context) => {
|
||||||
|
const { state, pos, explicit, view } = context
|
||||||
|
let value = view?.plugin(plugin)
|
||||||
|
if (!value) return null
|
||||||
|
|
||||||
|
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
|
||||||
|
if (
|
||||||
|
nodeBefore.name === 'BlockComment' ||
|
||||||
|
nodeBefore.name === 'LineComment'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const line = state.doc.lineAt(pos)
|
||||||
|
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
|
||||||
|
let trigChar: string | undefined
|
||||||
|
if (
|
||||||
|
!explicit &&
|
||||||
|
value.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 value.requestCompletion(
|
||||||
|
context,
|
||||||
|
offsetToPos(state.doc, pos),
|
||||||
|
{
|
||||||
|
triggerKind: trigKind,
|
||||||
|
triggerCharacter: trigChar,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
27
packages/codemirror-lsp-client/src/plugin/format.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Extension, Prec } from '@codemirror/state'
|
||||||
|
import { EditorView, keymap, KeyBinding, ViewPlugin } from '@codemirror/view'
|
||||||
|
|
||||||
|
import { LanguageServerPlugin } from './lsp'
|
||||||
|
|
||||||
|
export default function lspFormatExt(
|
||||||
|
plugin: ViewPlugin<LanguageServerPlugin>
|
||||||
|
): Extension {
|
||||||
|
const formatKeymap: readonly KeyBinding[] = [
|
||||||
|
{
|
||||||
|
key: 'Alt-Shift-f',
|
||||||
|
run: (view: EditorView) => {
|
||||||
|
let value = view.plugin(plugin)
|
||||||
|
if (!value) return false
|
||||||
|
value.requestFormatting()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create an extension for the key mappings.
|
||||||
|
const formatKeymapExt = Prec.highest(
|
||||||
|
keymap.computeN([], () => [formatKeymap])
|
||||||
|
)
|
||||||
|
|
||||||
|
return formatKeymapExt
|
||||||
|
}
|
22
packages/codemirror-lsp-client/src/plugin/hover.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Extension } from '@codemirror/state'
|
||||||
|
import { hoverTooltip, tooltips, ViewPlugin } from '@codemirror/view'
|
||||||
|
|
||||||
|
import { LanguageServerPlugin } from './lsp'
|
||||||
|
import { offsetToPos } from './util'
|
||||||
|
|
||||||
|
export default function lspHoverExt(
|
||||||
|
plugin: ViewPlugin<LanguageServerPlugin>
|
||||||
|
): Extension {
|
||||||
|
return [
|
||||||
|
hoverTooltip((view, pos) => {
|
||||||
|
const value = view.plugin(plugin)
|
||||||
|
return (
|
||||||
|
value?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
tooltips({
|
||||||
|
position: 'absolute',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
21
packages/codemirror-lsp-client/src/plugin/indent.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { indentService } from '@codemirror/language'
|
||||||
|
import { Extension } from '@codemirror/state'
|
||||||
|
|
||||||
|
export default function lspIndentExt(): Extension {
|
||||||
|
// Match the indentation of the previous line (if present).
|
||||||
|
return indentService.of((context, pos) => {
|
||||||
|
try {
|
||||||
|
const previousLine = context.lineAt(pos, -1)
|
||||||
|
const previousLineText = previousLine.text.replaceAll(
|
||||||
|
'\t',
|
||||||
|
' '.repeat(context.state.tabSize)
|
||||||
|
)
|
||||||
|
const match = previousLineText.match(/^(\s)*/)
|
||||||
|
if (match === null || match.length <= 0) return null
|
||||||
|
return match[0].length
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in codemirror indentService', err)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
12
packages/codemirror-lsp-client/src/plugin/lint.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Extension } from '@codemirror/state'
|
||||||
|
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
|
||||||
|
|
||||||
|
export default function lspLintExt(): Extension {
|
||||||
|
return linter((view) => {
|
||||||
|
let diagnostics: Diagnostic[] = []
|
||||||
|
forEachDiagnostic(view.state, (d: Diagnostic, from: number, to: number) => {
|
||||||
|
diagnostics.push(d)
|
||||||
|
})
|
||||||
|
return diagnostics
|
||||||
|
})
|
||||||
|
}
|
@ -1,115 +1,155 @@
|
|||||||
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
|
|
||||||
import { setDiagnostics } from '@codemirror/lint'
|
|
||||||
import { Facet } from '@codemirror/state'
|
|
||||||
import { EditorView, Tooltip } from '@codemirror/view'
|
|
||||||
import {
|
|
||||||
DiagnosticSeverity,
|
|
||||||
CompletionItemKind,
|
|
||||||
CompletionTriggerKind,
|
|
||||||
} from 'vscode-languageserver-protocol'
|
|
||||||
|
|
||||||
import { deferExecution } from 'lib/utils'
|
|
||||||
import type {
|
import type {
|
||||||
Completion,
|
Completion,
|
||||||
CompletionContext,
|
CompletionContext,
|
||||||
CompletionResult,
|
CompletionResult,
|
||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
|
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
|
||||||
|
import {
|
||||||
|
Facet,
|
||||||
|
StateEffect,
|
||||||
|
Extension,
|
||||||
|
Transaction,
|
||||||
|
Annotation,
|
||||||
|
} from '@codemirror/state'
|
||||||
|
import type {
|
||||||
|
ViewUpdate,
|
||||||
|
PluginValue,
|
||||||
|
PluginSpec,
|
||||||
|
ViewPlugin,
|
||||||
|
} from '@codemirror/view'
|
||||||
|
import { EditorView, Tooltip } from '@codemirror/view'
|
||||||
|
|
||||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||||
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 } from 'editor/plugins/lsp'
|
import {
|
||||||
import { Marked } from '@ts-stack/markdown'
|
DiagnosticSeverity,
|
||||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
CompletionTriggerKind,
|
||||||
import { Program, ProgramMemory } from 'lang/wasm'
|
} from 'vscode-languageserver-protocol'
|
||||||
import { codeManager, editorManager, kclManager } from 'lib/singletons'
|
import { URI } from 'vscode-uri'
|
||||||
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
|
||||||
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
import { LanguageServerClient } from '../client'
|
||||||
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
import { CompletionItemKindMap } from './autocomplete'
|
||||||
|
import { addToken, SemanticToken } from './semantic-tokens'
|
||||||
|
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
|
||||||
|
import lspAutocompleteExt from './autocomplete'
|
||||||
|
import lspHoverExt from './hover'
|
||||||
|
import lspFormatExt from './format'
|
||||||
|
import lspIndentExt from './indent'
|
||||||
|
import lspLintExt from './lint'
|
||||||
|
import lspSemanticTokensExt from './semantic-tokens'
|
||||||
|
|
||||||
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
||||||
export const documentUri = Facet.define<string, string>({ combine: useLast })
|
export const docPathFacet = Facet.define<string, string>({
|
||||||
|
combine: useLast,
|
||||||
|
})
|
||||||
export const languageId = Facet.define<string, string>({ combine: useLast })
|
export const languageId = Facet.define<string, string>({ combine: useLast })
|
||||||
export const workspaceFolders = Facet.define<
|
export const workspaceFolders = Facet.define<
|
||||||
LSP.WorkspaceFolder[],
|
LSP.WorkspaceFolder[],
|
||||||
LSP.WorkspaceFolder[]
|
LSP.WorkspaceFolder[]
|
||||||
>({ combine: useLast })
|
>({ combine: useLast })
|
||||||
|
|
||||||
const CompletionItemKindMap = Object.fromEntries(
|
export enum LspAnnotation {
|
||||||
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
SemanticTokens = 'semantic-tokens',
|
||||||
) as Record<CompletionItemKind, string>
|
FormatCode = 'format-code',
|
||||||
|
Diagnostics = 'diagnostics',
|
||||||
|
}
|
||||||
|
|
||||||
const changesDelay = 600
|
const lspEvent = Annotation.define<LspAnnotation>()
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
|
||||||
const updateDelay = 100
|
export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
|
||||||
|
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
|
||||||
|
|
||||||
|
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
|
||||||
|
processLspNotification?: (
|
||||||
|
plugin: LanguageServerPlugin,
|
||||||
|
notification: LSP.NotificationMessage
|
||||||
|
) => void
|
||||||
|
|
||||||
|
changesDelay?: number
|
||||||
|
}
|
||||||
|
|
||||||
export class LanguageServerPlugin implements PluginValue {
|
export class LanguageServerPlugin implements PluginValue {
|
||||||
public client: LanguageServerClient
|
public client: LanguageServerClient
|
||||||
public documentUri: string
|
|
||||||
public languageId: string
|
|
||||||
public workspaceFolders: LSP.WorkspaceFolder[]
|
|
||||||
private documentVersion: number
|
private documentVersion: number
|
||||||
private foldingRanges: LSP.FoldingRange[] | null = null
|
private foldingRanges: LSP.FoldingRange[] | null = null
|
||||||
private viewUpdate: ViewUpdate | null = null
|
|
||||||
|
private previousSemanticTokens: SemanticToken[] = []
|
||||||
|
|
||||||
|
private allowHTMLContent: boolean = true
|
||||||
|
private changesDelay: number = 600
|
||||||
|
private processLspNotification?: (
|
||||||
|
plugin: LanguageServerPlugin,
|
||||||
|
notification: LSP.NotificationMessage
|
||||||
|
) => void
|
||||||
|
|
||||||
private _defferer = deferExecution((code: string) => {
|
private _defferer = deferExecution((code: string) => {
|
||||||
try {
|
try {
|
||||||
// Update the state (not the editor) with the new code.
|
// Update the state (not the editor) with the new code.
|
||||||
this.client.textDocumentDidChange({
|
this.client.textDocumentDidChange({
|
||||||
textDocument: {
|
textDocument: {
|
||||||
uri: this.documentUri,
|
uri: this.getDocUri(),
|
||||||
version: this.documentVersion++,
|
version: this.documentVersion++,
|
||||||
},
|
},
|
||||||
contentChanges: [{ text: code }],
|
contentChanges: [{ text: code }],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.viewUpdate) {
|
this.requestSemanticTokens()
|
||||||
editorManager.handleOnViewUpdate(this.viewUpdate)
|
this.updateFoldingRanges()
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}, changesDelay)
|
}, this.changesDelay)
|
||||||
|
|
||||||
constructor(
|
constructor(options: LanguageServerOptions, private view: EditorView) {
|
||||||
client: LanguageServerClient,
|
this.client = options.client
|
||||||
private view: EditorView,
|
|
||||||
private allowHTMLContent: boolean
|
|
||||||
) {
|
|
||||||
this.client = client
|
|
||||||
this.documentUri = this.view.state.facet(documentUri)
|
|
||||||
this.languageId = this.view.state.facet(languageId)
|
|
||||||
this.workspaceFolders = this.view.state.facet(workspaceFolders)
|
|
||||||
this.documentVersion = 0
|
this.documentVersion = 0
|
||||||
|
|
||||||
|
if (options.changesDelay) {
|
||||||
|
this.changesDelay = options.changesDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.allowHTMLContent !== undefined) {
|
||||||
|
this.allowHTMLContent = options.allowHTMLContent
|
||||||
|
}
|
||||||
|
|
||||||
this.client.attachPlugin(this)
|
this.client.attachPlugin(this)
|
||||||
|
|
||||||
|
this.processLspNotification = options.processLspNotification
|
||||||
|
|
||||||
this.initialize({
|
this.initialize({
|
||||||
documentText: this.view.state.doc.toString(),
|
documentText: this.getDocText(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
update(viewUpdate: ViewUpdate) {
|
private getDocPath(view = this.view) {
|
||||||
this.viewUpdate = viewUpdate
|
return view.state.facet(docPathFacet)
|
||||||
if (!viewUpdate.docChanged) {
|
}
|
||||||
// debounce the view update.
|
|
||||||
// otherwise it is laggy for typing.
|
|
||||||
if (debounceTimer) {
|
|
||||||
clearTimeout(debounceTimer)
|
|
||||||
}
|
|
||||||
|
|
||||||
debounceTimer = setTimeout(() => {
|
private getDocText(view = this.view) {
|
||||||
editorManager.handleOnViewUpdate(viewUpdate)
|
return view.state.doc.toString()
|
||||||
}, updateDelay)
|
}
|
||||||
|
|
||||||
|
private getDocUri(view = this.view) {
|
||||||
|
return URI.file(this.getDocPath(view)).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLanguageId(view = this.view) {
|
||||||
|
return view.state.facet(languageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(viewUpdate: ViewUpdate) {
|
||||||
|
// If the doc didn't change we can return early.
|
||||||
|
if (!viewUpdate.docChanged) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCode = this.view.state.doc.toString()
|
|
||||||
|
|
||||||
codeManager.code = newCode
|
|
||||||
codeManager.writeToFile()
|
|
||||||
kclManager.executeCode()
|
|
||||||
|
|
||||||
this.sendChange({
|
this.sendChange({
|
||||||
documentText: newCode,
|
documentText: viewUpdate.state.doc.toString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,14 +161,18 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
if (this.client.initializePromise) {
|
if (this.client.initializePromise) {
|
||||||
await this.client.initializePromise
|
await this.client.initializePromise
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.textDocumentDidOpen({
|
this.client.textDocumentDidOpen({
|
||||||
textDocument: {
|
textDocument: {
|
||||||
uri: this.documentUri,
|
uri: this.getDocUri(),
|
||||||
languageId: this.languageId,
|
languageId: this.getLanguageId(),
|
||||||
text: documentText,
|
text: documentText,
|
||||||
version: this.documentVersion,
|
version: this.documentVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.requestSemanticTokens()
|
||||||
|
this.updateFoldingRanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendChange({ documentText }: { documentText: string }) {
|
async sendChange({ documentText }: { documentText: string }) {
|
||||||
@ -137,8 +181,8 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
this._defferer(documentText)
|
this._defferer(documentText)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestDiagnostics(view: EditorView) {
|
requestDiagnostics() {
|
||||||
this.sendChange({ documentText: view.state.doc.toString() })
|
this.sendChange({ documentText: this.getDocText() })
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestHoverTooltip(
|
async requestHoverTooltip(
|
||||||
@ -151,9 +195,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
this.sendChange({ documentText: view.state.doc.toString() })
|
this.sendChange({ documentText: this.getDocText() })
|
||||||
const result = await this.client.textDocumentHover({
|
const result = await this.client.textDocumentHover({
|
||||||
textDocument: { uri: this.documentUri },
|
textDocument: { uri: this.getDocUri() },
|
||||||
position: { line, character },
|
position: { line, character },
|
||||||
})
|
})
|
||||||
if (!result) return null
|
if (!result) return null
|
||||||
@ -169,8 +213,8 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
dom.classList.add('documentation')
|
dom.classList.add('documentation')
|
||||||
dom.classList.add('hover-tooltip')
|
dom.classList.add('hover-tooltip')
|
||||||
dom.style.zIndex = '99999999'
|
dom.style.zIndex = '99999999'
|
||||||
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
|
if (this.allowHTMLContent) dom.innerHTML = formatMarkdownContents(contents)
|
||||||
else dom.textContent = formatContents(contents)
|
else dom.textContent = formatMarkdownContents(contents)
|
||||||
return { pos, end, create: (view) => ({ dom }), above: true }
|
return { pos, end, create: (view) => ({ dom }), above: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,8 +224,9 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
!this.client.getServerCapabilities().foldingRangeProvider
|
!this.client.getServerCapabilities().foldingRangeProvider
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const result = await this.client.textDocumentFoldingRange({
|
const result = await this.client.textDocumentFoldingRange({
|
||||||
textDocument: { uri: this.documentUri },
|
textDocument: { uri: this.getDocUri() },
|
||||||
})
|
})
|
||||||
|
|
||||||
return result || null
|
return result || null
|
||||||
@ -222,42 +267,6 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUnits(units: UnitLength): Promise<UpdateUnitsResponse | null> {
|
|
||||||
if (this.client.name !== 'kcl') return null
|
|
||||||
if (!this.client.ready) return null
|
|
||||||
|
|
||||||
return await this.client.updateUnits({
|
|
||||||
textDocument: {
|
|
||||||
uri: this.documentUri,
|
|
||||||
},
|
|
||||||
text: this.view.state.doc.toString(),
|
|
||||||
units,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async updateCanExecute(
|
|
||||||
canExecute: boolean
|
|
||||||
): Promise<UpdateCanExecuteResponse | null> {
|
|
||||||
if (this.client.name !== 'kcl') return null
|
|
||||||
if (!this.client.ready) return null
|
|
||||||
|
|
||||||
let response = await this.client.updateCanExecute({
|
|
||||||
canExecute,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!canExecute && response.isExecuting) {
|
|
||||||
// We want to wait until the server is not busy before we reply to the
|
|
||||||
// caller.
|
|
||||||
while (response.isExecuting) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
response = await this.client.updateCanExecute({
|
|
||||||
canExecute,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('[lsp] kcl: updated canExecute', canExecute, response)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestFormatting() {
|
async requestFormatting() {
|
||||||
if (
|
if (
|
||||||
!this.client.ready ||
|
!this.client.ready ||
|
||||||
@ -267,14 +276,14 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
|
|
||||||
this.client.textDocumentDidChange({
|
this.client.textDocumentDidChange({
|
||||||
textDocument: {
|
textDocument: {
|
||||||
uri: this.documentUri,
|
uri: this.getDocUri(),
|
||||||
version: this.documentVersion++,
|
version: this.documentVersion++,
|
||||||
},
|
},
|
||||||
contentChanges: [{ text: this.view.state.doc.toString() }],
|
contentChanges: [{ text: this.getDocText() }],
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await this.client.textDocumentFormatting({
|
const result = await this.client.textDocumentFormatting({
|
||||||
textDocument: { uri: this.documentUri },
|
textDocument: { uri: this.getDocUri() },
|
||||||
options: {
|
options: {
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
insertSpaces: true,
|
insertSpaces: true,
|
||||||
@ -282,20 +291,16 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) return null
|
if (!result || !result.length) return null
|
||||||
|
|
||||||
for (let i = 0; i < result.length; i++) {
|
this.view.dispatch({
|
||||||
const { range, newText } = result[i]
|
changes: result.map(({ range, newText }) => ({
|
||||||
this.view.dispatch({
|
from: posToOffset(this.view.state.doc, range.start)!,
|
||||||
changes: [
|
to: posToOffset(this.view.state.doc, range.end)!,
|
||||||
{
|
insert: newText,
|
||||||
from: posToOffset(this.view.state.doc, range.start)!,
|
})),
|
||||||
to: posToOffset(this.view.state.doc, range.end)!,
|
annotations: lspFormatCodeEvent,
|
||||||
insert: newText,
|
})
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestCompletion(
|
async requestCompletion(
|
||||||
@ -320,7 +325,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const result = await this.client.textDocumentCompletion({
|
const result = await this.client.textDocumentCompletion({
|
||||||
textDocument: { uri: this.documentUri },
|
textDocument: { uri: this.getDocUri() },
|
||||||
position: { line, character },
|
position: { line, character },
|
||||||
context: {
|
context: {
|
||||||
triggerKind,
|
triggerKind,
|
||||||
@ -360,7 +365,7 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
if (documentation) {
|
if (documentation) {
|
||||||
completion.info = () => {
|
completion.info = () => {
|
||||||
const htmlString = formatContents(documentation)
|
const htmlString = formatMarkdownContents(documentation)
|
||||||
const htmlNode = document.createElement('div')
|
const htmlNode = document.createElement('div')
|
||||||
htmlNode.style.display = 'contents'
|
htmlNode.style.display = 'contents'
|
||||||
htmlNode.innerHTML = htmlString
|
htmlNode.innerHTML = htmlString
|
||||||
@ -379,16 +384,107 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
return completeFromList(options)(context)
|
return completeFromList(options)(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseSemanticTokens(view: EditorView, data: number[]) {
|
||||||
|
// decode the lsp semantic token types
|
||||||
|
const tokens = []
|
||||||
|
for (let i = 0; i < data.length; i += 5) {
|
||||||
|
tokens.push({
|
||||||
|
deltaLine: data[i],
|
||||||
|
startChar: data[i + 1],
|
||||||
|
length: data[i + 2],
|
||||||
|
tokenType: data[i + 3],
|
||||||
|
modifiers: data[i + 4],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert the tokens into an array of {to, from, type} objects
|
||||||
|
const tokenTypes =
|
||||||
|
this.client.getServerCapabilities().semanticTokensProvider!.legend
|
||||||
|
.tokenTypes
|
||||||
|
const tokenModifiers =
|
||||||
|
this.client.getServerCapabilities().semanticTokensProvider!.legend
|
||||||
|
.tokenModifiers
|
||||||
|
const tokenRanges: any = []
|
||||||
|
let curLine = 0
|
||||||
|
let prevStart = 0
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i]
|
||||||
|
const tokenType = tokenTypes[token.tokenType]
|
||||||
|
// get a list of modifiers
|
||||||
|
const tokenModifier = []
|
||||||
|
for (let j = 0; j < tokenModifiers.length; j++) {
|
||||||
|
if (token.modifiers & (1 << j)) {
|
||||||
|
tokenModifier.push(tokenModifiers[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.deltaLine !== 0) prevStart = 0
|
||||||
|
|
||||||
|
const tokenRange = {
|
||||||
|
from: posToOffset(view.state.doc, {
|
||||||
|
line: curLine + token.deltaLine,
|
||||||
|
character: prevStart + token.startChar,
|
||||||
|
})!,
|
||||||
|
to: posToOffset(view.state.doc, {
|
||||||
|
line: curLine + token.deltaLine,
|
||||||
|
character: prevStart + token.startChar + token.length,
|
||||||
|
})!,
|
||||||
|
type: tokenType,
|
||||||
|
modifiers: tokenModifier,
|
||||||
|
}
|
||||||
|
tokenRanges.push(tokenRange)
|
||||||
|
|
||||||
|
curLine += token.deltaLine
|
||||||
|
prevStart += token.startChar
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by from
|
||||||
|
tokenRanges.sort((a: any, b: any) => a.from - b.from)
|
||||||
|
return tokenRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestSemanticTokens() {
|
||||||
|
if (
|
||||||
|
!this.client.ready ||
|
||||||
|
!this.client.getServerCapabilities().semanticTokensProvider
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.client.textDocumentSemanticTokensFull({
|
||||||
|
textDocument: { uri: this.getDocUri() },
|
||||||
|
})
|
||||||
|
if (!result) return null
|
||||||
|
|
||||||
|
const { data } = result
|
||||||
|
this.previousSemanticTokens = this.parseSemanticTokens(this.view, data)
|
||||||
|
|
||||||
|
const effects: StateEffect<SemanticToken | Extension>[] =
|
||||||
|
this.previousSemanticTokens.map((tokenRange: any) =>
|
||||||
|
addToken.of(tokenRange)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.view.dispatch({
|
||||||
|
effects,
|
||||||
|
|
||||||
|
annotations: [lspSemanticTokensEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async processNotification(notification: LSP.NotificationMessage) {
|
async processNotification(notification: LSP.NotificationMessage) {
|
||||||
try {
|
try {
|
||||||
switch (notification.method) {
|
switch (notification.method) {
|
||||||
case 'textDocument/publishDiagnostics':
|
case 'textDocument/publishDiagnostics':
|
||||||
|
if (notification === undefined) break
|
||||||
|
if (notification.params === undefined) break
|
||||||
|
if (!notification.params) break
|
||||||
|
const params = notification.params as PublishDiagnosticsParams
|
||||||
|
if (!params) break
|
||||||
console.log(
|
console.log(
|
||||||
'[lsp] [window/publishDiagnostics]',
|
'[lsp] [window/publishDiagnostics]',
|
||||||
this.client.getName(),
|
this.client.getName(),
|
||||||
notification.params
|
params
|
||||||
)
|
)
|
||||||
const params = notification.params as PublishDiagnosticsParams
|
|
||||||
// this is sometimes slower than our actual typing.
|
// this is sometimes slower than our actual typing.
|
||||||
this.processDiagnostics(params)
|
this.processDiagnostics(params)
|
||||||
break
|
break
|
||||||
@ -406,30 +502,17 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
notification.params
|
notification.params
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'kcl/astUpdated':
|
|
||||||
// The server has updated the AST, we should update elsewhere.
|
|
||||||
let updatedAst = notification.params as Program
|
|
||||||
console.log('[lsp]: Updated AST', updatedAst)
|
|
||||||
|
|
||||||
// Update the folding ranges, since the AST has changed.
|
|
||||||
// This is a hack since codemirror does not support async foldService.
|
|
||||||
// When they do we can delete this.
|
|
||||||
this.updateFoldingRanges()
|
|
||||||
break
|
|
||||||
case 'kcl/memoryUpdated':
|
|
||||||
// The server has updated the memory, we should update elsewhere.
|
|
||||||
let updatedMemory = notification.params as ProgramMemory
|
|
||||||
console.log('[lsp]: Updated Memory', updatedMemory)
|
|
||||||
kclManager.programMemory = updatedMemory
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send it to the plugin
|
||||||
|
this.processLspNotification?.(this, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
processDiagnostics(params: PublishDiagnosticsParams) {
|
processDiagnostics(params: PublishDiagnosticsParams) {
|
||||||
if (params.uri !== this.documentUri) return
|
if (params.uri !== this.getDocUri()) return
|
||||||
|
|
||||||
const diagnostics = params.diagnostics
|
const diagnostics = params.diagnostics
|
||||||
.map(({ range, message, severity }) => ({
|
.map(({ range, message, severity }) => ({
|
||||||
@ -459,18 +542,26 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
|
/* This creates infighting with the others.
|
||||||
|
* TODO: turn it back on when we have a better way to handle it.
|
||||||
|
* this.view.dispatch({
|
||||||
|
effects: [setDiagnosticsEffect.of(diagnostics)],
|
||||||
|
annotations: [lspDiagnosticsEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContents(
|
export class LanguageServerPluginSpec
|
||||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
implements PluginSpec<LanguageServerPlugin>
|
||||||
): string {
|
{
|
||||||
if (Array.isArray(contents)) {
|
provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension {
|
||||||
return contents.map((c) => formatContents(c) + '\n\n').join('')
|
return [
|
||||||
} else if (typeof contents === 'string') {
|
lspAutocompleteExt(plugin),
|
||||||
return Marked.parse(contents)
|
lspFormatExt(plugin),
|
||||||
} else {
|
lspHoverExt(plugin),
|
||||||
return Marked.parse(contents.value)
|
lspIndentExt(),
|
||||||
|
lspLintExt(),
|
||||||
|
lspSemanticTokensExt(),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
175
packages/codemirror-lsp-client/src/plugin/semantic-tokens.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { highlightingFor } from '@codemirror/language'
|
||||||
|
import { StateEffect, StateField, Extension } from '@codemirror/state'
|
||||||
|
import { EditorView, Decoration, DecorationSet } from '@codemirror/view'
|
||||||
|
|
||||||
|
import { Tag, tags } from '@lezer/highlight'
|
||||||
|
|
||||||
|
import { lspSemanticTokensEvent } from './lsp'
|
||||||
|
|
||||||
|
export interface SemanticToken {
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
type: string
|
||||||
|
modifiers: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addToken = StateEffect.define<SemanticToken>({
|
||||||
|
map: (token: SemanticToken, change) => ({
|
||||||
|
...token,
|
||||||
|
from: change.mapPos(token.from),
|
||||||
|
to: change.mapPos(token.to),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function lspSemanticTokenExt(): Extension {
|
||||||
|
return StateField.define<DecorationSet>({
|
||||||
|
create() {
|
||||||
|
return Decoration.none
|
||||||
|
},
|
||||||
|
update(highlights, tr) {
|
||||||
|
// Nothing can come before this line, this is very important!
|
||||||
|
// It makes sure the highlights are updated correctly for the changes.
|
||||||
|
highlights = highlights.map(tr.changes)
|
||||||
|
|
||||||
|
const isSemanticTokensEvent = tr.annotation(lspSemanticTokensEvent.type)
|
||||||
|
if (!isSemanticTokensEvent) {
|
||||||
|
return highlights
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any of the changes are addToken
|
||||||
|
const hasAddToken = tr.effects.some((e) => e.is(addToken))
|
||||||
|
if (hasAddToken) {
|
||||||
|
highlights = highlights.update({
|
||||||
|
filter: (from, to) => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of tr.effects)
|
||||||
|
if (e.is(addToken)) {
|
||||||
|
const tag = getTag(e.value)
|
||||||
|
const className = tag
|
||||||
|
? highlightingFor(tr.startState, [tag])
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (e.value.from < e.value.to && tag) {
|
||||||
|
if (className) {
|
||||||
|
highlights = highlights.update({
|
||||||
|
add: [
|
||||||
|
Decoration.mark({ class: className }).range(
|
||||||
|
e.value.from,
|
||||||
|
e.value.to
|
||||||
|
),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highlights
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTag(semanticToken: SemanticToken): Tag | null {
|
||||||
|
let tokenType = convertSemanticTokenTypeToCodeMirrorTag(semanticToken.type)
|
||||||
|
|
||||||
|
if (
|
||||||
|
semanticToken.modifiers === undefined ||
|
||||||
|
semanticToken.modifiers === null ||
|
||||||
|
semanticToken.modifiers.length === 0
|
||||||
|
) {
|
||||||
|
return tokenType
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let modifier of semanticToken.modifiers) {
|
||||||
|
tokenType = convertSemanticTokenToCodeMirrorTag(
|
||||||
|
'',
|
||||||
|
modifier,
|
||||||
|
tokenType || undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenType
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTagName(semanticToken: SemanticToken): string {
|
||||||
|
let tokenType = semanticToken.type
|
||||||
|
|
||||||
|
if (
|
||||||
|
semanticToken.modifiers === undefined ||
|
||||||
|
semanticToken.modifiers === null ||
|
||||||
|
semanticToken.modifiers.length === 0
|
||||||
|
) {
|
||||||
|
return tokenType
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let modifier of semanticToken.modifiers) {
|
||||||
|
tokenType = `${tokenType}.${modifier}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenType
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSemanticTokenTypeToCodeMirrorTag(
|
||||||
|
tokenType: string
|
||||||
|
): Tag | null {
|
||||||
|
switch (tokenType) {
|
||||||
|
case 'keyword':
|
||||||
|
return tags.keyword
|
||||||
|
case 'variable':
|
||||||
|
return tags.variableName
|
||||||
|
case 'string':
|
||||||
|
return tags.string
|
||||||
|
case 'number':
|
||||||
|
return tags.number
|
||||||
|
case 'comment':
|
||||||
|
return tags.comment
|
||||||
|
case 'operator':
|
||||||
|
return tags.operator
|
||||||
|
case 'function':
|
||||||
|
return tags.function(tags.name)
|
||||||
|
case 'type':
|
||||||
|
return tags.typeName
|
||||||
|
case 'property':
|
||||||
|
return tags.propertyName
|
||||||
|
case 'parameter':
|
||||||
|
return tags.local(tags.name)
|
||||||
|
default:
|
||||||
|
console.error('Unknown token type:', tokenType)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSemanticTokenToCodeMirrorTag(
|
||||||
|
tokenType: string,
|
||||||
|
tokenModifier: string,
|
||||||
|
givenTag?: Tag
|
||||||
|
): Tag | null {
|
||||||
|
let tag = givenTag
|
||||||
|
? givenTag
|
||||||
|
: convertSemanticTokenTypeToCodeMirrorTag(tokenType)
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenModifier) {
|
||||||
|
switch (tokenModifier) {
|
||||||
|
case 'definition':
|
||||||
|
return tags.definition(tag)
|
||||||
|
case 'declaration':
|
||||||
|
return tags.definition(tag)
|
||||||
|
case 'readonly':
|
||||||
|
return tags.constant(tag)
|
||||||
|
case 'static':
|
||||||
|
return tags.constant(tag)
|
||||||
|
case 'defaultLibrary':
|
||||||
|
return tags.standard(tag)
|
||||||
|
default:
|
||||||
|
console.error('Unknown token modifier:', tokenModifier)
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
55
packages/codemirror-lsp-client/src/plugin/util.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Text } from '@codemirror/state'
|
||||||
|
import { Marked } from '@ts-stack/markdown'
|
||||||
|
|
||||||
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
|
|
||||||
|
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
|
||||||
|
export function deferExecution<T>(func: (args: T) => any, wait: number) {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null
|
||||||
|
let latestArgs: T
|
||||||
|
|
||||||
|
function later() {
|
||||||
|
timeout = null
|
||||||
|
func(latestArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred(args: T) {
|
||||||
|
latestArgs = args
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(later, wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deferred
|
||||||
|
}
|
||||||
|
|
||||||
|
export function posToOffset(
|
||||||
|
doc: Text,
|
||||||
|
pos: { line: number; character: number }
|
||||||
|
): number | undefined {
|
||||||
|
if (pos.line >= doc.lines) return
|
||||||
|
const offset = doc.line(pos.line + 1).from + pos.character
|
||||||
|
if (offset > doc.length) return
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offsetToPos(doc: Text, offset: number) {
|
||||||
|
const line = doc.lineAt(offset)
|
||||||
|
return {
|
||||||
|
line: line.number - 1,
|
||||||
|
character: offset - line.from,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMarkdownContents(
|
||||||
|
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||||
|
): string {
|
||||||
|
if (Array.isArray(contents)) {
|
||||||
|
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
|
||||||
|
} else if (typeof contents === 'string') {
|
||||||
|
return Marked.parse(contents)
|
||||||
|
} else {
|
||||||
|
return Marked.parse(contents.value)
|
||||||
|
}
|
||||||
|
}
|
18
packages/codemirror-lsp-client/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src", "./*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
231
packages/codemirror-lsp-client/yarn.lock
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@codemirror/autocomplete@^6.16.3":
|
||||||
|
version "6.16.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz#04d5a4e4e44ccae1ba525d47db53a5479bf46338"
|
||||||
|
integrity sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA==
|
||||||
|
dependencies:
|
||||||
|
"@codemirror/language" "^6.0.0"
|
||||||
|
"@codemirror/state" "^6.0.0"
|
||||||
|
"@codemirror/view" "^6.17.0"
|
||||||
|
"@lezer/common" "^1.0.0"
|
||||||
|
|
||||||
|
"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.2":
|
||||||
|
version "6.10.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61"
|
||||||
|
integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==
|
||||||
|
dependencies:
|
||||||
|
"@codemirror/state" "^6.0.0"
|
||||||
|
"@codemirror/view" "^6.23.0"
|
||||||
|
"@lezer/common" "^1.1.0"
|
||||||
|
"@lezer/highlight" "^1.0.0"
|
||||||
|
"@lezer/lr" "^1.0.0"
|
||||||
|
style-mod "^4.0.0"
|
||||||
|
|
||||||
|
"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1":
|
||||||
|
version "6.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b"
|
||||||
|
integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==
|
||||||
|
|
||||||
|
"@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0":
|
||||||
|
version "6.28.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.28.2.tgz#026d5d2bd315aa015c1a1573b6358eeba7acd004"
|
||||||
|
integrity sha512-A3DmyVfjgPsGIjiJqM/zvODUAPQdQl3ci0ghehYNnbt5x+o76xq+dL5+mMBuysDXnI3kapgOkoeJ0sbtL/3qPw==
|
||||||
|
dependencies:
|
||||||
|
"@codemirror/state" "^6.4.0"
|
||||||
|
style-mod "^4.1.0"
|
||||||
|
w3c-keyname "^2.2.4"
|
||||||
|
|
||||||
|
"@cspotcode/source-map-support@^0.8.0":
|
||||||
|
version "0.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
|
||||||
|
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/trace-mapping" "0.3.9"
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri@^3.0.3":
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
||||||
|
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||||
|
version "1.4.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||||
|
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping@0.3.9":
|
||||||
|
version "0.3.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
|
||||||
|
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
|
|
||||||
|
"@lezer/common@^1.0.0", "@lezer/common@^1.1.0":
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049"
|
||||||
|
integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==
|
||||||
|
|
||||||
|
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.2.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780"
|
||||||
|
integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==
|
||||||
|
dependencies:
|
||||||
|
"@lezer/common" "^1.0.0"
|
||||||
|
|
||||||
|
"@lezer/lr@^1.0.0":
|
||||||
|
version "1.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.1.tgz#fe25f051880a754e820b28148d90aa2a96b8bdd2"
|
||||||
|
integrity sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==
|
||||||
|
dependencies:
|
||||||
|
"@lezer/common" "^1.0.0"
|
||||||
|
|
||||||
|
"@ts-stack/markdown@^1.5.0":
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ts-stack/markdown/-/markdown-1.5.0.tgz#5dc298a20dc3dc040143c5a5948201eb6bf5419d"
|
||||||
|
integrity sha512-ntVX2Kmb2jyTdH94plJohokvDVPvp6CwXHqsa9NVZTK8cOmHDCYNW0j6thIadUVRTStJhxhfdeovLd0owqDxLw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.3.0"
|
||||||
|
|
||||||
|
"@tsconfig/node10@^1.0.7":
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
|
||||||
|
integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==
|
||||||
|
|
||||||
|
"@tsconfig/node12@^1.0.7":
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
|
||||||
|
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
|
||||||
|
|
||||||
|
"@tsconfig/node14@^1.0.0":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
|
||||||
|
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
|
||||||
|
|
||||||
|
"@tsconfig/node16@^1.0.2":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||||
|
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||||
|
|
||||||
|
"@types/node@^20.14.9":
|
||||||
|
version "20.14.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420"
|
||||||
|
integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
|
acorn-walk@^8.1.1:
|
||||||
|
version "8.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e"
|
||||||
|
integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==
|
||||||
|
dependencies:
|
||||||
|
acorn "^8.11.0"
|
||||||
|
|
||||||
|
acorn@^8.11.0, acorn@^8.4.1:
|
||||||
|
version "8.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c"
|
||||||
|
integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==
|
||||||
|
|
||||||
|
arg@^4.1.0:
|
||||||
|
version "4.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||||
|
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||||
|
|
||||||
|
create-require@^1.1.0:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
|
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||||
|
|
||||||
|
diff@^4.0.1:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
|
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||||
|
|
||||||
|
json-rpc-2.0@^1.7.0:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-rpc-2.0/-/json-rpc-2.0-1.7.0.tgz#840deb0bc168463e12bceb462f7fe225e793fc17"
|
||||||
|
integrity sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg==
|
||||||
|
|
||||||
|
make-error@^1.1.1:
|
||||||
|
version "1.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||||
|
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||||
|
|
||||||
|
style-mod@^4.0.0, style-mod@^4.1.0:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
|
||||||
|
integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
|
||||||
|
|
||||||
|
ts-node@^10.9.2:
|
||||||
|
version "10.9.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
||||||
|
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
|
||||||
|
dependencies:
|
||||||
|
"@cspotcode/source-map-support" "^0.8.0"
|
||||||
|
"@tsconfig/node10" "^1.0.7"
|
||||||
|
"@tsconfig/node12" "^1.0.7"
|
||||||
|
"@tsconfig/node14" "^1.0.0"
|
||||||
|
"@tsconfig/node16" "^1.0.2"
|
||||||
|
acorn "^8.4.1"
|
||||||
|
acorn-walk "^8.1.1"
|
||||||
|
arg "^4.1.0"
|
||||||
|
create-require "^1.1.0"
|
||||||
|
diff "^4.0.1"
|
||||||
|
make-error "^1.1.1"
|
||||||
|
v8-compile-cache-lib "^3.0.1"
|
||||||
|
yn "3.1.1"
|
||||||
|
|
||||||
|
tslib@^2.3.0:
|
||||||
|
version "2.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
|
||||||
|
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
|
||||||
|
|
||||||
|
typescript@^5.5.2:
|
||||||
|
version "5.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507"
|
||||||
|
integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==
|
||||||
|
|
||||||
|
undici-types@~5.26.4:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||||
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||||
|
|
||||||
|
v8-compile-cache-lib@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||||
|
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||||
|
|
||||||
|
vscode-jsonrpc@8.2.0:
|
||||||
|
version "8.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
|
||||||
|
integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
|
||||||
|
|
||||||
|
vscode-languageserver-protocol@^3.17.5:
|
||||||
|
version "3.17.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea"
|
||||||
|
integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==
|
||||||
|
dependencies:
|
||||||
|
vscode-jsonrpc "8.2.0"
|
||||||
|
vscode-languageserver-types "3.17.5"
|
||||||
|
|
||||||
|
vscode-languageserver-types@3.17.5:
|
||||||
|
version "3.17.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
|
||||||
|
integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
|
||||||
|
|
||||||
|
vscode-uri@^3.0.8:
|
||||||
|
version "3.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
|
||||||
|
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
|
||||||
|
|
||||||
|
w3c-keyname@^2.2.4:
|
||||||
|
version "2.2.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
|
||||||
|
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
|
||||||
|
|
||||||
|
yn@3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||||
|
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
@ -15,8 +15,8 @@ export default defineConfig({
|
|||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
/* Do not retry */
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: process.env.CI ? 0 : 0,
|
||||||
/* Different amount of parallelism on CI and local. */
|
/* Different amount of parallelism on CI and local. */
|
||||||
workers: process.env.CI ? 4 : 4,
|
workers: process.env.CI ? 4 : 4,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
|
'@csstools/postcss-oklab-function': { preserve: true },
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
545
src-tauri/Cargo.lock
generated
@ -11,7 +11,7 @@ rust-version = "1.70"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.0-beta.13", features = [] }
|
tauri-build = { version = "2.0.0-beta.18", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
@ -20,18 +20,18 @@ kittycad = "0.3.5"
|
|||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
tauri = { version = "2.0.0-beta.23", features = [ "devtools", "unstable"] }
|
||||||
tauri-plugin-cli = { version = "2.0.0-beta.3" }
|
tauri-plugin-cli = { version = "2.0.0-beta.7" }
|
||||||
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
|
tauri-plugin-deep-link = { version = "2.0.0-beta.8" }
|
||||||
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
tauri-plugin-fs = { version = "2.0.0-beta.10" }
|
||||||
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
tauri-plugin-http = { version = "2.0.0-beta.11" }
|
||||||
tauri-plugin-log = { version = "2.0.0-beta.4" }
|
tauri-plugin-log = { version = "2.0.0-beta.7" }
|
||||||
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
tauri-plugin-os = { version = "2.0.0-beta.7" }
|
||||||
tauri-plugin-persisted-scope = { version = "2.0.0-beta.7" }
|
tauri-plugin-persisted-scope = { version = "2.0.0-beta.10" }
|
||||||
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
tauri-plugin-process = { version = "2.0.0-beta.7" }
|
||||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
tauri-plugin-shell = { version = "2.0.0-beta.8" }
|
||||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
tauri-plugin-updater = { version = "2.0.0-beta.9" }
|
||||||
tokio = { version = "1.37.0", features = ["time", "fs", "process"] }
|
tokio = { version = "1.37.0", features = ["time", "fs", "process"] }
|
||||||
toml = "0.8.2"
|
toml = "0.8.2"
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
|
@ -63,16 +63,22 @@
|
|||||||
"subcommands": {}
|
"subcommands": {}
|
||||||
},
|
},
|
||||||
"deep-link": {
|
"deep-link": {
|
||||||
"domains": [
|
"mobile": [
|
||||||
{
|
{
|
||||||
"host": "app.zoo.dev"
|
"host": "app.zoo.dev"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"desktop": {
|
||||||
|
"schemes": [
|
||||||
|
"zoo",
|
||||||
|
"zoo-modeling-app"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.22.6"
|
"version": "0.23.1"
|
||||||
}
|
}
|
||||||
|
28
src/App.tsx
@ -1,6 +1,5 @@
|
|||||||
import { MouseEventHandler, useEffect, useRef } from 'react'
|
import { MouseEventHandler, useEffect, useMemo, useRef } from 'react'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { useStore } from './useStore'
|
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
import { Stream } from './components/Stream'
|
||||||
import { EngineCommand } from './lang/std/engineConnection'
|
import { EngineCommand } from './lang/std/engineConnection'
|
||||||
@ -44,22 +43,15 @@ export function App() {
|
|||||||
}, [projectName, projectPath])
|
}, [projectName, projectPath])
|
||||||
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const { buttonDownInStream, didDragInStream, streamDimensions, setHtmlRef } =
|
const { context } = useModelingContext()
|
||||||
useStore((s) => ({
|
|
||||||
buttonDownInStream: s.buttonDownInStream,
|
|
||||||
didDragInStream: s.didDragInStream,
|
|
||||||
streamDimensions: s.streamDimensions,
|
|
||||||
setHtmlRef: s.setHtmlRef,
|
|
||||||
}))
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setHtmlRef(ref)
|
|
||||||
}, [ref])
|
|
||||||
|
|
||||||
const { auth, settings } = useSettingsAuthContext()
|
const { auth, settings } = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
|
|
||||||
const coreDumpManager = new CoreDumpManager(engineCommandManager, ref, token)
|
const coreDumpManager = useMemo(
|
||||||
|
() => new CoreDumpManager(engineCommandManager, token),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
@ -81,7 +73,7 @@ export function App() {
|
|||||||
(p) => p === onboardingStatus.current
|
(p) => p === onboardingStatus.current
|
||||||
)
|
)
|
||||||
? 'opacity-20'
|
? 'opacity-20'
|
||||||
: didDragInStream
|
: context.store?.didDragInStream
|
||||||
? 'opacity-40'
|
? 'opacity-40'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
@ -99,11 +91,11 @@ export function App() {
|
|||||||
clientX: e.clientX,
|
clientX: e.clientX,
|
||||||
clientY: e.clientY,
|
clientY: e.clientY,
|
||||||
el: e.currentTarget,
|
el: e.currentTarget,
|
||||||
...streamDimensions,
|
...context.store?.streamDimensions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
if (buttonDownInStream === undefined) {
|
if (context.store?.buttonDownInStream === undefined) {
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -125,7 +117,7 @@ export function App() {
|
|||||||
className={
|
className={
|
||||||
'transition-opacity transition-duration-75 ' +
|
'transition-opacity transition-duration-75 ' +
|
||||||
paneOpacity +
|
paneOpacity +
|
||||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
(context.store?.buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
project={{ project, file }}
|
project={{ project, file }}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
|
41
src/AppState.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { createContext, useContext, useState, ReactNode } from 'react'
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This is for a very small handful of global state we need that doesn't fit into
|
||||||
|
any of the Xstate machines.
|
||||||
|
Please do not fill this up with junk.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
isStreamReady: boolean
|
||||||
|
setAppState: (newAppState: Partial<AppState>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppStateContext = createContext<AppState>({
|
||||||
|
isStreamReady: false,
|
||||||
|
setAppState: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useAppState = () => useContext(AppStateContext)
|
||||||
|
|
||||||
|
export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [appState, _setAppState] = useState<AppState>({
|
||||||
|
isStreamReady: false,
|
||||||
|
setAppState: () => {},
|
||||||
|
})
|
||||||
|
const setAppState = (newAppState: Partial<AppState>) =>
|
||||||
|
_setAppState({ ...appState, ...newAppState })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppStateContext.Provider
|
||||||
|
value={{
|
||||||
|
isStreamReady: appState.isStreamReady,
|
||||||
|
setAppState,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AppStateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
@ -33,6 +33,14 @@ import LspProvider from 'components/LspProvider'
|
|||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import { getState, setState } from 'lib/tauri'
|
import { getState, setState } from 'lib/tauri'
|
||||||
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { coreDump } from 'lang/wasm'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { AppStateProvider } from 'AppState'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -45,7 +53,9 @@ const router = createBrowserRouter([
|
|||||||
<SettingsAuthProvider>
|
<SettingsAuthProvider>
|
||||||
<LspProvider>
|
<LspProvider>
|
||||||
<KclContextProvider>
|
<KclContextProvider>
|
||||||
<Outlet />
|
<AppStateProvider>
|
||||||
|
<Outlet />
|
||||||
|
</AppStateProvider>
|
||||||
</KclContextProvider>
|
</KclContextProvider>
|
||||||
</LspProvider>
|
</LspProvider>
|
||||||
</SettingsAuthProvider>
|
</SettingsAuthProvider>
|
||||||
@ -87,6 +97,7 @@ const router = createBrowserRouter([
|
|||||||
<Auth>
|
<Auth>
|
||||||
<FileMachineProvider>
|
<FileMachineProvider>
|
||||||
<ModelingMachineProvider>
|
<ModelingMachineProvider>
|
||||||
|
<CoreDump />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<App />
|
<App />
|
||||||
<CommandBar />
|
<CommandBar />
|
||||||
@ -165,3 +176,30 @@ export const Router = () => {
|
|||||||
</NetworkContext.Provider>
|
</NetworkContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CoreDump() {
|
||||||
|
const { auth } = useSettingsAuthContext()
|
||||||
|
const token = auth?.context?.token
|
||||||
|
const coreDumpManager = useMemo(
|
||||||
|
() => new CoreDumpManager(engineCommandManager, token),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
useHotkeyWrapper(['meta + shift + .'], () => {
|
||||||
|
toast.promise(
|
||||||
|
coreDump(coreDumpManager, true),
|
||||||
|
{
|
||||||
|
loading: 'Starting core dump...',
|
||||||
|
success: 'Core dump completed successfully',
|
||||||
|
error: 'Error while exporting core dump',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: {
|
||||||
|
// Note: this extended duration is especially important for Playwright e2e testing
|
||||||
|
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
||||||
|
duration: 6000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
@ -8,10 +8,14 @@ import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
|||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { useAppState } from 'AppState'
|
||||||
|
import {
|
||||||
|
canRectangleTool,
|
||||||
|
isEditingExistingSketch,
|
||||||
|
} from 'machines/modelingMachine'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -38,38 +42,56 @@ export function Toolbar({
|
|||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useStore((s) => ({
|
const { isStreamReady } = useAppState()
|
||||||
isStreamReady: s.isStreamReady,
|
|
||||||
}))
|
|
||||||
const disableAllButtons =
|
const disableAllButtons =
|
||||||
(overallState !== NetworkHealthState.Ok &&
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
overallState !== NetworkHealthState.Weak) ||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
isExecuting ||
|
isExecuting ||
|
||||||
!isStreamReady
|
!isStreamReady
|
||||||
|
|
||||||
|
const disableLineButton =
|
||||||
|
state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
|
||||||
|
disableAllButtons
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'l',
|
'l',
|
||||||
() =>
|
() =>
|
||||||
state.matches('Sketch.Line tool')
|
state.matches('Sketch.Line tool')
|
||||||
? send('CancelSketch')
|
? send('CancelSketch')
|
||||||
: send('Equip Line tool'),
|
: send({
|
||||||
{ enabled: !disableAllButtons, scopes: ['sketch'] }
|
type: 'change tool',
|
||||||
|
data: 'line',
|
||||||
|
}),
|
||||||
|
{ enabled: !disableLineButton, scopes: ['sketch'] }
|
||||||
)
|
)
|
||||||
|
const disableTangentialArc =
|
||||||
|
(!isEditingExistingSketch(context) &&
|
||||||
|
!state.matches('Sketch.Tangential arc to')) ||
|
||||||
|
disableAllButtons
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'a',
|
'a',
|
||||||
() =>
|
() =>
|
||||||
state.matches('Sketch.Tangential arc to')
|
state.matches('Sketch.Tangential arc to')
|
||||||
? send('CancelSketch')
|
? send('CancelSketch')
|
||||||
: send('Equip tangential arc to'),
|
: send({
|
||||||
{ enabled: !disableAllButtons, scopes: ['sketch'] }
|
type: 'change tool',
|
||||||
|
data: 'tangentialArc',
|
||||||
|
}),
|
||||||
|
{ enabled: !disableTangentialArc, scopes: ['sketch'] }
|
||||||
)
|
)
|
||||||
|
const disableRectangle =
|
||||||
|
(!canRectangleTool(context) && !state.matches('Sketch.Rectangle tool')) ||
|
||||||
|
disableAllButtons
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'r',
|
'r',
|
||||||
() =>
|
() =>
|
||||||
state.matches('Sketch.Rectangle tool')
|
state.matches('Sketch.Rectangle tool')
|
||||||
? send('CancelSketch')
|
? send('CancelSketch')
|
||||||
: send('Equip rectangle tool'),
|
: send({
|
||||||
{ enabled: !disableAllButtons, scopes: ['sketch'] }
|
type: 'change tool',
|
||||||
|
data: 'rectangle',
|
||||||
|
}),
|
||||||
|
{ enabled: !disableRectangle, scopes: ['sketch'] }
|
||||||
)
|
)
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
's',
|
's',
|
||||||
@ -82,7 +104,7 @@ export function Toolbar({
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
'esc',
|
'esc',
|
||||||
() =>
|
() =>
|
||||||
state.matches('Sketch.SketchIdle')
|
['Sketch no face', 'Sketch.SketchIdle'].some(state.matches)
|
||||||
? send('Cancel')
|
? send('Cancel')
|
||||||
: send('CancelSketch'),
|
: send('CancelSketch'),
|
||||||
{ enabled: !disableAllButtons, scopes: ['sketch'] }
|
{ enabled: !disableAllButtons, scopes: ['sketch'] }
|
||||||
@ -225,6 +247,11 @@ export function Toolbar({
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
{state.matches('Sketch no face') && (
|
||||||
|
<li className="contents">
|
||||||
|
<div className="mx-2 text-sm">click plane or face to sketch on</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{state.matches('Sketch') && !state.matches('idle') && (
|
{state.matches('Sketch') && !state.matches('idle') && (
|
||||||
<>
|
<>
|
||||||
<li className="contents" key="line-button">
|
<li className="contents" key="line-button">
|
||||||
@ -234,7 +261,10 @@ export function Toolbar({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
state?.matches('Sketch.Line tool')
|
state?.matches('Sketch.Line tool')
|
||||||
? send('CancelSketch')
|
? send('CancelSketch')
|
||||||
: send('Equip Line tool')
|
: send({
|
||||||
|
type: 'change tool',
|
||||||
|
data: 'line',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
aria-pressed={state?.matches('Sketch.Line tool')}
|
aria-pressed={state?.matches('Sketch.Line tool')}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
@ -242,7 +272,7 @@ export function Toolbar({
|
|||||||
iconClassName,
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={disableAllButtons}
|
disabled={disableLineButton}
|
||||||
>
|
>
|
||||||
Line
|
Line
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -261,7 +291,10 @@ export function Toolbar({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
state.matches('Sketch.Tangential arc to')
|
state.matches('Sketch.Tangential arc to')
|
||||||
? send('CancelSketch')
|
? send('CancelSketch')
|
||||||
: send('Equip tangential arc to')
|
: send({
|
||||||
|
type: 'change tool',
|
||||||
|
data: 'tangentialArc',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
aria-pressed={state.matches('Sketch.Tangential arc to')}
|
aria-pressed={state.matches('Sketch.Tangential arc to')}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
@ -269,11 +302,7 @@ export function Toolbar({
|
|||||||
iconClassName,
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={disableTangentialArc}
|
||||||
(!state.can('Equip tangential arc to') &&
|
|
||||||
!state.matches('Sketch.Tangential arc to')) ||
|
|
||||||
disableAllButtons
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Tangential Arc
|
Tangential Arc
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -292,7 +321,10 @@ export function Toolbar({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
state.matches('Sketch.Rectangle tool')
|
state.matches('Sketch.Rectangle tool')
|
||||||
? send('CancelSketch')
|
? send('CancelSketch')
|
||||||
: send('Equip rectangle tool')
|
: send({
|
||||||
|
type: 'change tool',
|
||||||
|
data: 'rectangle',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
aria-pressed={state.matches('Sketch.Rectangle tool')}
|
aria-pressed={state.matches('Sketch.Rectangle tool')}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
@ -300,13 +332,9 @@ export function Toolbar({
|
|||||||
iconClassName,
|
iconClassName,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={disableRectangle}
|
||||||
(!state.can('Equip rectangle tool') &&
|
|
||||||
!state.matches('Sketch.Rectangle tool')) ||
|
|
||||||
disableAllButtons
|
|
||||||
}
|
|
||||||
title={
|
title={
|
||||||
state.can('Equip rectangle tool')
|
canRectangleTool(context)
|
||||||
? 'Rectangle'
|
? 'Rectangle'
|
||||||
: 'Can only be used when a sketch is empty currently'
|
: 'Can only be used when a sketch is empty currently'
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,9 @@ export class CameraControls {
|
|||||||
enableRotate = true
|
enableRotate = true
|
||||||
enablePan = true
|
enablePan = true
|
||||||
enableZoom = true
|
enableZoom = true
|
||||||
|
zoomDataFromLastFrame?: number = undefined
|
||||||
|
// holds coordinates, and interaction
|
||||||
|
moveDataFromLastFrame?: [number, number, string] = undefined
|
||||||
lastPerspectiveFov: number = 45
|
lastPerspectiveFov: number = 45
|
||||||
pendingZoom: number | null = null
|
pendingZoom: number | null = null
|
||||||
pendingRotation: Vector2 | null = null
|
pendingRotation: Vector2 | null = null
|
||||||
@ -101,16 +104,12 @@ export class CameraControls {
|
|||||||
get isPerspective() {
|
get isPerspective() {
|
||||||
return this.camera instanceof PerspectiveCamera
|
return this.camera instanceof PerspectiveCamera
|
||||||
}
|
}
|
||||||
private debounceTimer = 0
|
|
||||||
|
|
||||||
handleStart = () => {
|
handleStart = () => {
|
||||||
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
|
||||||
this._isCamMovingCallback(true, false)
|
this._isCamMovingCallback(true, false)
|
||||||
}
|
}
|
||||||
handleEnd = () => {
|
handleEnd = () => {
|
||||||
this.debounceTimer = setTimeout(() => {
|
this._isCamMovingCallback(false, false)
|
||||||
this._isCamMovingCallback(false, false)
|
|
||||||
}, 400) as any as number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCam = (camProps: ReactCameraProperties) => {
|
setCam = (camProps: ReactCameraProperties) => {
|
||||||
@ -230,6 +229,7 @@ export class CameraControls {
|
|||||||
camSettings.orientation.z,
|
camSettings.orientation.z,
|
||||||
camSettings.orientation.w
|
camSettings.orientation.w
|
||||||
).invert()
|
).invert()
|
||||||
|
|
||||||
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
|
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
|
||||||
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
||||||
this.useOrthographicCamera()
|
this.useOrthographicCamera()
|
||||||
@ -258,6 +258,48 @@ export class CameraControls {
|
|||||||
}
|
}
|
||||||
this.onCameraChange()
|
this.onCameraChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Our stream is never more than 60fps.
|
||||||
|
// We can get away with capping our "virtual fps" to 60 then.
|
||||||
|
const FPS_VIRTUAL = 60
|
||||||
|
|
||||||
|
const doZoom = () => {
|
||||||
|
if (this.zoomDataFromLastFrame !== undefined) {
|
||||||
|
this.handleStart()
|
||||||
|
this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_zoom',
|
||||||
|
magnitude:
|
||||||
|
(-1 * this.zoomDataFromLastFrame) / window.devicePixelRatio,
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
this.handleEnd()
|
||||||
|
}
|
||||||
|
this.zoomDataFromLastFrame = undefined
|
||||||
|
}
|
||||||
|
setInterval(doZoom, 1000 / FPS_VIRTUAL)
|
||||||
|
|
||||||
|
const doMove = () => {
|
||||||
|
if (this.moveDataFromLastFrame !== undefined) {
|
||||||
|
this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_move',
|
||||||
|
interaction: this.moveDataFromLastFrame[2] as any,
|
||||||
|
window: {
|
||||||
|
x: this.moveDataFromLastFrame[0],
|
||||||
|
y: this.moveDataFromLastFrame[1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.moveDataFromLastFrame = undefined
|
||||||
|
}
|
||||||
|
setInterval(doMove, 1000 / FPS_VIRTUAL)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.engineCommandManager.subscribeTo({
|
this.engineCommandManager.subscribeTo({
|
||||||
event: 'camera_drag_end',
|
event: 'camera_drag_end',
|
||||||
@ -342,15 +384,7 @@ export class CameraControls {
|
|||||||
if (interaction === 'none') return
|
if (interaction === 'none') return
|
||||||
|
|
||||||
if (this.syncDirection === 'engineToClient') {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
this.throttledEngCmd({
|
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'camera_drag_move',
|
|
||||||
interaction,
|
|
||||||
window: { x: event.clientX, y: event.clientY },
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,34 +432,19 @@ export class CameraControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheel = (event: WheelEvent) => {
|
onMouseWheel = (event: WheelEvent) => {
|
||||||
// Assume trackpad if the deltas are small and integers
|
|
||||||
this.handleStart()
|
|
||||||
|
|
||||||
if (this.syncDirection === 'engineToClient') {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
const interactions = this.interactionGuards.zoom.scrollCallback(
|
this.zoomDataFromLastFrame = event.deltaY
|
||||||
event as any
|
|
||||||
)
|
|
||||||
if (!interactions) {
|
|
||||||
this.handleEnd()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_zoom',
|
|
||||||
magnitude: -event.deltaY * 0.4,
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
this.handleEnd()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else "clientToEngine" (Sketch Mode) or forceUpdate
|
// else "clientToEngine" (Sketch Mode) or forceUpdate
|
||||||
|
|
||||||
|
// We need to simulate similar behavior as when we send
|
||||||
|
// zoom commands to engine. This means dropping some zoom
|
||||||
|
// commands too.
|
||||||
// From onMouseMove zoom handling which seems to be really smooth
|
// From onMouseMove zoom handling which seems to be really smooth
|
||||||
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
this.handleStart()
|
||||||
this.pendingZoom *= 1 + event.deltaY * 0.01
|
this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001
|
||||||
this.handleEnd()
|
this.handleEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ import { Dialog, Popover, Transition } from '@headlessui/react'
|
|||||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { InstanceProps, create } from 'react-modal-promise'
|
import { InstanceProps, create } from 'react-modal-promise'
|
||||||
import { executeAst } from 'useStore'
|
import { executeAst } from 'lang/langHelpers'
|
||||||
import {
|
import {
|
||||||
deleteSegmentFromPipeExpression,
|
deleteSegmentFromPipeExpression,
|
||||||
makeRemoveSingleConstraintInput,
|
makeRemoveSingleConstraintInput,
|
||||||
|
@ -58,7 +58,7 @@ import {
|
|||||||
editorManager,
|
editorManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { executeAst, useStore } from 'useStore'
|
import { executeAst } from 'lang/langHelpers'
|
||||||
import {
|
import {
|
||||||
createArcGeometry,
|
createArcGeometry,
|
||||||
dashedStraight,
|
dashedStraight,
|
||||||
@ -534,7 +534,7 @@ export class SceneEntities {
|
|||||||
segmentName: 'line' | 'tangentialArcTo' = 'line',
|
segmentName: 'line' | 'tangentialArcTo' = 'line',
|
||||||
shouldTearDown = true
|
shouldTearDown = true
|
||||||
) => {
|
) => {
|
||||||
const _ast = kclManager.ast
|
const _ast = JSON.parse(JSON.stringify(kclManager.ast))
|
||||||
|
|
||||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||||
_ast,
|
_ast,
|
||||||
@ -568,6 +568,7 @@ export class SceneEntities {
|
|||||||
|
|
||||||
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
||||||
sceneInfra.resetMouseListeners()
|
sceneInfra.resetMouseListeners()
|
||||||
|
|
||||||
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
||||||
await this.setupSketch({
|
await this.setupSketch({
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
@ -692,7 +693,7 @@ export class SceneEntities {
|
|||||||
sketchOrigin: [number, number, number],
|
sketchOrigin: [number, number, number],
|
||||||
rectangleOrigin: [x: number, y: number]
|
rectangleOrigin: [x: number, y: number]
|
||||||
) => {
|
) => {
|
||||||
let _ast = kclManager.ast
|
let _ast = JSON.parse(JSON.stringify(kclManager.ast))
|
||||||
|
|
||||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||||
_ast,
|
_ast,
|
||||||
@ -723,9 +724,7 @@ export class SceneEntities {
|
|||||||
...getRectangleCallExpressions(rectangleOrigin, tags),
|
...getRectangleCallExpressions(rectangleOrigin, tags),
|
||||||
])
|
])
|
||||||
|
|
||||||
let result = parse(recast(_ast))
|
_ast = parse(recast(_ast))
|
||||||
if (trap(result)) return Promise.reject(result)
|
|
||||||
_ast = result
|
|
||||||
|
|
||||||
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
@ -739,7 +738,7 @@ export class SceneEntities {
|
|||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onMove: async (args) => {
|
onMove: async (args) => {
|
||||||
// Update the width and height of the draft rectangle
|
// Update the width and height of the draft rectangle
|
||||||
const pathToNodeTwo = sketchPathToNode
|
const pathToNodeTwo = JSON.parse(JSON.stringify(sketchPathToNode))
|
||||||
pathToNodeTwo[1][0] = 0
|
pathToNodeTwo[1][0] = 0
|
||||||
|
|
||||||
const _node = getNodeFromPath<VariableDeclaration>(
|
const _node = getNodeFromPath<VariableDeclaration>(
|
||||||
@ -801,13 +800,11 @@ export class SceneEntities {
|
|||||||
if (sketchInit.type === 'PipeExpression') {
|
if (sketchInit.type === 'PipeExpression') {
|
||||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||||
|
|
||||||
let result = parse(recast(_ast))
|
_ast = parse(recast(_ast))
|
||||||
if (trap(result)) return Promise.reject(result)
|
|
||||||
_ast = result
|
|
||||||
|
|
||||||
// Update the primary AST and unequip the rectangle tool
|
// Update the primary AST and unequip the rectangle tool
|
||||||
await kclManager.executeAstMock(_ast)
|
await kclManager.executeAstMock(_ast)
|
||||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||||
|
|
||||||
const { programMemory } = await executeAst({
|
const { programMemory } = await executeAst({
|
||||||
ast: _ast,
|
ast: _ast,
|
||||||
@ -1007,8 +1004,10 @@ export class SceneEntities {
|
|||||||
PROFILE_START,
|
PROFILE_START,
|
||||||
])
|
])
|
||||||
if (!group) return
|
if (!group) return
|
||||||
const pathToNode: PathToNode = group.userData.pathToNode
|
const pathToNode: PathToNode = JSON.parse(
|
||||||
const varDecIndex: number = pathToNode[1][0] as number
|
JSON.stringify(group.userData.pathToNode)
|
||||||
|
)
|
||||||
|
const varDecIndex = JSON.parse(JSON.stringify(pathToNode[1][0]))
|
||||||
if (draftInfo) {
|
if (draftInfo) {
|
||||||
pathToNode[1][0] = 0
|
pathToNode[1][0] = 0
|
||||||
}
|
}
|
||||||
@ -1445,11 +1444,10 @@ export class SceneEntities {
|
|||||||
selected.material.color = defaultPlaneColor(type)
|
selected.material.color = defaultPlaneColor(type)
|
||||||
},
|
},
|
||||||
onClick: async (args) => {
|
onClick: async (args) => {
|
||||||
const { streamDimensions } = useStore.getState()
|
|
||||||
const { entity_id } = await sendSelectEventToEngine(
|
const { entity_id } = await sendSelectEventToEngine(
|
||||||
args?.mouseEvent,
|
args?.mouseEvent,
|
||||||
document.getElementById('video-stream') as HTMLVideoElement,
|
document.getElementById('video-stream') as HTMLVideoElement,
|
||||||
streamDimensions
|
sceneInfra._streamDimensions
|
||||||
)
|
)
|
||||||
|
|
||||||
let _entity_id = entity_id
|
let _entity_id = entity_id
|
||||||
@ -1721,7 +1719,7 @@ function prepareTruncatedMemoryAndAst(
|
|||||||
}
|
}
|
||||||
| Error {
|
| Error {
|
||||||
const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0
|
const bodyIndex = Number(sketchPathToNode?.[1]?.[0]) || 0
|
||||||
const _ast = ast
|
const _ast = JSON.parse(JSON.stringify(ast))
|
||||||
|
|
||||||
const _node = getNodeFromPath<VariableDeclaration>(
|
const _node = getNodeFromPath<VariableDeclaration>(
|
||||||
_ast,
|
_ast,
|
||||||
@ -1780,7 +1778,7 @@ function prepareTruncatedMemoryAndAst(
|
|||||||
}
|
}
|
||||||
const truncatedAst: Program = {
|
const truncatedAst: Program = {
|
||||||
..._ast,
|
..._ast,
|
||||||
body: [_ast.body[bodyIndex]],
|
body: [JSON.parse(JSON.stringify(_ast.body[bodyIndex]))],
|
||||||
}
|
}
|
||||||
const programMemoryOverride = programMemoryInit()
|
const programMemoryOverride = programMemoryInit()
|
||||||
if (err(programMemoryOverride)) return programMemoryOverride
|
if (err(programMemoryOverride)) return programMemoryOverride
|
||||||
@ -1806,7 +1804,7 @@ function prepareTruncatedMemoryAndAst(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value.type === 'TagIdentifier') {
|
if (value.type === 'TagIdentifier') {
|
||||||
programMemoryOverride.root[key] = value
|
programMemoryOverride.root[key] = JSON.parse(JSON.stringify(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1821,7 +1819,7 @@ function prepareTruncatedMemoryAndAst(
|
|||||||
if (!memoryItem) {
|
if (!memoryItem) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
programMemoryOverride.root[name] = memoryItem
|
programMemoryOverride.root[name] = JSON.parse(JSON.stringify(memoryItem))
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
truncatedAst,
|
truncatedAst,
|
||||||
|
@ -103,6 +103,10 @@ export class SceneInfra {
|
|||||||
_baseUnit: BaseUnit = 'mm'
|
_baseUnit: BaseUnit = 'mm'
|
||||||
_baseUnitMultiplier = 1
|
_baseUnitMultiplier = 1
|
||||||
_theme: Themes = Themes.System
|
_theme: Themes = Themes.System
|
||||||
|
_streamDimensions: { streamWidth: number; streamHeight: number } = {
|
||||||
|
streamWidth: 1280,
|
||||||
|
streamHeight: 720,
|
||||||
|
}
|
||||||
extraSegmentTexture: Texture
|
extraSegmentTexture: Texture
|
||||||
lastMouseState: MouseState = { type: 'idle' }
|
lastMouseState: MouseState = { type: 'idle' }
|
||||||
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
|
@ -26,6 +26,7 @@ export const AppHeader = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
|
id="app-header"
|
||||||
className={
|
className={
|
||||||
'w-full grid ' +
|
'w-full grid ' +
|
||||||
styles.header +
|
styles.header +
|
||||||
|
@ -10,7 +10,7 @@ import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
|||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { executeAst } from 'useStore'
|
import { executeAst } from 'lang/langHelpers'
|
||||||
import { trap } from 'lib/trap'
|
import { trap } from 'lib/trap'
|
||||||
|
|
||||||
export const AvailableVars = ({
|
export const AvailableVars = ({
|
||||||
@ -151,7 +151,9 @@ export function useCalc({
|
|||||||
ast,
|
ast,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
programMemoryOverride: kclManager.programMemory,
|
programMemoryOverride: JSON.parse(
|
||||||
|
JSON.stringify(kclManager.programMemory)
|
||||||
|
),
|
||||||
}).then(({ programMemory }) => {
|
}).then(({ programMemory }) => {
|
||||||
const resultDeclaration = ast.body.find(
|
const resultDeclaration = ast.body.find(
|
||||||
(a) =>
|
(a) =>
|
||||||
|
@ -24,7 +24,7 @@ export const CommandBar = () => {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
// Hook up keyboard shortcuts
|
// Hook up keyboard shortcuts
|
||||||
useHotkeyWrapper(['mod+k', 'ctrl+c'], () => {
|
useHotkeyWrapper(['mod+k'], () => {
|
||||||
if (commandBarState.context.commands.length === 0) return
|
if (commandBarState.context.commands.length === 0) return
|
||||||
if (commandBarState.matches('Closed')) {
|
if (commandBarState.matches('Closed')) {
|
||||||
commandBarSend({ type: 'Open' })
|
commandBarSend({ type: 'Open' })
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Completion } from '@codemirror/autocomplete'
|
import { Completion } from '@codemirror/autocomplete'
|
||||||
import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror'
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
|
import { EditorState } from '@codemirror/state'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
@ -12,6 +13,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import styles from './CommandBarKclInput.module.css'
|
import styles from './CommandBarKclInput.module.css'
|
||||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||||
|
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||||
|
|
||||||
function CommandBarKclInput({
|
function CommandBarKclInput({
|
||||||
arg,
|
arg,
|
||||||
@ -63,9 +65,7 @@ function CommandBarKclInput({
|
|||||||
|
|
||||||
const { setContainer } = useCodeMirror({
|
const { setContainer } = useCodeMirror({
|
||||||
container: editorRef.current,
|
container: editorRef.current,
|
||||||
value,
|
initialDocValue: value,
|
||||||
indentWithTab: false,
|
|
||||||
basicSetup: false,
|
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
selection: {
|
selection: {
|
||||||
anchor: 0,
|
anchor: 0,
|
||||||
@ -74,7 +74,6 @@ function CommandBarKclInput({
|
|||||||
? previouslySetValue.valueText.length
|
? previouslySetValue.valueText.length
|
||||||
: defaultValue.length,
|
: defaultValue.length,
|
||||||
},
|
},
|
||||||
accessKey: 'command-bar',
|
|
||||||
theme:
|
theme:
|
||||||
settings.context.app.theme.current === 'system'
|
settings.context.app.theme.current === 'system'
|
||||||
? getSystemTheme()
|
? getSystemTheme()
|
||||||
@ -96,8 +95,12 @@ function CommandBarKclInput({
|
|||||||
}
|
}
|
||||||
return tr
|
return tr
|
||||||
}),
|
}),
|
||||||
|
EditorView.updateListener.of((vu: ViewUpdate) => {
|
||||||
|
if (vu.docChanged) {
|
||||||
|
setValue(vu.state.doc.toString())
|
||||||
|
}
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
onChange: (newValue) => setValue(newValue),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -175,7 +175,11 @@ const FileTreeItem = ({
|
|||||||
codeManager.code
|
codeManager.code
|
||||||
)
|
)
|
||||||
codeManager.writeToFile()
|
codeManager.writeToFile()
|
||||||
kclManager.executeCode(true, true)
|
|
||||||
|
kclManager.isFirstRender = true
|
||||||
|
kclManager.executeCode(true, true).then(() => {
|
||||||
|
kclManager.isFirstRender = false
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Let the lsp servers know we closed a file.
|
// Let the lsp servers know we closed a file.
|
||||||
onFileClose(currentFile?.path || null, project?.path || null)
|
onFileClose(currentFile?.path || null, project?.path || null)
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
|
||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
import type * as LSP from 'vscode-languageserver-protocol'
|
||||||
import React, {
|
import React, { createContext, useMemo, useContext, useState } from 'react'
|
||||||
createContext,
|
import {
|
||||||
useMemo,
|
LanguageServerClient,
|
||||||
useEffect,
|
FromServer,
|
||||||
useContext,
|
IntoServer,
|
||||||
useState,
|
LspWorkerEventType,
|
||||||
} from 'react'
|
LanguageServerPlugin,
|
||||||
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
} from '@kittycad/codemirror-lsp-client'
|
||||||
import Client from '../editor/plugins/lsp/client'
|
|
||||||
import { TEST, VITE_KC_API_BASE_URL } from 'env'
|
import { TEST, VITE_KC_API_BASE_URL } from 'env'
|
||||||
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
import KclLanguageSupport from 'editor/plugins/lsp/kcl/language'
|
||||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { Extension } from '@codemirror/state'
|
import { Extension } from '@codemirror/state'
|
||||||
import { LanguageSupport } from '@codemirror/language'
|
import { LanguageSupport } from '@codemirror/language'
|
||||||
@ -21,16 +18,15 @@ import { paths } from 'lib/paths'
|
|||||||
import { FileEntry } from 'lib/types'
|
import { FileEntry } from 'lib/types'
|
||||||
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
||||||
import {
|
import {
|
||||||
LspWorkerEventType,
|
|
||||||
KclWorkerOptions,
|
KclWorkerOptions,
|
||||||
CopilotWorkerOptions,
|
CopilotWorkerOptions,
|
||||||
LspWorker,
|
LspWorker,
|
||||||
} from 'editor/plugins/lsp/types'
|
} from 'editor/plugins/lsp/types'
|
||||||
import { wasmUrl } from 'lang/wasm'
|
import { wasmUrl } from 'lang/wasm'
|
||||||
import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { codeManager } from 'lib/singletons'
|
||||||
|
|
||||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||||
return []
|
return []
|
||||||
@ -70,21 +66,8 @@ type LspContext = {
|
|||||||
|
|
||||||
export const LspStateContext = createContext({} as LspContext)
|
export const LspStateContext = createContext({} as LspContext)
|
||||||
export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const {
|
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||||
isKclLspServerReady,
|
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||||
isCopilotLspServerReady,
|
|
||||||
setIsKclLspServerReady,
|
|
||||||
setIsCopilotLspServerReady,
|
|
||||||
isStreamReady,
|
|
||||||
} = useStore((s) => ({
|
|
||||||
isKclLspServerReady: s.isKclLspServerReady,
|
|
||||||
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
|
||||||
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
|
||||||
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
|
||||||
isStreamReady: s.isStreamReady,
|
|
||||||
}))
|
|
||||||
const [isLspReady, setIsLspReady] = useState(false)
|
|
||||||
const [isCopilotReady, setIsCopilotReady] = useState(false)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
auth,
|
auth,
|
||||||
@ -96,8 +79,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
const token = auth?.context.token
|
const token = auth?.context.token
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { overallState } = useNetworkContext()
|
|
||||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
|
||||||
|
|
||||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||||
// But the server happens async so we break this into two parts.
|
// But the server happens async so we break this into two parts.
|
||||||
@ -128,17 +109,34 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const fromServer: FromServer | Error = FromServer.create()
|
const fromServer: FromServer | Error = FromServer.create()
|
||||||
if (err(fromServer)) return { lspClient: null }
|
if (err(fromServer)) return { lspClient: null }
|
||||||
|
|
||||||
const client = new Client(fromServer, intoServer)
|
const lspClient = new LanguageServerClient({
|
||||||
|
name: LspWorker.Kcl,
|
||||||
|
fromServer,
|
||||||
|
intoServer,
|
||||||
|
initializedCallback: () => {
|
||||||
|
setIsKclLspReady(true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
setIsLspReady(true)
|
|
||||||
|
|
||||||
const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl })
|
|
||||||
return { lspClient }
|
return { lspClient }
|
||||||
}, [
|
}, [
|
||||||
// We need a token for authenticating the server.
|
// We need a token for authenticating the server.
|
||||||
token,
|
token,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useMemo(() => {
|
||||||
|
if (!isTauri() && isKclLspReady && kclLspClient && codeManager.code) {
|
||||||
|
kclLspClient.textDocumentDidOpen({
|
||||||
|
textDocument: {
|
||||||
|
uri: `file:///${PROJECT_ENTRYPOINT}`,
|
||||||
|
languageId: 'kcl',
|
||||||
|
version: 1,
|
||||||
|
text: codeManager.code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [kclLspClient, isKclLspReady])
|
||||||
|
|
||||||
// Here we initialize the plugin which will start the client.
|
// Here we initialize the plugin which will start the client.
|
||||||
// Now that we have multi-file support the name of the file is a dep of
|
// Now that we have multi-file support the name of the file is a dep of
|
||||||
// this use memo, as well as the directory structure, which I think is
|
// this use memo, as well as the directory structure, which I think is
|
||||||
@ -146,39 +144,38 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
// We do not want to restart the server, its just wasteful.
|
// We do not want to restart the server, its just wasteful.
|
||||||
const kclLSP = useMemo(() => {
|
const kclLSP = useMemo(() => {
|
||||||
let plugin = null
|
let plugin = null
|
||||||
if (isKclLspServerReady && !TEST && kclLspClient) {
|
if (isKclLspReady && !TEST && kclLspClient) {
|
||||||
// Set up the lsp plugin.
|
// Set up the lsp plugin.
|
||||||
const lsp = kclLanguage({
|
const lsp = new KclLanguageSupport({
|
||||||
documentUri: `file:///${PROJECT_ENTRYPOINT}`,
|
documentUri: `file:///${PROJECT_ENTRYPOINT}`,
|
||||||
workspaceFolders: getWorkspaceFolders(),
|
workspaceFolders: getWorkspaceFolders(),
|
||||||
client: kclLspClient,
|
client: kclLspClient,
|
||||||
|
processLspNotification: (
|
||||||
|
plugin: LanguageServerPlugin,
|
||||||
|
notification: LSP.NotificationMessage
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
switch (notification.method) {
|
||||||
|
case 'kcl/astUpdated':
|
||||||
|
// Update the folding ranges, since the AST has changed.
|
||||||
|
// This is a hack since codemirror does not support async foldService.
|
||||||
|
// When they do we can delete this.
|
||||||
|
plugin.updateFoldingRanges()
|
||||||
|
plugin.requestSemanticTokens()
|
||||||
|
break
|
||||||
|
case 'kcl/memoryUpdated':
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
plugin = lsp
|
plugin = lsp
|
||||||
}
|
}
|
||||||
return plugin
|
return plugin
|
||||||
}, [kclLspClient, isKclLspServerReady])
|
}, [kclLspClient, isKclLspReady])
|
||||||
|
|
||||||
// Re-execute the scene when the units change.
|
|
||||||
useEffect(() => {
|
|
||||||
if (kclLspClient) {
|
|
||||||
let plugins = kclLspClient.plugins
|
|
||||||
for (let plugin of plugins) {
|
|
||||||
if (plugin.updateUnits && isStreamReady && isNetworkOkay) {
|
|
||||||
plugin.updateUnits(defaultUnit.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
kclLspClient,
|
|
||||||
defaultUnit.current,
|
|
||||||
|
|
||||||
// We want to re-execute the scene if the network comes back online.
|
|
||||||
// The lsp server will only re-execute if there were previous errors or
|
|
||||||
// changes, so it's fine to send it thru here.
|
|
||||||
isStreamReady,
|
|
||||||
isNetworkOkay,
|
|
||||||
])
|
|
||||||
|
|
||||||
const { lspClient: copilotLspClient } = useMemo(() => {
|
const { lspClient: copilotLspClient } = useMemo(() => {
|
||||||
if (!token || token === '' || TEST) {
|
if (!token || token === '' || TEST) {
|
||||||
@ -205,13 +202,13 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const fromServer: FromServer | Error = FromServer.create()
|
const fromServer: FromServer | Error = FromServer.create()
|
||||||
if (err(fromServer)) return { lspClient: null }
|
if (err(fromServer)) return { lspClient: null }
|
||||||
|
|
||||||
const client = new Client(fromServer, intoServer)
|
|
||||||
|
|
||||||
setIsCopilotReady(true)
|
|
||||||
|
|
||||||
const lspClient = new LanguageServerClient({
|
const lspClient = new LanguageServerClient({
|
||||||
client,
|
|
||||||
name: LspWorker.Copilot,
|
name: LspWorker.Copilot,
|
||||||
|
fromServer,
|
||||||
|
intoServer,
|
||||||
|
initializedCallback: () => {
|
||||||
|
setIsCopilotLspReady(true)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return { lspClient }
|
return { lspClient }
|
||||||
}, [token])
|
}, [token])
|
||||||
@ -223,7 +220,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
// We do not want to restart the server, its just wasteful.
|
// We do not want to restart the server, its just wasteful.
|
||||||
const copilotLSP = useMemo(() => {
|
const copilotLSP = useMemo(() => {
|
||||||
let plugin = null
|
let plugin = null
|
||||||
if (isCopilotLspServerReady && !TEST && copilotLspClient) {
|
if (isCopilotLspReady && !TEST && copilotLspClient) {
|
||||||
// Set up the lsp plugin.
|
// Set up the lsp plugin.
|
||||||
const lsp = copilotPlugin({
|
const lsp = copilotPlugin({
|
||||||
documentUri: `file:///${PROJECT_ENTRYPOINT}`,
|
documentUri: `file:///${PROJECT_ENTRYPOINT}`,
|
||||||
@ -235,7 +232,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
plugin = lsp
|
plugin = lsp
|
||||||
}
|
}
|
||||||
return plugin
|
return plugin
|
||||||
}, [copilotLspClient, isCopilotLspServerReady])
|
}, [copilotLspClient, isCopilotLspReady])
|
||||||
|
|
||||||
let lspClients: LanguageServerClient[] = []
|
let lspClients: LanguageServerClient[] = []
|
||||||
if (kclLspClient) {
|
if (kclLspClient) {
|
||||||
@ -245,13 +242,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
lspClients.push(copilotLspClient)
|
lspClients.push(copilotLspClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsKclLspServerReady(isLspReady)
|
|
||||||
}, [isLspReady])
|
|
||||||
useEffect(() => {
|
|
||||||
setIsCopilotLspServerReady(isCopilotReady)
|
|
||||||
}, [isCopilotReady])
|
|
||||||
|
|
||||||
const onProjectClose = (
|
const onProjectClose = (
|
||||||
file: FileEntry | null,
|
file: FileEntry | null,
|
||||||
projectPath: string | null,
|
projectPath: string | null,
|
||||||
|
@ -30,7 +30,6 @@ import {
|
|||||||
applyConstraintAngleBetween,
|
applyConstraintAngleBetween,
|
||||||
} from './Toolbar/SetAngleBetween'
|
} from './Toolbar/SetAngleBetween'
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import {
|
import {
|
||||||
Selections,
|
Selections,
|
||||||
canExtrudeSelection,
|
canExtrudeSelection,
|
||||||
@ -54,13 +53,7 @@ import {
|
|||||||
sketchOnExtrudedFace,
|
sketchOnExtrudedFace,
|
||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import { Program, VariableDeclaration, parse, recast } from 'lang/wasm'
|
||||||
Program,
|
|
||||||
VariableDeclaration,
|
|
||||||
coreDump,
|
|
||||||
parse,
|
|
||||||
recast,
|
|
||||||
} from 'lang/wasm'
|
|
||||||
import {
|
import {
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
@ -71,15 +64,14 @@ import { TEST } from 'env'
|
|||||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||||
import { Models } from '@kittycad/lib/dist/types/src'
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { EditorSelection } from '@uiw/react-codemirror'
|
import { EditorSelection, Transaction } from '@codemirror/state'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { modelingMachineEvent } from 'editor/manager'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -101,7 +93,7 @@ export const ModelingMachineProvider = ({
|
|||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme, enableSSAO },
|
app: { theme, enableSSAO },
|
||||||
modeling: { defaultUnit, highlightEdges },
|
modeling: { defaultUnit, highlightEdges, showScaleGrid },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
@ -111,37 +103,6 @@ export const ModelingMachineProvider = ({
|
|||||||
let [searchParams] = useSearchParams()
|
let [searchParams] = useSearchParams()
|
||||||
const pool = searchParams.get('pool')
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
useSetupEngineManager(streamRef, token, {
|
|
||||||
pool: pool,
|
|
||||||
theme: theme.current,
|
|
||||||
highlightEdges: highlightEdges.current,
|
|
||||||
enableSSAO: enableSSAO.current,
|
|
||||||
})
|
|
||||||
const { htmlRef } = useStore((s) => ({
|
|
||||||
htmlRef: s.htmlRef,
|
|
||||||
}))
|
|
||||||
const coreDumpManager = new CoreDumpManager(
|
|
||||||
engineCommandManager,
|
|
||||||
htmlRef,
|
|
||||||
token
|
|
||||||
)
|
|
||||||
useHotkeyWrapper(['meta + shift + .'], () => {
|
|
||||||
toast.promise(
|
|
||||||
coreDump(coreDumpManager, true),
|
|
||||||
{
|
|
||||||
loading: 'Starting core dump...',
|
|
||||||
success: 'Core dump completed successfully',
|
|
||||||
error: 'Error while exporting core dump',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: {
|
|
||||||
// Note: this extended duration is especially important for Playwright e2e testing
|
|
||||||
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
|
|
||||||
duration: 6000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
const { commandBarState } = useCommandsContext()
|
const { commandBarState } = useCommandsContext()
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
@ -161,7 +122,13 @@ export const ModelingMachineProvider = ({
|
|||||||
modelingMachine,
|
modelingMachine,
|
||||||
{
|
{
|
||||||
actions: {
|
actions: {
|
||||||
'sketch exit execute': () => {
|
'disable copilot': () => {
|
||||||
|
editorManager.setCopilotEnabled(false)
|
||||||
|
},
|
||||||
|
'enable copilot': () => {
|
||||||
|
editorManager.setCopilotEnabled(true)
|
||||||
|
},
|
||||||
|
'sketch exit execute': ({ store }) => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||||
|
|
||||||
@ -195,7 +162,10 @@ export const ModelingMachineProvider = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
kclManager.executeCode(true)
|
store.videoElement?.pause()
|
||||||
|
kclManager.executeCode(true).then(() => {
|
||||||
|
store.videoElement?.play()
|
||||||
|
})
|
||||||
})()
|
})()
|
||||||
},
|
},
|
||||||
'Set mouse state': assign({
|
'Set mouse state': assign({
|
||||||
@ -281,11 +251,15 @@ export const ModelingMachineProvider = ({
|
|||||||
const dispatchSelection = (selection?: EditorSelection) => {
|
const dispatchSelection = (selection?: EditorSelection) => {
|
||||||
if (!selection) return // TODO less of hack for the below please
|
if (!selection) return // TODO less of hack for the below please
|
||||||
if (!editorManager.editorView) return
|
if (!editorManager.editorView) return
|
||||||
editorManager.lastSelectionEvent = Date.now()
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (editorManager.editorView) {
|
if (!editorManager.editorView) return
|
||||||
editorManager.editorView.dispatch({ selection })
|
editorManager.editorView.dispatch({
|
||||||
}
|
selection,
|
||||||
|
annotations: [
|
||||||
|
modelingMachineEvent,
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let selections: Selections = {
|
let selections: Selections = {
|
||||||
@ -328,11 +302,6 @@ export const ModelingMachineProvider = ({
|
|||||||
)
|
)
|
||||||
updateSceneObjectColors()
|
updateSceneObjectColors()
|
||||||
|
|
||||||
// side effect to stop code mirror from updating the same selections again
|
|
||||||
editorManager.lastSelection = selections.codeBasedSelections
|
|
||||||
.map(({ range }) => `${range[1]}->${range[1]}`)
|
|
||||||
.join('&')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectionRanges: selections,
|
selectionRanges: selections,
|
||||||
}
|
}
|
||||||
@ -472,17 +441,6 @@ export const ModelingMachineProvider = ({
|
|||||||
if (selectionRanges.codeBasedSelections.length <= 0) return false
|
if (selectionRanges.codeBasedSelections.length <= 0) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
'Sketch is empty': ({ sketchDetails }) => {
|
|
||||||
const node = getNodeFromPath<VariableDeclaration>(
|
|
||||||
kclManager.ast,
|
|
||||||
sketchDetails?.sketchPathToNode || [],
|
|
||||||
'VariableDeclaration'
|
|
||||||
)
|
|
||||||
// This should not be returning false, and it should be caught
|
|
||||||
// but we need to simulate old behavior to move on.
|
|
||||||
if (err(node)) return false
|
|
||||||
return node.node?.declarations?.[0]?.init.type !== 'PipeExpression'
|
|
||||||
},
|
|
||||||
'Selection is on face': ({ selectionRanges }, { data }) => {
|
'Selection is on face': ({ selectionRanges }, { data }) => {
|
||||||
if (data?.forceNewSketch) return false
|
if (data?.forceNewSketch) return false
|
||||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
||||||
@ -513,11 +471,15 @@ export const ModelingMachineProvider = ({
|
|||||||
services: {
|
services: {
|
||||||
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
const newAst: Program = kclManager.ast
|
if (kclManager.ast.body.length) {
|
||||||
const varDecIndex = sketchDetails.sketchPathToNode[1][0]
|
// this assumes no changes have been made to the sketch besides what we did when entering the sketch
|
||||||
// remove body item at varDecIndex
|
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
|
||||||
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast))
|
||||||
await kclManager.executeAstMock(newAst)
|
const varDecIndex = sketchDetails.sketchPathToNode[1][0]
|
||||||
|
// remove body item at varDecIndex
|
||||||
|
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
||||||
|
await kclManager.executeAstMock(newAst)
|
||||||
|
}
|
||||||
sceneInfra.setCallbacks({
|
sceneInfra.setCallbacks({
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
onDrag: () => {},
|
onDrag: () => {},
|
||||||
@ -897,6 +859,16 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useSetupEngineManager(streamRef, token, {
|
||||||
|
pool: pool,
|
||||||
|
theme: theme.current,
|
||||||
|
highlightEdges: highlightEdges.current,
|
||||||
|
enableSSAO: enableSSAO.current,
|
||||||
|
modelingSend,
|
||||||
|
modelingContext: modelingState.context,
|
||||||
|
showScaleGrid: showScaleGrid.current,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kclManager.registerExecuteCallback(() => {
|
kclManager.registerExecuteCallback(() => {
|
||||||
modelingSend({ type: 'Re-execute' })
|
modelingSend({ type: 'Re-execute' })
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useStore } from 'useStore'
|
|
||||||
import styles from './ModelingPane.module.css'
|
import styles from './ModelingPane.module.css'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
export interface ModelingPaneProps
|
export interface ModelingPaneProps
|
||||||
extends React.PropsWithChildren,
|
extends React.PropsWithChildren,
|
||||||
@ -33,11 +33,9 @@ export const ModelingPane = ({
|
|||||||
}: ModelingPaneProps) => {
|
}: ModelingPaneProps) => {
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
const { buttonDownInStream } = useStore((s) => ({
|
const { context } = useModelingContext()
|
||||||
buttonDownInStream: s.buttonDownInStream,
|
|
||||||
}))
|
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
buttonDownInStream || onboardingStatus.current === 'camera'
|
context.store?.buttonDownInStream || onboardingStatus.current === 'camera'
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
return (
|
return (
|
||||||
|
171
src/components/ModelingSidebar/ModelingPanes/CodeEditor.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
EditorState,
|
||||||
|
EditorStateConfig,
|
||||||
|
Extension,
|
||||||
|
StateEffect,
|
||||||
|
} from '@codemirror/state'
|
||||||
|
import { EditorView } from '@codemirror/view'
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark'
|
||||||
|
|
||||||
|
//reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
|
||||||
|
const useFirstRender = () => {
|
||||||
|
const firstRender = useRef(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
firstRender.current = false
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return firstRender.current
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLightThemeOption = EditorView.theme(
|
||||||
|
{
|
||||||
|
'&': {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dark: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
interface CodeEditorRef {
|
||||||
|
editor?: HTMLDivElement | null
|
||||||
|
view?: EditorView
|
||||||
|
state?: EditorState
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeEditorProps {
|
||||||
|
onCreateEditor?: (view: EditorView | null) => void
|
||||||
|
initialDocValue?: EditorStateConfig['doc']
|
||||||
|
extensions?: Extension
|
||||||
|
theme: 'light' | 'dark'
|
||||||
|
autoFocus?: boolean
|
||||||
|
selection?: EditorStateConfig['selection']
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCodeMirror extends CodeEditorProps {
|
||||||
|
container?: HTMLDivElement | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
||||||
|
const {
|
||||||
|
onCreateEditor,
|
||||||
|
extensions = [],
|
||||||
|
initialDocValue,
|
||||||
|
theme,
|
||||||
|
autoFocus = false,
|
||||||
|
selection,
|
||||||
|
} = props
|
||||||
|
const editor = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const { view, state, container } = useCodeMirror({
|
||||||
|
container: editor.current,
|
||||||
|
onCreateEditor,
|
||||||
|
extensions,
|
||||||
|
initialDocValue,
|
||||||
|
theme,
|
||||||
|
autoFocus,
|
||||||
|
selection,
|
||||||
|
})
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({ editor: editor.current, view: view, state: state }),
|
||||||
|
[editor, container, view, state]
|
||||||
|
)
|
||||||
|
|
||||||
|
return <div ref={editor}></div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useCodeMirror(props: UseCodeMirror) {
|
||||||
|
const {
|
||||||
|
onCreateEditor,
|
||||||
|
extensions = [],
|
||||||
|
initialDocValue,
|
||||||
|
theme,
|
||||||
|
autoFocus = false,
|
||||||
|
selection,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>()
|
||||||
|
const [view, setView] = useState<EditorView>()
|
||||||
|
const [state, setState] = useState<EditorState>()
|
||||||
|
|
||||||
|
const isFirstRender = useFirstRender()
|
||||||
|
|
||||||
|
const targetExtensions = useMemo(() => {
|
||||||
|
let exts = Array.isArray(extensions) ? extensions : []
|
||||||
|
if (theme === 'dark') {
|
||||||
|
exts = [...exts, oneDark]
|
||||||
|
} else if (theme === 'light') {
|
||||||
|
exts = [...exts, defaultLightThemeOption]
|
||||||
|
}
|
||||||
|
|
||||||
|
return exts
|
||||||
|
}, [extensions, theme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (container && !state) {
|
||||||
|
const config = {
|
||||||
|
doc: initialDocValue,
|
||||||
|
selection,
|
||||||
|
extensions: [...Array.of(extensions)],
|
||||||
|
}
|
||||||
|
const stateCurrent = EditorState.create(config)
|
||||||
|
setState(stateCurrent)
|
||||||
|
if (!view) {
|
||||||
|
const viewCurrent = new EditorView({
|
||||||
|
state: stateCurrent,
|
||||||
|
parent: container,
|
||||||
|
})
|
||||||
|
setView(viewCurrent)
|
||||||
|
onCreateEditor && onCreateEditor(viewCurrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (view) {
|
||||||
|
setState(undefined)
|
||||||
|
setView(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [container, state])
|
||||||
|
|
||||||
|
useEffect(() => setContainer(props.container), [props.container])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (view) {
|
||||||
|
view.destroy()
|
||||||
|
setView(undefined)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[view]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus && view) {
|
||||||
|
view.focus()
|
||||||
|
}
|
||||||
|
}, [autoFocus, view])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (view && !isFirstRender) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: StateEffect.reconfigure.of(targetExtensions),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [targetExtensions])
|
||||||
|
|
||||||
|
return { view, setView, container, setContainer, state, setState }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeEditor
|
@ -1,4 +1,3 @@
|
|||||||
import ReactCodeMirror from '@uiw/react-codemirror'
|
|
||||||
import { TEST } from 'env'
|
import { TEST } from 'env'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
@ -43,6 +42,7 @@ import {
|
|||||||
closeBracketsKeymap,
|
closeBracketsKeymap,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
|
import CodeEditor from './CodeEditor'
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -84,6 +84,10 @@ export const KclEditorPane = () => {
|
|||||||
|
|
||||||
const textWrapping = context.textEditor.textWrapping
|
const textWrapping = context.textEditor.textWrapping
|
||||||
const cursorBlinking = context.textEditor.blinkingCursor
|
const cursorBlinking = context.textEditor.blinkingCursor
|
||||||
|
// DO NOT ADD THE CODEMIRROR HOTKEYS HERE TO THE DEPENDENCY ARRAY
|
||||||
|
// It reloads the editor every time we do _anything_ in the editor
|
||||||
|
// I have no idea why.
|
||||||
|
// Instead, hot load hotkeys via code mirror native.
|
||||||
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
|
const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
|
||||||
|
|
||||||
const editorExtensions = useMemo(() => {
|
const editorExtensions = useMemo(() => {
|
||||||
@ -134,7 +138,6 @@ export const KclEditorPane = () => {
|
|||||||
highlightSelectionMatches(),
|
highlightSelectionMatches(),
|
||||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
rectangularSelection(),
|
rectangularSelection(),
|
||||||
drawSelection(),
|
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
interact({
|
interact({
|
||||||
rules: [
|
rules: [
|
||||||
@ -173,13 +176,7 @@ export const KclEditorPane = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}, [
|
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
|
||||||
kclLSP,
|
|
||||||
copilotLSP,
|
|
||||||
textWrapping.current,
|
|
||||||
cursorBlinking.current,
|
|
||||||
codeMirrorHotkeys,
|
|
||||||
])
|
|
||||||
|
|
||||||
const initialCode = useRef(codeManager.code)
|
const initialCode = useRef(codeManager.code)
|
||||||
|
|
||||||
@ -188,15 +185,15 @@ export const KclEditorPane = () => {
|
|||||||
id="code-mirror-override"
|
id="code-mirror-override"
|
||||||
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
|
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
|
||||||
>
|
>
|
||||||
<ReactCodeMirror
|
<CodeEditor
|
||||||
value={initialCode.current}
|
initialDocValue={initialCode.current}
|
||||||
extensions={editorExtensions}
|
extensions={editorExtensions}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onCreateEditor={(_editorView) =>
|
onCreateEditor={(_editorView) => {
|
||||||
|
if (_editorView === null) return
|
||||||
|
|
||||||
editorManager.setEditorView(_editorView)
|
editorManager.setEditorView(_editorView)
|
||||||
}
|
}}
|
||||||
indentWithTab={false}
|
|
||||||
basicSetup={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { HTMLAttributes, useCallback, useEffect, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { Tab } from '@headlessui/react'
|
import { Tab } from '@headlessui/react'
|
||||||
import {
|
import {
|
||||||
SidebarPane,
|
SidebarPane,
|
||||||
@ -15,6 +14,7 @@ import { ActionIcon } from 'components/ActionIcon'
|
|||||||
import styles from './ModelingSidebar.module.css'
|
import styles from './ModelingSidebar.module.css'
|
||||||
import { ModelingPane } from './ModelingPane'
|
import { ModelingPane } from './ModelingPane'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -23,14 +23,11 @@ interface ModelingSidebarProps {
|
|||||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
const { openPanes, buttonDownInStream } = useStore((s) => ({
|
const { context } = useModelingContext()
|
||||||
buttonDownInStream: s.buttonDownInStream,
|
|
||||||
openPanes: s.openPanes,
|
|
||||||
}))
|
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
buttonDownInStream ||
|
context.store?.buttonDownInStream ||
|
||||||
onboardingStatus.current === 'camera' ||
|
onboardingStatus.current === 'camera' ||
|
||||||
openPanes.length === 0
|
context.store?.openPanes.length === 0
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
|
|
||||||
@ -45,7 +42,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
maxWidth={800}
|
maxWidth={800}
|
||||||
handleClasses={{
|
handleClasses={{
|
||||||
right:
|
right:
|
||||||
(openPanes.length === 0 ? 'hidden ' : 'block ') +
|
(context.store?.openPanes.length === 0 ? 'hidden ' : 'block ') +
|
||||||
'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ',
|
'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ',
|
||||||
left: 'hidden',
|
left: 'hidden',
|
||||||
top: 'hidden',
|
top: 'hidden',
|
||||||
@ -56,15 +53,19 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
bottomRight: 'hidden',
|
bottomRight: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.grid + ' flex-1'}>
|
<div id="app-sidebar" className={styles.grid + ' flex-1'}>
|
||||||
<ModelingSidebarSection panes={topPanes} />
|
<ModelingSidebarSection id="sidebar-top" panes={topPanes} />
|
||||||
<ModelingSidebarSection panes={bottomPanes} alignButtons="end" />
|
<ModelingSidebarSection
|
||||||
|
id="sidebar-bottom"
|
||||||
|
panes={bottomPanes}
|
||||||
|
alignButtons="end"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelingSidebarSectionProps {
|
interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
panes: SidebarPane[]
|
panes: SidebarPane[]
|
||||||
alignButtons?: 'start' | 'end'
|
alignButtons?: 'start' | 'end'
|
||||||
}
|
}
|
||||||
@ -72,15 +73,16 @@ interface ModelingSidebarSectionProps {
|
|||||||
function ModelingSidebarSection({
|
function ModelingSidebarSection({
|
||||||
panes,
|
panes,
|
||||||
alignButtons = 'start',
|
alignButtons = 'start',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
}: ModelingSidebarSectionProps) {
|
}: ModelingSidebarSectionProps) {
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||||
const paneIds = panes.map((pane) => pane.id)
|
const paneIds = panes.map((pane) => pane.id)
|
||||||
const { openPanes, setOpenPanes } = useStore((s) => ({
|
const { send, context } = useModelingContext()
|
||||||
openPanes: s.openPanes,
|
const foundOpenPane = context.store?.openPanes.find((pane) =>
|
||||||
setOpenPanes: s.setOpenPanes,
|
paneIds.includes(pane)
|
||||||
}))
|
)
|
||||||
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
|
||||||
const [currentPane, setCurrentPane] = useState(
|
const [currentPane, setCurrentPane] = useState(
|
||||||
foundOpenPane || ('none' as SidebarType | 'none')
|
foundOpenPane || ('none' as SidebarType | 'none')
|
||||||
)
|
)
|
||||||
@ -88,17 +90,37 @@ function ModelingSidebarSection({
|
|||||||
const togglePane = useCallback(
|
const togglePane = useCallback(
|
||||||
(newPane: SidebarType | 'none') => {
|
(newPane: SidebarType | 'none') => {
|
||||||
if (newPane === 'none') {
|
if (newPane === 'none') {
|
||||||
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
openPanes: context.store?.openPanes.filter(
|
||||||
|
(p) => p !== currentPane
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
setCurrentPane('none')
|
setCurrentPane('none')
|
||||||
} else if (newPane === currentPane) {
|
} else if (newPane === currentPane) {
|
||||||
setCurrentPane('none')
|
setCurrentPane('none')
|
||||||
setOpenPanes(openPanes.filter((p) => p !== newPane))
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
openPanes: context.store?.openPanes.filter((p) => p !== newPane),
|
||||||
|
},
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
setOpenPanes([...openPanes.filter((p) => p !== currentPane), newPane])
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
openPanes: [
|
||||||
|
...context.store?.openPanes.filter((p) => p !== currentPane),
|
||||||
|
newPane,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
setCurrentPane(newPane)
|
setCurrentPane(newPane)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[openPanes, setOpenPanes, currentPane, setCurrentPane]
|
[context.store?.openPanes, send, currentPane, setCurrentPane]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Filter out the debug panel if it's not supposed to be shown
|
// Filter out the debug panel if it's not supposed to be shown
|
||||||
@ -116,14 +138,14 @@ function ModelingSidebarSection({
|
|||||||
if (
|
if (
|
||||||
!showDebugPanel.current &&
|
!showDebugPanel.current &&
|
||||||
currentPane === 'debug' &&
|
currentPane === 'debug' &&
|
||||||
openPanes.includes('debug')
|
context.store?.openPanes.includes('debug')
|
||||||
) {
|
) {
|
||||||
togglePane('debug')
|
togglePane('debug')
|
||||||
}
|
}
|
||||||
}, [showDebugPanel.current, togglePane, openPanes])
|
}, [showDebugPanel.current, togglePane, context.store?.openPanes])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group contents">
|
<div className={'group contents ' + className} {...props}>
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
vertical
|
vertical
|
||||||
selectedIndex={
|
selectedIndex={
|
||||||
@ -135,6 +157,7 @@ function ModelingSidebarSection({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab.List
|
<Tab.List
|
||||||
|
id={`${props.id}-ribbon`}
|
||||||
className={
|
className={
|
||||||
'pointer-events-auto ' +
|
'pointer-events-auto ' +
|
||||||
(alignButtons === 'start'
|
(alignButtons === 'start'
|
||||||
@ -145,7 +168,9 @@ function ModelingSidebarSection({
|
|||||||
: ' border-r-0') +
|
: ' border-r-0') +
|
||||||
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
|
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
|
||||||
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
|
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
|
||||||
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
|
(context.store?.openPanes.length === 1 && currentPane === 'none'
|
||||||
|
? 'pr-0.5'
|
||||||
|
: '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tab key="none" className="sr-only">
|
<Tab key="none" className="sr-only">
|
||||||
@ -161,10 +186,11 @@ function ModelingSidebarSection({
|
|||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels
|
<Tab.Panels
|
||||||
|
id={`${props.id}-pane`}
|
||||||
as="article"
|
as="article"
|
||||||
className={
|
className={
|
||||||
'col-start-2 col-span-1 ' +
|
'col-start-2 col-span-1 ' +
|
||||||
(openPanes.length === 1
|
(context.store?.openPanes.length === 1
|
||||||
? currentPane !== 'none'
|
? currentPane !== 'none'
|
||||||
? `row-start-1 row-end-3`
|
? `row-start-1 row-end-3`
|
||||||
: `hidden`
|
: `hidden`
|
||||||
|
@ -2,22 +2,17 @@ import { coreDump } from 'lang/wasm'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
|
||||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||||
const { auth } = useSettingsAuthContext()
|
const { auth } = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const { htmlRef } = useStore((s) => ({
|
const coreDumpManager = useMemo(
|
||||||
htmlRef: s.htmlRef,
|
() => new CoreDumpManager(engineCommandManager, token),
|
||||||
}))
|
[]
|
||||||
const coreDumpManager = new CoreDumpManager(
|
|
||||||
engineCommandManager,
|
|
||||||
htmlRef,
|
|
||||||
token
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
@ -134,6 +134,11 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
setEngineScaleGridVisibility: (context) => {
|
||||||
|
engineCommandManager.setScaleGridVisibility(
|
||||||
|
context.modeling.showScaleGrid.current
|
||||||
|
)
|
||||||
|
},
|
||||||
setClientTheme: (context) => {
|
setClientTheme: (context) => {
|
||||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||||
sceneInfra.theme = opposingTheme
|
sceneInfra.theme = opposingTheme
|
||||||
@ -170,7 +175,12 @@ export const SettingsAuthProviderBase = ({
|
|||||||
id: `${event.type}.success`,
|
id: `${event.type}.success`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'Execute AST': () => kclManager.executeCode(true, true),
|
'Execute AST': () => {
|
||||||
|
kclManager.isFirstRender = true
|
||||||
|
kclManager.executeCode(true, true).then(() => {
|
||||||
|
kclManager.isFirstRender = false
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
'Persist settings': (context) =>
|
'Persist settings': (context) =>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { getNormalisedCoordinates } from '../lib/utils'
|
import { getNormalisedCoordinates } from '../lib/utils'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
@ -9,26 +8,15 @@ import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
|||||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
import { butName } from 'lib/cameraControls'
|
import { butName } from 'lib/cameraControls'
|
||||||
import { sendSelectEventToEngine } from 'lib/selections'
|
import { sendSelectEventToEngine } from 'lib/selections'
|
||||||
|
import { kclManager } from 'lib/singletons'
|
||||||
|
|
||||||
export const Stream = ({ className = '' }: { className?: string }) => {
|
export const Stream = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isFirstRender, setIsFirstRender] = useState(kclManager.isFirstRender)
|
||||||
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
|
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const {
|
|
||||||
mediaStream,
|
|
||||||
setButtonDownInStream,
|
|
||||||
didDragInStream,
|
|
||||||
setDidDragInStream,
|
|
||||||
streamDimensions,
|
|
||||||
} = useStore((s) => ({
|
|
||||||
mediaStream: s.mediaStream,
|
|
||||||
setButtonDownInStream: s.setButtonDownInStream,
|
|
||||||
didDragInStream: s.didDragInStream,
|
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
|
||||||
streamDimensions: s.streamDimensions,
|
|
||||||
}))
|
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
|
|
||||||
const isNetworkOkay =
|
const isNetworkOkay =
|
||||||
@ -67,6 +55,10 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsFirstRender(kclManager.isFirstRender)
|
||||||
|
}, [kclManager.isFirstRender])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
typeof window === 'undefined' ||
|
typeof window === 'undefined' ||
|
||||||
@ -74,9 +66,16 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
if (!mediaStream) return
|
if (!context.store?.mediaStream) return
|
||||||
videoRef.current.srcObject = mediaStream
|
videoRef.current.srcObject = context.store.mediaStream
|
||||||
}, [mediaStream])
|
|
||||||
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
videoElement: videoRef.current,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [context.store?.mediaStream])
|
||||||
|
|
||||||
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
if (!isNetworkOkay) return
|
if (!isNetworkOkay) return
|
||||||
@ -88,25 +87,44 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
clientX: e.clientX,
|
clientX: e.clientX,
|
||||||
clientY: e.clientY,
|
clientY: e.clientY,
|
||||||
el: videoRef.current,
|
el: videoRef.current,
|
||||||
...streamDimensions,
|
...context.store?.streamDimensions,
|
||||||
})
|
})
|
||||||
|
|
||||||
setButtonDownInStream(e.button)
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
buttonDownInStream: e.button,
|
||||||
|
},
|
||||||
|
})
|
||||||
setClickCoords({ x, y })
|
setClickCoords({ x, y })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
if (!isNetworkOkay) return
|
if (!isNetworkOkay) return
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
setButtonDownInStream(undefined)
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
buttonDownInStream: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
if (state.matches('Sketch')) return
|
if (state.matches('Sketch')) return
|
||||||
if (state.matches('Sketch no face')) return
|
if (state.matches('Sketch no face')) return
|
||||||
|
|
||||||
if (!didDragInStream && butName(e).left) {
|
if (!context.store?.didDragInStream && butName(e).left) {
|
||||||
sendSelectEventToEngine(e, videoRef.current, streamDimensions)
|
sendSelectEventToEngine(
|
||||||
|
e,
|
||||||
|
videoRef.current,
|
||||||
|
context.store?.streamDimensions
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setDidDragInStream(false)
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
didDragInStream: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
setClickCoords(undefined)
|
setClickCoords(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +138,13 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
|
((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
|
||||||
0.5
|
0.5
|
||||||
|
|
||||||
if (delta > 5 && !didDragInStream) {
|
if (delta > 5 && !context.store?.didDragInStream) {
|
||||||
setDidDragInStream(true)
|
send({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
didDragInStream: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,10 +179,14 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{(isLoading || isFirstRender) && (
|
||||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div className="text-center absolute inset-0">
|
||||||
<Loading>
|
<Loading>
|
||||||
<span data-testid="loading-stream">Loading stream...</span>
|
{!isLoading && isFirstRender ? (
|
||||||
|
<span data-testid="loading-stream">Building scene...</span>
|
||||||
|
) : (
|
||||||
|
<span data-testid="loading-stream">Loading stream...</span>
|
||||||
|
)}
|
||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
@ -145,7 +145,7 @@ export async function applyConstraintIntersect({
|
|||||||
const { transforms, forcedSelectionRanges } = info
|
const { transforms, forcedSelectionRanges } = info
|
||||||
|
|
||||||
const transform1 = transformSecondarySketchLinesTagFirst({
|
const transform1 = transformSecondarySketchLinesTagFirst({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges: forcedSelectionRanges,
|
selectionRanges: forcedSelectionRanges,
|
||||||
transformInfos: transforms,
|
transformInfos: transforms,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selection, Selections } from 'lib/selections'
|
import { Selection, Selections } from 'lib/selections'
|
||||||
import { PathToNode, Program, Value } from '../../lang/wasm'
|
import { PathToNode, Program, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
@ -106,7 +106,7 @@ export async function applyConstraintAbsDistance({
|
|||||||
const transformInfos = info.transforms
|
const transformInfos = info.transforms
|
||||||
|
|
||||||
const transform1 = transformAstSketchLines({
|
const transform1 = transformAstSketchLines({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges: selectionRanges,
|
selectionRanges: selectionRanges,
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
@ -128,7 +128,7 @@ export async function applyConstraintAbsDistance({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const transform2 = transformAstSketchLines({
|
const transform2 = transformAstSketchLines({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges: selectionRanges,
|
selectionRanges: selectionRanges,
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
@ -176,7 +176,7 @@ export function applyConstraintAxisAlign({
|
|||||||
let finalValue = createIdentifier('ZERO')
|
let finalValue = createIdentifier('ZERO')
|
||||||
|
|
||||||
return transformAstSketchLines({
|
return transformAstSketchLines({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges: selectionRanges,
|
selectionRanges: selectionRanges,
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
@ -100,7 +100,7 @@ export async function applyConstraintAngleBetween({
|
|||||||
const transformInfos = info.transforms
|
const transformInfos = info.transforms
|
||||||
|
|
||||||
const transformed1 = transformSecondarySketchLinesTagFirst({
|
const transformed1 = transformSecondarySketchLinesTagFirst({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
@ -108,7 +108,7 @@ export async function applyConstraintHorzVertDistance({
|
|||||||
if (err(info)) return Promise.reject(info)
|
if (err(info)) return Promise.reject(info)
|
||||||
const transformInfos = info.transforms
|
const transformInfos = info.transforms
|
||||||
const transformed = transformSecondarySketchLinesTagFirst({
|
const transformed = transformSecondarySketchLinesTagFirst({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
transformInfos,
|
transformInfos,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
@ -84,7 +84,7 @@ export async function applyConstraintAngleLength({
|
|||||||
|
|
||||||
const { transforms } = angleLength
|
const { transforms } = angleLength
|
||||||
const sketched = transformAstSketchLines({
|
const sketched = transformAstSketchLines({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
transformInfos: transforms,
|
transformInfos: transforms,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
@ -139,7 +139,7 @@ export async function applyConstraintAngleLength({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retval = transformAstSketchLines({
|
const retval = transformAstSketchLines({
|
||||||
ast: kclManager.ast,
|
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
transformInfos: transforms,
|
transformInfos: transforms,
|
||||||
programMemory: kclManager.programMemory,
|
programMemory: kclManager.programMemory,
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
import { StateField, StateEffect } from '@codemirror/state'
|
import { StateField, StateEffect, Annotation } from '@codemirror/state'
|
||||||
import { EditorView, Decoration } from '@codemirror/view'
|
import { EditorView, Decoration } from '@codemirror/view'
|
||||||
|
|
||||||
export { EditorView }
|
export { EditorView }
|
||||||
|
|
||||||
export const addLineHighlight = StateEffect.define<[number, number]>()
|
export const addLineHighlight = StateEffect.define<[number, number]>()
|
||||||
|
|
||||||
|
const addLineHighlightAnnotation = Annotation.define<null>()
|
||||||
|
export const addLineHighlightEvent = addLineHighlightAnnotation.of(null)
|
||||||
|
|
||||||
export const lineHighlightField = StateField.define({
|
export const lineHighlightField = StateField.define({
|
||||||
create() {
|
create() {
|
||||||
return Decoration.none
|
return Decoration.none
|
||||||
},
|
},
|
||||||
update(lines, tr) {
|
update(lines, tr) {
|
||||||
lines = lines.map(tr.changes)
|
lines = lines.map(tr.changes)
|
||||||
|
|
||||||
|
const isLineHighlightEvent = tr.annotation(addLineHighlightEvent.type)
|
||||||
|
if (isLineHighlightEvent === undefined) {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
const deco = []
|
const deco = []
|
||||||
for (let e of tr.effects) {
|
for (let e of tr.effects) {
|
||||||
if (e.is(addLineHighlight)) {
|
if (e.is(addLineHighlight)) {
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
import { hasNextSnippetField } from '@codemirror/autocomplete'
|
|
||||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
import { EditorSelection, SelectionRange } from '@codemirror/state'
|
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||||
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||||
import { undo, redo } from '@codemirror/commands'
|
import { undo, redo } from '@codemirror/commands'
|
||||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||||
import { addLineHighlight } from './highlightextension'
|
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
||||||
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
|
import {
|
||||||
|
forEachDiagnostic,
|
||||||
|
Diagnostic,
|
||||||
|
setDiagnosticsEffect,
|
||||||
|
} from '@codemirror/lint'
|
||||||
|
|
||||||
|
const updateOutsideEditorAnnotation = Annotation.define<null>()
|
||||||
|
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(null)
|
||||||
|
|
||||||
|
const modelingMachineAnnotation = Annotation.define<null>()
|
||||||
|
export const modelingMachineEvent = modelingMachineAnnotation.of(null)
|
||||||
|
|
||||||
|
const setDiagnosticsAnnotation = Annotation.define<null>()
|
||||||
|
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(null)
|
||||||
|
|
||||||
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
|
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
|
||||||
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
|
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
|
||||||
@ -15,6 +27,7 @@ function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
|
|||||||
|
|
||||||
export default class EditorManager {
|
export default class EditorManager {
|
||||||
private _editorView: EditorView | null = null
|
private _editorView: EditorView | null = null
|
||||||
|
private _copilotEnabled: boolean = true
|
||||||
|
|
||||||
private _isShiftDown: boolean = false
|
private _isShiftDown: boolean = false
|
||||||
private _selectionRanges: Selections = {
|
private _selectionRanges: Selections = {
|
||||||
@ -22,8 +35,6 @@ export default class EditorManager {
|
|||||||
codeBasedSelections: [],
|
codeBasedSelections: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
private _lastSelectionEvent: number | null = null
|
|
||||||
lastSelection: string = ''
|
|
||||||
private _lastEvent: { event: string; time: number } | null = null
|
private _lastEvent: { event: string; time: number } | null = null
|
||||||
|
|
||||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||||
@ -37,6 +48,14 @@ export default class EditorManager {
|
|||||||
|
|
||||||
private _highlightRange: [number, number] = [0, 0]
|
private _highlightRange: [number, number] = [0, 0]
|
||||||
|
|
||||||
|
setCopilotEnabled(enabled: boolean) {
|
||||||
|
this._copilotEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
get copilotEnabled(): boolean {
|
||||||
|
return this._copilotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
setEditorView(editorView: EditorView) {
|
setEditorView(editorView: EditorView) {
|
||||||
this._editorView = editorView
|
this._editorView = editorView
|
||||||
}
|
}
|
||||||
@ -57,10 +76,6 @@ export default class EditorManager {
|
|||||||
this._selectionRanges = selectionRanges
|
this._selectionRanges = selectionRanges
|
||||||
}
|
}
|
||||||
|
|
||||||
set lastSelectionEvent(time: number) {
|
|
||||||
this._lastSelectionEvent = time
|
|
||||||
}
|
|
||||||
|
|
||||||
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
|
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
|
||||||
this._modelingSend = send
|
this._modelingSend = send
|
||||||
}
|
}
|
||||||
@ -83,32 +98,39 @@ export default class EditorManager {
|
|||||||
|
|
||||||
setHighlightRange(selection: Selection['range']): void {
|
setHighlightRange(selection: Selection['range']): void {
|
||||||
this._highlightRange = selection
|
this._highlightRange = selection
|
||||||
const editorView = this.editorView
|
|
||||||
const safeEnd = Math.min(
|
const safeEnd = Math.min(
|
||||||
selection[1],
|
selection[1],
|
||||||
editorView?.state.doc.length || selection[1]
|
this._editorView?.state.doc.length || selection[1]
|
||||||
)
|
)
|
||||||
if (editorView) {
|
if (this._editorView) {
|
||||||
editorView.dispatch({
|
this._editorView.dispatch({
|
||||||
effects: addLineHighlight.of([selection[0], safeEnd]),
|
effects: addLineHighlight.of([selection[0], safeEnd]),
|
||||||
|
annotations: [
|
||||||
|
updateOutsideEditorEvent,
|
||||||
|
addLineHighlightEvent,
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDiagnostics(): void {
|
clearDiagnostics(): void {
|
||||||
if (!this.editorView) return
|
this.setDiagnostics([])
|
||||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||||
if (!this.editorView) return
|
if (!this._editorView) return
|
||||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
|
|
||||||
|
this._editorView.dispatch({
|
||||||
|
effects: [setDiagnosticsEffect.of(diagnostics)],
|
||||||
|
annotations: [setDiagnosticsEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addDiagnostics(diagnostics: Diagnostic[]): void {
|
addDiagnostics(diagnostics: Diagnostic[]): void {
|
||||||
if (!this.editorView) return
|
if (!this._editorView) return
|
||||||
|
|
||||||
forEachDiagnostic(this.editorView.state, function (diag) {
|
forEachDiagnostic(this._editorView.state, function (diag) {
|
||||||
diagnostics.push(diag)
|
diagnostics.push(diag)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -122,9 +144,7 @@ export default class EditorManager {
|
|||||||
uniqueDiagnostics.add(diagnostic)
|
uniqueDiagnostics.add(diagnostic)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.editorView.dispatch(
|
this.setDiagnostics([...uniqueDiagnostics])
|
||||||
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
@ -174,56 +194,35 @@ export default class EditorManager {
|
|||||||
].range[1]
|
].range[1]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (!this.editorView) {
|
|
||||||
|
if (!this._editorView) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.editorView.dispatch({
|
|
||||||
|
this._editorView.dispatch({
|
||||||
selection: EditorSelection.create(codeBasedSelections, 1),
|
selection: EditorSelection.create(codeBasedSelections, 1),
|
||||||
|
annotations: [
|
||||||
|
updateOutsideEditorEvent,
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We will ONLY get here if the user called a select event.
|
||||||
|
// This is handled by the code mirror kcl plugin.
|
||||||
|
// If you call this function from somewhere else, you best know wtf you are
|
||||||
|
// doing. (jess)
|
||||||
handleOnViewUpdate(viewUpdate: ViewUpdate): void {
|
handleOnViewUpdate(viewUpdate: ViewUpdate): void {
|
||||||
// If we are just fucking around in a snippet, return early and don't
|
if (!this._editorView) {
|
||||||
// trigger stuff below that might cause the component to re-render.
|
|
||||||
// Otherwise we will not be able to tab thru the snippet portions.
|
|
||||||
// We explicitly dont check HasPrevSnippetField because we always add
|
|
||||||
// a ${} to the end of the function so that's fine.
|
|
||||||
if (hasNextSnippetField(viewUpdate.view.state)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.editorView === null) {
|
|
||||||
this.setEditorView(viewUpdate.view)
|
this.setEditorView(viewUpdate.view)
|
||||||
}
|
}
|
||||||
const selString = stringifyRanges(
|
|
||||||
viewUpdate?.state?.selection?.ranges || []
|
|
||||||
)
|
|
||||||
|
|
||||||
if (selString === this.lastSelection) {
|
const ranges = viewUpdate?.state?.selection?.ranges || []
|
||||||
// onUpdate is noisy and is fired a lot by extensions
|
if (ranges.length === 0) {
|
||||||
// since we're only interested in selections changes we can ignore most of these.
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// note this is also set from the "Set selection" action to stop code mirror from updating selections right after
|
|
||||||
// selections are made from the scene
|
|
||||||
this.lastSelection = selString
|
|
||||||
|
|
||||||
if (
|
const ignoreEvents: ModelingMachineEvent['type'][] = ['change tool']
|
||||||
this._lastSelectionEvent &&
|
|
||||||
Date.now() - this._lastSelectionEvent < 150
|
|
||||||
) {
|
|
||||||
return // update triggered by scene selection
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sceneInfra.selected) {
|
|
||||||
return // mid drag
|
|
||||||
}
|
|
||||||
|
|
||||||
const ignoreEvents: ModelingMachineEvent['type'][] = [
|
|
||||||
'Equip Line tool',
|
|
||||||
'Equip tangential arc to',
|
|
||||||
'Equip rectangle tool',
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!this._modelingEvent) {
|
if (!this._modelingEvent) {
|
||||||
return
|
return
|
||||||
@ -266,7 +265,3 @@ export default class EditorManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyRanges(ranges: readonly SelectionRange[]): string {
|
|
||||||
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
/// Thanks to the Cursor folks for their heavy lifting here.
|
/// Thanks to the Cursor folks for their heavy lifting here.
|
||||||
|
/// This has been heavily modified from their original implementation but we are
|
||||||
|
/// still grateful.
|
||||||
import { indentUnit } from '@codemirror/language'
|
import { indentUnit } from '@codemirror/language'
|
||||||
import {
|
import {
|
||||||
Decoration,
|
Decoration,
|
||||||
DecorationSet,
|
DecorationSet,
|
||||||
EditorView,
|
EditorView,
|
||||||
|
KeyBinding,
|
||||||
|
PluginValue,
|
||||||
ViewPlugin,
|
ViewPlugin,
|
||||||
ViewUpdate,
|
ViewUpdate,
|
||||||
|
keymap,
|
||||||
} from '@codemirror/view'
|
} from '@codemirror/view'
|
||||||
import {
|
import {
|
||||||
Annotation,
|
Annotation,
|
||||||
@ -17,17 +22,37 @@ import {
|
|||||||
Transaction,
|
Transaction,
|
||||||
} from '@codemirror/state'
|
} from '@codemirror/state'
|
||||||
import { completionStatus } from '@codemirror/autocomplete'
|
import { completionStatus } from '@codemirror/autocomplete'
|
||||||
import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
|
|
||||||
import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
|
|
||||||
import {
|
import {
|
||||||
LanguageServerPlugin,
|
offsetToPos,
|
||||||
documentUri,
|
posToOffset,
|
||||||
|
LanguageServerOptions,
|
||||||
|
LanguageServerClient,
|
||||||
|
docPathFacet,
|
||||||
languageId,
|
languageId,
|
||||||
workspaceFolders,
|
} from '@kittycad/codemirror-lsp-client'
|
||||||
} from 'editor/plugins/lsp/plugin'
|
import { deferExecution } from 'lib/utils'
|
||||||
|
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
|
||||||
|
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
|
||||||
|
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
|
||||||
|
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
|
||||||
|
import { editorManager } from 'lib/singletons'
|
||||||
|
|
||||||
|
const copilotPluginAnnotation = Annotation.define<null>()
|
||||||
|
export const copilotPluginEvent = copilotPluginAnnotation.of(null)
|
||||||
|
|
||||||
|
const rejectSuggestionAnnotation = Annotation.define<null>()
|
||||||
|
export const rejectSuggestionCommand = rejectSuggestionAnnotation.of(null)
|
||||||
|
|
||||||
|
// Effects to tell StateEffect what to do with GhostText
|
||||||
|
const addSuggestion = StateEffect.define<Suggestion>()
|
||||||
|
const acceptSuggestion = StateEffect.define<null>()
|
||||||
|
const clearSuggestion = StateEffect.define<null>()
|
||||||
|
const typeFirst = StateEffect.define<number>()
|
||||||
|
|
||||||
const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
|
const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
|
||||||
|
|
||||||
|
const changesDelay = 600
|
||||||
|
|
||||||
interface Suggestion {
|
interface Suggestion {
|
||||||
text: string
|
text: string
|
||||||
displayText: string
|
displayText: string
|
||||||
@ -38,15 +63,10 @@ interface Suggestion {
|
|||||||
uuid: string
|
uuid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Effects to tell StateEffect what to do with GhostText
|
|
||||||
const addSuggestion = StateEffect.define<Suggestion>()
|
|
||||||
const acceptSuggestion = StateEffect.define<null>()
|
|
||||||
const clearSuggestion = StateEffect.define<null>()
|
|
||||||
const typeFirst = StateEffect.define<number>()
|
|
||||||
|
|
||||||
interface CompletionState {
|
interface CompletionState {
|
||||||
ghostText: GhostText | null
|
ghostText: GhostText | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GhostText {
|
interface GhostText {
|
||||||
text: string
|
text: string
|
||||||
displayText: string
|
displayText: string
|
||||||
@ -60,11 +80,19 @@ interface GhostText {
|
|||||||
uuid: string
|
uuid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const completionDecoration = StateField.define<CompletionState>({
|
const completionDecoration = StateField.define<CompletionState>({
|
||||||
create(_state: EditorState) {
|
create(_state: EditorState) {
|
||||||
return { ghostText: null }
|
return { ghostText: null }
|
||||||
},
|
},
|
||||||
update(state: CompletionState, transaction: Transaction) {
|
update(state: CompletionState, transaction: Transaction) {
|
||||||
|
// We only care about events from this plugin.
|
||||||
|
if (
|
||||||
|
transaction.annotation(copilotPluginEvent.type) === undefined &&
|
||||||
|
transaction.annotation(rejectSuggestionCommand.type) === undefined
|
||||||
|
) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
for (const effect of transaction.effects) {
|
for (const effect of transaction.effects) {
|
||||||
if (effect.is(addSuggestion)) {
|
if (effect.is(addSuggestion)) {
|
||||||
// When adding a suggestion, we set th ghostText
|
// When adding a suggestion, we set th ghostText
|
||||||
@ -160,331 +188,448 @@ export const completionDecoration = StateField.define<CompletionState>({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const copilotEvent = Annotation.define<null>()
|
// A view plugin that requests completions from the server after a delay
|
||||||
|
export class CompletionRequester implements PluginValue {
|
||||||
|
private client: LanguageServerClient
|
||||||
|
private lastPos: number = 0
|
||||||
|
|
||||||
/****************************************************************************
|
private queuedUids: string[] = []
|
||||||
************************* COMMANDS ******************************************
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
const acceptSuggestionCommand = (
|
private _deffererCodeUpdate = deferExecution(() => {
|
||||||
copilotClient: LanguageServerClient,
|
this.requestCompletions()
|
||||||
view: EditorView
|
}, changesDelay)
|
||||||
) => {
|
|
||||||
// We delete the ghost text and insert the suggestion.
|
private _deffererUserSelect = deferExecution(() => {
|
||||||
// We also set the cursor to the end of the suggestion.
|
this.rejectSuggestionCommand()
|
||||||
const ghostText = view.state.field(completionDecoration)!.ghostText
|
}, changesDelay)
|
||||||
if (!ghostText) {
|
|
||||||
return false
|
constructor(readonly view: EditorView, client: LanguageServerClient) {
|
||||||
|
this.client = client
|
||||||
}
|
}
|
||||||
|
|
||||||
const ghostTextStart = ghostText.displayPos
|
update(viewUpdate: ViewUpdate) {
|
||||||
const ghostTextEnd = ghostText.endGhostText
|
// Make sure we are in a state where we can request completions.
|
||||||
|
if (!editorManager.copilotEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const actualTextStart = ghostText.startPos
|
let isUserSelect = false
|
||||||
const actualTextEnd = ghostText.endPos
|
let isRelevant = false
|
||||||
|
for (const tr of viewUpdate.transactions) {
|
||||||
|
if (tr.isUserEvent('select')) {
|
||||||
|
isUserSelect = true
|
||||||
|
break
|
||||||
|
} else if (tr.isUserEvent('input')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('delete')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('undo')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('redo')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('move')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.annotation(copilotPluginEvent.type) !== undefined) {
|
||||||
|
isRelevant = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const replacementEnd = ghostText.endReplacement
|
// If we have a user select event, we want to clear the ghost text.
|
||||||
|
if (isUserSelect) {
|
||||||
|
this._deffererUserSelect(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const suggestion = ghostText.text
|
if (!isRelevant) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
view.dispatch({
|
this.lastPos = this.view.state.selection.main.head
|
||||||
changes: {
|
if (viewUpdate.docChanged) this._deffererCodeUpdate(true)
|
||||||
from: ghostTextStart,
|
|
||||||
to: ghostTextEnd,
|
|
||||||
insert: '',
|
|
||||||
},
|
|
||||||
// selection: {anchor: actualTextEnd},
|
|
||||||
effects: acceptSuggestion.of(null),
|
|
||||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
|
||||||
})
|
|
||||||
|
|
||||||
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
|
|
||||||
|
|
||||||
view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from: actualTextStart,
|
|
||||||
to: tmpTextEnd,
|
|
||||||
insert: suggestion,
|
|
||||||
},
|
|
||||||
selection: { anchor: actualTextEnd },
|
|
||||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)],
|
|
||||||
})
|
|
||||||
|
|
||||||
copilotClient.accept(ghostText.uuid)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
export const rejectSuggestionCommand = (
|
|
||||||
copilotClient: LanguageServerClient,
|
|
||||||
view: EditorView
|
|
||||||
) => {
|
|
||||||
// We delete the suggestion, then carry through with the original keypress
|
|
||||||
const ghostText = view.state.field(completionDecoration)!.ghostText
|
|
||||||
if (!ghostText) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ghostTextStart = ghostText.displayPos
|
ghostText(): GhostText | null {
|
||||||
const ghostTextEnd = ghostText.endGhostText
|
return this.view.state.field(completionDecoration)?.ghostText || null
|
||||||
|
|
||||||
view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from: ghostTextStart,
|
|
||||||
to: ghostTextEnd,
|
|
||||||
insert: '',
|
|
||||||
},
|
|
||||||
effects: clearSuggestion.of(null),
|
|
||||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
|
||||||
})
|
|
||||||
|
|
||||||
copilotClient.reject()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const sameKeyCommand = (
|
|
||||||
copilotClient: LanguageServerClient,
|
|
||||||
view: EditorView,
|
|
||||||
key: string
|
|
||||||
) => {
|
|
||||||
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
|
|
||||||
const ghostText = view.state.field(completionDecoration)!.ghostText
|
|
||||||
if (!ghostText) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
const ghostTextStart = ghostText.displayPos
|
|
||||||
const indent = view.state.facet(indentUnit)
|
|
||||||
|
|
||||||
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) {
|
containsGhostText(): boolean {
|
||||||
view.dispatch({
|
return this.ghostText() !== null
|
||||||
selection: { anchor: ghostTextStart + indent.length },
|
}
|
||||||
effects: typeFirst.of(indent.length),
|
|
||||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
autocompleting(): boolean {
|
||||||
})
|
return completionStatus(this.view.state) === 'active'
|
||||||
return true
|
}
|
||||||
} else if (key === 'Tab') {
|
|
||||||
return acceptSuggestionCommand(copilotClient, view)
|
notFocused(): boolean {
|
||||||
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
|
return !this.view.hasFocus
|
||||||
return rejectSuggestionCommand(copilotClient, view)
|
}
|
||||||
} else if (ghostText.displayText.length === 1) {
|
|
||||||
return acceptSuggestionCommand(copilotClient, view)
|
async requestCompletions(): Promise<void> {
|
||||||
} else {
|
if (
|
||||||
// Use this to delete the first letter of the suggestion
|
this.containsGhostText() ||
|
||||||
view.dispatch({
|
this.autocompleting() ||
|
||||||
selection: { anchor: ghostTextStart + 1 },
|
this.notFocused()
|
||||||
effects: typeFirst.of(1),
|
) {
|
||||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.view.state.selection.main.head
|
||||||
|
|
||||||
|
// Check if the position has changed
|
||||||
|
if (pos !== this.lastPos) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current position and source
|
||||||
|
const state = this.view.state
|
||||||
|
const dUri = state.facet(docPathFacet)
|
||||||
|
|
||||||
|
// Request completion from the server
|
||||||
|
const completionResult = await this.getCompletion({
|
||||||
|
doc: {
|
||||||
|
source: state.doc.toString(),
|
||||||
|
tabSize: state.facet(EditorState.tabSize),
|
||||||
|
indentSize: 1,
|
||||||
|
insertSpaces: true,
|
||||||
|
path: dUri.split('/').pop()!,
|
||||||
|
uri: dUri,
|
||||||
|
relativePath: dUri.replace('file://', ''),
|
||||||
|
languageId: state.facet(languageId),
|
||||||
|
position: offsetToPos(state.doc, pos),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (completionResult.completions.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
text,
|
||||||
|
displayText,
|
||||||
|
range: { start },
|
||||||
|
position,
|
||||||
|
uuid,
|
||||||
|
} = completionResult.completions[0]
|
||||||
|
|
||||||
|
if (text.length === 0 || displayText.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPos = posToOffset(state.doc, {
|
||||||
|
line: start.line,
|
||||||
|
character: start.character,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (startPos === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const endGhostOffset = posToOffset(state.doc, {
|
||||||
|
line: position.line,
|
||||||
|
character: position.character,
|
||||||
|
})
|
||||||
|
if (endGhostOffset === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const endGhostPos = endGhostOffset + displayText.length
|
||||||
|
// EndPos is the position that marks the complete end
|
||||||
|
// of what is to be replaced when we accept a completion
|
||||||
|
// result
|
||||||
|
const endPos = startPos + text.length
|
||||||
|
|
||||||
|
// Check if they changed position.
|
||||||
|
if (pos !== this.lastPos) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we are not currently completing.
|
||||||
|
if (this.autocompleting() || this.notFocused()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch an effect to add the suggestion
|
||||||
|
// If the completion starts before the end of the line, check the end of the line with the end of the completion.
|
||||||
|
const line = this.view.state.doc.lineAt(pos)
|
||||||
|
if (line.to !== pos) {
|
||||||
|
const ending = this.view.state.doc.sliceString(pos, line.to)
|
||||||
|
if (displayText.endsWith(ending)) {
|
||||||
|
displayText = displayText.slice(0, displayText.length - ending.length)
|
||||||
|
} else if (displayText.includes(ending)) {
|
||||||
|
// Remove the ending
|
||||||
|
this.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: pos,
|
||||||
|
to: line.to,
|
||||||
|
insert: '',
|
||||||
|
},
|
||||||
|
selection: { anchor: pos },
|
||||||
|
effects: typeFirst.of(ending.length),
|
||||||
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: pos,
|
||||||
|
to: pos,
|
||||||
|
insert: displayText,
|
||||||
|
},
|
||||||
|
effects: [
|
||||||
|
addSuggestion.of({
|
||||||
|
displayText,
|
||||||
|
endReplacement: endGhostPos,
|
||||||
|
text,
|
||||||
|
cursorPos: pos,
|
||||||
|
startPos,
|
||||||
|
endPos,
|
||||||
|
uuid,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
|
||||||
|
this.lastPos = pos
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptSuggestionCommand(): boolean {
|
||||||
|
const ghostText = this.ghostText()
|
||||||
|
if (!ghostText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We delete the ghost text and insert the suggestion.
|
||||||
|
// We also set the cursor to the end of the suggestion.
|
||||||
|
const ghostTextStart = ghostText.displayPos
|
||||||
|
const ghostTextEnd = ghostText.endGhostText
|
||||||
|
|
||||||
|
const actualTextStart = ghostText.startPos
|
||||||
|
const actualTextEnd = ghostText.endPos
|
||||||
|
|
||||||
|
const replacementEnd = ghostText.endReplacement
|
||||||
|
|
||||||
|
const suggestion = ghostText.text
|
||||||
|
|
||||||
|
this.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: ghostTextStart,
|
||||||
|
to: ghostTextEnd,
|
||||||
|
insert: '',
|
||||||
|
},
|
||||||
|
effects: acceptSuggestion.of(null),
|
||||||
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
|
||||||
|
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
|
||||||
|
|
||||||
|
this.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: actualTextStart,
|
||||||
|
to: tmpTextEnd,
|
||||||
|
insert: suggestion,
|
||||||
|
},
|
||||||
|
selection: { anchor: actualTextEnd },
|
||||||
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
|
||||||
|
})
|
||||||
|
|
||||||
|
this.accept(ghostText.uuid)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rejectSuggestionCommand(): boolean {
|
||||||
|
const ghostText = this.ghostText()
|
||||||
|
if (!ghostText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We delete the suggestion, then carry through with the original keypress
|
||||||
|
const ghostTextStart = ghostText.displayPos
|
||||||
|
const ghostTextEnd = ghostText.endGhostText
|
||||||
|
|
||||||
|
this.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: ghostTextStart,
|
||||||
|
to: ghostTextEnd,
|
||||||
|
insert: '',
|
||||||
|
},
|
||||||
|
effects: clearSuggestion.of(null),
|
||||||
|
annotations: [
|
||||||
|
rejectSuggestionCommand,
|
||||||
|
Transaction.addToHistory.of(false),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
this.reject()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sameKeyCommand(key: string) {
|
||||||
|
const ghostText = this.ghostText()
|
||||||
|
if (!ghostText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabKey = 'Tab'
|
||||||
|
|
||||||
|
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
|
||||||
|
const ghostTextStart = ghostText.displayPos
|
||||||
|
const indent = this.view.state.facet(indentUnit)
|
||||||
|
|
||||||
|
if (key === tabKey && ghostText.displayText.startsWith(indent)) {
|
||||||
|
this.view.dispatch({
|
||||||
|
selection: { anchor: ghostTextStart + indent.length },
|
||||||
|
effects: typeFirst.of(indent.length),
|
||||||
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} else if (key === tabKey) {
|
||||||
|
return this.acceptSuggestionCommand()
|
||||||
|
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
|
||||||
|
return this.rejectSuggestionCommand()
|
||||||
|
} else if (ghostText.displayText.length === 1) {
|
||||||
|
return this.acceptSuggestionCommand()
|
||||||
|
} else {
|
||||||
|
// Use this to delete the first letter of the suggestion
|
||||||
|
this.view.dispatch({
|
||||||
|
selection: { anchor: ghostTextStart + 1 },
|
||||||
|
effects: typeFirst.of(1),
|
||||||
|
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCompletion(
|
||||||
|
params: CopilotLspCompletionParams
|
||||||
|
): Promise<CopilotCompletionResponse> {
|
||||||
|
const response: CopilotCompletionResponse = await this.client.requestCustom(
|
||||||
|
'copilot/getCompletions',
|
||||||
|
params
|
||||||
|
)
|
||||||
|
//
|
||||||
|
this.queuedUids = [...response.completions.map((c) => c.uuid)]
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async accept(uuid: string) {
|
||||||
|
const badUids = this.queuedUids.filter((u) => u !== uuid)
|
||||||
|
this.queuedUids = []
|
||||||
|
this.acceptCompletion({ uuid })
|
||||||
|
this.rejectCompletions({ uuids: badUids })
|
||||||
|
}
|
||||||
|
|
||||||
|
async reject() {
|
||||||
|
const badUids = this.queuedUids
|
||||||
|
this.queuedUids = []
|
||||||
|
this.rejectCompletions({ uuids: badUids })
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptCompletion(params: CopilotAcceptCompletionParams) {
|
||||||
|
this.client.notifyCustom('copilot/notifyAccepted', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectCompletions(params: CopilotRejectCompletionParams) {
|
||||||
|
this.client.notifyCustom('copilot/notifyRejected', params)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const completionPlugin = (copilotClient: LanguageServerClient) =>
|
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||||
EditorView.domEventHandlers({
|
let plugin: CompletionRequester | null = null
|
||||||
|
const completionPlugin = ViewPlugin.define(
|
||||||
|
(view) => (plugin = new CompletionRequester(view, options.client))
|
||||||
|
)
|
||||||
|
|
||||||
|
const domHandlers = EditorView.domEventHandlers({
|
||||||
keydown(event, view) {
|
keydown(event, view) {
|
||||||
if (
|
if (
|
||||||
event.key !== 'Shift' &&
|
event.key !== 'Shift' &&
|
||||||
event.key !== 'Control' &&
|
event.key !== 'Control' &&
|
||||||
event.key !== 'Alt' &&
|
event.key !== 'Alt' &&
|
||||||
|
event.key !== 'Backspace' &&
|
||||||
|
event.key !== 'Delete' &&
|
||||||
event.key !== 'Meta'
|
event.key !== 'Meta'
|
||||||
) {
|
) {
|
||||||
return sameKeyCommand(copilotClient, view, event.key)
|
if (view.plugin === null) return false
|
||||||
|
|
||||||
|
// Get the current plugin from the map.
|
||||||
|
const p = view.plugin(completionPlugin)
|
||||||
|
if (p === null) return false
|
||||||
|
|
||||||
|
return p.sameKeyCommand(event.key)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mousedown(event, view) {
|
})
|
||||||
return rejectSuggestionCommand(copilotClient, view)
|
|
||||||
|
const rejectSuggestionCommand = (view: EditorView): boolean => {
|
||||||
|
// Get the current plugin from the map.
|
||||||
|
const p = view.plugin(completionPlugin)
|
||||||
|
if (p === null) return false
|
||||||
|
|
||||||
|
return p.rejectSuggestionCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
const copilotAutocompleteKeymap: readonly KeyBinding[] = [
|
||||||
|
{
|
||||||
|
key: 'Tab',
|
||||||
|
run: (view: EditorView): boolean => {
|
||||||
|
if (view.plugin === null) return false
|
||||||
|
|
||||||
|
// Get the current plugin from the map.
|
||||||
|
const p = view.plugin(completionPlugin)
|
||||||
|
if (p === null) return false
|
||||||
|
|
||||||
|
return p.sameKeyCommand('Tab')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
{
|
||||||
|
key: 'Backspace',
|
||||||
|
run: rejectSuggestionCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Delete',
|
||||||
|
run: rejectSuggestionCommand,
|
||||||
|
},
|
||||||
|
{ key: 'Mod-z', run: rejectSuggestionCommand, preventDefault: true },
|
||||||
|
{
|
||||||
|
key: 'Mod-y',
|
||||||
|
mac: 'Mod-Shift-z',
|
||||||
|
run: rejectSuggestionCommand,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
linux: 'Ctrl-Shift-z',
|
||||||
|
run: rejectSuggestionCommand,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
{ key: 'Mod-u', run: rejectSuggestionCommand, preventDefault: true },
|
||||||
|
{
|
||||||
|
key: 'Alt-u',
|
||||||
|
mac: 'Mod-Shift-u',
|
||||||
|
run: rejectSuggestionCommand,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const viewCompletionPlugin = (copilotClient: LanguageServerClient) =>
|
const copilotAutocompleteKeymapExt = Prec.highest(
|
||||||
EditorView.updateListener.of((update) => {
|
keymap.computeN([], () => [copilotAutocompleteKeymap])
|
||||||
if (update.focusChanged) {
|
)
|
||||||
rejectSuggestionCommand(copilotClient, update.view)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// A view plugin that requests completions from the server after a delay
|
|
||||||
const completionRequester = (client: LanguageServerClient) => {
|
|
||||||
let timeout: any = null
|
|
||||||
let lastPos = 0
|
|
||||||
|
|
||||||
const badUpdate = (update: ViewUpdate) => {
|
|
||||||
for (const tr of update.transactions) {
|
|
||||||
if (tr.annotation(copilotEvent) !== undefined) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const containsGhostText = (update: ViewUpdate) => {
|
|
||||||
return update.state.field(completionDecoration).ghostText != null
|
|
||||||
}
|
|
||||||
const autocompleting = (update: ViewUpdate) => {
|
|
||||||
return completionStatus(update.state) === 'active'
|
|
||||||
}
|
|
||||||
const notFocused = (update: ViewUpdate) => {
|
|
||||||
return !update.view.hasFocus
|
|
||||||
}
|
|
||||||
|
|
||||||
return EditorView.updateListener.of((update: ViewUpdate) => {
|
|
||||||
if (
|
|
||||||
update.docChanged &&
|
|
||||||
!update.transactions.some((tr) =>
|
|
||||||
tr.effects.some((e) => e.is(acceptSuggestion) || e.is(clearSuggestion))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Cancel the previous timeout
|
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
badUpdate(update) ||
|
|
||||||
containsGhostText(update) ||
|
|
||||||
autocompleting(update) ||
|
|
||||||
notFocused(update)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current position and source
|
|
||||||
const state = update.state
|
|
||||||
const pos = state.selection.main.head
|
|
||||||
const source = state.doc.toString()
|
|
||||||
|
|
||||||
const dUri = state.facet(documentUri)
|
|
||||||
const path = dUri.split('/').pop()!
|
|
||||||
const relativePath = dUri.replace('file://', '')
|
|
||||||
|
|
||||||
// Set a new timeout to request completion
|
|
||||||
timeout = setTimeout(async () => {
|
|
||||||
// Check if the position has changed
|
|
||||||
if (pos === lastPos) {
|
|
||||||
// Request completion from the server
|
|
||||||
try {
|
|
||||||
const completionResult = await client.getCompletion({
|
|
||||||
doc: {
|
|
||||||
source,
|
|
||||||
tabSize: state.facet(EditorState.tabSize),
|
|
||||||
indentSize: 1,
|
|
||||||
insertSpaces: true,
|
|
||||||
path,
|
|
||||||
uri: dUri,
|
|
||||||
relativePath,
|
|
||||||
languageId: state.facet(languageId),
|
|
||||||
position: offsetToPos(state.doc, pos),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (completionResult.completions.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
text,
|
|
||||||
displayText,
|
|
||||||
range: { start },
|
|
||||||
position,
|
|
||||||
uuid,
|
|
||||||
} = completionResult.completions[0]
|
|
||||||
|
|
||||||
const startPos = posToOffset(state.doc, {
|
|
||||||
line: start.line,
|
|
||||||
character: start.character,
|
|
||||||
})!
|
|
||||||
|
|
||||||
const endGhostPos =
|
|
||||||
posToOffset(state.doc, {
|
|
||||||
line: position.line,
|
|
||||||
character: position.character,
|
|
||||||
})! + displayText.length
|
|
||||||
// EndPos is the position that marks the complete end
|
|
||||||
// of what is to be replaced when we accept a completion
|
|
||||||
// result
|
|
||||||
const endPos = startPos + text.length
|
|
||||||
|
|
||||||
// Check if the position is still the same
|
|
||||||
if (
|
|
||||||
pos === lastPos &&
|
|
||||||
completionStatus(update.view.state) !== 'active' &&
|
|
||||||
update.view.hasFocus
|
|
||||||
) {
|
|
||||||
// Dispatch an effect to add the suggestion
|
|
||||||
// If the completion starts before the end of the line, check the end of the line with the end of the completion
|
|
||||||
const line = update.view.state.doc.lineAt(pos)
|
|
||||||
if (line.to !== pos) {
|
|
||||||
const ending = update.view.state.doc.sliceString(pos, line.to)
|
|
||||||
if (displayText.endsWith(ending)) {
|
|
||||||
displayText = displayText.slice(
|
|
||||||
0,
|
|
||||||
displayText.length - ending.length
|
|
||||||
)
|
|
||||||
} else if (displayText.includes(ending)) {
|
|
||||||
// Remove the ending
|
|
||||||
update.view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from: pos,
|
|
||||||
to: line.to,
|
|
||||||
insert: '',
|
|
||||||
},
|
|
||||||
selection: { anchor: pos },
|
|
||||||
effects: typeFirst.of(ending.length),
|
|
||||||
annotations: [
|
|
||||||
copilotEvent.of(null),
|
|
||||||
Transaction.addToHistory.of(false),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update.view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from: pos,
|
|
||||||
to: pos,
|
|
||||||
insert: displayText,
|
|
||||||
},
|
|
||||||
effects: [
|
|
||||||
addSuggestion.of({
|
|
||||||
displayText,
|
|
||||||
endReplacement: endGhostPos,
|
|
||||||
text,
|
|
||||||
cursorPos: pos,
|
|
||||||
startPos,
|
|
||||||
endPos,
|
|
||||||
uuid,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
annotations: [
|
|
||||||
copilotEvent.of(null),
|
|
||||||
Transaction.addToHistory.of(false),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('copilot completion failed', error)
|
|
||||||
// Javascript wait for 500ms for some reason is necessary here.
|
|
||||||
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 150)
|
|
||||||
// Update the last position
|
|
||||||
lastPos = pos
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
|
||||||
return [
|
return [
|
||||||
documentUri.of(options.documentUri),
|
completionPlugin,
|
||||||
languageId.of('kcl'),
|
copilotAutocompleteKeymapExt,
|
||||||
workspaceFolders.of(options.workspaceFolders),
|
domHandlers,
|
||||||
ViewPlugin.define(
|
|
||||||
(view) =>
|
|
||||||
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
|
|
||||||
),
|
|
||||||
completionDecoration,
|
completionDecoration,
|
||||||
Prec.highest(completionPlugin(options.client)),
|
EditorView.focusChangeEffect.of((_, focusing) => {
|
||||||
Prec.highest(viewCompletionPlugin(options.client)),
|
if (plugin === null) return null
|
||||||
completionRequester(options.client),
|
|
||||||
|
plugin.rejectSuggestionCommand()
|
||||||
|
|
||||||
|
return null
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,155 +1,108 @@
|
|||||||
import { autocompletion } from '@codemirror/autocomplete'
|
import { Extension } from '@codemirror/state'
|
||||||
import { Extension, EditorState, Prec } from '@codemirror/state'
|
import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view'
|
||||||
import {
|
import {
|
||||||
ViewPlugin,
|
LanguageServerOptions,
|
||||||
hoverTooltip,
|
LanguageServerClient,
|
||||||
EditorView,
|
lspPlugin,
|
||||||
keymap,
|
lspFormatCodeEvent,
|
||||||
KeyBinding,
|
} from '@kittycad/codemirror-lsp-client'
|
||||||
tooltips,
|
import { deferExecution } from 'lib/utils'
|
||||||
} from '@codemirror/view'
|
import { codeManager, editorManager, kclManager } from 'lib/singletons'
|
||||||
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
import { UpdateUnitsParams } from 'wasm-lib/kcl/bindings/UpdateUnitsParams'
|
||||||
import { offsetToPos } from 'editor/plugins/lsp/util'
|
import { UpdateCanExecuteParams } from 'wasm-lib/kcl/bindings/UpdateCanExecuteParams'
|
||||||
import { LanguageServerOptions } from 'editor/plugins/lsp'
|
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
||||||
import { syntaxTree, indentService, foldService } from '@codemirror/language'
|
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
||||||
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
|
|
||||||
import {
|
|
||||||
LanguageServerPlugin,
|
|
||||||
documentUri,
|
|
||||||
languageId,
|
|
||||||
workspaceFolders,
|
|
||||||
} from 'editor/plugins/lsp/plugin'
|
|
||||||
|
|
||||||
export const kclIndentService = () => {
|
const changesDelay = 600
|
||||||
// Match the indentation of the previous line (if present).
|
|
||||||
return indentService.of((context, pos) => {
|
// A view plugin that requests completions from the server after a delay
|
||||||
try {
|
export class KclPlugin implements PluginValue {
|
||||||
const previousLine = context.lineAt(pos, -1)
|
private viewUpdate: ViewUpdate | null = null
|
||||||
const previousLineText = previousLine.text.replaceAll(
|
private client: LanguageServerClient
|
||||||
'\t',
|
|
||||||
' '.repeat(context.state.tabSize)
|
constructor(client: LanguageServerClient) {
|
||||||
)
|
this.client = client
|
||||||
const match = previousLineText.match(/^(\s)*/)
|
}
|
||||||
if (match === null || match.length <= 0) return null
|
|
||||||
return match[0].length
|
private _deffererCodeUpdate = deferExecution(() => {
|
||||||
} catch (err) {
|
if (this.viewUpdate === null) {
|
||||||
console.error('Error in codemirror indentService', err)
|
return
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
})
|
kclManager.executeCode()
|
||||||
|
}, changesDelay)
|
||||||
|
|
||||||
|
private _deffererUserSelect = deferExecution(() => {
|
||||||
|
if (this.viewUpdate === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editorManager.handleOnViewUpdate(this.viewUpdate)
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
update(viewUpdate: ViewUpdate) {
|
||||||
|
this.viewUpdate = viewUpdate
|
||||||
|
editorManager.setEditorView(viewUpdate.view)
|
||||||
|
|
||||||
|
let isUserSelect = false
|
||||||
|
let isRelevant = false
|
||||||
|
for (const tr of viewUpdate.transactions) {
|
||||||
|
if (tr.isUserEvent('select')) {
|
||||||
|
isUserSelect = true
|
||||||
|
break
|
||||||
|
} else if (tr.isUserEvent('input')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('delete')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('undo')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('redo')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.isUserEvent('move')) {
|
||||||
|
isRelevant = true
|
||||||
|
} else if (tr.annotation(lspFormatCodeEvent.type)) {
|
||||||
|
isRelevant = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a user select event, we want to update what parts are
|
||||||
|
// highlighted.
|
||||||
|
if (isUserSelect) {
|
||||||
|
this._deffererUserSelect(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRelevant) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewUpdate.docChanged) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCode = viewUpdate.state.doc.toString()
|
||||||
|
codeManager.code = newCode
|
||||||
|
codeManager.writeToFile()
|
||||||
|
|
||||||
|
this._deffererCodeUpdate(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUnits(
|
||||||
|
params: UpdateUnitsParams
|
||||||
|
): Promise<UpdateUnitsResponse | null> {
|
||||||
|
return this.client.requestCustom('kcl/updateUnits', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCanExecute(
|
||||||
|
params: UpdateCanExecuteParams
|
||||||
|
): Promise<UpdateCanExecuteResponse> {
|
||||||
|
return this.client.requestCustom('kcl/updateCanExecute', params)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function kclPlugin(options: LanguageServerOptions): Extension {
|
export function kclPlugin(options: LanguageServerOptions): Extension {
|
||||||
let plugin: LanguageServerPlugin | null = null
|
|
||||||
const viewPlugin = ViewPlugin.define(
|
|
||||||
(view) =>
|
|
||||||
(plugin = new LanguageServerPlugin(
|
|
||||||
options.client,
|
|
||||||
view,
|
|
||||||
options.allowHTMLContent
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
const kclKeymap: readonly KeyBinding[] = [
|
|
||||||
{
|
|
||||||
key: 'Alt-Shift-f',
|
|
||||||
run: (view: EditorView) => {
|
|
||||||
if (view.plugin === null) return false
|
|
||||||
|
|
||||||
// Get the current plugin from the map.
|
|
||||||
const p = view.plugin(viewPlugin)
|
|
||||||
|
|
||||||
if (p === null) return false
|
|
||||||
p.requestFormatting()
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
// Create an extension for the key mappings.
|
|
||||||
const kclKeymapExt = Prec.highest(keymap.computeN([], () => [kclKeymap]))
|
|
||||||
|
|
||||||
const folding = foldService.of(
|
|
||||||
(state: EditorState, lineStart: number, lineEnd: number) => {
|
|
||||||
if (plugin == null) return null
|
|
||||||
|
|
||||||
// Get the folding ranges from the language server.
|
|
||||||
// Since this is async we directly need to update the folding ranges after.
|
|
||||||
return plugin?.foldingRange(lineStart, lineEnd)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
documentUri.of(options.documentUri),
|
lspPlugin(options),
|
||||||
languageId.of('kcl'),
|
ViewPlugin.define(() => new KclPlugin(options.client)),
|
||||||
workspaceFolders.of(options.workspaceFolders),
|
|
||||||
viewPlugin,
|
|
||||||
kclKeymapExt,
|
|
||||||
kclIndentService(),
|
|
||||||
hoverTooltip(
|
|
||||||
(view, pos) =>
|
|
||||||
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
|
|
||||||
null
|
|
||||||
),
|
|
||||||
tooltips({
|
|
||||||
position: 'absolute',
|
|
||||||
}),
|
|
||||||
linter((view) => {
|
|
||||||
let diagnostics: Diagnostic[] = []
|
|
||||||
forEachDiagnostic(
|
|
||||||
view.state,
|
|
||||||
(d: Diagnostic, from: number, to: number) => {
|
|
||||||
diagnostics.push(d)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return diagnostics
|
|
||||||
}),
|
|
||||||
folding,
|
|
||||||
autocompletion({
|
|
||||||
defaultKeymap: true,
|
|
||||||
override: [
|
|
||||||
async (context) => {
|
|
||||||
if (plugin == null) return null
|
|
||||||
|
|
||||||
const { state, pos, explicit } = context
|
|
||||||
|
|
||||||
let nodeBefore = syntaxTree(state).resolveInner(pos, -1)
|
|
||||||
if (
|
|
||||||
nodeBefore.name === 'BlockComment' ||
|
|
||||||
nodeBefore.name === 'LineComment'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -5,18 +5,33 @@ import {
|
|||||||
defineLanguageFacet,
|
defineLanguageFacet,
|
||||||
LanguageSupport,
|
LanguageSupport,
|
||||||
} from '@codemirror/language'
|
} from '@codemirror/language'
|
||||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
import {
|
||||||
|
LanguageServerClient,
|
||||||
|
LanguageServerPlugin,
|
||||||
|
} from '@kittycad/codemirror-lsp-client'
|
||||||
import { kclPlugin } from '.'
|
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 KclParser from './parser'
|
||||||
import { EditorState } from '@uiw/react-codemirror'
|
|
||||||
|
|
||||||
const data = defineLanguageFacet({})
|
const data = defineLanguageFacet({
|
||||||
|
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
||||||
|
commentTokens: {
|
||||||
|
line: '//',
|
||||||
|
block: {
|
||||||
|
open: '/*',
|
||||||
|
close: '*/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export interface LanguageOptions {
|
export interface LanguageOptions {
|
||||||
workspaceFolders: LSP.WorkspaceFolder[]
|
workspaceFolders: LSP.WorkspaceFolder[]
|
||||||
documentUri: string
|
documentUri: string
|
||||||
client: LanguageServerClient
|
client: LanguageServerClient
|
||||||
|
processLspNotification?: (
|
||||||
|
plugin: LanguageServerPlugin,
|
||||||
|
notification: LSP.NotificationMessage
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
class KclLanguage extends Language {
|
class KclLanguage extends Language {
|
||||||
@ -26,36 +41,19 @@ class KclLanguage extends Language {
|
|||||||
workspaceFolders: options.workspaceFolders,
|
workspaceFolders: options.workspaceFolders,
|
||||||
allowHTMLContent: true,
|
allowHTMLContent: true,
|
||||||
client: options.client,
|
client: options.client,
|
||||||
|
processLspNotification: options.processLspNotification,
|
||||||
})
|
})
|
||||||
|
|
||||||
super(
|
const parser = new KclParser()
|
||||||
data,
|
|
||||||
// For now let's use the javascript parser.
|
super(data, parser, [plugin], 'kcl')
|
||||||
// It works really well and has good syntax highlighting.
|
|
||||||
// We can use our lsp for the rest.
|
|
||||||
jsParser,
|
|
||||||
[
|
|
||||||
plugin,
|
|
||||||
EditorState.languageData.of(() => [
|
|
||||||
{
|
|
||||||
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
|
||||||
commentTokens: {
|
|
||||||
line: '//',
|
|
||||||
block: {
|
|
||||||
open: '/*',
|
|
||||||
close: '*/',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
'kcl'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
export default class KclLanguageSupport extends LanguageSupport {
|
||||||
const lang = new KclLanguage(options)
|
constructor(options: LanguageOptions) {
|
||||||
|
const lang = new KclLanguage(options)
|
||||||
|
|
||||||
return new LanguageSupport(lang)
|
super(lang)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Extends the codemirror Parser for kcl.
|
// Extends the codemirror Parser for kcl.
|
||||||
|
// This is really just a no-op parser since we use semantic tokens from the LSP
|
||||||
|
// server.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Parser,
|
Parser,
|
||||||
@ -7,91 +9,27 @@ import {
|
|||||||
PartialParse,
|
PartialParse,
|
||||||
Tree,
|
Tree,
|
||||||
NodeType,
|
NodeType,
|
||||||
NodeSet,
|
|
||||||
} from '@lezer/common'
|
} from '@lezer/common'
|
||||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
|
||||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
|
||||||
import { SemanticToken } from './semantic_tokens'
|
|
||||||
import { DocInput } from '@codemirror/language'
|
import { DocInput } from '@codemirror/language'
|
||||||
import { tags, styleTags } from '@lezer/highlight'
|
|
||||||
|
|
||||||
export default class KclParser extends Parser {
|
export default class KclParser extends Parser {
|
||||||
private client: LanguageServerClient
|
|
||||||
|
|
||||||
constructor(client: LanguageServerClient) {
|
|
||||||
super()
|
|
||||||
this.client = client
|
|
||||||
}
|
|
||||||
|
|
||||||
createParse(
|
createParse(
|
||||||
input: Input,
|
input: Input,
|
||||||
fragments: readonly TreeFragment[],
|
fragments: readonly TreeFragment[],
|
||||||
ranges: readonly { from: number; to: number }[]
|
ranges: readonly { from: number; to: number }[]
|
||||||
): PartialParse {
|
): PartialParse {
|
||||||
let parse: PartialParse = new Context(this, input, fragments, ranges)
|
let parse: PartialParse = new Context(input)
|
||||||
return parse
|
return parse
|
||||||
}
|
}
|
||||||
|
|
||||||
getTokenTypes(): string[] {
|
|
||||||
return this.client.getServerCapabilities().semanticTokensProvider!.legend
|
|
||||||
.tokenTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
getSemanticTokens(): SemanticToken[] {
|
|
||||||
return this.client.getSemanticTokens()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Context implements PartialParse {
|
class Context implements PartialParse {
|
||||||
private parser: KclParser
|
|
||||||
private input: DocInput
|
private input: DocInput
|
||||||
private fragments: readonly TreeFragment[]
|
|
||||||
private ranges: readonly { from: number; to: number }[]
|
|
||||||
|
|
||||||
private nodeTypes: { [key: string]: NodeType }
|
|
||||||
stoppedAt: number = 0
|
stoppedAt: number = 0
|
||||||
|
|
||||||
private semanticTokens: SemanticToken[] = []
|
constructor(input: Input) {
|
||||||
private currentLine: number = 0
|
|
||||||
private currentColumn: number = 0
|
|
||||||
private nodeSet: NodeSet
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
/// The parser configuration used.
|
|
||||||
parser: KclParser,
|
|
||||||
input: Input,
|
|
||||||
fragments: readonly TreeFragment[],
|
|
||||||
ranges: readonly { from: number; to: number }[]
|
|
||||||
) {
|
|
||||||
this.parser = parser
|
|
||||||
this.input = input as DocInput
|
this.input = input as DocInput
|
||||||
this.fragments = fragments
|
|
||||||
this.ranges = ranges
|
|
||||||
|
|
||||||
// Iterate over the semantic token types and create a node type for each.
|
|
||||||
this.nodeTypes = {}
|
|
||||||
let nodeArray: NodeType[] = []
|
|
||||||
this.parser.getTokenTypes().forEach((tokenType, index) => {
|
|
||||||
const nodeType = NodeType.define({
|
|
||||||
id: index,
|
|
||||||
name: tokenType,
|
|
||||||
// props: [this.styleTags],
|
|
||||||
})
|
|
||||||
this.nodeTypes[tokenType] = nodeType
|
|
||||||
nodeArray.push(nodeType)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.semanticTokens = this.parser.getSemanticTokens()
|
|
||||||
const styles = styleTags({
|
|
||||||
number: tags.number,
|
|
||||||
variable: tags.variableName,
|
|
||||||
operator: tags.operator,
|
|
||||||
keyword: tags.keyword,
|
|
||||||
string: tags.string,
|
|
||||||
comment: tags.comment,
|
|
||||||
function: tags.function(tags.variableName),
|
|
||||||
})
|
|
||||||
this.nodeSet = new NodeSet(nodeArray).extend(styles)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get parsedPos(): number {
|
get parsedPos(): number {
|
||||||
@ -99,67 +37,8 @@ class Context implements PartialParse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
advance(): Tree | null {
|
advance(): Tree | null {
|
||||||
if (this.semanticTokens.length === 0) {
|
|
||||||
return new Tree(NodeType.none, [], [], 0)
|
|
||||||
}
|
|
||||||
const tree = this.createTree(this.semanticTokens[0], 0)
|
|
||||||
this.stoppedAt = this.input.doc.length
|
this.stoppedAt = this.input.doc.length
|
||||||
return tree
|
return new Tree(NodeType.none, [], [], this.input.doc.length)
|
||||||
}
|
|
||||||
|
|
||||||
createTree(token: SemanticToken, index: number): Tree {
|
|
||||||
const changedLine = token.delta_line !== 0
|
|
||||||
this.currentLine += token.delta_line
|
|
||||||
if (changedLine) {
|
|
||||||
this.currentColumn = 0
|
|
||||||
}
|
|
||||||
this.currentColumn += token.delta_start
|
|
||||||
|
|
||||||
// Let's get our position relative to the start of the file.
|
|
||||||
let currentPosition = posToOffset(this.input.doc, {
|
|
||||||
line: this.currentLine,
|
|
||||||
character: this.currentColumn,
|
|
||||||
})
|
|
||||||
|
|
||||||
const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id]
|
|
||||||
|
|
||||||
if (currentPosition === undefined) {
|
|
||||||
// This is bad and weird.
|
|
||||||
return new Tree(nodeType, [], [], token.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= this.semanticTokens.length - 1) {
|
|
||||||
// We have no children.
|
|
||||||
return new Tree(nodeType, [], [], token.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextIndex = index + 1
|
|
||||||
const nextToken = this.semanticTokens[nextIndex]
|
|
||||||
const changedLineNext = nextToken.delta_line !== 0
|
|
||||||
const nextLine = this.currentLine + nextToken.delta_line
|
|
||||||
const nextColumn = changedLineNext
|
|
||||||
? nextToken.delta_start
|
|
||||||
: this.currentColumn + nextToken.delta_start
|
|
||||||
const nextPosition = posToOffset(this.input.doc, {
|
|
||||||
line: nextLine,
|
|
||||||
character: nextColumn,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (nextPosition === undefined) {
|
|
||||||
// This is bad and weird.
|
|
||||||
return new Tree(nodeType, [], [], token.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's get the
|
|
||||||
|
|
||||||
return new Tree(
|
|
||||||
nodeType,
|
|
||||||
[this.createTree(nextToken, nextIndex)],
|
|
||||||
|
|
||||||
// The positions (offsets relative to the start of this tree) of the children.
|
|
||||||
[nextPosition - currentPosition],
|
|
||||||
token.length
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopAt(pos: number) {
|
stopAt(pos: number) {
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
import type * as LSP from 'vscode-languageserver-protocol'
|
|
||||||
|
|
||||||
export class SemanticToken {
|
|
||||||
delta_line: number
|
|
||||||
delta_start: number
|
|
||||||
length: number
|
|
||||||
token_type: string
|
|
||||||
token_modifiers_bitset: string
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
delta_line = 0,
|
|
||||||
delta_start = 0,
|
|
||||||
length = 0,
|
|
||||||
token_type = '',
|
|
||||||
token_modifiers_bitset = ''
|
|
||||||
) {
|
|
||||||
this.delta_line = delta_line
|
|
||||||
this.delta_start = delta_start
|
|
||||||
this.length = length
|
|
||||||
this.token_type = token_type
|
|
||||||
this.token_modifiers_bitset = token_modifiers_bitset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deserializeTokens(
|
|
||||||
data: number[],
|
|
||||||
semanticTokensProvider?: LSP.SemanticTokensOptions
|
|
||||||
): Promise<SemanticToken[]> {
|
|
||||||
if (!semanticTokensProvider) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
// Check if data length is divisible by 5
|
|
||||||
if (data.length % 5 !== 0) {
|
|
||||||
return Promise.reject(new Error('Length is not divisible by 5'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = []
|
|
||||||
for (let i = 0; i < data.length; i += 5) {
|
|
||||||
tokens.push(
|
|
||||||
new SemanticToken(
|
|
||||||
data[i],
|
|
||||||
data[i + 1],
|
|
||||||
data[i + 2],
|
|
||||||
semanticTokensProvider.legend.tokenTypes[data[i + 3]],
|
|
||||||
semanticTokensProvider.legend.tokenModifiers[data[i + 4]]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { Message } from 'vscode-languageserver-protocol'
|
|
||||||
|
|
||||||
const env = import.meta.env.MODE
|
|
||||||
|
|
||||||
export default class Tracer {
|
|
||||||
static client(message: string): void {
|
|
||||||
// These are really noisy, so we have a special env var for them.
|
|
||||||
if (env === 'lsp_tracing') {
|
|
||||||
console.log('lsp client message', message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static server(input: string | Message): void {
|
|
||||||
// These are really noisy, so we have a special env var for them.
|
|
||||||
if (env === 'lsp_tracing') {
|
|
||||||
const message: string =
|
|
||||||
typeof input === 'string' ? input : JSON.stringify(input)
|
|
||||||
console.log('lsp server message', message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
import { LspWorkerEventType } from '@kittycad/codemirror-lsp-client'
|
||||||
|
|
||||||
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
import { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
||||||
|
|
||||||
export enum LspWorker {
|
export enum LspWorker {
|
||||||
@ -17,11 +19,6 @@ export interface CopilotWorkerOptions {
|
|||||||
apiBaseUrl: string
|
apiBaseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LspWorkerEventType {
|
|
||||||
Init = 'init',
|
|
||||||
Call = 'call',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LspWorkerEvent {
|
export interface LspWorkerEvent {
|
||||||
eventType: LspWorkerEventType
|
eventType: LspWorkerEventType
|
||||||
eventData: Uint8Array | KclWorkerOptions | CopilotWorkerOptions
|
eventData: Uint8Array | KclWorkerOptions | CopilotWorkerOptions
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { Text } from '@codemirror/state'
|
|
||||||
|
|
||||||
export function posToOffset(
|
|
||||||
doc: Text,
|
|
||||||
pos: { line: number; character: number }
|
|
||||||
): number | undefined {
|
|
||||||
if (pos.line >= doc.lines) return
|
|
||||||
const offset = doc.line(pos.line + 1).from + pos.character
|
|
||||||
if (offset > doc.length) return
|
|
||||||
return offset
|
|
||||||
}
|
|
||||||
|
|
||||||
export function offsetToPos(doc: Text, offset: number) {
|
|
||||||
const line = doc.lineAt(offset)
|
|
||||||
return {
|
|
||||||
line: line.number - 1,
|
|
||||||
character: offset - line.from,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,9 @@
|
|||||||
import { Codec, FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
import {
|
||||||
|
Codec,
|
||||||
|
FromServer,
|
||||||
|
IntoServer,
|
||||||
|
LspWorkerEventType,
|
||||||
|
} from '@kittycad/codemirror-lsp-client'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import init, {
|
import init, {
|
||||||
ServerConfig,
|
ServerConfig,
|
||||||
@ -7,7 +12,6 @@ import init, {
|
|||||||
} from 'wasm-lib/pkg/wasm_lib'
|
} from 'wasm-lib/pkg/wasm_lib'
|
||||||
import * as jsrpc from 'json-rpc-2.0'
|
import * as jsrpc from 'json-rpc-2.0'
|
||||||
import {
|
import {
|
||||||
LspWorkerEventType,
|
|
||||||
LspWorkerEvent,
|
LspWorkerEvent,
|
||||||
LspWorker,
|
LspWorker,
|
||||||
KclWorkerOptions,
|
KclWorkerOptions,
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useLayoutEffect, useEffect, useRef } from 'react'
|
import { useLayoutEffect, useEffect, useRef } from 'react'
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { deferExecution } from 'lib/utils'
|
import { deferExecution } from 'lib/utils'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { makeDefaultPlanes } from 'lang/wasm'
|
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
||||||
|
import { useModelingContext } from './useModelingContext'
|
||||||
|
import { useAppState } from 'AppState'
|
||||||
|
|
||||||
export function useSetupEngineManager(
|
export function useSetupEngineManager(
|
||||||
streamRef: React.RefObject<HTMLDivElement>,
|
streamRef: React.RefObject<HTMLDivElement>,
|
||||||
@ -13,24 +14,20 @@ export function useSetupEngineManager(
|
|||||||
theme: Themes.System,
|
theme: Themes.System,
|
||||||
highlightEdges: true,
|
highlightEdges: true,
|
||||||
enableSSAO: true,
|
enableSSAO: true,
|
||||||
|
modelingSend: (() => {}) as any,
|
||||||
|
modelingContext: {} as any,
|
||||||
|
showScaleGrid: false,
|
||||||
} as {
|
} as {
|
||||||
pool: string | null
|
pool: string | null
|
||||||
theme: Themes
|
theme: Themes
|
||||||
highlightEdges: boolean
|
highlightEdges: boolean
|
||||||
enableSSAO: boolean
|
enableSSAO: boolean
|
||||||
|
modelingSend: ReturnType<typeof useModelingContext>['send']
|
||||||
|
modelingContext: ReturnType<typeof useModelingContext>['context']
|
||||||
|
showScaleGrid: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const {
|
const { setAppState } = useAppState()
|
||||||
setMediaStream,
|
|
||||||
setIsStreamReady,
|
|
||||||
setStreamDimensions,
|
|
||||||
streamDimensions,
|
|
||||||
} = useStore((s) => ({
|
|
||||||
setMediaStream: s.setMediaStream,
|
|
||||||
setIsStreamReady: s.setIsStreamReady,
|
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
|
||||||
streamDimensions: s.streamDimensions,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const streamWidth = streamRef?.current?.offsetWidth
|
const streamWidth = streamRef?.current?.offsetWidth
|
||||||
const streamHeight = streamRef?.current?.offsetHeight
|
const streamHeight = streamRef?.current?.offsetHeight
|
||||||
@ -50,26 +47,46 @@ export function useSetupEngineManager(
|
|||||||
streamWidth,
|
streamWidth,
|
||||||
streamHeight
|
streamHeight
|
||||||
)
|
)
|
||||||
if (!hasSetNonZeroDimensions.current && quadHeight && quadWidth) {
|
if (
|
||||||
|
!hasSetNonZeroDimensions.current &&
|
||||||
|
quadHeight &&
|
||||||
|
quadWidth &&
|
||||||
|
settings.modelingSend
|
||||||
|
) {
|
||||||
engineCommandManager.start({
|
engineCommandManager.start({
|
||||||
setMediaStream,
|
setMediaStream: (mediaStream) =>
|
||||||
setIsStreamReady,
|
settings.modelingSend({
|
||||||
|
type: 'Set context',
|
||||||
|
data: { mediaStream },
|
||||||
|
}),
|
||||||
|
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
|
||||||
width: quadWidth,
|
width: quadWidth,
|
||||||
height: quadHeight,
|
height: quadHeight,
|
||||||
executeCode: () => {
|
executeCode: () => {
|
||||||
// We only want to execute the code here that we already have set.
|
// We only want to execute the code here that we already have set.
|
||||||
// Nothing else.
|
// Nothing else.
|
||||||
return kclManager.executeCode(true, true)
|
kclManager.isFirstRender = true
|
||||||
|
return kclManager.executeCode(true, true).then(() => {
|
||||||
|
kclManager.isFirstRender = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
settings,
|
settings,
|
||||||
makeDefaultPlanes: () => {
|
makeDefaultPlanes: () => {
|
||||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||||
},
|
},
|
||||||
|
modifyGrid: (hidden: boolean) => {
|
||||||
|
return modifyGrid(kclManager.engineCommandManager, hidden)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
setStreamDimensions({
|
settings.modelingSend({
|
||||||
streamWidth: quadWidth,
|
type: 'Set context',
|
||||||
streamHeight: quadHeight,
|
data: {
|
||||||
|
streamDimensions: {
|
||||||
|
streamWidth: quadWidth,
|
||||||
|
streamHeight: quadHeight,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
hasSetNonZeroDimensions.current = true
|
hasSetNonZeroDimensions.current = true
|
||||||
}
|
}
|
||||||
@ -78,6 +95,7 @@ export function useSetupEngineManager(
|
|||||||
useLayoutEffect(startEngineInstance, [
|
useLayoutEffect(startEngineInstance, [
|
||||||
streamRef?.current?.offsetWidth,
|
streamRef?.current?.offsetWidth,
|
||||||
streamRef?.current?.offsetHeight,
|
streamRef?.current?.offsetHeight,
|
||||||
|
settings.modelingSend,
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -87,16 +105,21 @@ export function useSetupEngineManager(
|
|||||||
streamRef?.current?.offsetHeight
|
streamRef?.current?.offsetHeight
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
streamDimensions.streamWidth !== width ||
|
settings.modelingContext.store.streamDimensions.streamWidth !== width ||
|
||||||
streamDimensions.streamHeight !== height
|
settings.modelingContext.store.streamDimensions.streamHeight !== height
|
||||||
) {
|
) {
|
||||||
engineCommandManager.handleResize({
|
engineCommandManager.handleResize({
|
||||||
streamWidth: width,
|
streamWidth: width,
|
||||||
streamHeight: height,
|
streamHeight: height,
|
||||||
})
|
})
|
||||||
setStreamDimensions({
|
settings.modelingSend({
|
||||||
streamWidth: width,
|
type: 'Set context',
|
||||||
streamHeight: height,
|
data: {
|
||||||
|
streamDimensions: {
|
||||||
|
streamWidth: width,
|
||||||
|
streamHeight: height,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
|
@ -8,9 +8,9 @@ import { settingsMachine } from 'machines/settingsMachine'
|
|||||||
import { homeMachine } from 'machines/homeMachine'
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { useStore } from 'useStore'
|
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
import { useAppState } from 'AppState'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -47,9 +47,7 @@ export default function useStateMachineCommands<
|
|||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useStore((s) => ({
|
const { isStreamReady } = useAppState()
|
||||||
isStreamReady: s.isStreamReady,
|
|
||||||
}))
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disableAllButtons =
|
const disableAllButtons =
|
||||||
|
@ -260,3 +260,8 @@ code {
|
|||||||
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
|
@apply bg-chalkboard-20 dark:bg-chalkboard-90;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#code-mirror-override .cm-scroller,
|
||||||
|
#code-mirror-override .cm-editor {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { executeAst, lintAst } from 'useStore'
|
import { executeAst, lintAst } from 'lang/langHelpers'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
ProgramMemory,
|
ProgramMemory,
|
||||||
recast,
|
recast,
|
||||||
SketchGroup,
|
SketchGroup,
|
||||||
|
SourceRange,
|
||||||
ExtrudeGroup,
|
ExtrudeGroup,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import { getNodeFromPath } from './queryAst'
|
import { getNodeFromPath } from './queryAst'
|
||||||
@ -65,6 +66,8 @@ export class KclManager {
|
|||||||
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
|
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
|
||||||
private _executeCallback: () => void = () => {}
|
private _executeCallback: () => void = () => {}
|
||||||
|
|
||||||
|
isFirstRender = true
|
||||||
|
|
||||||
get ast() {
|
get ast() {
|
||||||
return this._ast
|
return this._ast
|
||||||
}
|
}
|
||||||
@ -194,7 +197,11 @@ export class KclManager {
|
|||||||
async executeAst(
|
async executeAst(
|
||||||
ast: Program = this._ast,
|
ast: Program = this._ast,
|
||||||
zoomToFit?: boolean,
|
zoomToFit?: boolean,
|
||||||
executionId?: number
|
executionId?: number,
|
||||||
|
zoomOnRangeAndType?: {
|
||||||
|
range: SourceRange
|
||||||
|
type: string
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this?.engineCommandManager?.waitForReady
|
await this?.engineCommandManager?.waitForReady
|
||||||
const currentExecutionId = executionId || Date.now()
|
const currentExecutionId = executionId || Date.now()
|
||||||
@ -218,12 +225,20 @@ export class KclManager {
|
|||||||
defaultSelectionFilter(programMemory, this.engineCommandManager)
|
defaultSelectionFilter(programMemory, this.engineCommandManager)
|
||||||
|
|
||||||
if (zoomToFit) {
|
if (zoomToFit) {
|
||||||
|
let zoomObjectId: string | undefined = ''
|
||||||
|
if (zoomOnRangeAndType) {
|
||||||
|
zoomObjectId = this.engineCommandManager?.mapRangeToObjectId(
|
||||||
|
zoomOnRangeAndType.range,
|
||||||
|
zoomOnRangeAndType.type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await this.engineCommandManager.sendSceneCommand({
|
await this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'zoom_to_fit',
|
type: 'zoom_to_fit',
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
||||||
padding: 0.1, // padding around the objects
|
padding: 0.1, // padding around the objects
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -357,6 +372,11 @@ export class KclManager {
|
|||||||
execute: boolean,
|
execute: boolean,
|
||||||
optionalParams?: {
|
optionalParams?: {
|
||||||
focusPath?: PathToNode
|
focusPath?: PathToNode
|
||||||
|
zoomToFit?: boolean
|
||||||
|
zoomOnRangeAndType?: {
|
||||||
|
range: SourceRange
|
||||||
|
type: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
): Promise<{
|
): Promise<{
|
||||||
newAst: Program
|
newAst: Program
|
||||||
@ -400,7 +420,12 @@ export class KclManager {
|
|||||||
codeManager.updateCodeEditor(newCode)
|
codeManager.updateCodeEditor(newCode)
|
||||||
// Write the file to disk.
|
// Write the file to disk.
|
||||||
await codeManager.writeToFile()
|
await codeManager.writeToFile()
|
||||||
await this.executeAst(astWithUpdatedSource)
|
await this.executeAst(
|
||||||
|
astWithUpdatedSource,
|
||||||
|
optionalParams?.zoomToFit,
|
||||||
|
undefined,
|
||||||
|
optionalParams?.zoomOnRangeAndType
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// When we don't re-execute, we still want to update the program
|
// When we don't re-execute, we still want to update the program
|
||||||
// memory with the new ast. So we will hit the mock executor
|
// memory with the new ast. So we will hit the mock executor
|
||||||
|