Compare commits
15 Commits
remove-the
...
v0.22.7
Author | SHA1 | Date | |
---|---|---|---|
8f138109dd | |||
8972f53256 | |||
0c5b13ade5 | |||
446f92a53a | |||
2256e3bc09 | |||
9e2876edc6 | |||
a138af1ec8 | |||
684c585a48 | |||
500be20649 | |||
5fbbe2fa8c | |||
5f5ecc5afe | |||
3dafc31cad | |||
9c230bc678 | |||
1fad6966b6 | |||
c7efb4c006 |
14
.github/workflows/ci.yml
vendored
@ -238,12 +238,8 @@ jobs:
|
||||
shell: cmd
|
||||
|
||||
- name: Build the app (debug)
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
if: ${{ env.BUILD_RELEASE == 'false' }}
|
||||
with:
|
||||
includeRelease: false
|
||||
includeDebug: true
|
||||
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||
run: "yarn tauri build --debug ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||
|
||||
- name: Build for Mac TestFlight (nightly)
|
||||
if: ${{ github.event_name == 'schedule' && matrix.os == 'macos-14' }}
|
||||
@ -336,7 +332,6 @@ jobs:
|
||||
# specific and we want to overwrite it with the this new build after and
|
||||
# not upload the apple store build to the public bucket
|
||||
- name: Build the app (release) and sign
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
@ -348,8 +343,7 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
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' }}"
|
||||
with:
|
||||
args: "${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
@ -367,7 +361,7 @@ jobs:
|
||||
export VITE_KC_API_BASE_URL
|
||||
xvfb-run yarn test:e2e:tauri
|
||||
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 }}
|
||||
|
||||
- name: Run e2e tests (windows only)
|
||||
@ -376,7 +370,7 @@ jobs:
|
||||
cargo install tauri-driver --force
|
||||
yarn wdio run wdio.conf.ts
|
||||
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 }}
|
||||
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
|
||||
E2E_TAURI_ENABLED: true
|
||||
|
2
.gitignore
vendored
@ -56,3 +56,5 @@ src-tauri/gen
|
||||
|
||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||
Mac_App_Distribution.provisionprofile
|
||||
|
||||
*.tsbuildinfo
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
dist
|
||||
coverage
|
||||
|
||||
# Ignore Rust projects:
|
||||
@ -9,5 +10,6 @@ src/wasm-lib/pkg
|
||||
src/wasm-lib/kcl/bindings
|
||||
e2e/playwright/export-snapshots
|
||||
|
||||
|
||||
# XState generated files
|
||||
src/machines/**.typegen.ts
|
||||
|
7
.vscode/settings.json
vendored
@ -1,7 +0,0 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"geos"
|
||||
],
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
}
|
@ -91,8 +91,9 @@ const part001 = startSketchOn('-XZ')
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.waitForCmdReceive('extrude')
|
||||
@ -330,7 +331,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// wait for execution done
|
||||
@ -386,8 +387,8 @@ test('Draft segments should look right', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
@ -443,7 +444,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
@ -490,7 +491,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
@ -589,7 +590,7 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
@ -689,7 +690,7 @@ const part002 = startSketchOn(part001, 'seg01')
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
@ -739,7 +740,7 @@ test('Zoom to fit on load - solid 2d', async ({ page, context }) => {
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
@ -776,7 +777,7 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
@ -795,3 +796,83 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
|
||||
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: 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: 69 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 }
|
||||
}
|
||||
|
||||
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) {
|
||||
// Chrome devtools protocol session only works in Chromium
|
||||
const browserType = page.context().browser()?.browserType().name()
|
||||
@ -214,7 +231,7 @@ export async function getUtils(page: Page) {
|
||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||
|
||||
return {
|
||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||
removeCurrentCode: () => removeCurrentCode(page),
|
||||
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||
updateCamPosition: async (xyz: [number, number, number]) => {
|
||||
|
@ -1,3 +0,0 @@
|
||||
// comment
|
||||
|
||||
const hi = 5 + 4
|
58
package.json
@ -1,50 +1,43 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.22.6",
|
||||
"version": "0.22.7",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
"@codemirror/autocomplete": "^6.16.3",
|
||||
"@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",
|
||||
"@csstools/postcss-oklab-function": "^3.0.16",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.67",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@kittycad/lib": "^0.0.69",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
"@replit/codemirror-interact": "^6.3.1",
|
||||
"@tauri-apps/api": "2.0.0-beta.12",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-beta.5",
|
||||
"@tauri-apps/plugin-http": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-process": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0-beta.3",
|
||||
"@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",
|
||||
"@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/react": "^3.2.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"formik": "^2.4.6",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.4.3",
|
||||
"http-server": "^14.1.1",
|
||||
"html2canvas-pro": "^1.5.0",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
"jszip": "^3.10.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"re-resizable": "^6.9.11",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -55,18 +48,14 @@
|
||||
"react-modal-promise": "^1.0.2",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"swr": "^2.2.5",
|
||||
"three": "^0.164.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuid": "^9.0.1",
|
||||
"vitest": "^1.6.0",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"wasm-pack": "^0.12.1",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.17.0",
|
||||
"xstate": "^4.38.2",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
@ -85,8 +74,8 @@
|
||||
"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": "yarn pretest && http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
|
||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"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": "(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",
|
||||
@ -122,12 +111,15 @@
|
||||
"@babel/preset-env": "^7.24.3",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@tauri-apps/cli": "^2.0.0-beta.13",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/debounce-promise": "^3.1.9",
|
||||
"@tauri-apps/cli": "==2.0.0-beta.13",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^18.19.31",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
@ -147,21 +139,27 @@
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"happy-dom": "^14.3.10",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^9.0.11",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier": "^2.8.8",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.2.9",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wait-on": "^7.2.0",
|
||||
"wasm-pack": "^0.12.1",
|
||||
"ws": "^8.17.0",
|
||||
"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 { Codec } from '.'
|
||||
import Bytes from './bytes'
|
||||
import PromiseMap from './map'
|
||||
import Queue from './queue'
|
||||
import Tracer from '../tracer'
|
||||
import { Codec } from '../codec'
|
||||
import Tracer from './tracer'
|
||||
import PromiseMap from './map'
|
||||
|
||||
export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
|
||||
@ -15,9 +15,12 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
new Queue<vsrpc.RequestMessage>()
|
||||
|
||||
readonly #start: Promise<void>
|
||||
private trace: boolean = false
|
||||
|
||||
constructor() {
|
||||
constructor(trace?: boolean) {
|
||||
super()
|
||||
this.trace = trace || false
|
||||
|
||||
this.#start = this.start()
|
||||
}
|
||||
|
||||
@ -64,7 +67,10 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
contentLength = null
|
||||
|
||||
const message = JSON.parse(delimited) as vsrpc.Message
|
||||
Tracer.server(message)
|
||||
|
||||
if (this.trace) {
|
||||
Tracer.server(message)
|
||||
}
|
||||
|
||||
// demux the message stream
|
||||
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
||||
@ -85,7 +91,9 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
|
||||
add(bytes: Uint8Array): void {
|
||||
const message = Codec.decode(bytes) as vsrpc.Message
|
||||
Tracer.server(message)
|
||||
if (this.trace) {
|
||||
Tracer.server(message)
|
||||
}
|
||||
|
||||
// demux the message stream
|
||||
if (vsrpc.Message.isResponse(message) && null != message.id) {
|
@ -1,12 +1,16 @@
|
||||
import * as jsrpc from 'json-rpc-2.0'
|
||||
import * as vsrpc from 'vscode-jsonrpc'
|
||||
|
||||
import Bytes from './codec/bytes'
|
||||
import StreamDemuxer from './codec/demuxer'
|
||||
import Headers from './codec/headers'
|
||||
import Queue from './codec/queue'
|
||||
import Bytes from './bytes'
|
||||
import StreamDemuxer from './demuxer'
|
||||
import Headers from './headers'
|
||||
import Queue from './queue'
|
||||
import Tracer from './tracer'
|
||||
import { LspWorkerEventType, LspWorker } from './types'
|
||||
|
||||
export enum LspWorkerEventType {
|
||||
Init = 'init',
|
||||
Call = 'call',
|
||||
}
|
||||
|
||||
export const encoder = new TextEncoder()
|
||||
export const decoder = new TextDecoder()
|
||||
@ -33,16 +37,24 @@ export class IntoServer
|
||||
implements AsyncGenerator<Uint8Array, never, void>
|
||||
{
|
||||
private worker: Worker | null = null
|
||||
private type_: LspWorker | null = null
|
||||
constructor(type_?: LspWorker, worker?: Worker) {
|
||||
private type_: String | null = null
|
||||
|
||||
private trace: boolean = false
|
||||
|
||||
constructor(type_?: String, worker?: Worker, trace?: boolean) {
|
||||
super()
|
||||
if (worker && type_) {
|
||||
this.worker = worker
|
||||
this.type_ = type_
|
||||
}
|
||||
|
||||
this.trace = trace || false
|
||||
}
|
||||
enqueue(item: Uint8Array): void {
|
||||
Tracer.client(Headers.remove(decoder.decode(item)))
|
||||
if (this.trace) {
|
||||
Tracer.client(Headers.remove(decoder.decode(item)))
|
||||
}
|
||||
|
||||
if (this.worker) {
|
||||
this.worker.postMessage({
|
||||
worker: this.type_,
|
||||
@ -71,7 +83,7 @@ export namespace FromServer {
|
||||
// Calls private method .start() which can throw.
|
||||
// This is an odd one of the bunch but try/catch seems most suitable here.
|
||||
try {
|
||||
return new StreamDemuxer()
|
||||
return new StreamDemuxer(false)
|
||||
} catch (e: any) {
|
||||
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 Client from './client'
|
||||
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
|
||||
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
|
||||
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 { 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'
|
||||
|
||||
import { FromServer, IntoServer } from './codec'
|
||||
import Client from './jsonrpc'
|
||||
import { LanguageServerPlugin } from '../plugin/lsp'
|
||||
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||
|
||||
@ -31,12 +23,6 @@ interface LSPRequestMap {
|
||||
LSP.TextEdit[] | null
|
||||
]
|
||||
'textDocument/foldingRange': [LSP.FoldingRangeParams, LSP.FoldingRange[]]
|
||||
'copilot/getCompletions': [
|
||||
CopilotLspCompletionParams,
|
||||
CopilotCompletionResponse
|
||||
]
|
||||
'kcl/updateUnits': [UpdateUnitsParams, UpdateUnitsResponse | null]
|
||||
'kcl/updateCanExecute': [UpdateCanExecuteParams, UpdateCanExecuteResponse]
|
||||
}
|
||||
|
||||
// Client to server
|
||||
@ -49,21 +35,13 @@ interface LSPNotifyMap {
|
||||
'workspace/didCreateFiles': LSP.CreateFilesParams
|
||||
'workspace/didRenameFiles': LSP.RenameFilesParams
|
||||
'workspace/didDeleteFiles': LSP.DeleteFilesParams
|
||||
'copilot/notifyAccepted': CopilotAcceptCompletionParams
|
||||
'copilot/notifyRejected': CopilotRejectCompletionParams
|
||||
}
|
||||
|
||||
export interface LanguageServerClientOptions {
|
||||
client: Client
|
||||
name: LspWorker
|
||||
}
|
||||
|
||||
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
|
||||
name: string
|
||||
fromServer: FromServer
|
||||
intoServer: IntoServer
|
||||
initializedCallback: () => void
|
||||
}
|
||||
|
||||
export class LanguageServerClient {
|
||||
@ -76,18 +54,18 @@ export class LanguageServerClient {
|
||||
|
||||
public initializePromise: Promise<void>
|
||||
|
||||
private isUpdatingSemanticTokens: boolean = false
|
||||
private semanticTokens: SemanticToken[] = []
|
||||
private queuedUids: string[] = []
|
||||
|
||||
constructor(options: LanguageServerClientOptions) {
|
||||
this.plugins = []
|
||||
this.client = options.client
|
||||
this.name = options.name
|
||||
this.plugins = []
|
||||
|
||||
this.client = new Client(
|
||||
options.fromServer,
|
||||
options.intoServer,
|
||||
options.initializedCallback
|
||||
)
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.queuedUids = []
|
||||
this.initializePromise = this.initialize()
|
||||
}
|
||||
|
||||
@ -111,19 +89,10 @@ export class LanguageServerClient {
|
||||
|
||||
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
|
||||
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) {
|
||||
this.notify('textDocument/didChange', params)
|
||||
this.updateSemanticTokens(params.textDocument.uri)
|
||||
}
|
||||
|
||||
textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) {
|
||||
@ -134,18 +103,9 @@ export class LanguageServerClient {
|
||||
added: 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', {
|
||||
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) {
|
||||
@ -160,33 +120,13 @@ export class LanguageServerClient {
|
||||
this.notify('workspace/didDeleteFiles', params)
|
||||
}
|
||||
|
||||
async updateSemanticTokens(uri: string) {
|
||||
async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) {
|
||||
const serverCapabilities = this.getServerCapabilities()
|
||||
if (!serverCapabilities.semanticTokensProvider) {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure we can only run, if we aren't already running.
|
||||
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
|
||||
return this.request('textDocument/semanticTokens/full', params)
|
||||
}
|
||||
|
||||
async textDocumentHover(params: LSP.HoverParams) {
|
||||
@ -239,6 +179,10 @@ export class LanguageServerClient {
|
||||
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>(
|
||||
method: K,
|
||||
params: LSPNotifyMap[K]
|
||||
@ -246,44 +190,8 @@ export class LanguageServerClient {
|
||||
return this.client.notify(method, params)
|
||||
}
|
||||
|
||||
async getCompletion(params: CopilotLspCompletionParams) {
|
||||
const response = await this.request('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.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)
|
||||
notifyCustom<P>(method: string, params: P): void {
|
||||
return this.client.notify(method, params)
|
||||
}
|
||||
|
||||
private processNotifications(notification: LSP.NotificationMessage) {
|
@ -6,7 +6,6 @@ import {
|
||||
unregisterServerCapability,
|
||||
} from './server-capability-registration'
|
||||
import { Codec, FromServer, IntoServer } from './codec'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
const client_capabilities: LSP.ClientCapabilities = {
|
||||
textDocument: {
|
||||
@ -67,8 +66,13 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
#fromServer: FromServer
|
||||
private serverCapabilities: LSP.ServerCapabilities<any> = {}
|
||||
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
|
||||
private initializedCallback: () => void
|
||||
|
||||
constructor(fromServer: FromServer, intoServer: IntoServer) {
|
||||
constructor(
|
||||
fromServer: FromServer,
|
||||
intoServer: IntoServer,
|
||||
initializedCallback: () => void
|
||||
) {
|
||||
super(
|
||||
new jsrpc.JSONRPCServer(),
|
||||
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
|
||||
@ -82,6 +86,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
})
|
||||
)
|
||||
this.#fromServer = fromServer
|
||||
this.initializedCallback = initializedCallback
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
@ -124,7 +129,9 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
this.serverCapabilities,
|
||||
capabilityRegistration
|
||||
)
|
||||
if (err(caps)) return (this.serverCapabilities = {})
|
||||
if (caps instanceof Error) {
|
||||
return (this.serverCapabilities = {})
|
||||
}
|
||||
this.serverCapabilities = caps
|
||||
}
|
||||
)
|
||||
@ -139,7 +146,9 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
this.serverCapabilities,
|
||||
capabilityUnregistration
|
||||
)
|
||||
if (err(caps)) return (this.serverCapabilities = {})
|
||||
if (caps instanceof Error) {
|
||||
return (this.serverCapabilities = {})
|
||||
}
|
||||
this.serverCapabilities = caps
|
||||
}
|
||||
)
|
||||
@ -151,7 +160,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
{
|
||||
processId: null,
|
||||
clientInfo: {
|
||||
name: 'kcl-language-client',
|
||||
name: 'codemirror-lsp-client',
|
||||
},
|
||||
capabilities: client_capabilities,
|
||||
rootUri: null,
|
||||
@ -163,6 +172,8 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
|
||||
// notify "initialized": client --> server
|
||||
this.notify(LSP.InitializedNotification.type.method, {})
|
||||
|
||||
this.initializedCallback()
|
||||
|
||||
await Promise.all(
|
||||
this.afterInitializedHooks.map((f: () => Promise<void>) => f())
|
||||
)
|
113
packages/codemirror-lsp-client/src/index.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { autocompletion } from '@codemirror/autocomplete'
|
||||
import { foldService, syntaxTree } from '@codemirror/language'
|
||||
import { Extension, EditorState } from '@codemirror/state'
|
||||
import { ViewPlugin } from '@codemirror/view'
|
||||
|
||||
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
||||
|
||||
import {
|
||||
docPathFacet,
|
||||
LanguageServerPlugin,
|
||||
LanguageServerPluginSpec,
|
||||
languageId,
|
||||
workspaceFolders,
|
||||
LanguageServerOptions,
|
||||
} from './plugin/lsp'
|
||||
import { offsetToPos } from './plugin/util'
|
||||
|
||||
export type { LanguageServerClientOptions } from './client'
|
||||
export { LanguageServerClient } from './client'
|
||||
export {
|
||||
Codec,
|
||||
FromServer,
|
||||
IntoServer,
|
||||
LspWorkerEventType,
|
||||
} from './client/codec'
|
||||
export type { LanguageServerOptions } from './plugin/lsp'
|
||||
export type { TransactionInfo, RelevantUpdate } from './plugin/annotations'
|
||||
export { updateInfo, TransactionAnnotation } from './plugin/annotations'
|
||||
export {
|
||||
LanguageServerPlugin,
|
||||
LanguageServerPluginSpec,
|
||||
docPathFacet,
|
||||
languageId,
|
||||
workspaceFolders,
|
||||
} 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.
|
||||
return plugin?.foldingRange(lineStart, lineEnd)
|
||||
}),
|
||||
]
|
||||
|
||||
if (options.client.getServerCapabilities().completionProvider) {
|
||||
ext.push(
|
||||
autocompletion({
|
||||
defaultKeymap: false,
|
||||
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,
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return ext
|
||||
}
|
131
packages/codemirror-lsp-client/src/plugin/annotations.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { hasNextSnippetField, pickedCompletion } from '@codemirror/autocomplete'
|
||||
import { Annotation, Transaction } from '@codemirror/state'
|
||||
import type { ViewUpdate } from '@codemirror/view'
|
||||
|
||||
export enum LspAnnotation {
|
||||
SemanticTokens = 'semantic-tokens',
|
||||
FormatCode = 'format-code',
|
||||
Diagnostics = 'diagnostics',
|
||||
}
|
||||
|
||||
const lspEvent = Annotation.define<LspAnnotation>()
|
||||
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
|
||||
export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
|
||||
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
|
||||
|
||||
export enum TransactionAnnotation {
|
||||
Remote = 'remote',
|
||||
UserSelect = 'user.select',
|
||||
UserInput = 'user.input',
|
||||
UserMove = 'user.move',
|
||||
UserDelete = 'user.delete',
|
||||
UserUndo = 'user.undo',
|
||||
UserRedo = 'user.redo',
|
||||
|
||||
SemanticTokens = 'SemanticTokens',
|
||||
FormatCode = 'FormatCode',
|
||||
Diagnostics = 'Diagnostics',
|
||||
|
||||
PickedCompletion = 'PickedCompletion',
|
||||
}
|
||||
|
||||
export interface TransactionInfo {
|
||||
annotations: TransactionAnnotation[]
|
||||
time: number | null
|
||||
docChanged: boolean
|
||||
addToHistory: boolean
|
||||
inSnippet: boolean
|
||||
transaction: Transaction
|
||||
}
|
||||
|
||||
export const updateInfo = (update: ViewUpdate): TransactionInfo[] => {
|
||||
let transactionInfos: TransactionInfo[] = []
|
||||
|
||||
for (const tr of update.transactions) {
|
||||
let annotations: TransactionAnnotation[] = []
|
||||
|
||||
if (tr.isUserEvent('select')) {
|
||||
annotations.push(TransactionAnnotation.UserSelect)
|
||||
}
|
||||
|
||||
if (tr.isUserEvent('input')) {
|
||||
annotations.push(TransactionAnnotation.UserInput)
|
||||
}
|
||||
if (tr.isUserEvent('delete')) {
|
||||
annotations.push(TransactionAnnotation.UserDelete)
|
||||
}
|
||||
if (tr.isUserEvent('undo')) {
|
||||
annotations.push(TransactionAnnotation.UserUndo)
|
||||
}
|
||||
if (tr.isUserEvent('redo')) {
|
||||
annotations.push(TransactionAnnotation.UserRedo)
|
||||
}
|
||||
if (tr.isUserEvent('move')) {
|
||||
annotations.push(TransactionAnnotation.UserMove)
|
||||
}
|
||||
|
||||
if (tr.annotation(pickedCompletion) !== undefined) {
|
||||
annotations.push(TransactionAnnotation.PickedCompletion)
|
||||
}
|
||||
|
||||
if (tr.annotation(lspSemanticTokensEvent.type) !== undefined) {
|
||||
annotations.push(TransactionAnnotation.SemanticTokens)
|
||||
}
|
||||
|
||||
if (tr.annotation(lspFormatCodeEvent.type) !== undefined) {
|
||||
annotations.push(TransactionAnnotation.FormatCode)
|
||||
}
|
||||
|
||||
if (tr.annotation(lspDiagnosticsEvent.type) !== undefined) {
|
||||
annotations.push(TransactionAnnotation.Diagnostics)
|
||||
}
|
||||
|
||||
if (tr.annotation(Transaction.remote) !== undefined) {
|
||||
annotations.push(TransactionAnnotation.Remote)
|
||||
}
|
||||
|
||||
transactionInfos.push({
|
||||
annotations,
|
||||
time: tr.annotation(Transaction.time) || null,
|
||||
docChanged: tr.docChanged,
|
||||
addToHistory: tr.annotation(Transaction.addToHistory) || false,
|
||||
inSnippet: hasNextSnippetField(update.state),
|
||||
transaction: tr,
|
||||
})
|
||||
}
|
||||
|
||||
return transactionInfos
|
||||
}
|
||||
|
||||
export interface RelevantUpdate {
|
||||
overall: boolean
|
||||
userSelect: boolean
|
||||
time: number | null
|
||||
}
|
||||
|
||||
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
|
||||
const infos = updateInfo(update)
|
||||
// Make sure we are not in a snippet
|
||||
if (infos.some((info) => info.inSnippet)) {
|
||||
return {
|
||||
overall: false,
|
||||
userSelect: false,
|
||||
time: null,
|
||||
}
|
||||
}
|
||||
return {
|
||||
overall: infos.some(
|
||||
(info) =>
|
||||
info.docChanged ||
|
||||
info.annotations.includes(TransactionAnnotation.UserInput) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserMove)
|
||||
),
|
||||
userSelect: infos.some((info) =>
|
||||
info.annotations.includes(TransactionAnnotation.UserSelect)
|
||||
),
|
||||
time: infos.length ? infos[0].time : null,
|
||||
}
|
||||
}
|
51
packages/codemirror-lsp-client/src/plugin/autocomplete.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import {
|
||||
acceptCompletion,
|
||||
clearSnippet,
|
||||
closeCompletion,
|
||||
hasNextSnippetField,
|
||||
moveCompletionSelection,
|
||||
nextSnippetField,
|
||||
prevSnippetField,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete'
|
||||
import { Prec } from '@codemirror/state'
|
||||
import { EditorView, keymap, KeyBinding } from '@codemirror/view'
|
||||
|
||||
import { CompletionItemKind } from 'vscode-languageserver-protocol'
|
||||
|
||||
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,
|
||||
},
|
||||
]
|
||||
|
||||
export const lspAutocompleteKeymapExt = Prec.highest(
|
||||
keymap.computeN([], () => [lspAutocompleteKeymap])
|
||||
)
|
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,148 @@
|
||||
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 {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
} from '@codemirror/autocomplete'
|
||||
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
|
||||
import { Facet, StateEffect, Extension, Transaction } 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 { ViewUpdate, PluginValue } from '@codemirror/view'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
||||
import { Program, ProgramMemory } from 'lang/wasm'
|
||||
import { codeManager, editorManager, kclManager } from 'lib/singletons'
|
||||
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
||||
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
||||
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
||||
import {
|
||||
DiagnosticSeverity,
|
||||
CompletionTriggerKind,
|
||||
} from 'vscode-languageserver-protocol'
|
||||
import { URI } from 'vscode-uri'
|
||||
|
||||
import { LanguageServerClient } from '../client'
|
||||
import {
|
||||
lspSemanticTokensEvent,
|
||||
lspFormatCodeEvent,
|
||||
relevantUpdate,
|
||||
} from './annotations'
|
||||
import { CompletionItemKindMap } from './autocomplete'
|
||||
import { addToken, SemanticToken } from './semantic-tokens'
|
||||
import { deferExecution, posToOffset, formatMarkdownContents } from './util'
|
||||
import { lspAutocompleteKeymapExt } 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, '')
|
||||
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 workspaceFolders = Facet.define<
|
||||
LSP.WorkspaceFolder[],
|
||||
LSP.WorkspaceFolder[]
|
||||
>({ combine: useLast })
|
||||
|
||||
const CompletionItemKindMap = Object.fromEntries(
|
||||
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
||||
) as Record<CompletionItemKind, string>
|
||||
export interface LanguageServerOptions {
|
||||
// We assume this is the main project directory, we are currently working in.
|
||||
workspaceFolders: LSP.WorkspaceFolder[]
|
||||
documentUri: string
|
||||
allowHTMLContent: boolean
|
||||
client: LanguageServerClient
|
||||
processLspNotification?: (
|
||||
plugin: LanguageServerPlugin,
|
||||
notification: LSP.NotificationMessage
|
||||
) => void
|
||||
|
||||
const changesDelay = 600
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const updateDelay = 100
|
||||
changesDelay?: number
|
||||
}
|
||||
|
||||
export class LanguageServerPlugin implements PluginValue {
|
||||
public client: LanguageServerClient
|
||||
public documentUri: string
|
||||
public languageId: string
|
||||
public workspaceFolders: LSP.WorkspaceFolder[]
|
||||
private documentVersion: number
|
||||
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) => {
|
||||
try {
|
||||
// Update the state (not the editor) with the new code.
|
||||
this.client.textDocumentDidChange({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
uri: this.getDocUri(),
|
||||
version: this.documentVersion++,
|
||||
},
|
||||
contentChanges: [{ text: code }],
|
||||
})
|
||||
|
||||
if (this.viewUpdate) {
|
||||
editorManager.handleOnViewUpdate(this.viewUpdate)
|
||||
}
|
||||
this.requestSemanticTokens()
|
||||
this.updateFoldingRanges()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, changesDelay)
|
||||
}, this.changesDelay)
|
||||
|
||||
constructor(
|
||||
client: LanguageServerClient,
|
||||
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)
|
||||
constructor(options: LanguageServerOptions, private view: EditorView) {
|
||||
this.client = options.client
|
||||
this.documentVersion = 0
|
||||
|
||||
if (options.changesDelay) {
|
||||
this.changesDelay = options.changesDelay
|
||||
}
|
||||
|
||||
if (options.allowHTMLContent !== undefined) {
|
||||
this.allowHTMLContent = options.allowHTMLContent
|
||||
}
|
||||
|
||||
this.client.attachPlugin(this)
|
||||
|
||||
this.processLspNotification = options.processLspNotification
|
||||
|
||||
this.initialize({
|
||||
documentText: this.view.state.doc.toString(),
|
||||
documentText: this.getDocText(),
|
||||
})
|
||||
}
|
||||
|
||||
update(viewUpdate: ViewUpdate) {
|
||||
this.viewUpdate = viewUpdate
|
||||
if (!viewUpdate.docChanged) {
|
||||
// debounce the view update.
|
||||
// otherwise it is laggy for typing.
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
}
|
||||
private getDocPath(view = this.view) {
|
||||
return view.state.facet(docPathFacet)
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(() => {
|
||||
editorManager.handleOnViewUpdate(viewUpdate)
|
||||
}, updateDelay)
|
||||
private getDocText(view = this.view) {
|
||||
return view.state.doc.toString()
|
||||
}
|
||||
|
||||
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) {
|
||||
const isRelevant = relevantUpdate(viewUpdate)
|
||||
if (!isRelevant.overall) {
|
||||
return
|
||||
}
|
||||
|
||||
const newCode = this.view.state.doc.toString()
|
||||
|
||||
codeManager.code = newCode
|
||||
codeManager.writeToFile()
|
||||
kclManager.executeCode()
|
||||
// If the doc didn't change we can return early.
|
||||
if (!viewUpdate.docChanged) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sendChange({
|
||||
documentText: newCode,
|
||||
documentText: viewUpdate.state.doc.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -121,14 +154,18 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
if (this.client.initializePromise) {
|
||||
await this.client.initializePromise
|
||||
}
|
||||
|
||||
this.client.textDocumentDidOpen({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
languageId: this.languageId,
|
||||
uri: this.getDocUri(),
|
||||
languageId: this.getLanguageId(),
|
||||
text: documentText,
|
||||
version: this.documentVersion,
|
||||
},
|
||||
})
|
||||
|
||||
this.requestSemanticTokens()
|
||||
this.updateFoldingRanges()
|
||||
}
|
||||
|
||||
async sendChange({ documentText }: { documentText: string }) {
|
||||
@ -137,8 +174,8 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
this._defferer(documentText)
|
||||
}
|
||||
|
||||
requestDiagnostics(view: EditorView) {
|
||||
this.sendChange({ documentText: view.state.doc.toString() })
|
||||
requestDiagnostics() {
|
||||
this.sendChange({ documentText: this.getDocText() })
|
||||
}
|
||||
|
||||
async requestHoverTooltip(
|
||||
@ -151,9 +188,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
)
|
||||
return null
|
||||
|
||||
this.sendChange({ documentText: view.state.doc.toString() })
|
||||
this.sendChange({ documentText: this.getDocText() })
|
||||
const result = await this.client.textDocumentHover({
|
||||
textDocument: { uri: this.documentUri },
|
||||
textDocument: { uri: this.getDocUri() },
|
||||
position: { line, character },
|
||||
})
|
||||
if (!result) return null
|
||||
@ -169,8 +206,8 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
dom.classList.add('documentation')
|
||||
dom.classList.add('hover-tooltip')
|
||||
dom.style.zIndex = '99999999'
|
||||
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
|
||||
else dom.textContent = formatContents(contents)
|
||||
if (this.allowHTMLContent) dom.innerHTML = formatMarkdownContents(contents)
|
||||
else dom.textContent = formatMarkdownContents(contents)
|
||||
return { pos, end, create: (view) => ({ dom }), above: true }
|
||||
}
|
||||
|
||||
@ -180,8 +217,9 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
!this.client.getServerCapabilities().foldingRangeProvider
|
||||
)
|
||||
return null
|
||||
|
||||
const result = await this.client.textDocumentFoldingRange({
|
||||
textDocument: { uri: this.documentUri },
|
||||
textDocument: { uri: this.getDocUri() },
|
||||
})
|
||||
|
||||
return result || null
|
||||
@ -222,42 +260,6 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
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() {
|
||||
if (
|
||||
!this.client.ready ||
|
||||
@ -267,14 +269,14 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
|
||||
this.client.textDocumentDidChange({
|
||||
textDocument: {
|
||||
uri: this.documentUri,
|
||||
uri: this.getDocUri(),
|
||||
version: this.documentVersion++,
|
||||
},
|
||||
contentChanges: [{ text: this.view.state.doc.toString() }],
|
||||
contentChanges: [{ text: this.getDocText() }],
|
||||
})
|
||||
|
||||
const result = await this.client.textDocumentFormatting({
|
||||
textDocument: { uri: this.documentUri },
|
||||
textDocument: { uri: this.getDocUri() },
|
||||
options: {
|
||||
tabSize: 2,
|
||||
insertSpaces: true,
|
||||
@ -287,13 +289,12 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const { range, newText } = result[i]
|
||||
this.view.dispatch({
|
||||
changes: [
|
||||
{
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
insert: newText,
|
||||
},
|
||||
],
|
||||
changes: {
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
insert: newText,
|
||||
},
|
||||
annotations: [lspFormatCodeEvent, Transaction.addToHistory.of(true)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -320,7 +321,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
})
|
||||
|
||||
const result = await this.client.textDocumentCompletion({
|
||||
textDocument: { uri: this.documentUri },
|
||||
textDocument: { uri: this.getDocUri() },
|
||||
position: { line, character },
|
||||
context: {
|
||||
triggerKind,
|
||||
@ -360,7 +361,7 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
if (documentation) {
|
||||
completion.info = () => {
|
||||
const htmlString = formatContents(documentation)
|
||||
const htmlString = formatMarkdownContents(documentation)
|
||||
const htmlNode = document.createElement('div')
|
||||
htmlNode.style.display = 'contents'
|
||||
htmlNode.innerHTML = htmlString
|
||||
@ -379,16 +380,107 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
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) {
|
||||
try {
|
||||
switch (notification.method) {
|
||||
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(
|
||||
'[lsp] [window/publishDiagnostics]',
|
||||
this.client.getName(),
|
||||
notification.params
|
||||
params
|
||||
)
|
||||
const params = notification.params as PublishDiagnosticsParams
|
||||
// this is sometimes slower than our actual typing.
|
||||
this.processDiagnostics(params)
|
||||
break
|
||||
@ -406,30 +498,17 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
notification.params
|
||||
)
|
||||
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) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
// Send it to the plugin
|
||||
this.processLspNotification?.(this, notification)
|
||||
}
|
||||
|
||||
processDiagnostics(params: PublishDiagnosticsParams) {
|
||||
if (params.uri !== this.documentUri) return
|
||||
if (params.uri !== this.getDocUri()) return
|
||||
|
||||
const diagnostics = params.diagnostics
|
||||
.map(({ range, message, severity }) => ({
|
||||
@ -459,18 +538,26 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
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(
|
||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||
): string {
|
||||
if (Array.isArray(contents)) {
|
||||
return contents.map((c) => formatContents(c) + '\n\n').join('')
|
||||
} else if (typeof contents === 'string') {
|
||||
return Marked.parse(contents)
|
||||
} else {
|
||||
return Marked.parse(contents.value)
|
||||
export class LanguageServerPluginSpec
|
||||
implements PluginSpec<LanguageServerPlugin>
|
||||
{
|
||||
provide(plugin: ViewPlugin<LanguageServerPlugin>): Extension {
|
||||
return [
|
||||
lspAutocompleteKeymapExt,
|
||||
lspFormatExt(plugin),
|
||||
lspHoverExt(plugin),
|
||||
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 './annotations'
|
||||
|
||||
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,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* Do not retry */
|
||||
retries: process.env.CI ? 0 : 0,
|
||||
/* Different amount of parallelism on CI and local. */
|
||||
workers: process.env.CI ? 4 : 4,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
|
@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@csstools/postcss-oklab-function': { preserve: true },
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
160
src-tauri/Cargo.lock
generated
@ -1212,7 +1212,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-docs"
|
||||
version = "0.1.18"
|
||||
version = "0.1.20"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"convert_case 0.6.0",
|
||||
@ -1260,6 +1260,15 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
@ -1270,6 +1279,18 @@ dependencies = [
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
@ -2576,7 +2597,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.67"
|
||||
version = "0.1.69"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -2946,9 +2967,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.13.1"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f428b4e9db3d17e2f809dfb1ff9ddfbbf16c71790d1656d10aee320877e1392f"
|
||||
checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"crossbeam-channel",
|
||||
@ -3228,6 +3249,12 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@ -4396,9 +4423,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.17"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f55c82c700538496bdc329bb4918a81f87cc8888811bd123cf325a0f2f8d309"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
dependencies = [
|
||||
"bigdecimal",
|
||||
"bytes",
|
||||
@ -4414,9 +4441,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.17"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83263746fe5e32097f06356968a077f96089739c927a61450efa069905eec108"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5054,9 +5081,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.27.1"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92bcf8885e147b56d6e26751263b45876284f32ca404703f6d3b8f80d16ff4dd"
|
||||
checksum = "ea538df05fbc2dcbbd740ba0cfe8607688535f4798d213cbbfa13ce494f3451f"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"cocoa",
|
||||
@ -5085,8 +5112,8 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows 0.56.0",
|
||||
"windows-core 0.56.0",
|
||||
"windows 0.57.0",
|
||||
"windows-core 0.57.0",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
]
|
||||
@ -5136,9 +5163,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.0.0-beta.17"
|
||||
version = "2.0.0-beta.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fedd5490eddf117253945f0baedafded43474c971cba546a818f527d5c26266"
|
||||
checksum = "5a258ecc5ac7ddade525f512c4962fd01cd0f5265e917b4572579c32c027bb31"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -5185,9 +5212,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abcf98a9b4527567c3e5ca9723431d121e001c2145651b3fa044d22b5e025a7e"
|
||||
checksum = "82b964bb6d03d97e24e12f896aab463b02a3c2ff76a60f728cc37b5548eb470e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@ -5207,9 +5234,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b383f341efb803852b0235a2f330ca90c4c113f422dd6d646b888685b372cace"
|
||||
checksum = "3529cfa977ed7c097f2a5e8da19ecffbe61982450a6c819e6165b6d0cfd3dd3a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@ -5234,11 +5261,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71be71718cfe48b149507157bfbad0e2ba0e98ea51658be26c7c677eb188fb0c"
|
||||
checksum = "36f97dd80334f29314aa5f40b5fad10cb9feffd08e5a5324fd728613841e5d33"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
@ -5248,9 +5275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6baaee0a083db1e04a1b7a3b0670d86a4d95dd2a54e7cbfb5547762b8ed098d9"
|
||||
checksum = "7c8385fd0a4f661f5652b0d9e2d7256187d553bb174f88564d10ebcfa6a3af53"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@ -5313,9 +5340,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.0.0-beta.7"
|
||||
version = "2.0.0-beta.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35377195c6923beda5f29482a16b492d431de964389fca9aaf81a0f7e908023f"
|
||||
checksum = "3aa91955751f329e0aa431b87c199b7378b6f91ec0765d2ad9d4c64e017c3cda"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@ -5466,9 +5493,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.0.0-beta.14"
|
||||
version = "2.0.0-beta.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "148b6e6aff8e63fe5d4ae1d50159d50cfc0b4309abdeca64833c887c6b5631ef"
|
||||
checksum = "d7dc96172a43536236ab55b7da7b8461bf75810985e668589e2395cb476937cb"
|
||||
dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
@ -5485,9 +5512,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.0.0-beta.14"
|
||||
version = "2.0.0-beta.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "398d065c6e0fbf3c4304583759b6e153bc1e0daeb033bede6834ebe4df371fc3"
|
||||
checksum = "5d4fd913b1f14a9b618c7f3ae35656d3aa759767fcb95b72006357c12b9d0b09"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@ -5509,16 +5536,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4709765385f035338ecc330f3fba753b8ee283c659c235da9768949cdb25469"
|
||||
checksum = "4f24a9c20d676a3f025331cc1c3841256ba88c9f25fb7fae709d2b3089c50d90"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"cargo_metadata",
|
||||
"ctor",
|
||||
"dunce",
|
||||
"glob",
|
||||
"heck 0.5.0",
|
||||
"html5ever",
|
||||
"infer",
|
||||
"json-patch",
|
||||
@ -5994,14 +6020,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tray-icon"
|
||||
version = "0.13.4"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97ec55956c54569e74209ae9d29a7a79193b252d17a6ac28bcffd4c11a384ad"
|
||||
checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dirs-next",
|
||||
"dirs",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc",
|
||||
@ -6029,9 +6055,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "9.0.0"
|
||||
version = "9.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
||||
checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde_json",
|
||||
@ -6043,9 +6069,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "9.0.0"
|
||||
version = "9.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577"
|
||||
checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -6522,8 +6548,8 @@ dependencies = [
|
||||
"webview2-com-sys",
|
||||
"windows 0.56.0",
|
||||
"windows-core 0.56.0",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.56.0",
|
||||
"windows-interface 0.56.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6611,6 +6637,16 @@ dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core 0.57.0",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@ -6626,8 +6662,20 @@ version = "0.56.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.56.0",
|
||||
"windows-interface 0.56.0",
|
||||
"windows-result",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement 0.57.0",
|
||||
"windows-interface 0.57.0",
|
||||
"windows-result",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
@ -6643,6 +6691,17 @@ dependencies = [
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.56.0"
|
||||
@ -6654,6 +6713,17 @@ dependencies = [
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.1"
|
||||
@ -6917,9 +6987,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.39.3"
|
||||
version = "0.40.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e180ac2740d6cb4d5cec0abf63eacbea90f1b7e5e3803043b13c1c84c4b7884"
|
||||
checksum = "1fa597526af53f310a8e6218630c5024fdde8271f229e70d7d2fc70b52b8fb1e"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block",
|
||||
|
@ -24,7 +24,7 @@ tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||
tauri-plugin-cli = { version = "2.0.0-beta.3" }
|
||||
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
|
||||
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.9" }
|
||||
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
||||
tauri-plugin-log = { version = "2.0.0-beta.4" }
|
||||
tauri-plugin-os = { version = "2.0.0-beta.2" }
|
||||
|
@ -74,5 +74,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.22.6"
|
||||
"version": "0.22.7"
|
||||
}
|
||||
|
@ -74,6 +74,9 @@ export class CameraControls {
|
||||
enableRotate = true
|
||||
enablePan = true
|
||||
enableZoom = true
|
||||
zoomDataFromLastFrame?: number = undefined
|
||||
// holds coordinates, and interaction
|
||||
moveDataFromLastFrame?: [number, number, string] = undefined
|
||||
lastPerspectiveFov: number = 45
|
||||
pendingZoom: number | null = null
|
||||
pendingRotation: Vector2 | null = null
|
||||
@ -101,16 +104,12 @@ export class CameraControls {
|
||||
get isPerspective() {
|
||||
return this.camera instanceof PerspectiveCamera
|
||||
}
|
||||
private debounceTimer = 0
|
||||
|
||||
handleStart = () => {
|
||||
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
||||
this._isCamMovingCallback(true, false)
|
||||
}
|
||||
handleEnd = () => {
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this._isCamMovingCallback(false, false)
|
||||
}, 400) as any as number
|
||||
this._isCamMovingCallback(false, false)
|
||||
}
|
||||
|
||||
setCam = (camProps: ReactCameraProperties) => {
|
||||
@ -230,6 +229,7 @@ export class CameraControls {
|
||||
camSettings.orientation.z,
|
||||
camSettings.orientation.w
|
||||
).invert()
|
||||
|
||||
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
|
||||
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
||||
this.useOrthographicCamera()
|
||||
@ -258,6 +258,48 @@ export class CameraControls {
|
||||
}
|
||||
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(() => {
|
||||
this.engineCommandManager.subscribeTo({
|
||||
event: 'camera_drag_end',
|
||||
@ -342,15 +384,7 @@ export class CameraControls {
|
||||
if (interaction === 'none') return
|
||||
|
||||
if (this.syncDirection === 'engineToClient') {
|
||||
this.throttledEngCmd({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_move',
|
||||
interaction,
|
||||
window: { x: event.clientX, y: event.clientY },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
this.moveDataFromLastFrame = [event.clientX, event.clientY, interaction]
|
||||
return
|
||||
}
|
||||
|
||||
@ -398,34 +432,19 @@ export class CameraControls {
|
||||
}
|
||||
|
||||
onMouseWheel = (event: WheelEvent) => {
|
||||
// Assume trackpad if the deltas are small and integers
|
||||
this.handleStart()
|
||||
|
||||
if (this.syncDirection === 'engineToClient') {
|
||||
const interactions = this.interactionGuards.zoom.scrollCallback(
|
||||
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()
|
||||
this.zoomDataFromLastFrame = event.deltaY
|
||||
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
|
||||
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||
this.pendingZoom *= 1 + event.deltaY * 0.01
|
||||
this.handleStart()
|
||||
this.pendingZoom = 1 + (event.deltaY / window.devicePixelRatio) * 0.001
|
||||
this.handleEnd()
|
||||
}
|
||||
|
||||
|
@ -568,6 +568,7 @@ export class SceneEntities {
|
||||
|
||||
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
||||
sceneInfra.resetMouseListeners()
|
||||
|
||||
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
||||
await this.setupSketch({
|
||||
sketchPathToNode,
|
||||
|
@ -26,6 +26,7 @@ export const AppHeader = ({
|
||||
|
||||
return (
|
||||
<header
|
||||
id="app-header"
|
||||
className={
|
||||
'w-full grid ' +
|
||||
styles.header +
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import React, {
|
||||
createContext,
|
||||
@ -7,10 +6,15 @@ import React, {
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||
import Client from '../editor/plugins/lsp/client'
|
||||
import {
|
||||
LanguageServerClient,
|
||||
FromServer,
|
||||
IntoServer,
|
||||
LspWorkerEventType,
|
||||
LanguageServerPlugin,
|
||||
} from '@kittycad/codemirror-lsp-client'
|
||||
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 { useStore } from 'useStore'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
@ -21,16 +25,15 @@ import { paths } from 'lib/paths'
|
||||
import { FileEntry } from 'lib/types'
|
||||
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
||||
import {
|
||||
LspWorkerEventType,
|
||||
KclWorkerOptions,
|
||||
CopilotWorkerOptions,
|
||||
LspWorker,
|
||||
} from 'editor/plugins/lsp/types'
|
||||
import { wasmUrl } from 'lang/wasm'
|
||||
import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { err } from 'lib/trap'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
|
||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||
return []
|
||||
@ -75,13 +78,11 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
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)
|
||||
@ -96,8 +97,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context.token
|
||||
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.
|
||||
// But the server happens async so we break this into two parts.
|
||||
@ -128,17 +127,34 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const fromServer: FromServer | Error = FromServer.create()
|
||||
if (err(fromServer)) return { lspClient: null }
|
||||
|
||||
const client = new Client(fromServer, intoServer)
|
||||
const lspClient = new LanguageServerClient({
|
||||
name: LspWorker.Kcl,
|
||||
fromServer,
|
||||
intoServer,
|
||||
initializedCallback: () => {
|
||||
setIsLspReady(true)
|
||||
},
|
||||
})
|
||||
|
||||
setIsLspReady(true)
|
||||
|
||||
const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl })
|
||||
return { lspClient }
|
||||
}, [
|
||||
// We need a token for authenticating the server.
|
||||
token,
|
||||
])
|
||||
|
||||
useMemo(() => {
|
||||
if (!isTauri() && isKclLspServerReady && kclLspClient && codeManager.code) {
|
||||
kclLspClient.textDocumentDidOpen({
|
||||
textDocument: {
|
||||
uri: `file:///${PROJECT_ENTRYPOINT}`,
|
||||
languageId: 'kcl',
|
||||
version: 1,
|
||||
text: codeManager.code,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [kclLspClient, isKclLspServerReady])
|
||||
|
||||
// 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
|
||||
// this use memo, as well as the directory structure, which I think is
|
||||
@ -148,10 +164,30 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
let plugin = null
|
||||
if (isKclLspServerReady && !TEST && kclLspClient) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = kclLanguage({
|
||||
const lsp = new KclLanguageSupport({
|
||||
documentUri: `file:///${PROJECT_ENTRYPOINT}`,
|
||||
workspaceFolders: getWorkspaceFolders(),
|
||||
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
|
||||
@ -159,27 +195,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return plugin
|
||||
}, [kclLspClient, isKclLspServerReady])
|
||||
|
||||
// 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(() => {
|
||||
if (!token || token === '' || TEST) {
|
||||
return { lspClient: null }
|
||||
@ -205,13 +220,13 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const fromServer: FromServer | Error = FromServer.create()
|
||||
if (err(fromServer)) return { lspClient: null }
|
||||
|
||||
const client = new Client(fromServer, intoServer)
|
||||
|
||||
setIsCopilotReady(true)
|
||||
|
||||
const lspClient = new LanguageServerClient({
|
||||
client,
|
||||
name: LspWorker.Copilot,
|
||||
fromServer,
|
||||
intoServer,
|
||||
initializedCallback: () => {
|
||||
setIsCopilotReady(true)
|
||||
},
|
||||
})
|
||||
return { lspClient }
|
||||
}, [token])
|
||||
|
@ -71,7 +71,7 @@ import { TEST } from 'env'
|
||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import toast from 'react-hot-toast'
|
||||
import { EditorSelection } from '@uiw/react-codemirror'
|
||||
import { EditorSelection, Transaction } from '@uiw/react-codemirror'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
@ -80,6 +80,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -101,7 +102,7 @@ export const ModelingMachineProvider = ({
|
||||
settings: {
|
||||
context: {
|
||||
app: { theme, enableSSAO },
|
||||
modeling: { defaultUnit, highlightEdges },
|
||||
modeling: { defaultUnit, highlightEdges, showScaleGrid },
|
||||
},
|
||||
},
|
||||
} = useSettingsAuthContext()
|
||||
@ -116,6 +117,7 @@ export const ModelingMachineProvider = ({
|
||||
theme: theme.current,
|
||||
highlightEdges: highlightEdges.current,
|
||||
enableSSAO: enableSSAO.current,
|
||||
showScaleGrid: showScaleGrid.current,
|
||||
})
|
||||
const { htmlRef } = useStore((s) => ({
|
||||
htmlRef: s.htmlRef,
|
||||
@ -281,11 +283,15 @@ export const ModelingMachineProvider = ({
|
||||
const dispatchSelection = (selection?: EditorSelection) => {
|
||||
if (!selection) return // TODO less of hack for the below please
|
||||
if (!editorManager.editorView) return
|
||||
editorManager.lastSelectionEvent = Date.now()
|
||||
setTimeout(() => {
|
||||
if (editorManager.editorView) {
|
||||
editorManager.editorView.dispatch({ selection })
|
||||
}
|
||||
if (!editorManager.editorView) return
|
||||
editorManager.editorView.dispatch({
|
||||
selection,
|
||||
annotations: [
|
||||
modelingMachineEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
let selections: Selections = {
|
||||
@ -328,11 +334,6 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
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 {
|
||||
selectionRanges: selections,
|
||||
}
|
||||
|
@ -84,6 +84,10 @@ export const KclEditorPane = () => {
|
||||
|
||||
const textWrapping = context.textEditor.textWrapping
|
||||
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 editorExtensions = useMemo(() => {
|
||||
@ -134,7 +138,6 @@ export const KclEditorPane = () => {
|
||||
highlightSelectionMatches(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
rectangularSelection(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
interact({
|
||||
rules: [
|
||||
@ -173,13 +176,7 @@ export const KclEditorPane = () => {
|
||||
}
|
||||
|
||||
return extensions
|
||||
}, [
|
||||
kclLSP,
|
||||
copilotLSP,
|
||||
textWrapping.current,
|
||||
cursorBlinking.current,
|
||||
codeMirrorHotkeys,
|
||||
])
|
||||
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
|
||||
|
||||
const initialCode = useRef(codeManager.code)
|
||||
|
||||
@ -192,9 +189,9 @@ export const KclEditorPane = () => {
|
||||
value={initialCode.current}
|
||||
extensions={editorExtensions}
|
||||
theme={theme}
|
||||
onCreateEditor={(_editorView) =>
|
||||
onCreateEditor={(_editorView) => {
|
||||
editorManager.setEditorView(_editorView)
|
||||
}
|
||||
}}
|
||||
indentWithTab={false}
|
||||
basicSetup={false}
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
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 { useStore } from 'useStore'
|
||||
import { Tab } from '@headlessui/react'
|
||||
@ -56,15 +56,19 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
bottomRight: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div className={styles.grid + ' flex-1'}>
|
||||
<ModelingSidebarSection panes={topPanes} />
|
||||
<ModelingSidebarSection panes={bottomPanes} alignButtons="end" />
|
||||
<div id="app-sidebar" className={styles.grid + ' flex-1'}>
|
||||
<ModelingSidebarSection id="sidebar-top" panes={topPanes} />
|
||||
<ModelingSidebarSection
|
||||
id="sidebar-bottom"
|
||||
panes={bottomPanes}
|
||||
alignButtons="end"
|
||||
/>
|
||||
</div>
|
||||
</Resizable>
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelingSidebarSectionProps {
|
||||
interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
panes: SidebarPane[]
|
||||
alignButtons?: 'start' | 'end'
|
||||
}
|
||||
@ -72,6 +76,8 @@ interface ModelingSidebarSectionProps {
|
||||
function ModelingSidebarSection({
|
||||
panes,
|
||||
alignButtons = 'start',
|
||||
className,
|
||||
...props
|
||||
}: ModelingSidebarSectionProps) {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||
@ -123,7 +129,7 @@ function ModelingSidebarSection({
|
||||
}, [showDebugPanel.current, togglePane, openPanes])
|
||||
|
||||
return (
|
||||
<div className="group contents">
|
||||
<div className={'group contents ' + className} {...props}>
|
||||
<Tab.Group
|
||||
vertical
|
||||
selectedIndex={
|
||||
@ -135,6 +141,7 @@ function ModelingSidebarSection({
|
||||
}}
|
||||
>
|
||||
<Tab.List
|
||||
id={`${props.id}-ribbon`}
|
||||
className={
|
||||
'pointer-events-auto ' +
|
||||
(alignButtons === 'start'
|
||||
@ -161,6 +168,7 @@ function ModelingSidebarSection({
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels
|
||||
id={`${props.id}-pane`}
|
||||
as="article"
|
||||
className={
|
||||
'col-start-2 col-span-1 ' +
|
||||
|
@ -134,6 +134,11 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
})
|
||||
},
|
||||
setEngineScaleGridVisibility: (context) => {
|
||||
engineCommandManager.setScaleGridVisibility(
|
||||
context.modeling.showScaleGrid.current
|
||||
)
|
||||
},
|
||||
setClientTheme: (context) => {
|
||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||
sceneInfra.theme = opposingTheme
|
||||
|
@ -1,16 +1,25 @@
|
||||
import { StateField, StateEffect } from '@codemirror/state'
|
||||
import { StateField, StateEffect, Annotation } from '@codemirror/state'
|
||||
import { EditorView, Decoration } from '@codemirror/view'
|
||||
|
||||
export { EditorView }
|
||||
|
||||
export const addLineHighlight = StateEffect.define<[number, number]>()
|
||||
|
||||
const addLineHighlightAnnotation = Annotation.define<null>()
|
||||
export const addLineHighlightEvent = addLineHighlightAnnotation.of(null)
|
||||
|
||||
export const lineHighlightField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none
|
||||
},
|
||||
update(lines, tr) {
|
||||
lines = lines.map(tr.changes)
|
||||
|
||||
const isLineHighlightEvent = tr.annotation(addLineHighlightEvent.type)
|
||||
if (isLineHighlightEvent === undefined) {
|
||||
return lines
|
||||
}
|
||||
|
||||
const deco = []
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(addLineHighlight)) {
|
||||
|
@ -1,13 +1,25 @@
|
||||
import { hasNextSnippetField } from '@codemirror/autocomplete'
|
||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||
import { EditorSelection, SelectionRange } from '@codemirror/state'
|
||||
import { engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { addLineHighlight } from './highlightextension'
|
||||
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint'
|
||||
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
||||
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 {
|
||||
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
|
||||
@ -22,8 +34,6 @@ export default class EditorManager {
|
||||
codeBasedSelections: [],
|
||||
}
|
||||
|
||||
private _lastSelectionEvent: number | null = null
|
||||
lastSelection: string = ''
|
||||
private _lastEvent: { event: string; time: number } | null = null
|
||||
|
||||
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
|
||||
@ -57,10 +67,6 @@ export default class EditorManager {
|
||||
this._selectionRanges = selectionRanges
|
||||
}
|
||||
|
||||
set lastSelectionEvent(time: number) {
|
||||
this._lastSelectionEvent = time
|
||||
}
|
||||
|
||||
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
|
||||
this._modelingSend = send
|
||||
}
|
||||
@ -83,32 +89,39 @@ export default class EditorManager {
|
||||
|
||||
setHighlightRange(selection: Selection['range']): void {
|
||||
this._highlightRange = selection
|
||||
const editorView = this.editorView
|
||||
const safeEnd = Math.min(
|
||||
selection[1],
|
||||
editorView?.state.doc.length || selection[1]
|
||||
this._editorView?.state.doc.length || selection[1]
|
||||
)
|
||||
if (editorView) {
|
||||
editorView.dispatch({
|
||||
if (this._editorView) {
|
||||
this._editorView.dispatch({
|
||||
effects: addLineHighlight.of([selection[0], safeEnd]),
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
addLineHighlightEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
clearDiagnostics(): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
|
||||
this.setDiagnostics([])
|
||||
}
|
||||
|
||||
setDiagnostics(diagnostics: Diagnostic[]): void {
|
||||
if (!this.editorView) return
|
||||
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
|
||||
if (!this._editorView) return
|
||||
|
||||
this._editorView.dispatch({
|
||||
effects: [setDiagnosticsEffect.of(diagnostics)],
|
||||
annotations: [setDiagnosticsEvent, Transaction.addToHistory.of(false)],
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
@ -122,9 +135,7 @@ export default class EditorManager {
|
||||
uniqueDiagnostics.add(diagnostic)
|
||||
})
|
||||
|
||||
this.editorView.dispatch(
|
||||
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
|
||||
)
|
||||
this.setDiagnostics([...uniqueDiagnostics])
|
||||
}
|
||||
|
||||
undo() {
|
||||
@ -174,50 +185,33 @@ export default class EditorManager {
|
||||
].range[1]
|
||||
)
|
||||
)
|
||||
if (!this.editorView) {
|
||||
|
||||
if (!this._editorView) {
|
||||
return
|
||||
}
|
||||
this.editorView.dispatch({
|
||||
|
||||
this._editorView.dispatch({
|
||||
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 {
|
||||
// If we are just fucking around in a snippet, return early and don't
|
||||
// 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) {
|
||||
if (!this._editorView) {
|
||||
this.setEditorView(viewUpdate.view)
|
||||
}
|
||||
const selString = stringifyRanges(
|
||||
viewUpdate?.state?.selection?.ranges || []
|
||||
)
|
||||
|
||||
if (selString === this.lastSelection) {
|
||||
// onUpdate is noisy and is fired a lot by extensions
|
||||
// since we're only interested in selections changes we can ignore most of these.
|
||||
const ranges = viewUpdate?.state?.selection?.ranges || []
|
||||
if (ranges.length === 0) {
|
||||
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 (
|
||||
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',
|
||||
@ -266,7 +260,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.
|
||||
/// This has been heavily modified from their original implementation but we are
|
||||
/// still grateful.
|
||||
import { indentUnit } from '@codemirror/language'
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
KeyBinding,
|
||||
PluginValue,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
keymap,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
Annotation,
|
||||
@ -17,17 +22,41 @@ import {
|
||||
Transaction,
|
||||
} from '@codemirror/state'
|
||||
import { completionStatus } from '@codemirror/autocomplete'
|
||||
import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
|
||||
import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import {
|
||||
LanguageServerPlugin,
|
||||
documentUri,
|
||||
TransactionAnnotation,
|
||||
offsetToPos,
|
||||
posToOffset,
|
||||
LanguageServerOptions,
|
||||
LanguageServerClient,
|
||||
docPathFacet,
|
||||
languageId,
|
||||
workspaceFolders,
|
||||
} from 'editor/plugins/lsp/plugin'
|
||||
TransactionInfo,
|
||||
updateInfo,
|
||||
RelevantUpdate,
|
||||
lspPlugin,
|
||||
} from '@kittycad/codemirror-lsp-client'
|
||||
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'
|
||||
|
||||
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 changesDelay = 600
|
||||
|
||||
interface Suggestion {
|
||||
text: string
|
||||
displayText: string
|
||||
@ -38,15 +67,10 @@ interface Suggestion {
|
||||
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 {
|
||||
ghostText: GhostText | null
|
||||
}
|
||||
|
||||
interface GhostText {
|
||||
text: string
|
||||
displayText: string
|
||||
@ -60,11 +84,24 @@ interface GhostText {
|
||||
uuid: string
|
||||
}
|
||||
|
||||
export const completionDecoration = StateField.define<CompletionState>({
|
||||
const completionDecoration = StateField.define<CompletionState>({
|
||||
create(_state: EditorState) {
|
||||
return { ghostText: null }
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
// We only care about transactions with effects.
|
||||
if (!transaction.effects) {
|
||||
return state
|
||||
}
|
||||
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(addSuggestion)) {
|
||||
// When adding a suggestion, we set th ghostText
|
||||
@ -160,331 +197,482 @@ export const completionDecoration = StateField.define<CompletionState>({
|
||||
),
|
||||
})
|
||||
|
||||
const copilotEvent = Annotation.define<null>()
|
||||
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
|
||||
const infos = updateInfo(update)
|
||||
|
||||
/****************************************************************************
|
||||
************************* COMMANDS ******************************************
|
||||
*****************************************************************************/
|
||||
|
||||
const acceptSuggestionCommand = (
|
||||
copilotClient: LanguageServerClient,
|
||||
view: EditorView
|
||||
) => {
|
||||
// We delete the ghost text and insert the suggestion.
|
||||
// We also set the cursor to the end of the suggestion.
|
||||
const ghostText = view.state.field(completionDecoration)!.ghostText
|
||||
if (!ghostText) {
|
||||
return false
|
||||
// Make sure we are not in a snippet
|
||||
if (infos.some((info: TransactionInfo) => info.inSnippet)) {
|
||||
return {
|
||||
overall: false,
|
||||
userSelect: false,
|
||||
time: null,
|
||||
}
|
||||
}
|
||||
|
||||
const ghostTextStart = ghostText.displayPos
|
||||
const ghostTextEnd = ghostText.endGhostText
|
||||
|
||||
const actualTextStart = ghostText.startPos
|
||||
const actualTextEnd = ghostText.endPos
|
||||
|
||||
const replacementEnd = ghostText.endReplacement
|
||||
|
||||
const suggestion = ghostText.text
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
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
|
||||
return {
|
||||
overall: infos.some(
|
||||
(info: TransactionInfo) =>
|
||||
info.transaction.annotation(copilotPluginEvent.type) !== undefined ||
|
||||
info.annotations.includes(TransactionAnnotation.UserSelect) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserInput) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserMove)
|
||||
),
|
||||
userSelect: infos.some((info: TransactionInfo) =>
|
||||
info.annotations.includes(TransactionAnnotation.UserSelect)
|
||||
),
|
||||
time: infos.length ? infos[0].time : null,
|
||||
}
|
||||
|
||||
const ghostTextStart = ghostText.displayPos
|
||||
const ghostTextEnd = ghostText.endGhostText
|
||||
|
||||
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)
|
||||
// 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 viewUpdate: ViewUpdate | null = null
|
||||
|
||||
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) {
|
||||
view.dispatch({
|
||||
selection: { anchor: ghostTextStart + indent.length },
|
||||
effects: typeFirst.of(indent.length),
|
||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||
})
|
||||
return true
|
||||
} else if (key === 'Tab') {
|
||||
return acceptSuggestionCommand(copilotClient, view)
|
||||
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
|
||||
return rejectSuggestionCommand(copilotClient, view)
|
||||
} else if (ghostText.displayText.length === 1) {
|
||||
return acceptSuggestionCommand(copilotClient, view)
|
||||
} else {
|
||||
// Use this to delete the first letter of the suggestion
|
||||
view.dispatch({
|
||||
selection: { anchor: ghostTextStart + 1 },
|
||||
effects: typeFirst.of(1),
|
||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||
private queuedUids: string[] = []
|
||||
|
||||
private _deffererCodeUpdate = deferExecution(() => {
|
||||
if (this.viewUpdate === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.requestCompletions()
|
||||
}, changesDelay)
|
||||
|
||||
private _deffererUserSelect = deferExecution(() => {
|
||||
if (this.viewUpdate === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.rejectSuggestionCommand()
|
||||
}, changesDelay)
|
||||
|
||||
constructor(client: LanguageServerClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
update(viewUpdate: ViewUpdate) {
|
||||
this.viewUpdate = viewUpdate
|
||||
|
||||
const isRelevant = relevantUpdate(viewUpdate)
|
||||
if (!isRelevant.overall) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we have a user select event, we want to clear the ghost text.
|
||||
if (isRelevant.userSelect) {
|
||||
this._deffererUserSelect(true)
|
||||
return
|
||||
}
|
||||
|
||||
this.lastPos = this.viewUpdate.state.selection.main.head
|
||||
this._deffererCodeUpdate(true)
|
||||
}
|
||||
|
||||
ghostText(): GhostText | null {
|
||||
if (!this.viewUpdate) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null
|
||||
)
|
||||
}
|
||||
|
||||
containsGhostText(): boolean {
|
||||
return this.ghostText() !== null
|
||||
}
|
||||
|
||||
autocompleting(): boolean {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
return completionStatus(this.viewUpdate.state) === 'active'
|
||||
}
|
||||
|
||||
notFocused(): boolean {
|
||||
if (!this.viewUpdate) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !this.viewUpdate.view.hasFocus
|
||||
}
|
||||
|
||||
async requestCompletions(): Promise<void> {
|
||||
if (
|
||||
this.viewUpdate === null ||
|
||||
this.containsGhostText() ||
|
||||
this.autocompleting() ||
|
||||
this.notFocused() ||
|
||||
!this.viewUpdate.docChanged
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const pos = this.viewUpdate.state.selection.main.head
|
||||
|
||||
// Check if the position has changed
|
||||
if (pos !== this.lastPos) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current position and source
|
||||
const state = this.viewUpdate.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.viewUpdate.view.state.doc.lineAt(pos)
|
||||
if (line.to !== pos) {
|
||||
const ending = this.viewUpdate.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.viewUpdate.view.dispatch({
|
||||
changes: {
|
||||
from: pos,
|
||||
to: line.to,
|
||||
insert: '',
|
||||
},
|
||||
selection: { anchor: pos },
|
||||
effects: typeFirst.of(ending.length),
|
||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.viewUpdate.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 {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
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.viewUpdate.view.dispatch({
|
||||
changes: {
|
||||
from: ghostTextStart,
|
||||
to: ghostTextEnd,
|
||||
insert: '',
|
||||
},
|
||||
effects: acceptSuggestion.of(null),
|
||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||
})
|
||||
|
||||
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
|
||||
|
||||
this.viewUpdate.view.dispatch({
|
||||
changes: {
|
||||
from: actualTextStart,
|
||||
to: tmpTextEnd,
|
||||
insert: suggestion,
|
||||
},
|
||||
selection: { anchor: actualTextEnd },
|
||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
|
||||
})
|
||||
|
||||
this.accept(ghostText.uuid)
|
||||
return true
|
||||
}
|
||||
|
||||
rejectSuggestionCommand(): boolean {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
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.viewUpdate.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) {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
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.viewUpdate.view.state.facet(indentUnit)
|
||||
|
||||
if (key === tabKey && ghostText.displayText.startsWith(indent)) {
|
||||
this.viewUpdate.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.viewUpdate.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) =>
|
||||
EditorView.domEventHandlers({
|
||||
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||
let plugin: CompletionRequester | null = null
|
||||
const completionPlugin = ViewPlugin.define(
|
||||
(view) => (plugin = new CompletionRequester(options.client))
|
||||
)
|
||||
|
||||
const domHandlers = EditorView.domEventHandlers({
|
||||
keydown(event, view) {
|
||||
if (
|
||||
event.key !== 'Shift' &&
|
||||
event.key !== 'Control' &&
|
||||
event.key !== 'Alt' &&
|
||||
event.key !== 'Backspace' &&
|
||||
event.key !== 'Delete' &&
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
},
|
||||
mousedown(event, view) {
|
||||
return rejectSuggestionCommand(copilotClient, view)
|
||||
})
|
||||
|
||||
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: (view: EditorView): boolean => {
|
||||
if (view.plugin === null) return false
|
||||
|
||||
const viewCompletionPlugin = (copilotClient: LanguageServerClient) =>
|
||||
EditorView.updateListener.of((update) => {
|
||||
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
|
||||
// Get the current plugin from the map.
|
||||
const p = view.plugin(completionPlugin)
|
||||
if (p === null) return false
|
||||
|
||||
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 p.rejectSuggestionCommand()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Delete',
|
||||
run: (view: EditorView): boolean => {
|
||||
if (view.plugin === null) return false
|
||||
|
||||
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 plugin from the map.
|
||||
const p = view.plugin(completionPlugin)
|
||||
if (p === null) return false
|
||||
|
||||
// Get the current position and source
|
||||
const state = update.state
|
||||
const pos = state.selection.main.head
|
||||
const source = state.doc.toString()
|
||||
return p.rejectSuggestionCommand()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const dUri = state.facet(documentUri)
|
||||
const path = dUri.split('/').pop()!
|
||||
const relativePath = dUri.replace('file://', '')
|
||||
const copilotAutocompleteKeymapExt = Prec.highest(
|
||||
keymap.computeN([], () => [copilotAutocompleteKeymap])
|
||||
)
|
||||
|
||||
// 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 [
|
||||
documentUri.of(options.documentUri),
|
||||
languageId.of('kcl'),
|
||||
workspaceFolders.of(options.workspaceFolders),
|
||||
ViewPlugin.define(
|
||||
(view) =>
|
||||
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
|
||||
),
|
||||
lspPlugin(options),
|
||||
completionPlugin,
|
||||
copilotAutocompleteKeymapExt,
|
||||
domHandlers,
|
||||
completionDecoration,
|
||||
Prec.highest(completionPlugin(options.client)),
|
||||
Prec.highest(viewCompletionPlugin(options.client)),
|
||||
completionRequester(options.client),
|
||||
EditorView.focusChangeEffect.of((_, focusing) => {
|
||||
if (plugin === null) return null
|
||||
|
||||
plugin.rejectSuggestionCommand()
|
||||
|
||||
return null
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
@ -1,155 +1,119 @@
|
||||
import { autocompletion } from '@codemirror/autocomplete'
|
||||
import { Extension, EditorState, Prec } from '@codemirror/state'
|
||||
import { Extension } from '@codemirror/state'
|
||||
import { ViewPlugin, PluginValue, ViewUpdate } from '@codemirror/view'
|
||||
import {
|
||||
ViewPlugin,
|
||||
hoverTooltip,
|
||||
EditorView,
|
||||
keymap,
|
||||
KeyBinding,
|
||||
tooltips,
|
||||
} from '@codemirror/view'
|
||||
import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
|
||||
import { offsetToPos } from 'editor/plugins/lsp/util'
|
||||
import { LanguageServerOptions } from 'editor/plugins/lsp'
|
||||
import { syntaxTree, indentService, foldService } from '@codemirror/language'
|
||||
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
|
||||
import {
|
||||
LanguageServerPlugin,
|
||||
documentUri,
|
||||
languageId,
|
||||
workspaceFolders,
|
||||
} from 'editor/plugins/lsp/plugin'
|
||||
LanguageServerOptions,
|
||||
updateInfo,
|
||||
TransactionInfo,
|
||||
RelevantUpdate,
|
||||
TransactionAnnotation,
|
||||
LanguageServerClient,
|
||||
lspPlugin,
|
||||
} from '@kittycad/codemirror-lsp-client'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { codeManager, editorManager, kclManager } from 'lib/singletons'
|
||||
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'
|
||||
|
||||
export const kclIndentService = () => {
|
||||
// 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)
|
||||
const changesDelay = 600
|
||||
|
||||
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
|
||||
const infos = updateInfo(update)
|
||||
// Make sure we are not in a snippet
|
||||
if (infos.some((info: TransactionInfo) => info.inSnippet)) {
|
||||
return {
|
||||
overall: false,
|
||||
userSelect: false,
|
||||
time: null,
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
return {
|
||||
overall: infos.some(
|
||||
(info: TransactionInfo) =>
|
||||
info.annotations.includes(TransactionAnnotation.UserSelect) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserInput) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserDelete) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserRedo) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserMove) ||
|
||||
info.annotations.includes(TransactionAnnotation.FormatCode)
|
||||
),
|
||||
userSelect: infos.some((info: TransactionInfo) =>
|
||||
info.annotations.includes(TransactionAnnotation.UserSelect)
|
||||
),
|
||||
time: infos.length ? infos[0].time : null,
|
||||
}
|
||||
}
|
||||
|
||||
// A view plugin that requests completions from the server after a delay
|
||||
export class KclPlugin implements PluginValue {
|
||||
private viewUpdate: ViewUpdate | null = null
|
||||
private client: LanguageServerClient
|
||||
|
||||
constructor(client: LanguageServerClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
private _deffererCodeUpdate = deferExecution(() => {
|
||||
if (this.viewUpdate === null) {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const isRelevant = relevantUpdate(viewUpdate)
|
||||
if (!isRelevant.overall) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we have a user select event, we want to update what parts are
|
||||
// highlighted.
|
||||
if (isRelevant.userSelect) {
|
||||
this._deffererUserSelect(true)
|
||||
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 {
|
||||
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 [
|
||||
documentUri.of(options.documentUri),
|
||||
languageId.of('kcl'),
|
||||
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,
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
}),
|
||||
lspPlugin(options),
|
||||
ViewPlugin.define(() => new KclPlugin(options.client)),
|
||||
]
|
||||
}
|
||||
|
@ -5,18 +5,33 @@ import {
|
||||
defineLanguageFacet,
|
||||
LanguageSupport,
|
||||
} from '@codemirror/language'
|
||||
import { LanguageServerClient } from 'editor/plugins/lsp'
|
||||
import {
|
||||
LanguageServerClient,
|
||||
LanguageServerPlugin,
|
||||
} from '@kittycad/codemirror-lsp-client'
|
||||
import { kclPlugin } from '.'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { parser as jsParser } from '@lezer/javascript'
|
||||
import { EditorState } from '@uiw/react-codemirror'
|
||||
import KclParser from './parser'
|
||||
|
||||
const data = defineLanguageFacet({})
|
||||
const data = defineLanguageFacet({
|
||||
// https://codemirror.net/docs/ref/#commands.CommentTokens
|
||||
commentTokens: {
|
||||
line: '//',
|
||||
block: {
|
||||
open: '/*',
|
||||
close: '*/',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export interface LanguageOptions {
|
||||
workspaceFolders: LSP.WorkspaceFolder[]
|
||||
documentUri: string
|
||||
client: LanguageServerClient
|
||||
processLspNotification?: (
|
||||
plugin: LanguageServerPlugin,
|
||||
notification: LSP.NotificationMessage
|
||||
) => void
|
||||
}
|
||||
|
||||
class KclLanguage extends Language {
|
||||
@ -26,36 +41,27 @@ class KclLanguage extends Language {
|
||||
workspaceFolders: options.workspaceFolders,
|
||||
allowHTMLContent: true,
|
||||
client: options.client,
|
||||
processLspNotification: options.processLspNotification,
|
||||
})
|
||||
|
||||
const parser = new KclParser()
|
||||
|
||||
super(
|
||||
data,
|
||||
// For now let's use the javascript parser.
|
||||
// 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: '*/',
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
parser,
|
||||
[plugin],
|
||||
'kcl'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
|
||||
const lang = new KclLanguage(options)
|
||||
export default class KclLanguageSupport extends LanguageSupport {
|
||||
constructor(options: LanguageOptions) {
|
||||
const lang = new KclLanguage(options)
|
||||
|
||||
return new LanguageSupport(lang)
|
||||
super(lang)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
// Extends the codemirror Parser for kcl.
|
||||
// This is really just a no-op parser since we use semantic tokens from the LSP
|
||||
// server.
|
||||
|
||||
import {
|
||||
Parser,
|
||||
@ -7,91 +9,27 @@ import {
|
||||
PartialParse,
|
||||
Tree,
|
||||
NodeType,
|
||||
NodeSet,
|
||||
} 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 { tags, styleTags } from '@lezer/highlight'
|
||||
|
||||
export default class KclParser extends Parser {
|
||||
private client: LanguageServerClient
|
||||
|
||||
constructor(client: LanguageServerClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
createParse(
|
||||
input: Input,
|
||||
fragments: readonly TreeFragment[],
|
||||
ranges: readonly { from: number; to: number }[]
|
||||
): PartialParse {
|
||||
let parse: PartialParse = new Context(this, input, fragments, ranges)
|
||||
let parse: PartialParse = new Context(input)
|
||||
return parse
|
||||
}
|
||||
|
||||
getTokenTypes(): string[] {
|
||||
return this.client.getServerCapabilities().semanticTokensProvider!.legend
|
||||
.tokenTypes
|
||||
}
|
||||
|
||||
getSemanticTokens(): SemanticToken[] {
|
||||
return this.client.getSemanticTokens()
|
||||
}
|
||||
}
|
||||
|
||||
class Context implements PartialParse {
|
||||
private parser: KclParser
|
||||
private input: DocInput
|
||||
private fragments: readonly TreeFragment[]
|
||||
private ranges: readonly { from: number; to: number }[]
|
||||
|
||||
private nodeTypes: { [key: string]: NodeType }
|
||||
stoppedAt: number = 0
|
||||
|
||||
private semanticTokens: SemanticToken[] = []
|
||||
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
|
||||
constructor(input: Input) {
|
||||
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 {
|
||||
@ -99,67 +37,8 @@ class Context implements PartialParse {
|
||||
}
|
||||
|
||||
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
|
||||
return tree
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
return new Tree(NodeType.none, [], [], this.input.doc.length)
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
export enum LspWorker {
|
||||
@ -17,11 +19,6 @@ export interface CopilotWorkerOptions {
|
||||
apiBaseUrl: string
|
||||
}
|
||||
|
||||
export enum LspWorkerEventType {
|
||||
Init = 'init',
|
||||
Call = 'call',
|
||||
}
|
||||
|
||||
export interface LspWorkerEvent {
|
||||
eventType: LspWorkerEventType
|
||||
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 init, {
|
||||
ServerConfig,
|
||||
@ -7,7 +12,6 @@ import init, {
|
||||
} from 'wasm-lib/pkg/wasm_lib'
|
||||
import * as jsrpc from 'json-rpc-2.0'
|
||||
import {
|
||||
LspWorkerEventType,
|
||||
LspWorkerEvent,
|
||||
LspWorker,
|
||||
KclWorkerOptions,
|
||||
|
@ -3,7 +3,7 @@ import { useStore } from '../useStore'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { makeDefaultPlanes } from 'lang/wasm'
|
||||
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
||||
|
||||
export function useSetupEngineManager(
|
||||
streamRef: React.RefObject<HTMLDivElement>,
|
||||
@ -13,11 +13,13 @@ export function useSetupEngineManager(
|
||||
theme: Themes.System,
|
||||
highlightEdges: true,
|
||||
enableSSAO: true,
|
||||
showScaleGrid: false,
|
||||
} as {
|
||||
pool: string | null
|
||||
theme: Themes
|
||||
highlightEdges: boolean
|
||||
enableSSAO: boolean
|
||||
showScaleGrid: boolean
|
||||
}
|
||||
) {
|
||||
const {
|
||||
@ -66,6 +68,9 @@ export function useSetupEngineManager(
|
||||
makeDefaultPlanes: () => {
|
||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||
},
|
||||
modifyGrid: (hidden: boolean) => {
|
||||
return modifyGrid(kclManager.engineCommandManager, hidden)
|
||||
},
|
||||
})
|
||||
setStreamDimensions({
|
||||
streamWidth: quadWidth,
|
||||
|
@ -6,10 +6,13 @@ import { isTauri } from 'lib/isTauri'
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs'
|
||||
import toast from 'react-hot-toast'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { KeyBinding } from '@uiw/react-codemirror'
|
||||
import { Annotation, KeyBinding, Transaction } from '@uiw/react-codemirror'
|
||||
|
||||
const PERSIST_CODE_TOKEN = 'persistCode'
|
||||
|
||||
const codeManagerUpdateAnnotation = Annotation.define<null>()
|
||||
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(null)
|
||||
|
||||
export default class CodeManager {
|
||||
private _code: string = bracket
|
||||
#updateState: (arg: string) => void = () => {}
|
||||
@ -90,6 +93,10 @@ export default class CodeManager {
|
||||
to: editorManager.editorView.state.doc.length,
|
||||
insert: code,
|
||||
},
|
||||
annotations: [
|
||||
codeManagerUpdateEvent,
|
||||
Transaction.addToHistory.of(true),
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
||||
import { posToOffset } from 'editor/plugins/lsp/util'
|
||||
import { posToOffset } from '@kittycad/codemirror-lsp-client'
|
||||
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
|
||||
import { Text } from '@codemirror/state'
|
||||
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
createPipeSubstitution,
|
||||
} from './modifyAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { warn } from 'node:console'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
|
@ -282,8 +282,10 @@ function moreNodePathFromSourceRange(
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
if (_node.type === 'PipeSubstitution' && isInRange) return path
|
||||
console.error('not implemented: ' + node.type)
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
|
@ -1143,6 +1143,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.getAst = cb
|
||||
}
|
||||
private makeDefaultPlanes: () => Promise<DefaultPlanes> | null = () => null
|
||||
private modifyGrid: (hidden: boolean) => Promise<void> | null = () => null
|
||||
|
||||
start({
|
||||
setMediaStream,
|
||||
@ -1152,10 +1153,12 @@ export class EngineCommandManager extends EventTarget {
|
||||
executeCode,
|
||||
token,
|
||||
makeDefaultPlanes,
|
||||
modifyGrid,
|
||||
settings = {
|
||||
theme: Themes.Dark,
|
||||
highlightEdges: true,
|
||||
enableSSAO: true,
|
||||
showScaleGrid: false,
|
||||
},
|
||||
}: {
|
||||
setMediaStream: (stream: MediaStream) => void
|
||||
@ -1165,13 +1168,16 @@ export class EngineCommandManager extends EventTarget {
|
||||
executeCode: () => void
|
||||
token?: string
|
||||
makeDefaultPlanes: () => Promise<DefaultPlanes>
|
||||
modifyGrid: (hidden: boolean) => Promise<void>
|
||||
settings?: {
|
||||
theme: Themes
|
||||
highlightEdges: boolean
|
||||
enableSSAO: boolean
|
||||
showScaleGrid: boolean
|
||||
}
|
||||
}) {
|
||||
this.makeDefaultPlanes = makeDefaultPlanes
|
||||
this.modifyGrid = modifyGrid
|
||||
if (width === 0 || height === 0) {
|
||||
return
|
||||
}
|
||||
@ -1247,31 +1253,11 @@ export class EngineCommandManager extends EventTarget {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
|
||||
this.initPlanes().then(async () => {
|
||||
// Hide the grid and grid scale text.
|
||||
this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible' as any,
|
||||
// Found in engine/constants.h
|
||||
object_id: 'cfa78409-653d-4c26-96f1-7c45fb784840',
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
|
||||
this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible' as any,
|
||||
// Found in engine/constants.h
|
||||
object_id: '10782f33-f588-4668-8bcd-040502d26590',
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
|
||||
// We want modify the grid first because we don't want it to flash.
|
||||
// Ideally these would already be default hidden in engine (TODO do
|
||||
// that) https://github.com/KittyCAD/engine/issues/2282
|
||||
this.modifyGrid(!settings.showScaleGrid)?.then(async () => {
|
||||
await this.initPlanes()
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
await executeCode()
|
||||
@ -1753,6 +1739,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
if (
|
||||
(cmd.type === 'camera_drag_move' ||
|
||||
cmd.type === 'handle_mouse_drag_move' ||
|
||||
cmd.type === 'default_camera_zoom' ||
|
||||
cmd.type === ('default_camera_perspective_settings' as any)) &&
|
||||
this.engineConnection?.unreliableDataChannel &&
|
||||
!forceWebsocket
|
||||
@ -2089,4 +2076,12 @@ export class EngineCommandManager extends EventTarget {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visibility of the scale grid in the engine scene.
|
||||
* @param visible - whether to show or hide the scale grid
|
||||
*/
|
||||
setScaleGridVisibility(visible: boolean) {
|
||||
this.modifyGrid(!visible)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import init, {
|
||||
get_tangential_arc_to_info,
|
||||
program_memory_init,
|
||||
make_default_planes,
|
||||
modify_grid,
|
||||
coredump,
|
||||
toml_stringify,
|
||||
default_app_settings,
|
||||
@ -237,6 +238,20 @@ export const makeDefaultPlanes = async (
|
||||
}
|
||||
}
|
||||
|
||||
export const modifyGrid = async (
|
||||
engineCommandManager: EngineCommandManager,
|
||||
hidden: boolean
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await modify_grid(engineCommandManager, hidden)
|
||||
return
|
||||
} catch (e) {
|
||||
// TODO: do something real with the error.
|
||||
console.log('modify grid error', e)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
export function lexer(str: string): Token[] | Error {
|
||||
return lexer_wasm(str)
|
||||
}
|
||||
|
@ -98,123 +98,119 @@ export type CommandConfig<
|
||||
export type CommandArgumentConfig<
|
||||
OutputType,
|
||||
C = ContextFrom<AnyStateMachine>
|
||||
> =
|
||||
> = {
|
||||
description?: string
|
||||
required:
|
||||
| boolean
|
||||
| ((
|
||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: C
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
} & (
|
||||
| {
|
||||
description?: string
|
||||
required:
|
||||
| boolean
|
||||
inputType: 'options'
|
||||
options:
|
||||
| CommandArgumentOption<OutputType>[]
|
||||
| ((
|
||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
commandBarContext: {
|
||||
argumentsToSubmit: Record<string, unknown>
|
||||
}, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: C
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
} & (
|
||||
| {
|
||||
inputType: 'options'
|
||||
options:
|
||||
| CommandArgumentOption<OutputType>[]
|
||||
| ((
|
||||
commandBarContext: {
|
||||
argumentsToSubmit: Record<string, unknown>
|
||||
}, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: C
|
||||
) => CommandArgumentOption<OutputType>[])
|
||||
optionsFromContext?: (
|
||||
context: C
|
||||
) => CommandArgumentOption<OutputType>[]
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: C
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: C) => OutputType
|
||||
}
|
||||
| {
|
||||
inputType: 'selection'
|
||||
selectionTypes: Selection['type'][]
|
||||
multiple: boolean
|
||||
}
|
||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||
| {
|
||||
inputType: 'string'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: C
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: C) => OutputType
|
||||
}
|
||||
| {
|
||||
inputType: 'boolean'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: C
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: C) => OutputType
|
||||
}
|
||||
)
|
||||
) => CommandArgumentOption<OutputType>[])
|
||||
optionsFromContext?: (context: C) => CommandArgumentOption<OutputType>[]
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: C
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: C) => OutputType
|
||||
}
|
||||
| {
|
||||
inputType: 'selection'
|
||||
selectionTypes: Selection['type'][]
|
||||
multiple: boolean
|
||||
}
|
||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||
| {
|
||||
inputType: 'string'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: C
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: C) => OutputType
|
||||
}
|
||||
| {
|
||||
inputType: 'boolean'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: C
|
||||
) => OutputType)
|
||||
defaultValueFromContext?: (context: C) => OutputType
|
||||
}
|
||||
)
|
||||
|
||||
export type CommandArgument<
|
||||
OutputType,
|
||||
T extends AnyStateMachine = AnyStateMachine
|
||||
> =
|
||||
> = {
|
||||
description?: string
|
||||
required:
|
||||
| boolean
|
||||
| ((
|
||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: ContextFrom<T>
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
machineActor: InterpreterFrom<T>
|
||||
} & (
|
||||
| {
|
||||
description?: string
|
||||
required:
|
||||
| boolean
|
||||
inputType: Extract<CommandInputType, 'options'>
|
||||
options:
|
||||
| CommandArgumentOption<OutputType>[]
|
||||
| ((
|
||||
commandBarContext: { argumentsToSubmit: Record<string, unknown> }, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
commandBarContext: {
|
||||
argumentsToSubmit: Record<string, unknown>
|
||||
}, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: ContextFrom<T>
|
||||
) => boolean)
|
||||
skip?: boolean
|
||||
machineActor: InterpreterFrom<T>
|
||||
} & (
|
||||
| {
|
||||
inputType: Extract<CommandInputType, 'options'>
|
||||
options:
|
||||
| CommandArgumentOption<OutputType>[]
|
||||
| ((
|
||||
commandBarContext: {
|
||||
argumentsToSubmit: Record<string, unknown>
|
||||
}, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: ContextFrom<T>
|
||||
) => CommandArgumentOption<OutputType>[])
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: ContextFrom<T>
|
||||
) => OutputType)
|
||||
}
|
||||
| {
|
||||
inputType: 'selection'
|
||||
selectionTypes: Selection['type'][]
|
||||
multiple: boolean
|
||||
}
|
||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
|
||||
| {
|
||||
inputType: 'string'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: ContextFrom<T>
|
||||
) => OutputType)
|
||||
}
|
||||
| {
|
||||
inputType: 'boolean'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: ContextFrom<T>
|
||||
) => OutputType)
|
||||
}
|
||||
)
|
||||
) => CommandArgumentOption<OutputType>[])
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: ContextFrom<T>
|
||||
) => OutputType)
|
||||
}
|
||||
| {
|
||||
inputType: 'selection'
|
||||
selectionTypes: Selection['type'][]
|
||||
multiple: boolean
|
||||
}
|
||||
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
|
||||
| {
|
||||
inputType: 'string'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: ContextFrom<T>
|
||||
) => OutputType)
|
||||
}
|
||||
| {
|
||||
inputType: 'boolean'
|
||||
defaultValue?:
|
||||
| OutputType
|
||||
| ((
|
||||
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||
machineContext?: ContextFrom<T>
|
||||
) => OutputType)
|
||||
}
|
||||
)
|
||||
|
||||
export type CommandArgumentWithName<
|
||||
OutputType,
|
||||
|
@ -325,6 +325,18 @@ export function createSettings() {
|
||||
},
|
||||
hideOnLevel: 'project',
|
||||
}),
|
||||
/**
|
||||
* Whether to show a scale grid in the 3D modeling view
|
||||
*/
|
||||
showScaleGrid: new Setting<boolean>({
|
||||
defaultValue: false,
|
||||
description: 'Whether to show a scale grid in the 3D modeling view',
|
||||
validate: (v) => typeof v === 'boolean',
|
||||
commandConfig: {
|
||||
inputType: 'boolean',
|
||||
},
|
||||
hideOnLevel: 'project',
|
||||
}),
|
||||
/**
|
||||
* Whether to show the debug panel, which lets you see
|
||||
* various states of the app to aid in development
|
||||
|
@ -48,6 +48,7 @@ function configurationToSettingsPayload(
|
||||
),
|
||||
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
|
||||
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
|
||||
showScaleGrid: configuration?.settings?.modeling?.show_scale_grid,
|
||||
},
|
||||
textEditor: {
|
||||
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
|
||||
|
@ -105,6 +105,9 @@ export async function executor(
|
||||
makeDefaultPlanes: () => {
|
||||
return new Promise((resolve) => resolve(defaultPlanes))
|
||||
},
|
||||
modifyGrid: (hidden: boolean) => {
|
||||
return new Promise((resolve) => resolve())
|
||||
},
|
||||
})
|
||||
await engineCommandManager.waitForReady
|
||||
engineCommandManager.startNewSession()
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
|
||||
export const settingsMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmsGxfCMPM08PQaDNU0cXRG1tLwedTaKxif7+UJTKIgGJzPCERZJFYpfDpLJgC6lK6VaqIEx6fBmCw2Do2IJ6MxdRDvTT4MRDdRGEzWbQ6ELTGGzOIIxLLVbrTbbNKYKBpLaitAAUWgcExMjk11uoBqVgM3jMYhsAIMrVs6ipPWChOeYhC9KMFhGHNh3IS5AASnB0AACZZw0SSS4KnF3PFafADTV1YZ2IxiH7dNpGfCaIzAgE+IzWMzBa1c+Y88gxZ3UYjEV3pvBysrem5VX0IFq0y3aTXWOp6JmU34IKMxuz0joGEYWsxp2IZu1kABUxexZdxtRG+EmQMZmne3dNBs0jKewLBsbCwI81n77vwtDAHHksDhBYHeDIEGYYEI2AAbowANZ3o8nzBnm3zMelpWqRAAFp62sJ4jEsZ4AT0UJGwjPFzH6cwNW0AwWXpbRImhbABXgUpvzwL0KgnCtgJMMCII8KCYLsA11EGOkXneDxmXMCk92hfCFlIQjFXLZUgLjddWhaFkgRCaxOhbEYzBnXwXkmOjAjjfduXfU9zzdOIeJ9fiEEA6ckwMClQ2BFpmJXMF9DjYI6hZfxmMw8IgA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
|
||||
id: 'Settings',
|
||||
predictableActionArguments: true,
|
||||
context: {} as ReturnType<typeof createSettings>,
|
||||
@ -32,6 +32,7 @@ export const settingsMachine = createMachine(
|
||||
// No toast
|
||||
actions: ['setSettingAtLevel'],
|
||||
},
|
||||
|
||||
'set.app.themeColor': {
|
||||
target: 'persisting settings',
|
||||
|
||||
@ -93,6 +94,15 @@ export const settingsMachine = createMachine(
|
||||
'setClientTheme',
|
||||
],
|
||||
},
|
||||
|
||||
'set.modeling.showScaleGrid': {
|
||||
target: 'persisting settings',
|
||||
actions: [
|
||||
'setSettingAtLevel',
|
||||
'toastSuccess',
|
||||
'setEngineScaleGridVisibility',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
14
src/wasm-lib/Cargo.lock
generated
@ -712,7 +712,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive-docs"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -1385,7 +1385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.68"
|
||||
version = "0.1.69"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
@ -1453,7 +1453,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper",
|
||||
@ -1603,9 +1603,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.4"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
@ -2606,9 +2606,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.118"
|
||||
version = "1.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
|
||||
checksum = "e8eddb61f0697cc3989c5d64b452f5488e2b8a60fd7d5076a3045076ffef8cb0"
|
||||
dependencies = [
|
||||
"indexmap 2.2.5",
|
||||
"itoa",
|
||||
|
@ -15,7 +15,7 @@ clap = "4.5.7"
|
||||
gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
kittycad.workspace = true
|
||||
serde_json = "1.0.118"
|
||||
serde_json = "1.0.119"
|
||||
tokio = { version = "1.38.0", features = ["sync"] }
|
||||
toml = "0.8.14"
|
||||
uuid = { version = "1.9.1", features = ["v4", "js", "serde"] }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
description = "A test server for KCL"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
@ -11,5 +11,5 @@ hyper = { version = "0.14.29", features = ["server"] }
|
||||
kcl-lib = { version = "0.1.62", path = "../kcl" }
|
||||
pico-args = "0.5.0"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.118"
|
||||
serde_json = "1.0.119"
|
||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.1.68"
|
||||
version = "0.1.69"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
@ -19,21 +19,21 @@ chrono = "0.4.38"
|
||||
clap = { version = "4.5.7", default-features = false, optional = true }
|
||||
dashmap = "6.0.1"
|
||||
databake = { version = "0.1.8", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.19", path = "../derive-docs" }
|
||||
derive-docs = { version = "0.1.20", path = "../derive-docs" }
|
||||
form_urlencoded = "1.2.1"
|
||||
futures = { version = "0.3.30" }
|
||||
git_rev = "0.1.0"
|
||||
gltf-json = "1.4.1"
|
||||
kittycad = { workspace = true, features = ["clap"] }
|
||||
lazy_static = "1.5.0"
|
||||
mime_guess = "2.0.4"
|
||||
mime_guess = "2.0.5"
|
||||
parse-display = "0.9.1"
|
||||
pyo3 = { version = "0.22.0", optional = true }
|
||||
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
|
||||
ropey = "1.6.1"
|
||||
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] }
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.118"
|
||||
serde_json = "1.0.119"
|
||||
sha2 = "0.10.8"
|
||||
thiserror = "1.0.61"
|
||||
toml = "0.8.14"
|
||||
|
@ -22,6 +22,12 @@ use crate::{
|
||||
executor::{DefaultPlanes, Point3d},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref GRID_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("cfa78409-653d-4c26-96f1-7c45fb784840").unwrap();
|
||||
|
||||
pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
/// Get the batch of commands to be sent to the engine.
|
||||
@ -456,6 +462,34 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||
source_ranges: vec![],
|
||||
}))
|
||||
}
|
||||
|
||||
async fn modify_grid(&self, hidden: bool) -> Result<(), KclError> {
|
||||
// Hide/show the grid.
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
Default::default(),
|
||||
&ModelingCmd::ObjectVisible {
|
||||
hidden,
|
||||
object_id: *GRID_OBJECT_ID,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Hide/show the grid scale text.
|
||||
self.batch_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
Default::default(),
|
||||
&ModelingCmd::ObjectVisible {
|
||||
hidden,
|
||||
object_id: *GRID_SCALE_TEXT_OBJECT_ID,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.flush_batch(false, Default::default()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Eq, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
|
@ -70,11 +70,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
println!("on_change after check: {:?}", params);
|
||||
|
||||
self.insert_code_map(params.uri.to_string(), params.text.as_bytes().to_vec())
|
||||
.await;
|
||||
println!("on_change after insert: {:?}", params);
|
||||
self.inner_on_change(params, false).await;
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,9 @@ pub struct Backend {
|
||||
pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
|
||||
|
||||
pub is_initialized: Arc<tokio::sync::RwLock<bool>>,
|
||||
|
||||
/// Are we running in dev mode.
|
||||
pub dev_mode: bool,
|
||||
}
|
||||
|
||||
// Implement the shared backend trait for the language server.
|
||||
@ -209,7 +212,7 @@ impl Backend {
|
||||
// Let's not call it yet since it's not our model.
|
||||
// We will need to wrap in spawn_local like we do in kcl/mod.rs for wasm only.
|
||||
#[cfg(test)]
|
||||
let completion_list = self
|
||||
let mut completion_list = self
|
||||
.get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
|
||||
.await
|
||||
.map_err(|err| Error {
|
||||
@ -218,7 +221,25 @@ impl Backend {
|
||||
message: Cow::from(format!("Failed to get completions: {}", err)),
|
||||
})?;
|
||||
#[cfg(not(test))]
|
||||
let completion_list = vec![];
|
||||
let mut completion_list = vec![];
|
||||
|
||||
if self.dev_mode {
|
||||
completion_list.push(
|
||||
r#"fn cube = (pos, scale) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|
||||
return sg
|
||||
}
|
||||
const part001 = cube([0,0], 20)
|
||||
|> close(%)
|
||||
|> extrude(20, %)"#
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
|
||||
// Set the telemetry data for each completion.
|
||||
|
@ -39,11 +39,10 @@ use tower_lsp::{
|
||||
Client, LanguageServer,
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::lint::checks;
|
||||
use crate::{
|
||||
ast::types::{Value, VariableKind},
|
||||
executor::SourceRange,
|
||||
lint::checks,
|
||||
lsp::{backend::Backend as _, util::IntoDiagnostic},
|
||||
parser::PIPE_OPERATOR,
|
||||
token::TokenType,
|
||||
@ -269,15 +268,12 @@ impl crate::lsp::backend::Backend for Backend {
|
||||
// Update our semantic tokens.
|
||||
self.update_semantic_tokens(&tokens, ¶ms).await;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let discovered_findings = ast
|
||||
.lint(checks::lint_variables)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
self.add_to_diagnostics(¶ms, &discovered_findings, false).await;
|
||||
}
|
||||
let discovered_findings = ast
|
||||
.lint(checks::lint_variables)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
self.add_to_diagnostics(¶ms, &discovered_findings, false).await;
|
||||
}
|
||||
|
||||
// Send the notification to the client that the ast was updated.
|
||||
@ -533,9 +529,9 @@ impl Backend {
|
||||
diagnostics: &[DiagT],
|
||||
clear_all_before_add: bool,
|
||||
) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostics))
|
||||
.await;
|
||||
if diagnostics.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if clear_all_before_add {
|
||||
self.clear_diagnostics_map(¶ms.uri, None).await;
|
||||
@ -645,20 +641,6 @@ impl Backend {
|
||||
modifier
|
||||
}
|
||||
|
||||
async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec<CompletionItem> {
|
||||
let ast = match self.ast_map.get(file_name) {
|
||||
Some(ast) => ast,
|
||||
None => return vec![],
|
||||
};
|
||||
|
||||
// Get the completion items.
|
||||
match ast.completion_items() {
|
||||
Ok(items) => items,
|
||||
// TODO: don't ignore an error here.
|
||||
Err(_err) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_zip(&self) -> Result<Vec<u8>> {
|
||||
// Collect all the file data we know.
|
||||
let mut buf = vec![];
|
||||
@ -1055,9 +1037,34 @@ impl LanguageServer for Backend {
|
||||
|
||||
completions.extend(self.stdlib_completions.values().cloned());
|
||||
|
||||
let variables = self
|
||||
.completions_get_variables_from_ast(params.text_document_position.text_document.uri.as_ref())
|
||||
.await;
|
||||
// Add more to the completions if we have more.
|
||||
let Some(ast) = self
|
||||
.ast_map
|
||||
.get(params.text_document_position.text_document.uri.as_ref())
|
||||
else {
|
||||
return Ok(Some(CompletionResponse::Array(completions)));
|
||||
};
|
||||
|
||||
let Some(current_code) = self
|
||||
.code_map
|
||||
.get(params.text_document_position.text_document.uri.as_ref())
|
||||
else {
|
||||
return Ok(Some(CompletionResponse::Array(completions)));
|
||||
};
|
||||
let Ok(current_code) = std::str::from_utf8(¤t_code) else {
|
||||
return Ok(Some(CompletionResponse::Array(completions)));
|
||||
};
|
||||
|
||||
let position = position_to_char_index(params.text_document_position.position, current_code);
|
||||
if ast.get_non_code_meta_for_position(position).is_some() {
|
||||
// If we are in a code comment we don't want to show completions.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Get the completion items forem the ast.
|
||||
let Ok(variables) = ast.completion_items() else {
|
||||
return Ok(Some(CompletionResponse::Array(completions)));
|
||||
};
|
||||
|
||||
// Get our variables from our AST to include in our completions.
|
||||
completions.extend(variables);
|
||||
|
@ -99,6 +99,7 @@ pub async fn copilot_lsp_server() -> Result<crate::lsp::copilot::Backend> {
|
||||
telemetry: Default::default(),
|
||||
is_initialized: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
dev_mode: Default::default(),
|
||||
});
|
||||
let server = service.inner();
|
||||
|
||||
|
@ -660,6 +660,41 @@ st"#
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_kcl_lsp_completions_empty_in_comment() {
|
||||
let server = kcl_lsp_server(false).await.unwrap();
|
||||
|
||||
// Send open file.
|
||||
server
|
||||
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentItem {
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
language_id: "kcl".to_string(),
|
||||
version: 1,
|
||||
text: r#"const thing= 1 // st"#.to_string(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
// Send completion request.
|
||||
let completions = server
|
||||
.completion(tower_lsp::lsp_types::CompletionParams {
|
||||
text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
},
|
||||
position: tower_lsp::lsp_types::Position { line: 0, character: 19 },
|
||||
},
|
||||
context: None,
|
||||
partial_result_params: Default::default(),
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(completions.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_kcl_lsp_completions_tags() {
|
||||
let server = kcl_lsp_server(false).await.unwrap();
|
||||
|
@ -379,6 +379,9 @@ pub struct ModelingSettings {
|
||||
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub enable_ssao: DefaultTrue,
|
||||
/// Whether or not to show a scale grid in the 3D modeling view
|
||||
#[serde(default, alias = "showScaleGrid", skip_serializing_if = "is_default")]
|
||||
pub show_scale_grid: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
|
||||
@ -654,7 +657,8 @@ textWrapping = true
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: false.into()
|
||||
enable_ssao: false.into(),
|
||||
show_scale_grid: false,
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: true.into(),
|
||||
@ -712,7 +716,8 @@ includeSettings = false
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: true.into()
|
||||
enable_ssao: true.into(),
|
||||
show_scale_grid: false,
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: false.into(),
|
||||
@ -775,7 +780,8 @@ defaultProjectName = "projects-$nnn"
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: true.into()
|
||||
enable_ssao: true.into(),
|
||||
show_scale_grid: false,
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: false.into(),
|
||||
@ -850,7 +856,8 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: true.into(),
|
||||
show_debug_panel: false,
|
||||
enable_ssao: true.into()
|
||||
enable_ssao: true.into(),
|
||||
show_scale_grid: false,
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: true.into(),
|
||||
|
@ -129,7 +129,8 @@ includeSettings = false
|
||||
mouse_controls: Default::default(),
|
||||
highlight_edges: Default::default(),
|
||||
show_debug_panel: true,
|
||||
enable_ssao: true.into()
|
||||
enable_ssao: true.into(),
|
||||
show_scale_grid: false,
|
||||
},
|
||||
text_editor: TextEditorSettings {
|
||||
text_wrapping: false.into(),
|
||||
|
@ -93,6 +93,8 @@ impl TryFrom<TokenType> for SemanticTokenType {
|
||||
|
||||
impl TokenType {
|
||||
// This is for the lsp server.
|
||||
// Don't call this function directly in the code use a lazy_static instead
|
||||
// like we do in the lsp server.
|
||||
pub fn all_semantic_token_types() -> Result<Vec<SemanticTokenType>> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = true;
|
||||
|
@ -94,6 +94,23 @@ pub async fn make_default_planes(
|
||||
JsValue::from_serde(&default_planes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for modifying the grid
|
||||
#[wasm_bindgen]
|
||||
pub async fn modify_grid(
|
||||
engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
|
||||
hidden: bool,
|
||||
) -> Result<(), String> {
|
||||
console_error_panic_hook::set_once();
|
||||
// deserialize the ast from a stringified json
|
||||
|
||||
let engine = kcl_lib::engine::conn_wasm::EngineConnection::new(engine_manager)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
engine.modify_grid(hidden).await.map_err(String::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// wasm_bindgen wrapper for execute
|
||||
#[wasm_bindgen]
|
||||
pub async fn modify_ast_for_sketch_wasm(
|
||||
@ -356,6 +373,11 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: Strin
|
||||
|
||||
is_initialized: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
dev_mode: if baseurl == "https://api.dev.zoo.dev" {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
},
|
||||
})
|
||||
.custom_method("copilot/setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
|
||||
.custom_method(
|
||||
|
@ -2,6 +2,9 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@kittycad/codemirror-lsp-client": [
|
||||
"../packages/codemirror-lsp-client/src/index.ts"
|
||||
],
|
||||
"/*": ["src/*"]
|
||||
},
|
||||
"types": [
|
||||
@ -23,10 +26,12 @@
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"composite": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src", "e2e", "./*.ts"],
|
||||
"include": ["src", "e2e", "packages", "./*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|