Merge branch 'main' into franknoirot/xstate-toolbar
This commit is contained in:
		@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
 | 
			
		||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
 | 
			
		||||
VITE_KC_SKIP_AUTH=false
 | 
			
		||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
 | 
			
		||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
 | 
			
		||||
VITE_KC_SENTRY_DSN=
 | 
			
		||||
 | 
			
		||||
@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.kittycad.io
 | 
			
		||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
 | 
			
		||||
VITE_KC_SKIP_AUTH=false
 | 
			
		||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
 | 
			
		||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
 | 
			
		||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
 | 
			
		||||
 | 
			
		||||
@ -1 +1 @@
 | 
			
		||||
src/wasm-lib/pkg/wasm_lib.js
 | 
			
		||||
src/wasm-lib/*
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										65
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										65
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -13,17 +13,31 @@ jobs:
 | 
			
		||||
  check-format:
 | 
			
		||||
    runs-on: 'ubuntu-20.04'
 | 
			
		||||
    steps:
 | 
			
		||||
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: '.nvmrc'
 | 
			
		||||
 | 
			
		||||
          cache: 'yarn'
 | 
			
		||||
      - run: yarn install
 | 
			
		||||
 | 
			
		||||
      - run: yarn fmt-check
 | 
			
		||||
 | 
			
		||||
  check-types:
 | 
			
		||||
    runs-on: ubuntu-20.04
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: '.nvmrc'
 | 
			
		||||
          cache: 'yarn'
 | 
			
		||||
      - run: yarn install
 | 
			
		||||
      - uses: Swatinem/rust-cache@v2
 | 
			
		||||
        with:
 | 
			
		||||
          workspaces: "./src/wasm-lib"
 | 
			
		||||
 | 
			
		||||
      - run: yarn build:wasm
 | 
			
		||||
      - run: yarn tsc
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  build-test-web:
 | 
			
		||||
    runs-on: ubuntu-20.04
 | 
			
		||||
@ -36,12 +50,15 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: '.nvmrc'
 | 
			
		||||
          cache: 'yarn'
 | 
			
		||||
 | 
			
		||||
      - run: yarn install
 | 
			
		||||
 | 
			
		||||
      - run: yarn build:wasm
 | 
			
		||||
      - uses: Swatinem/rust-cache@v2
 | 
			
		||||
        with:
 | 
			
		||||
          workspaces: "./src/wasm-lib"
 | 
			
		||||
 | 
			
		||||
      - run: yarn tsc
 | 
			
		||||
      - run: yarn build:wasm
 | 
			
		||||
 | 
			
		||||
      - run: yarn simpleserver:ci
 | 
			
		||||
 | 
			
		||||
@ -49,14 +66,12 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - run: yarn test:cov
 | 
			
		||||
 | 
			
		||||
      - run: yarn test:rust
 | 
			
		||||
      
 | 
			
		||||
      - id: export_version
 | 
			
		||||
        run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  build-apps:
 | 
			
		||||
    needs: [check-format, build-test-web]
 | 
			
		||||
    needs: [check-format, build-test-web, check-types]
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -87,6 +102,10 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          workspaces: './src-tauri -> target'
 | 
			
		||||
 | 
			
		||||
      - uses: Swatinem/rust-cache@v2
 | 
			
		||||
        with:
 | 
			
		||||
          workspaces: "./src/wasm-lib"
 | 
			
		||||
 | 
			
		||||
      - name: wasm prep
 | 
			
		||||
        shell: bash
 | 
			
		||||
        run: |
 | 
			
		||||
@ -110,22 +129,27 @@ jobs:
 | 
			
		||||
      - name: Fix format
 | 
			
		||||
        run: yarn fmt
 | 
			
		||||
 | 
			
		||||
      - name: install apple silicon target mac
 | 
			
		||||
        if: matrix.os == 'macos-latest'
 | 
			
		||||
        run: |
 | 
			
		||||
          rustup target add aarch64-apple-darwin
 | 
			
		||||
 | 
			
		||||
      - name: Build the app for the current platform (no upload)
 | 
			
		||||
        uses: tauri-apps/tauri-action@v0
 | 
			
		||||
        env:
 | 
			
		||||
          TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
 | 
			
		||||
          TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
 | 
			
		||||
        with:
 | 
			
		||||
          args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
 | 
			
		||||
 | 
			
		||||
      - uses: actions/upload-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: src-tauri/target/release/bundle/*/*
 | 
			
		||||
          path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  publish-apps-release:
 | 
			
		||||
    runs-on: ubuntu-20.04
 | 
			
		||||
    if: github.event_name == 'release'
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: write
 | 
			
		||||
    needs: [build-test-web, build-apps]
 | 
			
		||||
    env:
 | 
			
		||||
      VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
 | 
			
		||||
@ -135,8 +159,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Generate the update static endpoint
 | 
			
		||||
        run: |
 | 
			
		||||
          ls -l artifact
 | 
			
		||||
          ls -l artifact/*
 | 
			
		||||
          ls -l artifact/*/*itty*
 | 
			
		||||
          DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
 | 
			
		||||
          LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
 | 
			
		||||
          WINDOWS_SIG=`cat artifact/nsis/*.nsis.zip.sig`
 | 
			
		||||
@ -144,11 +167,11 @@ jobs:
 | 
			
		||||
          jq --null-input \
 | 
			
		||||
            --arg version "v${VERSION_NO_V}" \
 | 
			
		||||
            --arg darwin_sig "$DARWIN_SIG" \
 | 
			
		||||
            --arg darwin_url "$RELEASE_DIR/macos/kittycad-modeling-app.app.tar.gz" \
 | 
			
		||||
            --arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
 | 
			
		||||
            --arg linux_sig "$LINUX_SIG" \
 | 
			
		||||
            --arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
 | 
			
		||||
            --arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
 | 
			
		||||
            --arg windows_sig "$WINDOWS_SIG" \
 | 
			
		||||
            --arg windows_url "$RELEASE_DIR/nsis/kittycad-modeling-app_${VERSION_NO_V}_x64-setup.nsis.zip" \
 | 
			
		||||
            --arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
 | 
			
		||||
            '{
 | 
			
		||||
              "version": $version,
 | 
			
		||||
              "platforms": {
 | 
			
		||||
@ -156,6 +179,10 @@ jobs:
 | 
			
		||||
                  "signature": $darwin_sig,
 | 
			
		||||
                  "url": $darwin_url
 | 
			
		||||
                },
 | 
			
		||||
                "darwin-aarch64": {
 | 
			
		||||
                  "signature": $darwin_sig,
 | 
			
		||||
                  "url": $darwin_url
 | 
			
		||||
                },
 | 
			
		||||
                "linux-x86_64": {
 | 
			
		||||
                  "signature": $linux_sig,
 | 
			
		||||
                  "url": $linux_url
 | 
			
		||||
@ -182,7 +209,7 @@ jobs:
 | 
			
		||||
        uses: google-github-actions/upload-cloud-storage@v1.0.3
 | 
			
		||||
        with:
 | 
			
		||||
          path: artifact
 | 
			
		||||
          glob: '*/kittycad-modeling-app*'
 | 
			
		||||
          glob: '*/*itty*'
 | 
			
		||||
          parent: false
 | 
			
		||||
          destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
 | 
			
		||||
 | 
			
		||||
@ -195,4 +222,4 @@ jobs:
 | 
			
		||||
      - name: Upload release files to Github
 | 
			
		||||
        uses: softprops/action-gh-release@v1
 | 
			
		||||
        with:
 | 
			
		||||
          files: artifact/*/kittycad-modeling-app*
 | 
			
		||||
          files: artifact/*/*itty*
 | 
			
		||||
 | 
			
		||||
@ -5,3 +5,5 @@ coverage
 | 
			
		||||
# Ignore Rust projects:
 | 
			
		||||
*.rs
 | 
			
		||||
target
 | 
			
		||||
src/wasm-lib/pkg
 | 
			
		||||
src/wasm-lib/kcl/bindings
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							@ -86,3 +86,24 @@ The PR may serve as a place to discuss the human-readable changelog and extra QA
 | 
			
		||||
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
 | 
			
		||||
 | 
			
		||||
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
 | 
			
		||||
 | 
			
		||||
## Fuzzing the parser
 | 
			
		||||
 | 
			
		||||
Make sure you install cargo fuzz:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
$ cargo install cargo-fuzz
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
$ cd src/wasm-lib/kcl
 | 
			
		||||
 | 
			
		||||
# list the fuzz targets
 | 
			
		||||
$ cargo fuzz list
 | 
			
		||||
 | 
			
		||||
# run the parser fuzzer
 | 
			
		||||
$ cargo +nightly fuzz run parser
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
For more information on fuzzing you can check out 
 | 
			
		||||
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22642
									
								
								docs/kcl.json
									
									
									
									
									
								
							
							
						
						
									
										22642
									
								
								docs/kcl.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										5427
									
								
								docs/kcl.md
									
									
									
									
									
								
							
							
						
						
									
										5427
									
								
								docs/kcl.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										17
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								package.json
									
									
									
									
									
								
							@ -1,31 +1,35 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "untitled-app",
 | 
			
		||||
  "version": "0.2.0",
 | 
			
		||||
  "version": "0.5.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@codemirror/autocomplete": "^6.9.0",
 | 
			
		||||
    "@fortawesome/fontawesome-svg-core": "^6.4.2",
 | 
			
		||||
    "@fortawesome/free-brands-svg-icons": "^6.4.2",
 | 
			
		||||
    "@fortawesome/free-solid-svg-icons": "^6.4.2",
 | 
			
		||||
    "@fortawesome/react-fontawesome": "^0.2.0",
 | 
			
		||||
    "@headlessui/react": "^1.7.13",
 | 
			
		||||
    "@headlessui/tailwindcss": "^0.2.0",
 | 
			
		||||
    "@kittycad/lib": "^0.0.35",
 | 
			
		||||
    "@kittycad/lib": "^0.0.37",
 | 
			
		||||
    "@lezer/javascript": "^1.4.7",
 | 
			
		||||
    "@open-rpc/client-js": "^1.8.1",
 | 
			
		||||
    "@react-hook/resize-observer": "^1.2.6",
 | 
			
		||||
    "@sentry/react": "^7.65.0",
 | 
			
		||||
    "@tauri-apps/api": "^1.3.0",
 | 
			
		||||
    "@testing-library/jest-dom": "^5.14.1",
 | 
			
		||||
    "@testing-library/react": "^13.0.0",
 | 
			
		||||
    "@testing-library/user-event": "^13.2.1",
 | 
			
		||||
    "@ts-stack/markdown": "^1.5.0",
 | 
			
		||||
    "@types/node": "^16.7.13",
 | 
			
		||||
    "@types/react": "^18.0.0",
 | 
			
		||||
    "@types/react-dom": "^18.0.0",
 | 
			
		||||
    "@uiw/codemirror-extensions-langs": "^4.21.9",
 | 
			
		||||
    "@uiw/react-codemirror": "^4.15.1",
 | 
			
		||||
    "@uiw/react-codemirror": "^4.21.13",
 | 
			
		||||
    "@xstate/react": "^3.2.2",
 | 
			
		||||
    "crypto-js": "^4.1.1",
 | 
			
		||||
    "formik": "^2.4.3",
 | 
			
		||||
    "fuse.js": "^6.6.2",
 | 
			
		||||
    "http-server": "^14.1.1",
 | 
			
		||||
    "json-rpc-2.0": "^1.6.0",
 | 
			
		||||
    "re-resizable": "^6.9.9",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
@ -43,6 +47,8 @@
 | 
			
		||||
    "typescript": "^4.4.2",
 | 
			
		||||
    "uuid": "^9.0.0",
 | 
			
		||||
    "vitest": "^0.34.1",
 | 
			
		||||
    "vscode-jsonrpc": "^8.1.0",
 | 
			
		||||
    "vscode-languageserver-protocol": "^3.17.3",
 | 
			
		||||
    "wasm-pack": "^0.12.1",
 | 
			
		||||
    "web-vitals": "^2.1.0",
 | 
			
		||||
    "ws": "^8.13.0",
 | 
			
		||||
@ -64,7 +70,7 @@
 | 
			
		||||
    "fmt": "prettier --write ./src",
 | 
			
		||||
    "fmt-check": "prettier --check ./src",
 | 
			
		||||
    "build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
 | 
			
		||||
    "remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
 | 
			
		||||
    "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
 | 
			
		||||
    "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
 | 
			
		||||
    "lint": "eslint --fix src",
 | 
			
		||||
    "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
 | 
			
		||||
@ -92,6 +98,7 @@
 | 
			
		||||
    "@babel/preset-env": "^7.22.9",
 | 
			
		||||
    "@tauri-apps/cli": "^1.3.1",
 | 
			
		||||
    "@types/crypto-js": "^4.1.1",
 | 
			
		||||
    "@types/debounce": "^1.2.1",
 | 
			
		||||
    "@types/isomorphic-fetch": "^0.0.36",
 | 
			
		||||
    "@types/react-modal": "^3.16.0",
 | 
			
		||||
    "@types/uuid": "^9.0.1",
 | 
			
		||||
 | 
			
		||||
@ -7,8 +7,8 @@
 | 
			
		||||
    "distDir": "../build"
 | 
			
		||||
  },
 | 
			
		||||
  "package": {
 | 
			
		||||
    "productName": "kittycad-modeling-app",
 | 
			
		||||
    "version": "0.2.0"
 | 
			
		||||
    "productName": "kittycad-modeling",
 | 
			
		||||
    "version": "0.5.0"
 | 
			
		||||
  },
 | 
			
		||||
  "tauri": {
 | 
			
		||||
    "allowlist": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								src-tauri/tauri.macos.conf.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src-tauri/tauri.macos.conf.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "../node_modules/@tauri-apps/cli/schema.json",
 | 
			
		||||
  "package": {
 | 
			
		||||
    "productName": "KittyCAD Modeling"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src-tauri/tauri.windows.conf.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src-tauri/tauri.windows.conf.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "../node_modules/@tauri-apps/cli/schema.json",
 | 
			
		||||
  "package": {
 | 
			
		||||
    "productName": "KittyCAD Modeling"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										245
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										245
									
								
								src/App.tsx
									
									
									
									
									
								
							@ -2,7 +2,6 @@ import {
 | 
			
		||||
  useRef,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useLayoutEffect,
 | 
			
		||||
  useMemo,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  MouseEventHandler,
 | 
			
		||||
} from 'react'
 | 
			
		||||
@ -10,15 +9,7 @@ import { DebugPanel } from './components/DebugPanel'
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { asyncParser } from './lang/abstractSyntaxTree'
 | 
			
		||||
import { _executor } from './lang/executor'
 | 
			
		||||
import CodeMirror from '@uiw/react-codemirror'
 | 
			
		||||
import { langs } from '@uiw/codemirror-extensions-langs'
 | 
			
		||||
import { linter, lintGutter } from '@codemirror/lint'
 | 
			
		||||
import { ViewUpdate } from '@codemirror/view'
 | 
			
		||||
import {
 | 
			
		||||
  lineHighlightField,
 | 
			
		||||
  addLineHighlight,
 | 
			
		||||
} from './editor/highlightextension'
 | 
			
		||||
import { PaneType, Selections, useStore } from './useStore'
 | 
			
		||||
import { PaneType, useStore } from './useStore'
 | 
			
		||||
import { Logs, KCLErrors } from './components/Logs'
 | 
			
		||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
 | 
			
		||||
import { MemoryPanel } from './components/MemoryPanel'
 | 
			
		||||
@ -29,9 +20,9 @@ import {
 | 
			
		||||
  EngineCommand,
 | 
			
		||||
  EngineCommandManager,
 | 
			
		||||
} from './lang/std/engineConnection'
 | 
			
		||||
import { isOverlap, throttle } from './lib/utils'
 | 
			
		||||
import { throttle } from './lib/utils'
 | 
			
		||||
import { AppHeader } from './components/AppHeader'
 | 
			
		||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
 | 
			
		||||
import { KCLError } from './lang/errors'
 | 
			
		||||
import { Resizable } from 're-resizable'
 | 
			
		||||
import {
 | 
			
		||||
  faCode,
 | 
			
		||||
@ -39,94 +30,75 @@ import {
 | 
			
		||||
  faSquareRootVariable,
 | 
			
		||||
} from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import { TEST } from './env'
 | 
			
		||||
import { getNormalisedCoordinates } from './lib/utils'
 | 
			
		||||
import { Themes, getSystemTheme } from './lib/theme'
 | 
			
		||||
import { isTauri } from './lib/isTauri'
 | 
			
		||||
import { useLoaderData, useParams } from 'react-router-dom'
 | 
			
		||||
import { writeTextFile } from '@tauri-apps/api/fs'
 | 
			
		||||
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
 | 
			
		||||
import { useLoaderData } from 'react-router-dom'
 | 
			
		||||
import { IndexLoaderData } from './Router'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
import { onboardingPaths } from 'routes/Onboarding'
 | 
			
		||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
 | 
			
		||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
 | 
			
		||||
import { CodeMenu } from 'components/CodeMenu'
 | 
			
		||||
import { TextEditor } from 'components/TextEditor'
 | 
			
		||||
import { Themes, getSystemTheme } from 'lib/theme'
 | 
			
		||||
 | 
			
		||||
export function App() {
 | 
			
		||||
  const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
 | 
			
		||||
  const pathParams = useParams()
 | 
			
		||||
 | 
			
		||||
  const streamRef = useRef<HTMLDivElement>(null)
 | 
			
		||||
  useHotKeyListener()
 | 
			
		||||
  const {
 | 
			
		||||
    editorView,
 | 
			
		||||
    setEditorView,
 | 
			
		||||
    setSelectionRanges,
 | 
			
		||||
    selectionRanges,
 | 
			
		||||
    addLog,
 | 
			
		||||
    addKCLError,
 | 
			
		||||
    code,
 | 
			
		||||
    setCode,
 | 
			
		||||
    setAst,
 | 
			
		||||
    setError,
 | 
			
		||||
    setProgramMemory,
 | 
			
		||||
    resetLogs,
 | 
			
		||||
    resetKCLErrors,
 | 
			
		||||
    selectionRangeTypeMap,
 | 
			
		||||
    setArtifactMap,
 | 
			
		||||
    engineCommandManager,
 | 
			
		||||
    setEngineCommandManager,
 | 
			
		||||
    highlightRange,
 | 
			
		||||
    setHighlightRange,
 | 
			
		||||
    setCursor2,
 | 
			
		||||
    sourceRangeMap,
 | 
			
		||||
    setMediaStream,
 | 
			
		||||
    setIsStreamReady,
 | 
			
		||||
    isStreamReady,
 | 
			
		||||
    isMouseDownInStream,
 | 
			
		||||
    cmdId,
 | 
			
		||||
    setCmdId,
 | 
			
		||||
    formatCode,
 | 
			
		||||
    buttonDownInStream,
 | 
			
		||||
    openPanes,
 | 
			
		||||
    setOpenPanes,
 | 
			
		||||
    didDragInStream,
 | 
			
		||||
    setDidDragInStream,
 | 
			
		||||
    setStreamDimensions,
 | 
			
		||||
    streamDimensions,
 | 
			
		||||
    setIsExecuting,
 | 
			
		||||
    defferedCode,
 | 
			
		||||
  } = useStore((s) => ({
 | 
			
		||||
    editorView: s.editorView,
 | 
			
		||||
    setEditorView: s.setEditorView,
 | 
			
		||||
    setSelectionRanges: s.setSelectionRanges,
 | 
			
		||||
    selectionRanges: s.selectionRanges,
 | 
			
		||||
    setGuiMode: s.setGuiMode,
 | 
			
		||||
    addLog: s.addLog,
 | 
			
		||||
    code: s.code,
 | 
			
		||||
    defferedCode: s.defferedCode,
 | 
			
		||||
    setCode: s.setCode,
 | 
			
		||||
    setAst: s.setAst,
 | 
			
		||||
    setError: s.setError,
 | 
			
		||||
    setProgramMemory: s.setProgramMemory,
 | 
			
		||||
    resetLogs: s.resetLogs,
 | 
			
		||||
    resetKCLErrors: s.resetKCLErrors,
 | 
			
		||||
    selectionRangeTypeMap: s.selectionRangeTypeMap,
 | 
			
		||||
    setArtifactMap: s.setArtifactNSourceRangeMaps,
 | 
			
		||||
    engineCommandManager: s.engineCommandManager,
 | 
			
		||||
    setEngineCommandManager: s.setEngineCommandManager,
 | 
			
		||||
    highlightRange: s.highlightRange,
 | 
			
		||||
    setHighlightRange: s.setHighlightRange,
 | 
			
		||||
    isShiftDown: s.isShiftDown,
 | 
			
		||||
    setCursor: s.setCursor,
 | 
			
		||||
    setCursor2: s.setCursor2,
 | 
			
		||||
    sourceRangeMap: s.sourceRangeMap,
 | 
			
		||||
    setMediaStream: s.setMediaStream,
 | 
			
		||||
    isStreamReady: s.isStreamReady,
 | 
			
		||||
    setIsStreamReady: s.setIsStreamReady,
 | 
			
		||||
    isMouseDownInStream: s.isMouseDownInStream,
 | 
			
		||||
    cmdId: s.cmdId,
 | 
			
		||||
    setCmdId: s.setCmdId,
 | 
			
		||||
    formatCode: s.formatCode,
 | 
			
		||||
    buttonDownInStream: s.buttonDownInStream,
 | 
			
		||||
    addKCLError: s.addKCLError,
 | 
			
		||||
    openPanes: s.openPanes,
 | 
			
		||||
    setOpenPanes: s.setOpenPanes,
 | 
			
		||||
    didDragInStream: s.didDragInStream,
 | 
			
		||||
    setDidDragInStream: s.setDidDragInStream,
 | 
			
		||||
    setStreamDimensions: s.setStreamDimensions,
 | 
			
		||||
    streamDimensions: s.streamDimensions,
 | 
			
		||||
    setIsExecuting: s.setIsExecuting,
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
@ -134,7 +106,7 @@ export function App() {
 | 
			
		||||
      context: { token },
 | 
			
		||||
    },
 | 
			
		||||
    settings: {
 | 
			
		||||
      context: { showDebugPanel, theme, onboardingStatus },
 | 
			
		||||
      context: { showDebugPanel, onboardingStatus, cameraControls, theme },
 | 
			
		||||
    },
 | 
			
		||||
  } = useGlobalStateContext()
 | 
			
		||||
 | 
			
		||||
@ -175,87 +147,12 @@ export function App() {
 | 
			
		||||
    }
 | 
			
		||||
  }, [loadedCode, setCode])
 | 
			
		||||
 | 
			
		||||
  // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
 | 
			
		||||
  const onChange = (value: string, viewUpdate: ViewUpdate) => {
 | 
			
		||||
    setCode(value)
 | 
			
		||||
    if (isTauri() && pathParams.id) {
 | 
			
		||||
      // Save the file to disk
 | 
			
		||||
      // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
 | 
			
		||||
      writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
 | 
			
		||||
        (err) => {
 | 
			
		||||
          // TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
 | 
			
		||||
          console.error('error saving file', err)
 | 
			
		||||
          toast.error('Error saving file, please check file permissions')
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    if (editorView) {
 | 
			
		||||
      editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
 | 
			
		||||
    }
 | 
			
		||||
  } //, []);
 | 
			
		||||
  const onUpdate = (viewUpdate: ViewUpdate) => {
 | 
			
		||||
    if (!editorView) {
 | 
			
		||||
      setEditorView(viewUpdate.view)
 | 
			
		||||
    }
 | 
			
		||||
    const ranges = viewUpdate.state.selection.ranges
 | 
			
		||||
 | 
			
		||||
    const isChange =
 | 
			
		||||
      ranges.length !== selectionRanges.codeBasedSelections.length ||
 | 
			
		||||
      ranges.some(({ from, to }, i) => {
 | 
			
		||||
        return (
 | 
			
		||||
          from !== selectionRanges.codeBasedSelections[i].range[0] ||
 | 
			
		||||
          to !== selectionRanges.codeBasedSelections[i].range[1]
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    if (!isChange) return
 | 
			
		||||
    const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
 | 
			
		||||
      ({ from, to }) => {
 | 
			
		||||
        if (selectionRangeTypeMap[to]) {
 | 
			
		||||
          return {
 | 
			
		||||
            type: selectionRangeTypeMap[to],
 | 
			
		||||
            range: [from, to],
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
          type: 'default',
 | 
			
		||||
          range: [from, to],
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    const idBasedSelections = codeBasedSelections
 | 
			
		||||
      .map(({ type, range }) => {
 | 
			
		||||
        const hasOverlap = Object.entries(sourceRangeMap).filter(
 | 
			
		||||
          ([_, sourceRange]) => {
 | 
			
		||||
            return isOverlap(sourceRange, range)
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
        if (hasOverlap.length) {
 | 
			
		||||
          return {
 | 
			
		||||
            type,
 | 
			
		||||
            id: hasOverlap[0][0],
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .filter(Boolean) as any
 | 
			
		||||
 | 
			
		||||
    engineCommandManager?.cusorsSelected({
 | 
			
		||||
      otherSelections: [],
 | 
			
		||||
      idBasedSelections,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    setSelectionRanges({
 | 
			
		||||
      otherSelections: [],
 | 
			
		||||
      codeBasedSelections,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  const pixelDensity = window.devicePixelRatio
 | 
			
		||||
  const streamWidth = streamRef?.current?.offsetWidth
 | 
			
		||||
  const streamHeight = streamRef?.current?.offsetHeight
 | 
			
		||||
 | 
			
		||||
  const width = streamWidth ? streamWidth * pixelDensity : 0
 | 
			
		||||
  const width = streamWidth ? streamWidth : 0
 | 
			
		||||
  const quadWidth = Math.round(width / 4) * 4
 | 
			
		||||
  const height = streamHeight ? streamHeight * pixelDensity : 0
 | 
			
		||||
  const height = streamHeight ? streamHeight : 0
 | 
			
		||||
  const quadHeight = Math.round(height / 4) * 4
 | 
			
		||||
 | 
			
		||||
  useLayoutEffect(() => {
 | 
			
		||||
@ -283,16 +180,17 @@ export function App() {
 | 
			
		||||
    let unsubFn: any[] = []
 | 
			
		||||
    const asyncWrap = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        if (!code) {
 | 
			
		||||
        if (!defferedCode) {
 | 
			
		||||
          setAst(null)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        const _ast = await asyncParser(code)
 | 
			
		||||
        const _ast = await asyncParser(defferedCode)
 | 
			
		||||
        setAst(_ast)
 | 
			
		||||
        resetLogs()
 | 
			
		||||
        resetKCLErrors()
 | 
			
		||||
        engineCommandManager.endSession()
 | 
			
		||||
        engineCommandManager.startNewSession()
 | 
			
		||||
        setIsExecuting(true)
 | 
			
		||||
        const programMemory = await _executor(
 | 
			
		||||
          _ast,
 | 
			
		||||
          {
 | 
			
		||||
@ -324,16 +222,20 @@ export function App() {
 | 
			
		||||
 | 
			
		||||
        const { artifactMap, sourceRangeMap } =
 | 
			
		||||
          await engineCommandManager.waitForAllCommands()
 | 
			
		||||
        setIsExecuting(false)
 | 
			
		||||
 | 
			
		||||
        setArtifactMap({ artifactMap, sourceRangeMap })
 | 
			
		||||
        const unSubHover = engineCommandManager.subscribeToUnreliable({
 | 
			
		||||
          event: 'highlight_set_entity',
 | 
			
		||||
          callback: ({ data }) => {
 | 
			
		||||
            if (!data?.entity_id) {
 | 
			
		||||
              setHighlightRange([0, 0])
 | 
			
		||||
            } else {
 | 
			
		||||
            if (data?.entity_id) {
 | 
			
		||||
              const sourceRange = sourceRangeMap[data.entity_id]
 | 
			
		||||
              setHighlightRange(sourceRange)
 | 
			
		||||
            } else if (
 | 
			
		||||
              !highlightRange ||
 | 
			
		||||
              (highlightRange[0] !== 0 && highlightRange[1] !== 0)
 | 
			
		||||
            ) {
 | 
			
		||||
              setHighlightRange([0, 0])
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
@ -355,6 +257,7 @@ export function App() {
 | 
			
		||||
 | 
			
		||||
        setError()
 | 
			
		||||
      } catch (e: any) {
 | 
			
		||||
        setIsExecuting(false)
 | 
			
		||||
        if (e instanceof KCLError) {
 | 
			
		||||
          addKCLError(e)
 | 
			
		||||
        } else {
 | 
			
		||||
@ -368,37 +271,39 @@ export function App() {
 | 
			
		||||
    return () => {
 | 
			
		||||
      unsubFn.forEach((fn) => fn())
 | 
			
		||||
    }
 | 
			
		||||
  }, [code, isStreamReady, engineCommandManager])
 | 
			
		||||
  }, [defferedCode, isStreamReady, engineCommandManager])
 | 
			
		||||
 | 
			
		||||
  const debounceSocketSend = throttle<EngineCommand>((message) => {
 | 
			
		||||
    engineCommandManager?.sendSceneCommand(message)
 | 
			
		||||
  }, 16)
 | 
			
		||||
  const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
 | 
			
		||||
    clientX,
 | 
			
		||||
    clientY,
 | 
			
		||||
    ctrlKey,
 | 
			
		||||
    shiftKey,
 | 
			
		||||
    currentTarget,
 | 
			
		||||
    nativeEvent,
 | 
			
		||||
  }) => {
 | 
			
		||||
    nativeEvent.preventDefault()
 | 
			
		||||
    if (isMouseDownInStream) {
 | 
			
		||||
      setDidDragInStream(true)
 | 
			
		||||
    }
 | 
			
		||||
  const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
 | 
			
		||||
    e.nativeEvent.preventDefault()
 | 
			
		||||
 | 
			
		||||
    const { x, y } = getNormalisedCoordinates({
 | 
			
		||||
      clientX,
 | 
			
		||||
      clientY,
 | 
			
		||||
      el: currentTarget,
 | 
			
		||||
      clientX: e.clientX,
 | 
			
		||||
      clientY: e.clientY,
 | 
			
		||||
      el: e.currentTarget,
 | 
			
		||||
      ...streamDimensions,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
 | 
			
		||||
 | 
			
		||||
    const newCmdId = uuidv4()
 | 
			
		||||
    setCmdId(newCmdId)
 | 
			
		||||
 | 
			
		||||
    if (cmdId && isMouseDownInStream) {
 | 
			
		||||
    if (buttonDownInStream !== undefined) {
 | 
			
		||||
      const interactionGuards = cameraMouseDragGuards[cameraControls]
 | 
			
		||||
      let interaction: CameraDragInteractionType_type
 | 
			
		||||
 | 
			
		||||
      const eWithButton = { ...e, button: buttonDownInStream }
 | 
			
		||||
 | 
			
		||||
      if (interactionGuards.pan.callback(eWithButton)) {
 | 
			
		||||
        interaction = 'pan'
 | 
			
		||||
      } else if (interactionGuards.rotate.callback(eWithButton)) {
 | 
			
		||||
        interaction = 'rotate'
 | 
			
		||||
      } else if (interactionGuards.zoom.dragCallback(eWithButton)) {
 | 
			
		||||
        interaction = 'zoom'
 | 
			
		||||
      } else {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      debounceSocketSend({
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
        cmd: {
 | 
			
		||||
@ -420,16 +325,6 @@ export function App() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const extraExtensions = useMemo(() => {
 | 
			
		||||
    if (TEST) return []
 | 
			
		||||
    return [
 | 
			
		||||
      lintGutter(),
 | 
			
		||||
      linter((_view) => {
 | 
			
		||||
        return kclErrToDiagnostic(useStore.getState().kclErrors)
 | 
			
		||||
      }),
 | 
			
		||||
    ]
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
 | 
			
		||||
@ -440,7 +335,7 @@ export function App() {
 | 
			
		||||
        className={
 | 
			
		||||
          'transition-opacity transition-duration-75 ' +
 | 
			
		||||
          paneOpacity +
 | 
			
		||||
          (isMouseDownInStream ? ' pointer-events-none' : '')
 | 
			
		||||
          (buttonDownInStream ? ' pointer-events-none' : '')
 | 
			
		||||
        }
 | 
			
		||||
        project={project}
 | 
			
		||||
        enableMenu={true}
 | 
			
		||||
@ -449,7 +344,7 @@ export function App() {
 | 
			
		||||
      <Resizable
 | 
			
		||||
        className={
 | 
			
		||||
          'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
 | 
			
		||||
          (isMouseDownInStream || onboardingStatus === 'camera'
 | 
			
		||||
          (buttonDownInStream || onboardingStatus === 'camera'
 | 
			
		||||
            ? ' pointer-events-none '
 | 
			
		||||
            : ' ') +
 | 
			
		||||
          paneOpacity
 | 
			
		||||
@ -473,31 +368,9 @@ export function App() {
 | 
			
		||||
            icon={faCode}
 | 
			
		||||
            className="open:!mb-2"
 | 
			
		||||
            open={openPanes.includes('code')}
 | 
			
		||||
            menu={<CodeMenu />}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="px-2 py-1">
 | 
			
		||||
              <button
 | 
			
		||||
                // disabled={!shouldFormat}
 | 
			
		||||
                onClick={formatCode}
 | 
			
		||||
                // className={`${!shouldFormat && 'text-gray-300'}`}
 | 
			
		||||
              >
 | 
			
		||||
                format
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="code-mirror-override">
 | 
			
		||||
              <CodeMirror
 | 
			
		||||
                className="h-full"
 | 
			
		||||
                value={code}
 | 
			
		||||
                extensions={[
 | 
			
		||||
                  langs.javascript({ jsx: true }),
 | 
			
		||||
                  lineHighlightField,
 | 
			
		||||
                  ...extraExtensions,
 | 
			
		||||
                ]}
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                onUpdate={onUpdate}
 | 
			
		||||
                theme={editorTheme}
 | 
			
		||||
                onCreateEditor={(_editorView) => setEditorView(_editorView)}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <TextEditor theme={editorTheme} />
 | 
			
		||||
          </CollapsiblePanel>
 | 
			
		||||
          <section className="flex flex-col">
 | 
			
		||||
            <MemoryPanel
 | 
			
		||||
@ -528,7 +401,7 @@ export function App() {
 | 
			
		||||
          className={
 | 
			
		||||
            'transition-opacity transition-duration-75 ' +
 | 
			
		||||
            paneOpacity +
 | 
			
		||||
            (isMouseDownInStream ? ' pointer-events-none' : '')
 | 
			
		||||
            (buttonDownInStream ? ' pointer-events-none' : '')
 | 
			
		||||
          }
 | 
			
		||||
          open={openPanes.includes('debug')}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
 | 
			
		||||
import { Intersect } from './components/Toolbar/Intersect'
 | 
			
		||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
 | 
			
		||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
 | 
			
		||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
 | 
			
		||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
 | 
			
		||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
 | 
			
		||||
import { Fragment, useEffect } from 'react'
 | 
			
		||||
@ -164,7 +163,6 @@ export const Toolbar = () => {
 | 
			
		||||
              </button>
 | 
			
		||||
            )
 | 
			
		||||
          })}
 | 
			
		||||
        <ConvertToVariable />
 | 
			
		||||
        <HorzVert horOrVert="horizontal" />
 | 
			
		||||
        <HorzVert horOrVert="vertical" />
 | 
			
		||||
        <EqualLength />
 | 
			
		||||
 | 
			
		||||
@ -198,29 +198,25 @@ export const CreateNewVariable = ({
 | 
			
		||||
  isNewVariableNameUnique,
 | 
			
		||||
  setNewVariableName,
 | 
			
		||||
  shouldCreateVariable,
 | 
			
		||||
  setShouldCreateVariable,
 | 
			
		||||
  setShouldCreateVariable = () => {},
 | 
			
		||||
  showCheckbox = true,
 | 
			
		||||
}: {
 | 
			
		||||
  isNewVariableNameUnique: boolean
 | 
			
		||||
  newVariableName: string
 | 
			
		||||
  setNewVariableName: (a: string) => void
 | 
			
		||||
  shouldCreateVariable: boolean
 | 
			
		||||
  setShouldCreateVariable: (a: boolean) => void
 | 
			
		||||
  shouldCreateVariable?: boolean
 | 
			
		||||
  setShouldCreateVariable?: (a: boolean) => void
 | 
			
		||||
  showCheckbox?: boolean
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <label
 | 
			
		||||
        htmlFor="create-new-variable"
 | 
			
		||||
        className="block text-sm font-medium text-gray-700 mt-3 font-mono"
 | 
			
		||||
      >
 | 
			
		||||
      <label htmlFor="create-new-variable" className="block mt-3 font-mono">
 | 
			
		||||
        Create new variable
 | 
			
		||||
      </label>
 | 
			
		||||
      <div className="mt-1 flex flex-1">
 | 
			
		||||
      <div className="mt-1 flex gap-2 items-center">
 | 
			
		||||
        {showCheckbox && (
 | 
			
		||||
          <input
 | 
			
		||||
            type="checkbox"
 | 
			
		||||
            className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
 | 
			
		||||
            checked={shouldCreateVariable}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              setShouldCreateVariable(e.target.checked)
 | 
			
		||||
@ -232,7 +228,10 @@ export const CreateNewVariable = ({
 | 
			
		||||
          disabled={!shouldCreateVariable}
 | 
			
		||||
          name="create-new-variable"
 | 
			
		||||
          id="create-new-variable"
 | 
			
		||||
          className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
 | 
			
		||||
          autoFocus={true}
 | 
			
		||||
          autoCapitalize="off"
 | 
			
		||||
          autoCorrect="off"
 | 
			
		||||
          className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
 | 
			
		||||
            !shouldCreateVariable ? 'opacity-50' : ''
 | 
			
		||||
          }`}
 | 
			
		||||
          value={newVariableName}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								src/components/CodeMenu.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/components/CodeMenu.module.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
.button {
 | 
			
		||||
  @apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
 | 
			
		||||
  @apply font-mono text-xs font-bold select-none text-chalkboard-90;
 | 
			
		||||
  @apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
 | 
			
		||||
  @apply transition-colors ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:global(.dark) .button {
 | 
			
		||||
  @apply text-chalkboard-30;
 | 
			
		||||
  @apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button small {
 | 
			
		||||
  @apply text-chalkboard-60;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:global(.dark) .button small {
 | 
			
		||||
  @apply text-chalkboard-40;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								src/components/CodeMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/CodeMenu.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
			
		||||
import { Menu } from '@headlessui/react'
 | 
			
		||||
import { PropsWithChildren } from 'react'
 | 
			
		||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { ActionIcon } from './ActionIcon'
 | 
			
		||||
import { useStore } from 'useStore'
 | 
			
		||||
import styles from './CodeMenu.module.css'
 | 
			
		||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
 | 
			
		||||
import { editorShortcutMeta } from './TextEditor'
 | 
			
		||||
 | 
			
		||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
 | 
			
		||||
  const { formatCode } = useStore((s) => ({
 | 
			
		||||
    formatCode: s.formatCode,
 | 
			
		||||
  }))
 | 
			
		||||
  const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
 | 
			
		||||
    useConvertToVariable()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Menu>
 | 
			
		||||
      <div
 | 
			
		||||
        className="relative"
 | 
			
		||||
        onClick={(e) => {
 | 
			
		||||
          if (e.eventPhase === 3) {
 | 
			
		||||
            e.stopPropagation()
 | 
			
		||||
            e.preventDefault()
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Menu.Button className="p-0 border-none relative">
 | 
			
		||||
          <ActionIcon
 | 
			
		||||
            icon={faEllipsis}
 | 
			
		||||
            bgClassName={
 | 
			
		||||
              'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90  rounded'
 | 
			
		||||
            }
 | 
			
		||||
            iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
 | 
			
		||||
          />
 | 
			
		||||
        </Menu.Button>
 | 
			
		||||
        <Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
 | 
			
		||||
          <Menu.Item>
 | 
			
		||||
            <button onClick={() => formatCode()} className={styles.button}>
 | 
			
		||||
              <span>Format code</span>
 | 
			
		||||
              <small>{editorShortcutMeta.formatCode.display}</small>
 | 
			
		||||
            </button>
 | 
			
		||||
          </Menu.Item>
 | 
			
		||||
          {convertToVarEnabled && (
 | 
			
		||||
            <Menu.Item>
 | 
			
		||||
              <button
 | 
			
		||||
                onClick={handleConvertToVarClick}
 | 
			
		||||
                className={styles.button}
 | 
			
		||||
              >
 | 
			
		||||
                <span>Convert to Variable</span>
 | 
			
		||||
                <small>{editorShortcutMeta.convertToVariable.display}</small>
 | 
			
		||||
              </button>
 | 
			
		||||
            </Menu.Item>
 | 
			
		||||
          )}
 | 
			
		||||
        </Menu.Items>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Menu>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
.panel {
 | 
			
		||||
  @apply relative overflow-auto z-0;
 | 
			
		||||
  @apply relative z-0;
 | 
			
		||||
  @apply bg-chalkboard-10/70 backdrop-blur-sm;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
  @apply sticky top-0 z-10 cursor-pointer;
 | 
			
		||||
  @apply flex items-center gap-2 w-full p-2;
 | 
			
		||||
  @apply flex items-center justify-between gap-2 w-full p-2;
 | 
			
		||||
  @apply font-mono text-xs font-bold select-none text-chalkboard-90;
 | 
			
		||||
  @apply bg-chalkboard-20;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
 | 
			
		||||
  title: string
 | 
			
		||||
  icon?: IconDefinition
 | 
			
		||||
  open?: boolean
 | 
			
		||||
  menu?: React.ReactNode
 | 
			
		||||
  iconClassNames?: {
 | 
			
		||||
    bg?: string
 | 
			
		||||
    icon?: string
 | 
			
		||||
@ -18,21 +19,27 @@ export const PanelHeader = ({
 | 
			
		||||
  title,
 | 
			
		||||
  icon,
 | 
			
		||||
  iconClassNames,
 | 
			
		||||
  menu,
 | 
			
		||||
}: CollapsiblePanelProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <summary className={styles.header}>
 | 
			
		||||
      <ActionIcon
 | 
			
		||||
        icon={icon}
 | 
			
		||||
        bgClassName={
 | 
			
		||||
          'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
 | 
			
		||||
          (iconClassNames?.bg || '')
 | 
			
		||||
        }
 | 
			
		||||
        iconClassName={
 | 
			
		||||
          'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
 | 
			
		||||
          (iconClassNames?.icon || '')
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      {title}
 | 
			
		||||
      <div className="flex gap-2 align-center flex-1">
 | 
			
		||||
        <ActionIcon
 | 
			
		||||
          icon={icon}
 | 
			
		||||
          bgClassName={
 | 
			
		||||
            'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
 | 
			
		||||
            (iconClassNames?.bg || '')
 | 
			
		||||
          }
 | 
			
		||||
          iconClassName={
 | 
			
		||||
            'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
 | 
			
		||||
            (iconClassNames?.icon || '')
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        {title}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
 | 
			
		||||
        {menu}
 | 
			
		||||
      </div>
 | 
			
		||||
    </summary>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
 | 
			
		||||
  children,
 | 
			
		||||
  className,
 | 
			
		||||
  iconClassNames,
 | 
			
		||||
  menu,
 | 
			
		||||
  ...props
 | 
			
		||||
}: CollapsiblePanelProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
 | 
			
		||||
      {...props}
 | 
			
		||||
      className={styles.panel + ' group ' + (className || '')}
 | 
			
		||||
    >
 | 
			
		||||
      <PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
 | 
			
		||||
      <PanelHeader
 | 
			
		||||
        title={title}
 | 
			
		||||
        icon={icon}
 | 
			
		||||
        iconClassNames={iconClassNames}
 | 
			
		||||
        menu={menu}
 | 
			
		||||
      />
 | 
			
		||||
      {children}
 | 
			
		||||
    </details>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -196,7 +196,7 @@ const CommandBar = () => {
 | 
			
		||||
          setCommandBarOpen(false)
 | 
			
		||||
          clearState()
 | 
			
		||||
        }}
 | 
			
		||||
        className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
 | 
			
		||||
        className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
 | 
			
		||||
      >
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          enter="duration-100 ease-out"
 | 
			
		||||
@ -207,7 +207,7 @@ const CommandBar = () => {
 | 
			
		||||
          leaveTo="opacity-0"
 | 
			
		||||
          as={Fragment}
 | 
			
		||||
        >
 | 
			
		||||
          <Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
 | 
			
		||||
          <Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
 | 
			
		||||
        </Transition.Child>
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          enter="duration-100 ease-out"
 | 
			
		||||
@ -221,7 +221,7 @@ const CommandBar = () => {
 | 
			
		||||
          <Combobox
 | 
			
		||||
            value={selectedCommand}
 | 
			
		||||
            onChange={handleCommandSelection}
 | 
			
		||||
            className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
 | 
			
		||||
            className="rounded relative mx-auto p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
 | 
			
		||||
            as="div"
 | 
			
		||||
          >
 | 
			
		||||
            <div className="flex gap-2 items-center">
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
 | 
			
		||||
  const initialValues: OutputFormat = {
 | 
			
		||||
    type: defaultType,
 | 
			
		||||
    storage: 'embedded',
 | 
			
		||||
    presentation: 'compact',
 | 
			
		||||
    presentation: 'pretty',
 | 
			
		||||
  }
 | 
			
		||||
  const formik = useFormik({
 | 
			
		||||
    initialValues,
 | 
			
		||||
@ -83,8 +83,6 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const yo = formik.values
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <ActionButton
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,9 @@
 | 
			
		||||
import { Dialog, Transition } from '@headlessui/react'
 | 
			
		||||
import { Fragment } from 'react'
 | 
			
		||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
 | 
			
		||||
import { ActionButton } from './ActionButton'
 | 
			
		||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
 | 
			
		||||
export const SetVarNameModal = ({
 | 
			
		||||
  isOpen,
 | 
			
		||||
@ -19,67 +22,65 @@ export const SetVarNameModal = ({
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Transition appear show={isOpen} as={Fragment}>
 | 
			
		||||
      <Dialog as="div" className="relative z-10" onClose={onReject}>
 | 
			
		||||
      <Dialog
 | 
			
		||||
        as="div"
 | 
			
		||||
        className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
 | 
			
		||||
        onClose={onReject}
 | 
			
		||||
      >
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          as={Fragment}
 | 
			
		||||
          enter="ease-out duration-300"
 | 
			
		||||
          enterFrom="opacity-0"
 | 
			
		||||
          enterTo="opacity-100"
 | 
			
		||||
          leave="ease-in duration-200"
 | 
			
		||||
          enterFrom="opacity-0 translate-y-4"
 | 
			
		||||
          enterTo="opacity-100 translate-y-0"
 | 
			
		||||
          leave="ease-in duration-75"
 | 
			
		||||
          leaveFrom="opacity-100"
 | 
			
		||||
          leaveTo="opacity-0"
 | 
			
		||||
        >
 | 
			
		||||
          <div className="fixed inset-0 bg-black bg-opacity-25" />
 | 
			
		||||
          <Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
 | 
			
		||||
        </Transition.Child>
 | 
			
		||||
 | 
			
		||||
        <div className="fixed inset-0 overflow-y-auto">
 | 
			
		||||
          <div className="flex min-h-full items-center justify-center p-4 text-center">
 | 
			
		||||
            <Transition.Child
 | 
			
		||||
              as={Fragment}
 | 
			
		||||
              enter="ease-out duration-300"
 | 
			
		||||
              enterFrom="opacity-0 scale-95"
 | 
			
		||||
              enterTo="opacity-100 scale-100"
 | 
			
		||||
              leave="ease-in duration-200"
 | 
			
		||||
              leaveFrom="opacity-100 scale-100"
 | 
			
		||||
              leaveTo="opacity-0 scale-95"
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          as={Fragment}
 | 
			
		||||
          enter="ease-out duration-300"
 | 
			
		||||
          enterFrom="opacity-0 scale-95"
 | 
			
		||||
          enterTo="opacity-100 scale-100"
 | 
			
		||||
          leave="ease-in duration-200"
 | 
			
		||||
          leaveFrom="opacity-100 scale-100"
 | 
			
		||||
          leaveTo="opacity-0 scale-95"
 | 
			
		||||
        >
 | 
			
		||||
          <Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
 | 
			
		||||
            <form
 | 
			
		||||
              onSubmit={(e) => {
 | 
			
		||||
                e.preventDefault()
 | 
			
		||||
                onResolve({
 | 
			
		||||
                  variableName: newVariableName,
 | 
			
		||||
                })
 | 
			
		||||
                toast.success(`Added variable ${newVariableName}`)
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
 | 
			
		||||
                <Dialog.Title
 | 
			
		||||
                  as="h3"
 | 
			
		||||
                  className="text-lg font-medium leading-6 text-gray-900 capitalize"
 | 
			
		||||
              <CreateNewVariable
 | 
			
		||||
                setNewVariableName={setNewVariableName}
 | 
			
		||||
                newVariableName={newVariableName}
 | 
			
		||||
                isNewVariableNameUnique={isNewVariableNameUnique}
 | 
			
		||||
                shouldCreateVariable={true}
 | 
			
		||||
                showCheckbox={false}
 | 
			
		||||
              />
 | 
			
		||||
              <div className="mt-8 flex justify-between">
 | 
			
		||||
                <ActionButton
 | 
			
		||||
                  Element="button"
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  disabled={!isNewVariableNameUnique}
 | 
			
		||||
                  icon={{ icon: faPlus }}
 | 
			
		||||
                >
 | 
			
		||||
                  Set {valueName}
 | 
			
		||||
                </Dialog.Title>
 | 
			
		||||
 | 
			
		||||
                <CreateNewVariable
 | 
			
		||||
                  setNewVariableName={setNewVariableName}
 | 
			
		||||
                  newVariableName={newVariableName}
 | 
			
		||||
                  isNewVariableNameUnique={isNewVariableNameUnique}
 | 
			
		||||
                  shouldCreateVariable={true}
 | 
			
		||||
                  setShouldCreateVariable={() => {}}
 | 
			
		||||
                />
 | 
			
		||||
                <div className="mt-4">
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    disabled={!isNewVariableNameUnique}
 | 
			
		||||
                    className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
 | 
			
		||||
                      !isNewVariableNameUnique
 | 
			
		||||
                        ? 'opacity-50 cursor-not-allowed'
 | 
			
		||||
                        : ''
 | 
			
		||||
                    }`}
 | 
			
		||||
                    onClick={() =>
 | 
			
		||||
                      onResolve({
 | 
			
		||||
                        variableName: newVariableName,
 | 
			
		||||
                      })
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    Add variable
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </Dialog.Panel>
 | 
			
		||||
            </Transition.Child>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
                  Add variable
 | 
			
		||||
                </ActionButton>
 | 
			
		||||
                <ActionButton Element="button" onClick={() => onReject(false)}>
 | 
			
		||||
                  Cancel
 | 
			
		||||
                </ActionButton>
 | 
			
		||||
              </div>
 | 
			
		||||
            </form>
 | 
			
		||||
          </Dialog.Panel>
 | 
			
		||||
        </Transition.Child>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </Transition>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -9,29 +9,37 @@ import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { useStore } from '../useStore'
 | 
			
		||||
import { getNormalisedCoordinates } from '../lib/utils'
 | 
			
		||||
import Loading from './Loading'
 | 
			
		||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
 | 
			
		||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
 | 
			
		||||
 | 
			
		||||
export const Stream = ({ className = '' }) => {
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(true)
 | 
			
		||||
  const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
 | 
			
		||||
  const videoRef = useRef<HTMLVideoElement>(null)
 | 
			
		||||
  const {
 | 
			
		||||
    mediaStream,
 | 
			
		||||
    engineCommandManager,
 | 
			
		||||
    setIsMouseDownInStream,
 | 
			
		||||
    setCmdId,
 | 
			
		||||
    setButtonDownInStream,
 | 
			
		||||
    didDragInStream,
 | 
			
		||||
    setDidDragInStream,
 | 
			
		||||
    streamDimensions,
 | 
			
		||||
    isExecuting,
 | 
			
		||||
  } = useStore((s) => ({
 | 
			
		||||
    mediaStream: s.mediaStream,
 | 
			
		||||
    engineCommandManager: s.engineCommandManager,
 | 
			
		||||
    isMouseDownInStream: s.isMouseDownInStream,
 | 
			
		||||
    setIsMouseDownInStream: s.setIsMouseDownInStream,
 | 
			
		||||
    setButtonDownInStream: s.setButtonDownInStream,
 | 
			
		||||
    fileId: s.fileId,
 | 
			
		||||
    setCmdId: s.setCmdId,
 | 
			
		||||
    didDragInStream: s.didDragInStream,
 | 
			
		||||
    setDidDragInStream: s.setDidDragInStream,
 | 
			
		||||
    streamDimensions: s.streamDimensions,
 | 
			
		||||
    isExecuting: s.isExecuting,
 | 
			
		||||
  }))
 | 
			
		||||
  const {
 | 
			
		||||
    settings: {
 | 
			
		||||
      context: { cameraControls },
 | 
			
		||||
    },
 | 
			
		||||
  } = useGlobalStateContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
@ -44,24 +52,38 @@ export const Stream = ({ className = '' }) => {
 | 
			
		||||
    videoRef.current.srcObject = mediaStream
 | 
			
		||||
  }, [mediaStream, engineCommandManager])
 | 
			
		||||
 | 
			
		||||
  const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
 | 
			
		||||
    clientX,
 | 
			
		||||
    clientY,
 | 
			
		||||
    ctrlKey,
 | 
			
		||||
  }) => {
 | 
			
		||||
  const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    const { x, y } = getNormalisedCoordinates({
 | 
			
		||||
      clientX,
 | 
			
		||||
      clientY,
 | 
			
		||||
      clientX: e.clientX,
 | 
			
		||||
      clientY: e.clientY,
 | 
			
		||||
      el: videoRef.current,
 | 
			
		||||
      ...streamDimensions,
 | 
			
		||||
    })
 | 
			
		||||
    console.log('click', x, y)
 | 
			
		||||
 | 
			
		||||
    const newId = uuidv4()
 | 
			
		||||
    setCmdId(newId)
 | 
			
		||||
 | 
			
		||||
    const interaction = ctrlKey ? 'pan' : 'rotate'
 | 
			
		||||
    const interactionGuards = cameraMouseDragGuards[cameraControls]
 | 
			
		||||
    let interaction: CameraDragInteractionType_type
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      interactionGuards.pan.callback(e) ||
 | 
			
		||||
      interactionGuards.pan.lenientDragStartButton === e.button
 | 
			
		||||
    ) {
 | 
			
		||||
      interaction = 'pan'
 | 
			
		||||
    } else if (
 | 
			
		||||
      interactionGuards.rotate.callback(e) ||
 | 
			
		||||
      interactionGuards.rotate.lenientDragStartButton === e.button
 | 
			
		||||
    ) {
 | 
			
		||||
      interaction = 'rotate'
 | 
			
		||||
    } else if (
 | 
			
		||||
      interactionGuards.zoom.dragCallback(e) ||
 | 
			
		||||
      interactionGuards.zoom.lenientDragStartButton === e.button
 | 
			
		||||
    ) {
 | 
			
		||||
      interaction = 'zoom'
 | 
			
		||||
    } else {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    engineCommandManager?.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
@ -73,11 +95,13 @@ export const Stream = ({ className = '' }) => {
 | 
			
		||||
      cmd_id: newId,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    setIsMouseDownInStream(true)
 | 
			
		||||
    setButtonDownInStream(e.button)
 | 
			
		||||
    setClickCoords({ x, y })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
 | 
			
		||||
 | 
			
		||||
    engineCommandManager?.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd: {
 | 
			
		||||
@ -114,7 +138,7 @@ export const Stream = ({ className = '' }) => {
 | 
			
		||||
      cmd_id: newCmdId,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    setIsMouseDownInStream(false)
 | 
			
		||||
    setButtonDownInStream(undefined)
 | 
			
		||||
    if (!didDragInStream) {
 | 
			
		||||
      engineCommandManager?.sendSceneCommand({
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
@ -127,6 +151,19 @@ export const Stream = ({ className = '' }) => {
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    setDidDragInStream(false)
 | 
			
		||||
    setClickCoords(undefined)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
 | 
			
		||||
    if (!clickCoords) return
 | 
			
		||||
 | 
			
		||||
    const delta =
 | 
			
		||||
      ((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) **
 | 
			
		||||
      0.5
 | 
			
		||||
 | 
			
		||||
    if (delta > 5 && !didDragInStream) {
 | 
			
		||||
      setDidDragInStream(true)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -142,7 +179,9 @@ export const Stream = ({ className = '' }) => {
 | 
			
		||||
        onContextMenuCapture={(e) => e.preventDefault()}
 | 
			
		||||
        onWheel={handleScroll}
 | 
			
		||||
        onPlay={() => setIsLoading(false)}
 | 
			
		||||
        className="w-full h-full"
 | 
			
		||||
        onMouseMoveCapture={handleMouseMove}
 | 
			
		||||
        className={`w-full h-full ${isExecuting && 'blur-md'}`}
 | 
			
		||||
        style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
 | 
			
		||||
      />
 | 
			
		||||
      {isLoading && (
 | 
			
		||||
        <div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										267
									
								
								src/components/TextEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								src/components/TextEditor.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,267 @@
 | 
			
		||||
import ReactCodeMirror, {
 | 
			
		||||
  Extension,
 | 
			
		||||
  ViewUpdate,
 | 
			
		||||
  keymap,
 | 
			
		||||
} from '@uiw/react-codemirror'
 | 
			
		||||
import { FromServer, IntoServer } from 'editor/lsp/codec'
 | 
			
		||||
import Server from '../editor/lsp/server'
 | 
			
		||||
import Client from '../editor/lsp/client'
 | 
			
		||||
import { TEST } from 'env'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
 | 
			
		||||
import { Themes } from 'lib/theme'
 | 
			
		||||
import { useMemo } from 'react'
 | 
			
		||||
import { linter, lintGutter } from '@codemirror/lint'
 | 
			
		||||
import { Selections, useStore } from 'useStore'
 | 
			
		||||
import { LanguageServerClient } from 'editor/lsp'
 | 
			
		||||
import kclLanguage from 'editor/lsp/language'
 | 
			
		||||
import { isTauri } from 'lib/isTauri'
 | 
			
		||||
import { useParams } from 'react-router-dom'
 | 
			
		||||
import { writeTextFile } from '@tauri-apps/api/fs'
 | 
			
		||||
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
import {
 | 
			
		||||
  EditorView,
 | 
			
		||||
  addLineHighlight,
 | 
			
		||||
  lineHighlightField,
 | 
			
		||||
} from 'editor/highlightextension'
 | 
			
		||||
import { isOverlap } from 'lib/utils'
 | 
			
		||||
import { kclErrToDiagnostic } from 'lang/errors'
 | 
			
		||||
import { CSSRuleObject } from 'tailwindcss/types/config'
 | 
			
		||||
 | 
			
		||||
export const editorShortcutMeta = {
 | 
			
		||||
  formatCode: {
 | 
			
		||||
    codeMirror: 'Alt-Shift-f',
 | 
			
		||||
    display: 'Alt + Shift + F',
 | 
			
		||||
  },
 | 
			
		||||
  convertToVariable: {
 | 
			
		||||
    codeMirror: 'Ctrl-Shift-c',
 | 
			
		||||
    display: 'Ctrl + Shift + C',
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TextEditor = ({
 | 
			
		||||
  theme,
 | 
			
		||||
}: {
 | 
			
		||||
  theme: Themes.Light | Themes.Dark
 | 
			
		||||
}) => {
 | 
			
		||||
  const pathParams = useParams()
 | 
			
		||||
  const {
 | 
			
		||||
    code,
 | 
			
		||||
    defferedSetCode,
 | 
			
		||||
    editorView,
 | 
			
		||||
    engineCommandManager,
 | 
			
		||||
    formatCode,
 | 
			
		||||
    isLSPServerReady,
 | 
			
		||||
    selectionRanges,
 | 
			
		||||
    selectionRangeTypeMap,
 | 
			
		||||
    setEditorView,
 | 
			
		||||
    setIsLSPServerReady,
 | 
			
		||||
    setSelectionRanges,
 | 
			
		||||
    sourceRangeMap,
 | 
			
		||||
  } = useStore((s) => ({
 | 
			
		||||
    code: s.code,
 | 
			
		||||
    defferedCode: s.defferedCode,
 | 
			
		||||
    defferedSetCode: s.defferedSetCode,
 | 
			
		||||
    editorView: s.editorView,
 | 
			
		||||
    engineCommandManager: s.engineCommandManager,
 | 
			
		||||
    formatCode: s.formatCode,
 | 
			
		||||
    isLSPServerReady: s.isLSPServerReady,
 | 
			
		||||
    selectionRanges: s.selectionRanges,
 | 
			
		||||
    selectionRangeTypeMap: s.selectionRangeTypeMap,
 | 
			
		||||
    setCode: s.setCode,
 | 
			
		||||
    setEditorView: s.setEditorView,
 | 
			
		||||
    setIsLSPServerReady: s.setIsLSPServerReady,
 | 
			
		||||
    setSelectionRanges: s.setSelectionRanges,
 | 
			
		||||
    sourceRangeMap: s.sourceRangeMap,
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    settings: {
 | 
			
		||||
      context: { textWrapping },
 | 
			
		||||
    },
 | 
			
		||||
  } = useGlobalStateContext()
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const { enable: convertEnabled, handleClick: convertCallback } =
 | 
			
		||||
    useConvertToVariable()
 | 
			
		||||
 | 
			
		||||
  // 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.
 | 
			
		||||
  // Below is the client and server promise.
 | 
			
		||||
  const { lspClient } = useMemo(() => {
 | 
			
		||||
    const intoServer: IntoServer = new IntoServer()
 | 
			
		||||
    const fromServer: FromServer = FromServer.create()
 | 
			
		||||
    const client = new Client(fromServer, intoServer)
 | 
			
		||||
    if (!TEST) {
 | 
			
		||||
      Server.initialize(intoServer, fromServer).then((lspServer) => {
 | 
			
		||||
        lspServer.start()
 | 
			
		||||
        setIsLSPServerReady(true)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lspClient = new LanguageServerClient({ client })
 | 
			
		||||
    return { lspClient }
 | 
			
		||||
  }, [setIsLSPServerReady])
 | 
			
		||||
 | 
			
		||||
  // Here we initialize the plugin which will start the client.
 | 
			
		||||
  // When we have multi-file support the name of the file will be a dep of
 | 
			
		||||
  // this use memo, as well as the directory structure, which I think is
 | 
			
		||||
  // a good setup becuase it will restart the client but not the server :)
 | 
			
		||||
  // We do not want to restart the server, its just wasteful.
 | 
			
		||||
  const kclLSP = useMemo(() => {
 | 
			
		||||
    let plugin = null
 | 
			
		||||
    if (isLSPServerReady && !TEST) {
 | 
			
		||||
      // Set up the lsp plugin.
 | 
			
		||||
      const lsp = kclLanguage({
 | 
			
		||||
        // When we have more than one file, we'll need to change this.
 | 
			
		||||
        documentUri: `file:///we-just-have-one-file-for-now.kcl`,
 | 
			
		||||
        workspaceFolders: null,
 | 
			
		||||
        client: lspClient,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      plugin = lsp
 | 
			
		||||
    }
 | 
			
		||||
    return plugin
 | 
			
		||||
  }, [lspClient, isLSPServerReady])
 | 
			
		||||
 | 
			
		||||
  // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
 | 
			
		||||
  const onChange = (value: string, viewUpdate: ViewUpdate) => {
 | 
			
		||||
    defferedSetCode(value)
 | 
			
		||||
    if (isTauri() && pathParams.id) {
 | 
			
		||||
      // Save the file to disk
 | 
			
		||||
      // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
 | 
			
		||||
      writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
 | 
			
		||||
        (err) => {
 | 
			
		||||
          // TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
 | 
			
		||||
          console.error('error saving file', err)
 | 
			
		||||
          toast.error('Error saving file, please check file permissions')
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    if (editorView) {
 | 
			
		||||
      editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
 | 
			
		||||
    }
 | 
			
		||||
  } //, []);
 | 
			
		||||
  const onUpdate = (viewUpdate: ViewUpdate) => {
 | 
			
		||||
    if (!editorView) {
 | 
			
		||||
      setEditorView(viewUpdate.view)
 | 
			
		||||
    }
 | 
			
		||||
    const ranges = viewUpdate.state.selection.ranges
 | 
			
		||||
 | 
			
		||||
    const isChange =
 | 
			
		||||
      ranges.length !== selectionRanges.codeBasedSelections.length ||
 | 
			
		||||
      ranges.some(({ from, to }, i) => {
 | 
			
		||||
        return (
 | 
			
		||||
          from !== selectionRanges.codeBasedSelections[i].range[0] ||
 | 
			
		||||
          to !== selectionRanges.codeBasedSelections[i].range[1]
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    if (!isChange) return
 | 
			
		||||
    const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
 | 
			
		||||
      ({ from, to }) => {
 | 
			
		||||
        if (selectionRangeTypeMap[to]) {
 | 
			
		||||
          return {
 | 
			
		||||
            type: selectionRangeTypeMap[to],
 | 
			
		||||
            range: [from, to],
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
          type: 'default',
 | 
			
		||||
          range: [from, to],
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    const idBasedSelections = codeBasedSelections
 | 
			
		||||
      .map(({ type, range }) => {
 | 
			
		||||
        const hasOverlap = Object.entries(sourceRangeMap).filter(
 | 
			
		||||
          ([_, sourceRange]) => {
 | 
			
		||||
            return isOverlap(sourceRange, range)
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
        if (hasOverlap.length) {
 | 
			
		||||
          return {
 | 
			
		||||
            type,
 | 
			
		||||
            id: hasOverlap[0][0],
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .filter(Boolean) as any
 | 
			
		||||
 | 
			
		||||
    engineCommandManager?.cusorsSelected({
 | 
			
		||||
      otherSelections: [],
 | 
			
		||||
      idBasedSelections,
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    setSelectionRanges({
 | 
			
		||||
      otherSelections: [],
 | 
			
		||||
      codeBasedSelections,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const editorExtensions = useMemo(() => {
 | 
			
		||||
    const extensions = [
 | 
			
		||||
      lineHighlightField,
 | 
			
		||||
      keymap.of([
 | 
			
		||||
        {
 | 
			
		||||
          key: 'Meta-k',
 | 
			
		||||
          run: () => {
 | 
			
		||||
            setCommandBarOpen(true)
 | 
			
		||||
            return false
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          key: editorShortcutMeta.formatCode.codeMirror,
 | 
			
		||||
          run: () => {
 | 
			
		||||
            formatCode()
 | 
			
		||||
            return true
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          key: editorShortcutMeta.convertToVariable.codeMirror,
 | 
			
		||||
          run: () => {
 | 
			
		||||
            if (convertEnabled) {
 | 
			
		||||
              convertCallback()
 | 
			
		||||
              return true
 | 
			
		||||
            }
 | 
			
		||||
            return false
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ]),
 | 
			
		||||
    ] as Extension[]
 | 
			
		||||
 | 
			
		||||
    if (kclLSP) extensions.push(kclLSP)
 | 
			
		||||
 | 
			
		||||
    // These extensions have proven to mess with vitest
 | 
			
		||||
    if (!TEST) {
 | 
			
		||||
      extensions.push(
 | 
			
		||||
        lintGutter(),
 | 
			
		||||
        linter((_view) => {
 | 
			
		||||
          return kclErrToDiagnostic(useStore.getState().kclErrors)
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return extensions
 | 
			
		||||
  }, [kclLSP, textWrapping])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      id="code-mirror-override"
 | 
			
		||||
      className="full-height-subtract"
 | 
			
		||||
      style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
 | 
			
		||||
    >
 | 
			
		||||
      <ReactCodeMirror
 | 
			
		||||
        className="h-full"
 | 
			
		||||
        value={code}
 | 
			
		||||
        extensions={editorExtensions}
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        onUpdate={onUpdate}
 | 
			
		||||
        theme={theme}
 | 
			
		||||
        onCreateEditor={(_editorView) => setEditorView(_editorView)}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -1,61 +0,0 @@
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
import { create } from 'react-modal-promise'
 | 
			
		||||
import { useStore } from '../../useStore'
 | 
			
		||||
import { isNodeSafeToReplace } from '../../lang/queryAst'
 | 
			
		||||
import { SetVarNameModal } from '../SetVarNameModal'
 | 
			
		||||
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
 | 
			
		||||
 | 
			
		||||
const getModalInfo = create(SetVarNameModal as any)
 | 
			
		||||
 | 
			
		||||
export const ConvertToVariable = () => {
 | 
			
		||||
  const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
 | 
			
		||||
    (s) => ({
 | 
			
		||||
      guiMode: s.guiMode,
 | 
			
		||||
      ast: s.ast,
 | 
			
		||||
      updateAst: s.updateAst,
 | 
			
		||||
      selectionRanges: s.selectionRanges,
 | 
			
		||||
      programMemory: s.programMemory,
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
  const [enableAngLen, setEnableAngLen] = useState(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!ast) return
 | 
			
		||||
 | 
			
		||||
    const { isSafe, value } = isNodeSafeToReplace(
 | 
			
		||||
      ast,
 | 
			
		||||
      selectionRanges.codeBasedSelections?.[0]?.range || []
 | 
			
		||||
    )
 | 
			
		||||
    const canReplace = isSafe && value.type !== 'Identifier'
 | 
			
		||||
    const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
 | 
			
		||||
 | 
			
		||||
    const _enableHorz = canReplace && isOnlyOneSelection
 | 
			
		||||
    setEnableAngLen(_enableHorz)
 | 
			
		||||
  }, [guiMode, selectionRanges])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      onClick={async () => {
 | 
			
		||||
        if (!ast) return
 | 
			
		||||
        try {
 | 
			
		||||
          const { variableName } = await getModalInfo({
 | 
			
		||||
            valueName: 'var',
 | 
			
		||||
          } as any)
 | 
			
		||||
 | 
			
		||||
          const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
 | 
			
		||||
            ast,
 | 
			
		||||
            programMemory,
 | 
			
		||||
            selectionRanges.codeBasedSelections[0].range,
 | 
			
		||||
            variableName
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          updateAst(_modifiedAst)
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.log('e', e)
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
      disabled={!enableAngLen}
 | 
			
		||||
    >
 | 
			
		||||
      ConvertToVariable
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										185
									
								
								src/editor/lsp/client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/editor/lsp/client.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,185 @@
 | 
			
		||||
import * as jsrpc from 'json-rpc-2.0'
 | 
			
		||||
import * as LSP from 'vscode-languageserver-protocol'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  registerServerCapability,
 | 
			
		||||
  unregisterServerCapability,
 | 
			
		||||
} from './server-capability-registration'
 | 
			
		||||
import { Codec, FromServer, IntoServer } from './codec'
 | 
			
		||||
 | 
			
		||||
const client_capabilities: LSP.ClientCapabilities = {
 | 
			
		||||
  textDocument: {
 | 
			
		||||
    hover: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      contentFormat: ['plaintext', 'markdown'],
 | 
			
		||||
    },
 | 
			
		||||
    moniker: {},
 | 
			
		||||
    synchronization: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      willSave: false,
 | 
			
		||||
      didSave: false,
 | 
			
		||||
      willSaveWaitUntil: false,
 | 
			
		||||
    },
 | 
			
		||||
    completion: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      completionItem: {
 | 
			
		||||
        snippetSupport: false,
 | 
			
		||||
        commitCharactersSupport: true,
 | 
			
		||||
        documentationFormat: ['plaintext', 'markdown'],
 | 
			
		||||
        deprecatedSupport: false,
 | 
			
		||||
        preselectSupport: false,
 | 
			
		||||
      },
 | 
			
		||||
      contextSupport: false,
 | 
			
		||||
    },
 | 
			
		||||
    signatureHelp: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      signatureInformation: {
 | 
			
		||||
        documentationFormat: ['plaintext', 'markdown'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    declaration: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      linkSupport: true,
 | 
			
		||||
    },
 | 
			
		||||
    definition: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      linkSupport: true,
 | 
			
		||||
    },
 | 
			
		||||
    typeDefinition: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      linkSupport: true,
 | 
			
		||||
    },
 | 
			
		||||
    implementation: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
      linkSupport: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  workspace: {
 | 
			
		||||
    didChangeConfiguration: {
 | 
			
		||||
      dynamicRegistration: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class Client extends jsrpc.JSONRPCServerAndClient {
 | 
			
		||||
  afterInitializedHooks: (() => Promise<void>)[] = []
 | 
			
		||||
  #fromServer: FromServer
 | 
			
		||||
  private serverCapabilities: LSP.ServerCapabilities<any> = {}
 | 
			
		||||
 | 
			
		||||
  constructor(fromServer: FromServer, intoServer: IntoServer) {
 | 
			
		||||
    super(
 | 
			
		||||
      new jsrpc.JSONRPCServer(),
 | 
			
		||||
      new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
 | 
			
		||||
        const encoded = Codec.encode(json)
 | 
			
		||||
        intoServer.enqueue(encoded)
 | 
			
		||||
        if (null != json.id) {
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
			
		||||
          const response = await fromServer.responses.get(json.id)!
 | 
			
		||||
          this.client.receive(response as jsrpc.JSONRPCResponse)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    this.#fromServer = fromServer
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async start(): Promise<void> {
 | 
			
		||||
    // process "window/logMessage": client <- server
 | 
			
		||||
    this.addMethod(LSP.LogMessageNotification.type.method, (params) => {
 | 
			
		||||
      const { type, message } = params as {
 | 
			
		||||
        type: LSP.MessageType
 | 
			
		||||
        message: string
 | 
			
		||||
      }
 | 
			
		||||
      let messageString = ''
 | 
			
		||||
      switch (type) {
 | 
			
		||||
        case LSP.MessageType.Error: {
 | 
			
		||||
          messageString += '[error] '
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case LSP.MessageType.Warning: {
 | 
			
		||||
          messageString += ' [warn] '
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case LSP.MessageType.Info: {
 | 
			
		||||
          messageString += ' [info] '
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        case LSP.MessageType.Log: {
 | 
			
		||||
          messageString += '  [log] '
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      messageString += message
 | 
			
		||||
      // console.log(messageString)
 | 
			
		||||
      return
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // process "client/registerCapability": client <- server
 | 
			
		||||
    this.addMethod(LSP.RegistrationRequest.type.method, (params) => {
 | 
			
		||||
      // Register a server capability.
 | 
			
		||||
      params.registrations.forEach(
 | 
			
		||||
        (capabilityRegistration: LSP.Registration) => {
 | 
			
		||||
          this.serverCapabilities = registerServerCapability(
 | 
			
		||||
            this.serverCapabilities,
 | 
			
		||||
            capabilityRegistration
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // process "client/unregisterCapability": client <- server
 | 
			
		||||
    this.addMethod(LSP.UnregistrationRequest.type.method, (params) => {
 | 
			
		||||
      // Unregister a server capability.
 | 
			
		||||
      params.unregisterations.forEach(
 | 
			
		||||
        (capabilityUnregistration: LSP.Unregistration) => {
 | 
			
		||||
          this.serverCapabilities = unregisterServerCapability(
 | 
			
		||||
            this.serverCapabilities,
 | 
			
		||||
            capabilityUnregistration
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // request "initialize": client <-> server
 | 
			
		||||
    const { capabilities } = await this.request(
 | 
			
		||||
      LSP.InitializeRequest.type.method,
 | 
			
		||||
      {
 | 
			
		||||
        processId: null,
 | 
			
		||||
        clientInfo: {
 | 
			
		||||
          name: 'kcl-language-client',
 | 
			
		||||
        },
 | 
			
		||||
        capabilities: client_capabilities,
 | 
			
		||||
        rootUri: null,
 | 
			
		||||
      } as LSP.InitializeParams
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    this.serverCapabilities = capabilities
 | 
			
		||||
 | 
			
		||||
    // notify "initialized": client --> server
 | 
			
		||||
    this.notify(LSP.InitializedNotification.type.method, {})
 | 
			
		||||
 | 
			
		||||
    await Promise.all(
 | 
			
		||||
      this.afterInitializedHooks.map((f: () => Promise<void>) => f())
 | 
			
		||||
    )
 | 
			
		||||
    await Promise.all([this.processNotifications(), this.processRequests()])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getServerCapabilities(): LSP.ServerCapabilities<any> {
 | 
			
		||||
    return this.serverCapabilities
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async processNotifications(): Promise<void> {
 | 
			
		||||
    for await (const notification of this.#fromServer.notifications) {
 | 
			
		||||
      await this.receiveAndSend(notification)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async processRequests(): Promise<void> {
 | 
			
		||||
    for await (const request of this.#fromServer.requests) {
 | 
			
		||||
      await this.receiveAndSend(request)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pushAfterInitializeHook(...hooks: (() => Promise<void>)[]): void {
 | 
			
		||||
    this.afterInitializedHooks.push(...hooks)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								src/editor/lsp/codec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/editor/lsp/codec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
			
		||||
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 Tracer from './tracer'
 | 
			
		||||
 | 
			
		||||
export const encoder = new TextEncoder()
 | 
			
		||||
export const decoder = new TextDecoder()
 | 
			
		||||
 | 
			
		||||
export class Codec {
 | 
			
		||||
  static encode(
 | 
			
		||||
    json: jsrpc.JSONRPCRequest | jsrpc.JSONRPCResponse
 | 
			
		||||
  ): Uint8Array {
 | 
			
		||||
    const message = JSON.stringify(json)
 | 
			
		||||
    const delimited = Headers.add(message)
 | 
			
		||||
    return Bytes.encode(delimited)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static decode<T>(data: Uint8Array): T {
 | 
			
		||||
    const delimited = Bytes.decode(data)
 | 
			
		||||
    const message = Headers.remove(delimited)
 | 
			
		||||
    return JSON.parse(message) as T
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME: tracing effiency
 | 
			
		||||
export class IntoServer
 | 
			
		||||
  extends Queue<Uint8Array>
 | 
			
		||||
  implements AsyncGenerator<Uint8Array, never, void>
 | 
			
		||||
{
 | 
			
		||||
  enqueue(item: Uint8Array): void {
 | 
			
		||||
    Tracer.client(Headers.remove(decoder.decode(item)))
 | 
			
		||||
    super.enqueue(item)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FromServer extends WritableStream<Uint8Array> {
 | 
			
		||||
  readonly responses: {
 | 
			
		||||
    get(key: number | string): null | Promise<vsrpc.ResponseMessage>
 | 
			
		||||
  }
 | 
			
		||||
  readonly notifications: AsyncGenerator<vsrpc.NotificationMessage, never, void>
 | 
			
		||||
  readonly requests: AsyncGenerator<vsrpc.RequestMessage, never, void>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-namespace
 | 
			
		||||
export namespace FromServer {
 | 
			
		||||
  export function create(): FromServer {
 | 
			
		||||
    return new StreamDemuxer()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/editor/lsp/codec/bytes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/editor/lsp/codec/bytes.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import { encoder, decoder } from '../codec'
 | 
			
		||||
 | 
			
		||||
export default class Bytes {
 | 
			
		||||
  static encode(input: string): Uint8Array {
 | 
			
		||||
    return encoder.encode(input)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static decode(input: Uint8Array): string {
 | 
			
		||||
    return decoder.decode(input)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static append<
 | 
			
		||||
    T extends { length: number; set(arr: T, offset: number): void }
 | 
			
		||||
  >(constructor: { new (length: number): T }, ...arrays: T[]) {
 | 
			
		||||
    let totalLength = 0
 | 
			
		||||
    for (const arr of arrays) {
 | 
			
		||||
      totalLength += arr.length
 | 
			
		||||
    }
 | 
			
		||||
    const result = new constructor(totalLength)
 | 
			
		||||
    let offset = 0
 | 
			
		||||
    for (const arr of arrays) {
 | 
			
		||||
      result.set(arr, offset)
 | 
			
		||||
      offset += arr.length
 | 
			
		||||
    }
 | 
			
		||||
    return result
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								src/editor/lsp/codec/demuxer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/editor/lsp/codec/demuxer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
import * as vsrpc from 'vscode-jsonrpc'
 | 
			
		||||
 | 
			
		||||
import Bytes from './bytes'
 | 
			
		||||
import PromiseMap from './map'
 | 
			
		||||
import Queue from './queue'
 | 
			
		||||
import Tracer from '../tracer'
 | 
			
		||||
 | 
			
		||||
export default class StreamDemuxer extends Queue<Uint8Array> {
 | 
			
		||||
  readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> =
 | 
			
		||||
    new PromiseMap()
 | 
			
		||||
  readonly notifications: Queue<vsrpc.NotificationMessage> =
 | 
			
		||||
    new Queue<vsrpc.NotificationMessage>()
 | 
			
		||||
  readonly requests: Queue<vsrpc.RequestMessage> =
 | 
			
		||||
    new Queue<vsrpc.RequestMessage>()
 | 
			
		||||
 | 
			
		||||
  readonly #start: Promise<void>
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super()
 | 
			
		||||
    this.#start = this.start()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async start(): Promise<void> {
 | 
			
		||||
    let contentLength: null | number = null
 | 
			
		||||
    let buffer = new Uint8Array()
 | 
			
		||||
 | 
			
		||||
    for await (const bytes of this) {
 | 
			
		||||
      buffer = Bytes.append(Uint8Array, buffer, bytes)
 | 
			
		||||
      while (buffer.length > 0) {
 | 
			
		||||
        // check if the content length is known
 | 
			
		||||
        if (null == contentLength) {
 | 
			
		||||
          // if not, try to match the prefixed headers
 | 
			
		||||
          const match = Bytes.decode(buffer).match(
 | 
			
		||||
            /^Content-Length:\s*(\d+)\s*/
 | 
			
		||||
          )
 | 
			
		||||
          if (null == match) continue
 | 
			
		||||
 | 
			
		||||
          // try to parse the content-length from the headers
 | 
			
		||||
          const length = parseInt(match[1])
 | 
			
		||||
          if (isNaN(length)) throw new Error('invalid content length')
 | 
			
		||||
 | 
			
		||||
          // slice the headers since we now have the content length
 | 
			
		||||
          buffer = buffer.slice(match[0].length)
 | 
			
		||||
 | 
			
		||||
          // set the content length
 | 
			
		||||
          contentLength = length
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if the buffer doesn't contain a full message; await another iteration
 | 
			
		||||
        if (buffer.length < contentLength) continue
 | 
			
		||||
 | 
			
		||||
        // Get just the slice of the buffer that is our content length.
 | 
			
		||||
        const slice = buffer.slice(0, contentLength)
 | 
			
		||||
 | 
			
		||||
        // decode buffer to a string
 | 
			
		||||
        const delimited = Bytes.decode(slice)
 | 
			
		||||
 | 
			
		||||
        // reset the buffer
 | 
			
		||||
        buffer = buffer.slice(contentLength)
 | 
			
		||||
        // reset the contentLength
 | 
			
		||||
        contentLength = null
 | 
			
		||||
 | 
			
		||||
        const message = JSON.parse(delimited) as vsrpc.Message
 | 
			
		||||
        Tracer.server(message)
 | 
			
		||||
 | 
			
		||||
        // demux the message stream
 | 
			
		||||
        if (vsrpc.Message.isResponse(message) && null != message.id) {
 | 
			
		||||
          this.responses.set(message.id, message)
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
        if (vsrpc.Message.isNotification(message)) {
 | 
			
		||||
          this.notifications.enqueue(message)
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
        if (vsrpc.Message.isRequest(message)) {
 | 
			
		||||
          this.requests.enqueue(message)
 | 
			
		||||
          continue
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/editor/lsp/codec/headers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/editor/lsp/codec/headers.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
export default class Headers {
 | 
			
		||||
  static add(message: string): string {
 | 
			
		||||
    return `Content-Length: ${message.length}\r\n\r\n${message}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static remove(delimited: string): string {
 | 
			
		||||
    return delimited.replace(/^Content-Length:\s*\d+\s*/, '')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								src/editor/lsp/codec/map.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/editor/lsp/codec/map.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
export default class PromiseMap<K, V extends { toString(): string }> {
 | 
			
		||||
  #map: Map<K, PromiseMap.Entry<V>> = new Map()
 | 
			
		||||
 | 
			
		||||
  get(key: K & { toString(): string }): null | Promise<V> {
 | 
			
		||||
    let initialized: PromiseMap.Entry<V>
 | 
			
		||||
    // if the entry doesn't exist, set it
 | 
			
		||||
    if (!this.#map.has(key)) {
 | 
			
		||||
      initialized = this.#set(key)
 | 
			
		||||
    } else {
 | 
			
		||||
      // otherwise return the entry
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
			
		||||
      initialized = this.#map.get(key)!
 | 
			
		||||
    }
 | 
			
		||||
    // if the entry is a pending promise, return it
 | 
			
		||||
    if (initialized.status === 'pending') {
 | 
			
		||||
      return initialized.promise
 | 
			
		||||
    } else {
 | 
			
		||||
      // otherwise return null
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #set(key: K, value?: V): PromiseMap.Entry<V> {
 | 
			
		||||
    if (this.#map.has(key)) {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
			
		||||
      return this.#map.get(key)!
 | 
			
		||||
    }
 | 
			
		||||
    // placeholder resolver for entry
 | 
			
		||||
    let resolve = (item: V) => {
 | 
			
		||||
      void item
 | 
			
		||||
    }
 | 
			
		||||
    // promise for entry (which assigns the resolver
 | 
			
		||||
    const promise = new Promise<V>((resolver) => {
 | 
			
		||||
      resolve = resolver
 | 
			
		||||
    })
 | 
			
		||||
    // the initialized entry
 | 
			
		||||
    const initialized: PromiseMap.Entry<V> = {
 | 
			
		||||
      status: 'pending',
 | 
			
		||||
      resolve,
 | 
			
		||||
      promise,
 | 
			
		||||
    }
 | 
			
		||||
    if (null != value) {
 | 
			
		||||
      initialized.resolve(value)
 | 
			
		||||
    }
 | 
			
		||||
    // set the entry
 | 
			
		||||
    this.#map.set(key, initialized)
 | 
			
		||||
    return initialized
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set(key: K & { toString(): string }, value: V): this {
 | 
			
		||||
    const initialized = this.#set(key, value)
 | 
			
		||||
    // if the promise is pending ...
 | 
			
		||||
    if (initialized.status === 'pending') {
 | 
			
		||||
      // ... set the entry status to resolved to free the promise
 | 
			
		||||
      this.#map.set(key, { status: 'resolved' })
 | 
			
		||||
      // ... and resolve the promise with the given value
 | 
			
		||||
      initialized.resolve(value)
 | 
			
		||||
    }
 | 
			
		||||
    return this
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get size(): number {
 | 
			
		||||
    return this.#map.size
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-namespace
 | 
			
		||||
export namespace PromiseMap {
 | 
			
		||||
  export type Entry<V> =
 | 
			
		||||
    | { status: 'pending'; resolve: (item: V) => void; promise: Promise<V> }
 | 
			
		||||
    | { status: 'resolved' }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								src/editor/lsp/codec/queue.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/editor/lsp/codec/queue.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,113 @@
 | 
			
		||||
export default class Queue<T>
 | 
			
		||||
  implements WritableStream<T>, AsyncGenerator<T, never, void>
 | 
			
		||||
{
 | 
			
		||||
  readonly #promises: Promise<T>[] = []
 | 
			
		||||
  readonly #resolvers: ((item: T) => void)[] = []
 | 
			
		||||
  readonly #observers: ((item: T) => void)[] = []
 | 
			
		||||
 | 
			
		||||
  #closed = false
 | 
			
		||||
  #locked = false
 | 
			
		||||
  readonly #stream: WritableStream<T>
 | 
			
		||||
 | 
			
		||||
  static #__add<X>(
 | 
			
		||||
    promises: Promise<X>[],
 | 
			
		||||
    resolvers: ((item: X) => void)[]
 | 
			
		||||
  ): void {
 | 
			
		||||
    promises.push(
 | 
			
		||||
      new Promise((resolve) => {
 | 
			
		||||
        resolvers.push(resolve)
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static #__enqueue<X>(
 | 
			
		||||
    closed: boolean,
 | 
			
		||||
    promises: Promise<X>[],
 | 
			
		||||
    resolvers: ((item: X) => void)[],
 | 
			
		||||
    item: X
 | 
			
		||||
  ): void {
 | 
			
		||||
    if (!closed) {
 | 
			
		||||
      if (!resolvers.length) Queue.#__add(promises, resolvers)
 | 
			
		||||
      const resolve = resolvers.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion
 | 
			
		||||
      resolve(item)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    const closed = this.#closed
 | 
			
		||||
    const promises = this.#promises
 | 
			
		||||
    const resolvers = this.#resolvers
 | 
			
		||||
    this.#stream = new WritableStream({
 | 
			
		||||
      write(item: T): void {
 | 
			
		||||
        Queue.#__enqueue(closed, promises, resolvers, item)
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #add(): void {
 | 
			
		||||
    return Queue.#__add(this.#promises, this.#resolvers)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  enqueue(item: T): void {
 | 
			
		||||
    return Queue.#__enqueue(this.#closed, this.#promises, this.#resolvers, item)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dequeue(): Promise<T> {
 | 
			
		||||
    if (!this.#promises.length) this.#add()
 | 
			
		||||
    const item = this.#promises.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion
 | 
			
		||||
    return item
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isEmpty(): boolean {
 | 
			
		||||
    return !this.#promises.length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isBlocked(): boolean {
 | 
			
		||||
    return !!this.#resolvers.length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get length(): number {
 | 
			
		||||
    return this.#promises.length - this.#resolvers.length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async next(): Promise<IteratorResult<T, never>> {
 | 
			
		||||
    const done = false
 | 
			
		||||
    const value = await this.dequeue()
 | 
			
		||||
    for (const observer of this.#observers) {
 | 
			
		||||
      observer(value)
 | 
			
		||||
    }
 | 
			
		||||
    return { done, value }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return(): Promise<IteratorResult<T, never>> {
 | 
			
		||||
    return new Promise(() => {
 | 
			
		||||
      // empty
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  throw(err: Error): Promise<IteratorResult<T, never>> {
 | 
			
		||||
    return new Promise((_resolve, reject) => {
 | 
			
		||||
      reject(err)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [Symbol.asyncIterator](): AsyncGenerator<T, never, void> {
 | 
			
		||||
    return this
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get locked(): boolean {
 | 
			
		||||
    return this.#stream.locked
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  abort(reason?: Error): Promise<void> {
 | 
			
		||||
    return this.#stream.abort(reason)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  close(): Promise<void> {
 | 
			
		||||
    return this.#stream.close()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWriter(): WritableStreamDefaultWriter<T> {
 | 
			
		||||
    return this.#stream.getWriter()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										151
									
								
								src/editor/lsp/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/editor/lsp/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,151 @@
 | 
			
		||||
import type * as LSP from 'vscode-languageserver-protocol'
 | 
			
		||||
import Client from './client'
 | 
			
		||||
import { LanguageServerPlugin } from './plugin'
 | 
			
		||||
import { SemanticToken, deserializeTokens } from './semantic_tokens'
 | 
			
		||||
 | 
			
		||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
 | 
			
		||||
 | 
			
		||||
// Client to server then server to client
 | 
			
		||||
interface LSPRequestMap {
 | 
			
		||||
  initialize: [LSP.InitializeParams, LSP.InitializeResult]
 | 
			
		||||
  'textDocument/hover': [LSP.HoverParams, LSP.Hover]
 | 
			
		||||
  'textDocument/completion': [
 | 
			
		||||
    LSP.CompletionParams,
 | 
			
		||||
    LSP.CompletionItem[] | LSP.CompletionList | null
 | 
			
		||||
  ]
 | 
			
		||||
  'textDocument/semanticTokens/full': [
 | 
			
		||||
    LSP.SemanticTokensParams,
 | 
			
		||||
    LSP.SemanticTokens
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Client to server
 | 
			
		||||
interface LSPNotifyMap {
 | 
			
		||||
  initialized: LSP.InitializedParams
 | 
			
		||||
  'textDocument/didChange': LSP.DidChangeTextDocumentParams
 | 
			
		||||
  'textDocument/didOpen': LSP.DidOpenTextDocumentParams
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Server to client
 | 
			
		||||
interface LSPEventMap {
 | 
			
		||||
  'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Notification = {
 | 
			
		||||
  [key in keyof LSPEventMap]: {
 | 
			
		||||
    jsonrpc: '2.0'
 | 
			
		||||
    id?: null | undefined
 | 
			
		||||
    method: key
 | 
			
		||||
    params: LSPEventMap[key]
 | 
			
		||||
  }
 | 
			
		||||
}[keyof LSPEventMap]
 | 
			
		||||
 | 
			
		||||
export interface LanguageServerClientOptions {
 | 
			
		||||
  client: Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class LanguageServerClient {
 | 
			
		||||
  private client: Client
 | 
			
		||||
 | 
			
		||||
  public ready: boolean
 | 
			
		||||
 | 
			
		||||
  private plugins: LanguageServerPlugin[]
 | 
			
		||||
 | 
			
		||||
  public initializePromise: Promise<void>
 | 
			
		||||
 | 
			
		||||
  private isUpdatingSemanticTokens: boolean = false
 | 
			
		||||
  private semanticTokens: SemanticToken[] = []
 | 
			
		||||
 | 
			
		||||
  constructor(options: LanguageServerClientOptions) {
 | 
			
		||||
    this.plugins = []
 | 
			
		||||
    this.client = options.client
 | 
			
		||||
 | 
			
		||||
    this.ready = false
 | 
			
		||||
 | 
			
		||||
    this.initializePromise = this.initialize()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async initialize() {
 | 
			
		||||
    // Start the client in the background.
 | 
			
		||||
    this.client.start()
 | 
			
		||||
 | 
			
		||||
    this.ready = true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getServerCapabilities(): LSP.ServerCapabilities<any> {
 | 
			
		||||
    return this.client.getServerCapabilities()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  close() {}
 | 
			
		||||
 | 
			
		||||
  textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
 | 
			
		||||
    this.notify('textDocument/didOpen', params)
 | 
			
		||||
 | 
			
		||||
    this.updateSemanticTokens(params.textDocument.uri)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
 | 
			
		||||
    this.notify('textDocument/didChange', params)
 | 
			
		||||
    this.updateSemanticTokens(params.textDocument.uri)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateSemanticTokens(uri: string) {
 | 
			
		||||
    // 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 = deserializeTokens(
 | 
			
		||||
        result.data,
 | 
			
		||||
        this.getServerCapabilities().semanticTokensProvider
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      this.isUpdatingSemanticTokens = false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getSemanticTokens(): SemanticToken[] {
 | 
			
		||||
    return this.semanticTokens
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async textDocumentHover(params: LSP.HoverParams) {
 | 
			
		||||
    return await this.request('textDocument/hover', params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async textDocumentCompletion(params: LSP.CompletionParams) {
 | 
			
		||||
    return await this.request('textDocument/completion', params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  attachPlugin(plugin: LanguageServerPlugin) {
 | 
			
		||||
    this.plugins.push(plugin)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  detachPlugin(plugin: LanguageServerPlugin) {
 | 
			
		||||
    const i = this.plugins.indexOf(plugin)
 | 
			
		||||
    if (i === -1) return
 | 
			
		||||
    this.plugins.splice(i, 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private request<K extends keyof LSPRequestMap>(
 | 
			
		||||
    method: K,
 | 
			
		||||
    params: LSPRequestMap[K][0]
 | 
			
		||||
  ): Promise<LSPRequestMap[K][1]> {
 | 
			
		||||
    return this.client.request(method, params) as Promise<LSPRequestMap[K][1]>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private notify<K extends keyof LSPNotifyMap>(
 | 
			
		||||
    method: K,
 | 
			
		||||
    params: LSPNotifyMap[K]
 | 
			
		||||
  ): void {
 | 
			
		||||
    return this.client.notify(method, params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processNotification(notification: Notification) {
 | 
			
		||||
    for (const plugin of this.plugins) plugin.processNotification(notification)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								src/editor/lsp/language.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/editor/lsp/language.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
// Code mirror language implementation for kcl.
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Language,
 | 
			
		||||
  defineLanguageFacet,
 | 
			
		||||
  LanguageSupport,
 | 
			
		||||
} from '@codemirror/language'
 | 
			
		||||
import { LanguageServerClient } from '.'
 | 
			
		||||
import { kclPlugin } from './plugin'
 | 
			
		||||
import type * as LSP from 'vscode-languageserver-protocol'
 | 
			
		||||
import { parser as jsParser } from '@lezer/javascript'
 | 
			
		||||
 | 
			
		||||
const data = defineLanguageFacet({})
 | 
			
		||||
 | 
			
		||||
export interface LanguageOptions {
 | 
			
		||||
  workspaceFolders: LSP.WorkspaceFolder[] | null
 | 
			
		||||
  documentUri: string
 | 
			
		||||
  client: LanguageServerClient
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function kclLanguage(options: LanguageOptions): LanguageSupport {
 | 
			
		||||
  // 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.
 | 
			
		||||
  const lang = new Language(data, jsParser, [], 'kcl')
 | 
			
		||||
 | 
			
		||||
  // Create our supporting extension.
 | 
			
		||||
  const kclLsp = kclPlugin({
 | 
			
		||||
    documentUri: options.documentUri,
 | 
			
		||||
    workspaceFolders: options.workspaceFolders,
 | 
			
		||||
    allowHTMLContent: true,
 | 
			
		||||
    client: options.client,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return new LanguageSupport(lang, [kclLsp])
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										168
									
								
								src/editor/lsp/parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/editor/lsp/parser.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,168 @@
 | 
			
		||||
// Extends the codemirror Parser for kcl.
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Parser,
 | 
			
		||||
  Input,
 | 
			
		||||
  TreeFragment,
 | 
			
		||||
  PartialParse,
 | 
			
		||||
  Tree,
 | 
			
		||||
  NodeType,
 | 
			
		||||
  NodeSet,
 | 
			
		||||
} from '@lezer/common'
 | 
			
		||||
import { LanguageServerClient } from '.'
 | 
			
		||||
import { posToOffset } from './plugin'
 | 
			
		||||
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)
 | 
			
		||||
    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
 | 
			
		||||
    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 {
 | 
			
		||||
    return 0
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopAt(pos: number) {
 | 
			
		||||
    this.stoppedAt = pos
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										360
									
								
								src/editor/lsp/plugin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								src/editor/lsp/plugin.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,360 @@
 | 
			
		||||
import { autocompletion, completeFromList } from '@codemirror/autocomplete'
 | 
			
		||||
import { setDiagnostics } from '@codemirror/lint'
 | 
			
		||||
import { Facet } from '@codemirror/state'
 | 
			
		||||
import {
 | 
			
		||||
  EditorView,
 | 
			
		||||
  ViewPlugin,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  hoverTooltip,
 | 
			
		||||
  tooltips,
 | 
			
		||||
} from '@codemirror/view'
 | 
			
		||||
import {
 | 
			
		||||
  DiagnosticSeverity,
 | 
			
		||||
  CompletionItemKind,
 | 
			
		||||
  CompletionTriggerKind,
 | 
			
		||||
} from 'vscode-languageserver-protocol'
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  Completion,
 | 
			
		||||
  CompletionContext,
 | 
			
		||||
  CompletionResult,
 | 
			
		||||
} from '@codemirror/autocomplete'
 | 
			
		||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
 | 
			
		||||
import type { ViewUpdate, PluginValue } from '@codemirror/view'
 | 
			
		||||
import type { Text } from '@codemirror/state'
 | 
			
		||||
import type * as LSP from 'vscode-languageserver-protocol'
 | 
			
		||||
import { LanguageServerClient, Notification } from '.'
 | 
			
		||||
import { Marked } from '@ts-stack/markdown'
 | 
			
		||||
 | 
			
		||||
const changesDelay = 500
 | 
			
		||||
 | 
			
		||||
const CompletionItemKindMap = Object.fromEntries(
 | 
			
		||||
  Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
 | 
			
		||||
) as Record<CompletionItemKind, string>
 | 
			
		||||
 | 
			
		||||
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
 | 
			
		||||
const documentUri = Facet.define<string, string>({ combine: useLast })
 | 
			
		||||
const languageId = Facet.define<string, string>({ combine: useLast })
 | 
			
		||||
const client = Facet.define<LanguageServerClient, LanguageServerClient>({
 | 
			
		||||
  combine: useLast,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export interface LanguageServerOptions {
 | 
			
		||||
  workspaceFolders: LSP.WorkspaceFolder[] | null
 | 
			
		||||
  documentUri: string
 | 
			
		||||
  allowHTMLContent: boolean
 | 
			
		||||
  client: LanguageServerClient
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class LanguageServerPlugin implements PluginValue {
 | 
			
		||||
  public client: LanguageServerClient
 | 
			
		||||
 | 
			
		||||
  private documentUri: string
 | 
			
		||||
  private languageId: string
 | 
			
		||||
  private documentVersion: number
 | 
			
		||||
 | 
			
		||||
  private changesTimeout: number
 | 
			
		||||
 | 
			
		||||
  constructor(private view: EditorView, private allowHTMLContent: boolean) {
 | 
			
		||||
    this.client = this.view.state.facet(client)
 | 
			
		||||
    this.documentUri = this.view.state.facet(documentUri)
 | 
			
		||||
    this.languageId = this.view.state.facet(languageId)
 | 
			
		||||
    this.documentVersion = 0
 | 
			
		||||
    this.changesTimeout = 0
 | 
			
		||||
 | 
			
		||||
    this.client.attachPlugin(this)
 | 
			
		||||
 | 
			
		||||
    this.initialize({
 | 
			
		||||
      documentText: this.view.state.doc.toString(),
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  update({ docChanged }: ViewUpdate) {
 | 
			
		||||
    if (!docChanged) return
 | 
			
		||||
    if (this.changesTimeout) clearTimeout(this.changesTimeout)
 | 
			
		||||
    this.changesTimeout = window.setTimeout(() => {
 | 
			
		||||
      this.sendChange({
 | 
			
		||||
        documentText: this.view.state.doc.toString(),
 | 
			
		||||
      })
 | 
			
		||||
    }, changesDelay)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy() {
 | 
			
		||||
    this.client.detachPlugin(this)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async initialize({ documentText }: { documentText: string }) {
 | 
			
		||||
    if (this.client.initializePromise) {
 | 
			
		||||
      await this.client.initializePromise
 | 
			
		||||
    }
 | 
			
		||||
    this.client.textDocumentDidOpen({
 | 
			
		||||
      textDocument: {
 | 
			
		||||
        uri: this.documentUri,
 | 
			
		||||
        languageId: this.languageId,
 | 
			
		||||
        text: documentText,
 | 
			
		||||
        version: this.documentVersion,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendChange({ documentText }: { documentText: string }) {
 | 
			
		||||
    if (!this.client.ready) return
 | 
			
		||||
    try {
 | 
			
		||||
      await this.client.textDocumentDidChange({
 | 
			
		||||
        textDocument: {
 | 
			
		||||
          uri: this.documentUri,
 | 
			
		||||
          version: this.documentVersion++,
 | 
			
		||||
        },
 | 
			
		||||
        contentChanges: [{ text: documentText }],
 | 
			
		||||
      })
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(e)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  requestDiagnostics(view: EditorView) {
 | 
			
		||||
    this.sendChange({ documentText: view.state.doc.toString() })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async requestHoverTooltip(
 | 
			
		||||
    view: EditorView,
 | 
			
		||||
    { line, character }: { line: number; character: number }
 | 
			
		||||
  ): Promise<Tooltip | null> {
 | 
			
		||||
    if (
 | 
			
		||||
      !this.client.ready ||
 | 
			
		||||
      !this.client.getServerCapabilities().hoverProvider
 | 
			
		||||
    )
 | 
			
		||||
      return null
 | 
			
		||||
 | 
			
		||||
    this.sendChange({ documentText: view.state.doc.toString() })
 | 
			
		||||
    const result = await this.client.textDocumentHover({
 | 
			
		||||
      textDocument: { uri: this.documentUri },
 | 
			
		||||
      position: { line, character },
 | 
			
		||||
    })
 | 
			
		||||
    if (!result) return null
 | 
			
		||||
    const { contents, range } = result
 | 
			
		||||
    let pos = posToOffset(view.state.doc, { line, character })!
 | 
			
		||||
    let end: number | undefined
 | 
			
		||||
    if (range) {
 | 
			
		||||
      pos = posToOffset(view.state.doc, range.start)!
 | 
			
		||||
      end = posToOffset(view.state.doc, range.end)
 | 
			
		||||
    }
 | 
			
		||||
    if (pos === null) return null
 | 
			
		||||
    const dom = document.createElement('div')
 | 
			
		||||
    dom.classList.add('documentation')
 | 
			
		||||
    if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
 | 
			
		||||
    else dom.textContent = formatContents(contents)
 | 
			
		||||
    return { pos, end, create: (view) => ({ dom }), above: true }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async requestCompletion(
 | 
			
		||||
    context: CompletionContext,
 | 
			
		||||
    { line, character }: { line: number; character: number },
 | 
			
		||||
    {
 | 
			
		||||
      triggerKind,
 | 
			
		||||
      triggerCharacter,
 | 
			
		||||
    }: {
 | 
			
		||||
      triggerKind: CompletionTriggerKind
 | 
			
		||||
      triggerCharacter: string | undefined
 | 
			
		||||
    }
 | 
			
		||||
  ): Promise<CompletionResult | null> {
 | 
			
		||||
    if (
 | 
			
		||||
      !this.client.ready ||
 | 
			
		||||
      !this.client.getServerCapabilities().completionProvider
 | 
			
		||||
    )
 | 
			
		||||
      return null
 | 
			
		||||
 | 
			
		||||
    this.sendChange({
 | 
			
		||||
      documentText: context.state.doc.toString(),
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const result = await this.client.textDocumentCompletion({
 | 
			
		||||
      textDocument: { uri: this.documentUri },
 | 
			
		||||
      position: { line, character },
 | 
			
		||||
      context: {
 | 
			
		||||
        triggerKind,
 | 
			
		||||
        triggerCharacter,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (!result) return null
 | 
			
		||||
 | 
			
		||||
    const items = 'items' in result ? result.items : result
 | 
			
		||||
 | 
			
		||||
    let options = items.map(
 | 
			
		||||
      ({
 | 
			
		||||
        detail,
 | 
			
		||||
        label,
 | 
			
		||||
        labelDetails,
 | 
			
		||||
        kind,
 | 
			
		||||
        textEdit,
 | 
			
		||||
        documentation,
 | 
			
		||||
        deprecated,
 | 
			
		||||
        insertText,
 | 
			
		||||
        insertTextFormat,
 | 
			
		||||
        sortText,
 | 
			
		||||
        filterText,
 | 
			
		||||
      }) => {
 | 
			
		||||
        const completion: Completion & {
 | 
			
		||||
          filterText: string
 | 
			
		||||
          sortText?: string
 | 
			
		||||
          apply: string
 | 
			
		||||
        } = {
 | 
			
		||||
          label,
 | 
			
		||||
          detail: labelDetails ? labelDetails.detail : detail,
 | 
			
		||||
          apply: label,
 | 
			
		||||
          type: kind && CompletionItemKindMap[kind].toLowerCase(),
 | 
			
		||||
          sortText: sortText ?? label,
 | 
			
		||||
          filterText: filterText ?? label,
 | 
			
		||||
        }
 | 
			
		||||
        if (documentation) {
 | 
			
		||||
          completion.info = () => {
 | 
			
		||||
            const htmlString = formatContents(documentation)
 | 
			
		||||
            const htmlNode = document.createElement('div')
 | 
			
		||||
            htmlNode.style.display = 'contents'
 | 
			
		||||
            htmlNode.innerHTML = htmlString
 | 
			
		||||
            return { dom: htmlNode }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return completion
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return completeFromList(options)(context)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  processNotification(notification: Notification) {
 | 
			
		||||
    try {
 | 
			
		||||
      switch (notification.method) {
 | 
			
		||||
        case 'textDocument/publishDiagnostics':
 | 
			
		||||
          this.processDiagnostics(notification.params)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  processDiagnostics(params: PublishDiagnosticsParams) {
 | 
			
		||||
    if (params.uri !== this.documentUri) return
 | 
			
		||||
 | 
			
		||||
    const diagnostics = params.diagnostics
 | 
			
		||||
      .map(({ range, message, severity }) => ({
 | 
			
		||||
        from: posToOffset(this.view.state.doc, range.start)!,
 | 
			
		||||
        to: posToOffset(this.view.state.doc, range.end)!,
 | 
			
		||||
        severity: (
 | 
			
		||||
          {
 | 
			
		||||
            [DiagnosticSeverity.Error]: 'error',
 | 
			
		||||
            [DiagnosticSeverity.Warning]: 'warning',
 | 
			
		||||
            [DiagnosticSeverity.Information]: 'info',
 | 
			
		||||
            [DiagnosticSeverity.Hint]: 'info',
 | 
			
		||||
          } as const
 | 
			
		||||
        )[severity!],
 | 
			
		||||
        message,
 | 
			
		||||
      }))
 | 
			
		||||
      .filter(
 | 
			
		||||
        ({ from, to }) =>
 | 
			
		||||
          from !== null && to !== null && from !== undefined && to !== undefined
 | 
			
		||||
      )
 | 
			
		||||
      .sort((a, b) => {
 | 
			
		||||
        switch (true) {
 | 
			
		||||
          case a.from < b.from:
 | 
			
		||||
            return -1
 | 
			
		||||
          case a.from > b.from:
 | 
			
		||||
            return 1
 | 
			
		||||
        }
 | 
			
		||||
        return 0
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function kclPlugin(options: LanguageServerOptions) {
 | 
			
		||||
  let plugin: LanguageServerPlugin | null = null
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    client.of(options.client),
 | 
			
		||||
    documentUri.of(options.documentUri),
 | 
			
		||||
    languageId.of('kcl'),
 | 
			
		||||
    ViewPlugin.define(
 | 
			
		||||
      (view) =>
 | 
			
		||||
        (plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
 | 
			
		||||
    ),
 | 
			
		||||
    hoverTooltip(
 | 
			
		||||
      (view, pos) =>
 | 
			
		||||
        plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
 | 
			
		||||
        null
 | 
			
		||||
    ),
 | 
			
		||||
    tooltips({
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
    }),
 | 
			
		||||
    autocompletion({
 | 
			
		||||
      override: [
 | 
			
		||||
        async (context) => {
 | 
			
		||||
          if (plugin == null) return null
 | 
			
		||||
 | 
			
		||||
          const { state, pos, explicit } = context
 | 
			
		||||
          const line = state.doc.lineAt(pos)
 | 
			
		||||
          let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
 | 
			
		||||
          let trigChar: string | undefined
 | 
			
		||||
          if (
 | 
			
		||||
            !explicit &&
 | 
			
		||||
            plugin.client
 | 
			
		||||
              .getServerCapabilities()
 | 
			
		||||
              .completionProvider?.triggerCharacters?.includes(
 | 
			
		||||
                line.text[pos - line.from - 1]
 | 
			
		||||
              )
 | 
			
		||||
          ) {
 | 
			
		||||
            trigKind = CompletionTriggerKind.TriggerCharacter
 | 
			
		||||
            trigChar = line.text[pos - line.from - 1]
 | 
			
		||||
          }
 | 
			
		||||
          if (
 | 
			
		||||
            trigKind === CompletionTriggerKind.Invoked &&
 | 
			
		||||
            !context.matchBefore(/\w+$/)
 | 
			
		||||
          ) {
 | 
			
		||||
            return null
 | 
			
		||||
          }
 | 
			
		||||
          return await plugin.requestCompletion(
 | 
			
		||||
            context,
 | 
			
		||||
            offsetToPos(state.doc, pos),
 | 
			
		||||
            {
 | 
			
		||||
              triggerKind: trigKind,
 | 
			
		||||
              triggerCharacter: trigChar,
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }),
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function offsetToPos(doc: Text, offset: number) {
 | 
			
		||||
  const line = doc.lineAt(offset)
 | 
			
		||||
  return {
 | 
			
		||||
    line: line.number - 1,
 | 
			
		||||
    character: offset - line.from,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/editor/lsp/semantic_tokens.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/editor/lsp/semantic_tokens.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
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 function deserializeTokens(
 | 
			
		||||
  data: number[],
 | 
			
		||||
  semanticTokensProvider?: LSP.SemanticTokensOptions
 | 
			
		||||
): SemanticToken[] {
 | 
			
		||||
  if (!semanticTokensProvider) {
 | 
			
		||||
    return []
 | 
			
		||||
  }
 | 
			
		||||
  // Check if data length is divisible by 5
 | 
			
		||||
  if (data.length % 5 !== 0) {
 | 
			
		||||
    throw 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								src/editor/lsp/server-capability-registration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/editor/lsp/server-capability-registration.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
import {
 | 
			
		||||
  Registration,
 | 
			
		||||
  ServerCapabilities,
 | 
			
		||||
  Unregistration,
 | 
			
		||||
} from 'vscode-languageserver-protocol'
 | 
			
		||||
 | 
			
		||||
interface IFlexibleServerCapabilities extends ServerCapabilities {
 | 
			
		||||
  [key: string]: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IMethodServerCapabilityProviderDictionary {
 | 
			
		||||
  [key: string]: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ServerCapabilitiesProviders: IMethodServerCapabilityProviderDictionary = {
 | 
			
		||||
  'textDocument/hover': 'hoverProvider',
 | 
			
		||||
  'textDocument/completion': 'completionProvider',
 | 
			
		||||
  'textDocument/signatureHelp': 'signatureHelpProvider',
 | 
			
		||||
  'textDocument/definition': 'definitionProvider',
 | 
			
		||||
  'textDocument/typeDefinition': 'typeDefinitionProvider',
 | 
			
		||||
  'textDocument/implementation': 'implementationProvider',
 | 
			
		||||
  'textDocument/references': 'referencesProvider',
 | 
			
		||||
  'textDocument/documentHighlight': 'documentHighlightProvider',
 | 
			
		||||
  'textDocument/documentSymbol': 'documentSymbolProvider',
 | 
			
		||||
  'textDocument/workspaceSymbol': 'workspaceSymbolProvider',
 | 
			
		||||
  'textDocument/codeAction': 'codeActionProvider',
 | 
			
		||||
  'textDocument/codeLens': 'codeLensProvider',
 | 
			
		||||
  'textDocument/documentFormatting': 'documentFormattingProvider',
 | 
			
		||||
  'textDocument/documentRangeFormatting': 'documentRangeFormattingProvider',
 | 
			
		||||
  'textDocument/documentOnTypeFormatting': 'documentOnTypeFormattingProvider',
 | 
			
		||||
  'textDocument/rename': 'renameProvider',
 | 
			
		||||
  'textDocument/documentLink': 'documentLinkProvider',
 | 
			
		||||
  'textDocument/color': 'colorProvider',
 | 
			
		||||
  'textDocument/foldingRange': 'foldingRangeProvider',
 | 
			
		||||
  'textDocument/declaration': 'declarationProvider',
 | 
			
		||||
  'textDocument/executeCommand': 'executeCommandProvider',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function registerServerCapability(
 | 
			
		||||
  serverCapabilities: ServerCapabilities,
 | 
			
		||||
  registration: Registration
 | 
			
		||||
): ServerCapabilities {
 | 
			
		||||
  const serverCapabilitiesCopy = JSON.parse(
 | 
			
		||||
    JSON.stringify(serverCapabilities)
 | 
			
		||||
  ) as IFlexibleServerCapabilities
 | 
			
		||||
  const { method, registerOptions } = registration
 | 
			
		||||
  const providerName = ServerCapabilitiesProviders[method]
 | 
			
		||||
 | 
			
		||||
  if (providerName) {
 | 
			
		||||
    if (!registerOptions) {
 | 
			
		||||
      serverCapabilitiesCopy[providerName] = true
 | 
			
		||||
    } else {
 | 
			
		||||
      serverCapabilitiesCopy[providerName] = Object.assign(
 | 
			
		||||
        {},
 | 
			
		||||
        JSON.parse(JSON.stringify(registerOptions))
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    throw new Error('Could not register server capability.')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return serverCapabilitiesCopy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function unregisterServerCapability(
 | 
			
		||||
  serverCapabilities: ServerCapabilities,
 | 
			
		||||
  unregistration: Unregistration
 | 
			
		||||
): ServerCapabilities {
 | 
			
		||||
  const serverCapabilitiesCopy = JSON.parse(
 | 
			
		||||
    JSON.stringify(serverCapabilities)
 | 
			
		||||
  ) as IFlexibleServerCapabilities
 | 
			
		||||
  const { method } = unregistration
 | 
			
		||||
  const providerName = ServerCapabilitiesProviders[method]
 | 
			
		||||
 | 
			
		||||
  delete serverCapabilitiesCopy[providerName]
 | 
			
		||||
 | 
			
		||||
  return serverCapabilitiesCopy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { registerServerCapability, unregisterServerCapability }
 | 
			
		||||
							
								
								
									
										42
									
								
								src/editor/lsp/server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/editor/lsp/server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
import init, {
 | 
			
		||||
  InitOutput,
 | 
			
		||||
  lsp_run,
 | 
			
		||||
  ServerConfig,
 | 
			
		||||
} from '../../wasm-lib/pkg/wasm_lib'
 | 
			
		||||
import { FromServer, IntoServer } from './codec'
 | 
			
		||||
 | 
			
		||||
let server: null | Server
 | 
			
		||||
 | 
			
		||||
export default class Server {
 | 
			
		||||
  readonly initOutput: InitOutput
 | 
			
		||||
  readonly #intoServer: IntoServer
 | 
			
		||||
  readonly #fromServer: FromServer
 | 
			
		||||
 | 
			
		||||
  private constructor(
 | 
			
		||||
    initOutput: InitOutput,
 | 
			
		||||
    intoServer: IntoServer,
 | 
			
		||||
    fromServer: FromServer
 | 
			
		||||
  ) {
 | 
			
		||||
    this.initOutput = initOutput
 | 
			
		||||
    this.#intoServer = intoServer
 | 
			
		||||
    this.#fromServer = fromServer
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async initialize(
 | 
			
		||||
    intoServer: IntoServer,
 | 
			
		||||
    fromServer: FromServer
 | 
			
		||||
  ): Promise<Server> {
 | 
			
		||||
    if (null == server) {
 | 
			
		||||
      const initOutput = await init()
 | 
			
		||||
      server = new Server(initOutput, intoServer, fromServer)
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn('Server already initialized; ignoring')
 | 
			
		||||
    }
 | 
			
		||||
    return server
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async start(): Promise<void> {
 | 
			
		||||
    const config = new ServerConfig(this.#intoServer, this.#fromServer)
 | 
			
		||||
    await lsp_run(config)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/editor/lsp/tracer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/editor/lsp/tracer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -8,8 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
 | 
			
		||||
  .VITE_KC_API_WS_MODELING_URL
 | 
			
		||||
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
 | 
			
		||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
 | 
			
		||||
export const VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env
 | 
			
		||||
  .VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS
 | 
			
		||||
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
 | 
			
		||||
  .VITE_KC_CONNECTION_TIMEOUT_MS
 | 
			
		||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										56
									
								
								src/hooks/useToolbarGuards.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/hooks/useToolbarGuards.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
import { SetVarNameModal } from 'components/SetVarNameModal'
 | 
			
		||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
 | 
			
		||||
import { isNodeSafeToReplace } from 'lang/queryAst'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { create } from 'react-modal-promise'
 | 
			
		||||
import { useStore } from 'useStore'
 | 
			
		||||
 | 
			
		||||
const getModalInfo = create(SetVarNameModal as any)
 | 
			
		||||
 | 
			
		||||
export function useConvertToVariable() {
 | 
			
		||||
  const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
 | 
			
		||||
    (s) => ({
 | 
			
		||||
      guiMode: s.guiMode,
 | 
			
		||||
      ast: s.ast,
 | 
			
		||||
      updateAst: s.updateAst,
 | 
			
		||||
      selectionRanges: s.selectionRanges,
 | 
			
		||||
      programMemory: s.programMemory,
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
  const [enable, setEnabled] = useState(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!ast) return
 | 
			
		||||
 | 
			
		||||
    const { isSafe, value } = isNodeSafeToReplace(
 | 
			
		||||
      ast,
 | 
			
		||||
      selectionRanges.codeBasedSelections?.[0]?.range || []
 | 
			
		||||
    )
 | 
			
		||||
    const canReplace = isSafe && value.type !== 'Identifier'
 | 
			
		||||
    const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
 | 
			
		||||
 | 
			
		||||
    const _enableHorz = canReplace && isOnlyOneSelection
 | 
			
		||||
    setEnabled(_enableHorz)
 | 
			
		||||
  }, [guiMode, selectionRanges])
 | 
			
		||||
 | 
			
		||||
  const handleClick = async () => {
 | 
			
		||||
    if (!ast) return
 | 
			
		||||
    try {
 | 
			
		||||
      const { variableName } = await getModalInfo({
 | 
			
		||||
        valueName: 'var',
 | 
			
		||||
      } as any)
 | 
			
		||||
 | 
			
		||||
      const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
 | 
			
		||||
        ast,
 | 
			
		||||
        programMemory,
 | 
			
		||||
        selectionRanges.codeBasedSelections[0].range,
 | 
			
		||||
        variableName
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      updateAst(_modifiedAst)
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('e', e)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { enable, handleClick }
 | 
			
		||||
}
 | 
			
		||||
@ -82,8 +82,22 @@ code {
 | 
			
		||||
    monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.full-height-subtract {
 | 
			
		||||
  --height-subtract: 2.25rem;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: calc(100% - var(--height-subtract));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-editor {
 | 
			
		||||
  @apply bg-transparent;
 | 
			
		||||
  @apply h-full bg-transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-scroller {
 | 
			
		||||
  @apply h-full;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-scroller::-webkit-scrollbar {
 | 
			
		||||
  @apply h-0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-activeLine,
 | 
			
		||||
@ -132,3 +146,45 @@ code {
 | 
			
		||||
.react-json-view {
 | 
			
		||||
  @apply bg-transparent !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-tooltip {
 | 
			
		||||
  @apply text-xs shadow-md;
 | 
			
		||||
  @apply bg-chalkboard-10 text-chalkboard-80;
 | 
			
		||||
  @apply rounded-sm border-solid border border-chalkboard-40/30 border-l-liquid-10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark #code-mirror-override .cm-tooltip {
 | 
			
		||||
  @apply bg-chalkboard-110 text-chalkboard-40;
 | 
			
		||||
  @apply border-chalkboard-70/20 border-l-liquid-70;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-tooltip-hover {
 | 
			
		||||
  @apply py-1 px-2 w-max max-w-md;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-completionInfo {
 | 
			
		||||
  @apply px-4 rounded-l-none;
 | 
			
		||||
  @apply bg-chalkboard-10 text-liquid-90;
 | 
			
		||||
  @apply border-liquid-40/30;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark #code-mirror-override .cm-completionInfo {
 | 
			
		||||
  @apply bg-liquid-120 text-liquid-50;
 | 
			
		||||
  @apply border-liquid-90/60;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-tooltip-autocomplete li {
 | 
			
		||||
  @apply px-2 py-1;
 | 
			
		||||
}
 | 
			
		||||
#code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
 | 
			
		||||
  @apply bg-liquid-10 text-liquid-110;
 | 
			
		||||
}
 | 
			
		||||
.dark #code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] {
 | 
			
		||||
  @apply bg-liquid-100 text-liquid-20;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#code-mirror-override .cm-content {
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  word-break: normal;
 | 
			
		||||
  word-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -179,6 +179,9 @@ const newVar = myVar + 1
 | 
			
		||||
              name: 'aIdentifier',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          function: {
 | 
			
		||||
            type: 'InMemory',
 | 
			
		||||
          },
 | 
			
		||||
          optional: false,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
@ -211,7 +214,6 @@ describe('testing function declaration', () => {
 | 
			
		||||
              type: 'FunctionExpression',
 | 
			
		||||
              start: 11,
 | 
			
		||||
              end: 19,
 | 
			
		||||
              id: null,
 | 
			
		||||
              params: [],
 | 
			
		||||
              body: {
 | 
			
		||||
                start: 17,
 | 
			
		||||
@ -250,7 +252,6 @@ describe('testing function declaration', () => {
 | 
			
		||||
              type: 'FunctionExpression',
 | 
			
		||||
              start: 11,
 | 
			
		||||
              end: 39,
 | 
			
		||||
              id: null,
 | 
			
		||||
              params: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'Identifier',
 | 
			
		||||
@ -326,7 +327,6 @@ const myVar = funcN(1, 2)`
 | 
			
		||||
              type: 'FunctionExpression',
 | 
			
		||||
              start: 11,
 | 
			
		||||
              end: 37,
 | 
			
		||||
              id: null,
 | 
			
		||||
              params: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'Identifier',
 | 
			
		||||
@ -416,6 +416,9 @@ const myVar = funcN(1, 2)`
 | 
			
		||||
                  raw: '2',
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
              function: {
 | 
			
		||||
                type: 'InMemory',
 | 
			
		||||
              },
 | 
			
		||||
              optional: false,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
@ -485,6 +488,7 @@ describe('testing pipe operator special', () => {
 | 
			
		||||
                      ],
 | 
			
		||||
                    },
 | 
			
		||||
                  ],
 | 
			
		||||
                  function: expect.any(Object),
 | 
			
		||||
                  optional: false,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@ -521,6 +525,7 @@ describe('testing pipe operator special', () => {
 | 
			
		||||
                    },
 | 
			
		||||
                    { type: 'PipeSubstitution', start: 59, end: 60 },
 | 
			
		||||
                  ],
 | 
			
		||||
                  function: expect.any(Object),
 | 
			
		||||
                  optional: false,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@ -593,6 +598,7 @@ describe('testing pipe operator special', () => {
 | 
			
		||||
                    },
 | 
			
		||||
                    { type: 'PipeSubstitution', start: 105, end: 106 },
 | 
			
		||||
                  ],
 | 
			
		||||
                  function: expect.any(Object),
 | 
			
		||||
                  optional: false,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@ -629,6 +635,7 @@ describe('testing pipe operator special', () => {
 | 
			
		||||
                    },
 | 
			
		||||
                    { type: 'PipeSubstitution', start: 128, end: 129 },
 | 
			
		||||
                  ],
 | 
			
		||||
                  function: expect.any(Object),
 | 
			
		||||
                  optional: false,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
@ -651,6 +658,9 @@ describe('testing pipe operator special', () => {
 | 
			
		||||
                    },
 | 
			
		||||
                    { type: 'PipeSubstitution', start: 143, end: 144 },
 | 
			
		||||
                  ],
 | 
			
		||||
                  function: {
 | 
			
		||||
                    type: 'InMemory',
 | 
			
		||||
                  },
 | 
			
		||||
                  optional: false,
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
@ -730,6 +740,9 @@ describe('testing pipe operator special', () => {
 | 
			
		||||
                      end: 35,
 | 
			
		||||
                    },
 | 
			
		||||
                  ],
 | 
			
		||||
                  function: {
 | 
			
		||||
                    type: 'InMemory',
 | 
			
		||||
                  },
 | 
			
		||||
                  optional: false,
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
@ -1550,7 +1563,10 @@ const key = 'c'`
 | 
			
		||||
      type: 'NoneCodeNode',
 | 
			
		||||
      start: code.indexOf('\n// this is a comment'),
 | 
			
		||||
      end: code.indexOf('const key'),
 | 
			
		||||
      value: '\n// this is a comment\n',
 | 
			
		||||
      value: {
 | 
			
		||||
        type: 'blockComment',
 | 
			
		||||
        value: 'this is a comment',
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    const { nonCodeMeta } = parser_wasm(code)
 | 
			
		||||
    expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
 | 
			
		||||
@ -1560,7 +1576,9 @@ const key = 'c'`
 | 
			
		||||
    const { nonCodeMeta: nonCodeMeta2 } = parser_wasm(
 | 
			
		||||
      codeWithExtraStartWhitespace
 | 
			
		||||
    )
 | 
			
		||||
    expect(nonCodeMeta2.noneCodeNodes[0].value).toBe(nonCodeMetaInstance.value)
 | 
			
		||||
    expect(nonCodeMeta2.noneCodeNodes[0].value).toStrictEqual(
 | 
			
		||||
      nonCodeMetaInstance.value
 | 
			
		||||
    )
 | 
			
		||||
    expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
 | 
			
		||||
      nonCodeMetaInstance.start
 | 
			
		||||
    )
 | 
			
		||||
@ -1583,7 +1601,10 @@ const key = 'c'`
 | 
			
		||||
      type: 'NoneCodeNode',
 | 
			
		||||
      start: 106,
 | 
			
		||||
      end: 166,
 | 
			
		||||
      value: ' /* this is\n      a comment\n      spanning a few lines */\n  ',
 | 
			
		||||
      value: {
 | 
			
		||||
        type: 'blockComment',
 | 
			
		||||
        value: 'this is\n      a comment\n      spanning a few lines',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
  it('comments in a pipe expression', () => {
 | 
			
		||||
@ -1603,7 +1624,10 @@ const key = 'c'`
 | 
			
		||||
      type: 'NoneCodeNode',
 | 
			
		||||
      start: 125,
 | 
			
		||||
      end: 141,
 | 
			
		||||
      value: '\n// a comment\n  ',
 | 
			
		||||
      value: {
 | 
			
		||||
        type: 'blockComment',
 | 
			
		||||
        value: 'a comment',
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@ -1627,6 +1651,7 @@ describe('test UnaryExpression', () => {
 | 
			
		||||
          { type: 'Literal', start: 19, end: 20, value: 4, raw: '4' },
 | 
			
		||||
          { type: 'Literal', start: 22, end: 25, value: 100, raw: '100' },
 | 
			
		||||
        ],
 | 
			
		||||
        function: expect.any(Object),
 | 
			
		||||
        optional: false,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
@ -1660,10 +1685,12 @@ describe('testing nested call expressions', () => {
 | 
			
		||||
              { type: 'Literal', start: 34, end: 35, value: 5, raw: '5' },
 | 
			
		||||
              { type: 'Literal', start: 37, end: 38, value: 3, raw: '3' },
 | 
			
		||||
            ],
 | 
			
		||||
            function: expect.any(Object),
 | 
			
		||||
            optional: false,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      function: expect.any(Object),
 | 
			
		||||
      optional: false,
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
@ -1695,6 +1722,7 @@ describe('should recognise callExpresions in binaryExpressions', () => {
 | 
			
		||||
            },
 | 
			
		||||
            { type: 'PipeSubstitution', start: 25, end: 26 },
 | 
			
		||||
          ],
 | 
			
		||||
          function: expect.any(Object),
 | 
			
		||||
          optional: false,
 | 
			
		||||
        },
 | 
			
		||||
        right: { type: 'Literal', value: 1, raw: '1', start: 30, end: 31 },
 | 
			
		||||
 | 
			
		||||
@ -114,7 +114,8 @@ describe('Testing addSketchTo', () => {
 | 
			
		||||
    expect(str).toBe(`const part001 = startSketchAt('default')
 | 
			
		||||
  |> ry(90, %)
 | 
			
		||||
  |> line('default', %)
 | 
			
		||||
show(part001)`)
 | 
			
		||||
show(part001)
 | 
			
		||||
`)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -179,7 +180,10 @@ describe('Testing moveValueIntoNewVariable', () => {
 | 
			
		||||
  return x
 | 
			
		||||
}
 | 
			
		||||
`
 | 
			
		||||
  const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
 | 
			
		||||
  const code = `${fn('def')}${fn('jkl')}${fn('hmm')}
 | 
			
		||||
fn ghi = (x) => {
 | 
			
		||||
    return 2
 | 
			
		||||
}
 | 
			
		||||
const abc = 3
 | 
			
		||||
const identifierGuy = 5
 | 
			
		||||
const yo = 5 + 6
 | 
			
		||||
 | 
			
		||||
@ -36,14 +36,14 @@ export function addSketchTo(
 | 
			
		||||
  const _node = { ...node }
 | 
			
		||||
  const _name = name || findUniqueName(node, 'part')
 | 
			
		||||
 | 
			
		||||
  const startSketchAt = createCallExpression('startSketchAt', [
 | 
			
		||||
  const startSketchAt = createCallExpressionStdLib('startSketchAt', [
 | 
			
		||||
    createLiteral('default'),
 | 
			
		||||
  ])
 | 
			
		||||
  const rotate = createCallExpression(axis === 'xz' ? 'rx' : 'ry', [
 | 
			
		||||
    createLiteral(90),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
  ])
 | 
			
		||||
  const initialLineTo = createCallExpression('line', [
 | 
			
		||||
  const initialLineTo = createCallExpressionStdLib('line', [
 | 
			
		||||
    createLiteral('default'),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
  ])
 | 
			
		||||
@ -112,7 +112,9 @@ function addToShow(node: Program, name: string): Program {
 | 
			
		||||
  const dumbyStartend = { start: 0, end: 0 }
 | 
			
		||||
  const showCallIndex = getShowIndex(_node)
 | 
			
		||||
  if (showCallIndex === -1) {
 | 
			
		||||
    const showCall = createCallExpression('show', [createIdentifier(name)])
 | 
			
		||||
    const showCall = createCallExpressionStdLib('show', [
 | 
			
		||||
      createIdentifier(name),
 | 
			
		||||
    ])
 | 
			
		||||
    const showExpressionStatement: ExpressionStatement = {
 | 
			
		||||
      type: 'ExpressionStatement',
 | 
			
		||||
      ...dumbyStartend,
 | 
			
		||||
@ -124,7 +126,7 @@ function addToShow(node: Program, name: string): Program {
 | 
			
		||||
  const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
 | 
			
		||||
  const showCallArgs = (showCall.expression as CallExpression).arguments
 | 
			
		||||
  const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)]
 | 
			
		||||
  const newShowExpression = createCallExpression('show', newShowCallArgs)
 | 
			
		||||
  const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs)
 | 
			
		||||
 | 
			
		||||
  _node.body[showCallIndex] = {
 | 
			
		||||
    ...showCall,
 | 
			
		||||
@ -225,7 +227,7 @@ export function extrudeSketch(
 | 
			
		||||
  const { node: variableDeclorator, shallowPath: pathToDecleration } =
 | 
			
		||||
    getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
 | 
			
		||||
 | 
			
		||||
  const extrudeCall = createCallExpression('extrude', [
 | 
			
		||||
  const extrudeCall = createCallExpressionStdLib('extrude', [
 | 
			
		||||
    createLiteral(4),
 | 
			
		||||
    shouldPipe
 | 
			
		||||
      ? createPipeSubstitution()
 | 
			
		||||
@ -313,15 +315,15 @@ export function sketchOnExtrudedFace(
 | 
			
		||||
  const newSketch = createVariableDeclaration(
 | 
			
		||||
    newSketchName,
 | 
			
		||||
    createPipeExpression([
 | 
			
		||||
      createCallExpression('startSketchAt', [
 | 
			
		||||
      createCallExpressionStdLib('startSketchAt', [
 | 
			
		||||
        createArrayExpression([createLiteral(0), createLiteral(0)]),
 | 
			
		||||
      ]),
 | 
			
		||||
      createCallExpression('lineTo', [
 | 
			
		||||
      createCallExpressionStdLib('lineTo', [
 | 
			
		||||
        createArrayExpression([createLiteral(1), createLiteral(1)]),
 | 
			
		||||
        createPipeSubstitution(),
 | 
			
		||||
      ]),
 | 
			
		||||
      createCallExpression('transform', [
 | 
			
		||||
        createCallExpression('getExtrudeWallTransform', [
 | 
			
		||||
        createCallExpressionStdLib('getExtrudeWallTransform', [
 | 
			
		||||
          createLiteral(tag),
 | 
			
		||||
          createIdentifier(oldSketchName),
 | 
			
		||||
        ]),
 | 
			
		||||
@ -414,6 +416,40 @@ export function createPipeSubstitution(): PipeSubstitution {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createCallExpressionStdLib(
 | 
			
		||||
  name: string,
 | 
			
		||||
  args: CallExpression['arguments']
 | 
			
		||||
): CallExpression {
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'CallExpression',
 | 
			
		||||
    start: 0,
 | 
			
		||||
    end: 0,
 | 
			
		||||
    callee: {
 | 
			
		||||
      type: 'Identifier',
 | 
			
		||||
      start: 0,
 | 
			
		||||
      end: 0,
 | 
			
		||||
      name,
 | 
			
		||||
    },
 | 
			
		||||
    function: {
 | 
			
		||||
      type: 'StdLib',
 | 
			
		||||
      func: {
 | 
			
		||||
        // We only need the name here to map it back when it serializes
 | 
			
		||||
        // to rust, don't worry about the rest.
 | 
			
		||||
        name,
 | 
			
		||||
        summary: '',
 | 
			
		||||
        description: '',
 | 
			
		||||
        tags: [],
 | 
			
		||||
        returnValue: { type: '', required: false, name: '', schema: {} },
 | 
			
		||||
        args: [],
 | 
			
		||||
        unpublished: false,
 | 
			
		||||
        deprecated: false,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    optional: false,
 | 
			
		||||
    arguments: args,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createCallExpression(
 | 
			
		||||
  name: string,
 | 
			
		||||
  args: CallExpression['arguments']
 | 
			
		||||
@ -428,6 +464,9 @@ export function createCallExpression(
 | 
			
		||||
      end: 0,
 | 
			
		||||
      name,
 | 
			
		||||
    },
 | 
			
		||||
    function: {
 | 
			
		||||
      type: 'InMemory',
 | 
			
		||||
    },
 | 
			
		||||
    optional: false,
 | 
			
		||||
    arguments: args,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -11,26 +11,27 @@ describe('recast', () => {
 | 
			
		||||
    const code = '1 + 2'
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('variable declaration', () => {
 | 
			
		||||
    const code = 'const myVar = 5'
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it("variable declaration that's binary with string", () => {
 | 
			
		||||
    const code = "const myVar = 5 + 'yo'"
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
    const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
 | 
			
		||||
    const { ast: ast2 } = code2ast(codeWithOtherQuotes)
 | 
			
		||||
    expect(recast(ast2)).toBe(codeWithOtherQuotes)
 | 
			
		||||
    expect(recast(ast2).trim()).toBe(codeWithOtherQuotes)
 | 
			
		||||
  })
 | 
			
		||||
  it('test assigning two variables, the second summing with the first', () => {
 | 
			
		||||
    const code = `const myVar = 5
 | 
			
		||||
const newVar = myVar + 1`
 | 
			
		||||
const newVar = myVar + 1
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
@ -42,12 +43,12 @@ const newVar = myVar + 1`
 | 
			
		||||
    )
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('test with function call', () => {
 | 
			
		||||
    const code = `
 | 
			
		||||
const myVar = "hello"
 | 
			
		||||
log(5, myVar)`
 | 
			
		||||
    const code = `const myVar = "hello"
 | 
			
		||||
log(5, myVar)
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
@ -62,7 +63,7 @@ log(5, myVar)`
 | 
			
		||||
    ].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('recast sketch declaration', () => {
 | 
			
		||||
    let code = `const mySketch = startSketchAt([0, 0])
 | 
			
		||||
@ -75,7 +76,7 @@ show(mySketch)
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('sketch piped into callExpression', () => {
 | 
			
		||||
    const code = [
 | 
			
		||||
@ -87,7 +88,7 @@ show(mySketch)
 | 
			
		||||
    ].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('recast BinaryExpression piped into CallExpression', () => {
 | 
			
		||||
    const code = [
 | 
			
		||||
@ -99,37 +100,37 @@ show(mySketch)
 | 
			
		||||
    ].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('recast nested binary expression', () => {
 | 
			
		||||
    const code = ['const myVar = 1 + 2 * 5'].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('recast nested binary expression with parans', () => {
 | 
			
		||||
    const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('unnecessary paran wrap will be remove', () => {
 | 
			
		||||
    const code = ['const myVar = 1 + (2 * 5)'].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.replace('(', '').replace(')', ''))
 | 
			
		||||
    expect(recasted.trim()).toBe(code.replace('(', '').replace(')', ''))
 | 
			
		||||
  })
 | 
			
		||||
  it('complex nested binary expression', () => {
 | 
			
		||||
    const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('multiplied paren expressions', () => {
 | 
			
		||||
    const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('recast array declaration', () => {
 | 
			
		||||
    const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
 | 
			
		||||
@ -137,7 +138,7 @@ show(mySketch)
 | 
			
		||||
    )
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('recast long array declaration', () => {
 | 
			
		||||
    const code = [
 | 
			
		||||
@ -152,7 +153,7 @@ show(mySketch)
 | 
			
		||||
    ].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted.trim()).toBe(code.trim())
 | 
			
		||||
  })
 | 
			
		||||
  it('recast long object exectution', () => {
 | 
			
		||||
    const code = `const three = 3
 | 
			
		||||
@ -161,35 +162,38 @@ const yo = {
 | 
			
		||||
  anum: 2,
 | 
			
		||||
  identifier: three,
 | 
			
		||||
  binExp: 4 + 5
 | 
			
		||||
}`
 | 
			
		||||
}
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('recast short object exectution', () => {
 | 
			
		||||
    const code = `const yo = { key: 'val' }`
 | 
			
		||||
    const code = `const yo = { key: 'val' }
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('recast object execution with member expression', () => {
 | 
			
		||||
    const code = `const yo = { a: { b: { c: '123' } } }
 | 
			
		||||
const key = 'c'
 | 
			
		||||
const myVar = yo.a['b'][key]
 | 
			
		||||
const key2 = 'b'
 | 
			
		||||
const myVar2 = yo['a'][key2].c`
 | 
			
		||||
const myVar2 = yo['a'][key2].c
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code.trim())
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('testing recasting with comments and whitespace', () => {
 | 
			
		||||
  it('code with comments', () => {
 | 
			
		||||
    const code = `
 | 
			
		||||
const yo = { a: { b: { c: '123' } } }
 | 
			
		||||
    const code = `const yo = { a: { b: { c: '123' } } }
 | 
			
		||||
// this is a comment
 | 
			
		||||
const key = 'c'`
 | 
			
		||||
const key = 'c'
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
@ -197,38 +201,39 @@ const key = 'c'`
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('code with comment and extra lines', () => {
 | 
			
		||||
    const code = `
 | 
			
		||||
const yo = 'c' /* this is
 | 
			
		||||
    const code = `const yo = 'c'
 | 
			
		||||
 | 
			
		||||
/* this is
 | 
			
		||||
a
 | 
			
		||||
comment */
 | 
			
		||||
 | 
			
		||||
const yo = 'bing'`
 | 
			
		||||
const yo = 'bing'
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('comments at the start and end', () => {
 | 
			
		||||
    const code = `
 | 
			
		||||
// this is a comment
 | 
			
		||||
 | 
			
		||||
    const code = `// this is a comment
 | 
			
		||||
const yo = { a: { b: { c: '123' } } }
 | 
			
		||||
const key = 'c'
 | 
			
		||||
 | 
			
		||||
// this is also a comment`
 | 
			
		||||
// this is also a comment
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('comments in a fn block', () => {
 | 
			
		||||
    const code = `
 | 
			
		||||
const myFn = () => {
 | 
			
		||||
    const code = `const myFn = () => {
 | 
			
		||||
  // this is a comment
 | 
			
		||||
  const yo = { a: { b: { c: '123' } } } /* block
 | 
			
		||||
  comment */
 | 
			
		||||
  const yo = { a: { b: { c: '123' } } }
 | 
			
		||||
 | 
			
		||||
  /* block
 | 
			
		||||
  comment */
 | 
			
		||||
  const key = 'c'
 | 
			
		||||
  // this is also a comment
 | 
			
		||||
}`
 | 
			
		||||
}
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
@ -244,7 +249,7 @@ const myFn = () => {
 | 
			
		||||
    ].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('comments sprinkled in all over the place', () => {
 | 
			
		||||
    const code = `
 | 
			
		||||
@ -266,10 +271,26 @@ const mySk1 = startSketchAt([0, 0])
 | 
			
		||||
  |> rx(45, %)
 | 
			
		||||
  /*
 | 
			
		||||
  one more for good measure
 | 
			
		||||
  */`
 | 
			
		||||
  */
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted).toBe(`// comment at start
 | 
			
		||||
const mySk1 = startSketchAt([0, 0])
 | 
			
		||||
  |> lineTo([1, 1], %)
 | 
			
		||||
  // comment here
 | 
			
		||||
  |> lineTo({ to: [0, 1], tag: 'myTag' }, %)
 | 
			
		||||
  |> lineTo([1, 1], %)
 | 
			
		||||
  /* and
 | 
			
		||||
  here
 | 
			
		||||
 | 
			
		||||
a comment between pipe expression statements */
 | 
			
		||||
  |> rx(90, %)
 | 
			
		||||
  // and another with just white space between others below
 | 
			
		||||
  |> ry(45, %)
 | 
			
		||||
  |> rx(45, %)
 | 
			
		||||
// one more for good measure
 | 
			
		||||
`)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -278,28 +299,28 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
 | 
			
		||||
    const code = 'const myVar = 2 + min(100, legLen(5, 3))'
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('nested callExpression in unaryExpression', () => {
 | 
			
		||||
    const code = 'const myVar = -min(100, legLen(5, 3))'
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('with unaryExpression in callExpression', () => {
 | 
			
		||||
    const code = 'const myVar = min(5, -legLen(5, 4))'
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
  it('with unaryExpression in sketch situation', () => {
 | 
			
		||||
    const code = [
 | 
			
		||||
      'const part001 = startSketchAt([0, 0])',
 | 
			
		||||
      '|> line([-2.21, -legLen(5, min(3, 999))], %)',
 | 
			
		||||
      '  |> line([-2.21, -legLen(5, min(3, 999))], %)',
 | 
			
		||||
    ].join('\n')
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
    expect(recasted.trim()).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -309,12 +330,13 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde
 | 
			
		||||
  |> line({ to: [0.62, 4.15], tag: 'seg01' }, %)
 | 
			
		||||
  |> line([2.77, -1.24], %)
 | 
			
		||||
  |> angledLineThatIntersects({
 | 
			
		||||
      angle: 201,
 | 
			
		||||
      offset: -1.35,
 | 
			
		||||
      intersectTag: 'seg01'
 | 
			
		||||
    }, %)
 | 
			
		||||
       angle: 201,
 | 
			
		||||
       offset: -1.35,
 | 
			
		||||
       intersectTag: 'seg01'
 | 
			
		||||
     }, %)
 | 
			
		||||
  |> line([-0.42, -1.72], %)
 | 
			
		||||
show(part001)`
 | 
			
		||||
show(part001)
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
@ -324,7 +346,8 @@ show(part001)`
 | 
			
		||||
  angle: 201,
 | 
			
		||||
  offset: -1.35,
 | 
			
		||||
  intersectTag: 'seg01'
 | 
			
		||||
}, %)`
 | 
			
		||||
}, %)
 | 
			
		||||
`
 | 
			
		||||
    const { ast } = code2ast(code)
 | 
			
		||||
    const recasted = recast(ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
@ -333,7 +356,8 @@ show(part001)`
 | 
			
		||||
 | 
			
		||||
describe('it recasts binary expression using brackets where needed', () => {
 | 
			
		||||
  it('when there are two minus in a row', () => {
 | 
			
		||||
    const code = `const part001 = 1 - (def - abc)`
 | 
			
		||||
    const code = `const part001 = 1 - (def - abc)
 | 
			
		||||
`
 | 
			
		||||
    const recasted = recast(code2ast(code).ast)
 | 
			
		||||
    expect(recasted).toBe(code)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,21 @@
 | 
			
		||||
import { SourceRange } from 'lang/executor'
 | 
			
		||||
import { Selections } from 'useStore'
 | 
			
		||||
import {
 | 
			
		||||
  VITE_KC_API_WS_MODELING_URL,
 | 
			
		||||
  VITE_KC_CONNECTION_TIMEOUT_MS,
 | 
			
		||||
  VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
 | 
			
		||||
} from 'env'
 | 
			
		||||
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { exportSave } from 'lib/exportSave'
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import * as Sentry from '@sentry/react'
 | 
			
		||||
 | 
			
		||||
interface ResultCommand {
 | 
			
		||||
interface CommandInfo {
 | 
			
		||||
  commandType: CommandTypes
 | 
			
		||||
  range: SourceRange
 | 
			
		||||
  parentId?: string
 | 
			
		||||
}
 | 
			
		||||
interface ResultCommand extends CommandInfo {
 | 
			
		||||
  type: 'result'
 | 
			
		||||
  data: any
 | 
			
		||||
}
 | 
			
		||||
interface PendingCommand {
 | 
			
		||||
interface PendingCommand extends CommandInfo {
 | 
			
		||||
  type: 'pending'
 | 
			
		||||
  promise: Promise<any>
 | 
			
		||||
  resolve: (val: any) => void
 | 
			
		||||
@ -34,6 +35,8 @@ interface NewTrackArgs {
 | 
			
		||||
 | 
			
		||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
 | 
			
		||||
 | 
			
		||||
type ClientMetrics = Models['ClientMetrics_type']
 | 
			
		||||
 | 
			
		||||
// EngineConnection encapsulates the connection(s) to the Engine
 | 
			
		||||
// for the EngineCommandManager; namely, the underlying WebSocket
 | 
			
		||||
// and WebRTC connections.
 | 
			
		||||
@ -53,6 +56,9 @@ export class EngineConnection {
 | 
			
		||||
  private onClose: (engineConnection: EngineConnection) => void
 | 
			
		||||
  private onNewTrack: (track: NewTrackArgs) => void
 | 
			
		||||
 | 
			
		||||
  // TODO: actual type is ClientMetrics
 | 
			
		||||
  private webrtcStatsCollector?: () => Promise<ClientMetrics>
 | 
			
		||||
 | 
			
		||||
  constructor({
 | 
			
		||||
    url,
 | 
			
		||||
    token,
 | 
			
		||||
@ -188,15 +194,17 @@ export class EngineConnection {
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      Promise.all([
 | 
			
		||||
        handshakeSpan.promise,
 | 
			
		||||
        iceSpan.promise,
 | 
			
		||||
        dataChannelSpan.promise,
 | 
			
		||||
        mediaTrackSpan.promise,
 | 
			
		||||
      ]).then(() => {
 | 
			
		||||
        console.log('All spans finished, reporting')
 | 
			
		||||
        webrtcMediaTransaction?.finish()
 | 
			
		||||
      })
 | 
			
		||||
      if (this.shouldTrace()) {
 | 
			
		||||
        Promise.all([
 | 
			
		||||
          handshakeSpan.promise,
 | 
			
		||||
          iceSpan.promise,
 | 
			
		||||
          dataChannelSpan.promise,
 | 
			
		||||
          mediaTrackSpan.promise,
 | 
			
		||||
        ]).then(() => {
 | 
			
		||||
          console.log('All spans finished, reporting')
 | 
			
		||||
          webrtcMediaTransaction?.finish()
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.onWebsocketOpen(this)
 | 
			
		||||
    })
 | 
			
		||||
@ -297,7 +305,9 @@ export class EngineConnection {
 | 
			
		||||
 | 
			
		||||
        this.pc.addEventListener('connectionstatechange', (event) => {
 | 
			
		||||
          if (this.pc?.iceConnectionState === 'connected') {
 | 
			
		||||
            iceSpan.resolve?.()
 | 
			
		||||
            if (this.shouldTrace()) {
 | 
			
		||||
              iceSpan.resolve?.()
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
@ -330,6 +340,17 @@ export class EngineConnection {
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
          .catch(console.log)
 | 
			
		||||
      } else if (resp.type === 'metrics_request') {
 | 
			
		||||
        if (this.webrtcStatsCollector === undefined) {
 | 
			
		||||
          // TODO: Error message here?
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        this.webrtcStatsCollector().then((client_metrics) => {
 | 
			
		||||
          this.send({
 | 
			
		||||
            type: 'metrics_response',
 | 
			
		||||
            metrics: client_metrics,
 | 
			
		||||
          })
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // TODO(paultag): This ought to be both controllable, as well as something
 | 
			
		||||
@ -361,127 +382,58 @@ export class EngineConnection {
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Set up the background thread to keep an eye on statistical
 | 
			
		||||
      // information about the WebRTC media stream from the server to
 | 
			
		||||
      // us. We'll also eventually want more global statistical information,
 | 
			
		||||
      // but this will give us a baseline.
 | 
			
		||||
      if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
 | 
			
		||||
        setInterval(() => {
 | 
			
		||||
          if (this.pc === undefined) {
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
          if (!this.shouldTrace()) {
 | 
			
		||||
      this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
          if (mediaStream.getVideoTracks().length !== 1) {
 | 
			
		||||
            reject(new Error('too many video tracks to report'))
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Use the WebRTC Statistics API to collect statistical information
 | 
			
		||||
          // about the WebRTC connection we're using to report to Sentry.
 | 
			
		||||
          mediaStream.getVideoTracks().forEach((videoTrack) => {
 | 
			
		||||
            let trackStats = new Map<string, any>()
 | 
			
		||||
            this.pc?.getStats(videoTrack).then((videoTrackStats) => {
 | 
			
		||||
              // Sentry only allows 10 metrics per transaction. We're going
 | 
			
		||||
              // to have to pick carefully here, eventually send like a prom
 | 
			
		||||
              // file or something to the peer.
 | 
			
		||||
          let videoTrack = mediaStream.getVideoTracks()[0]
 | 
			
		||||
          this.pc?.getStats(videoTrack).then((videoTrackStats) => {
 | 
			
		||||
            // TODO(paultag): this needs type information from the KittyCAD typescript
 | 
			
		||||
            // library once it's updated
 | 
			
		||||
            let client_metrics: ClientMetrics = {
 | 
			
		||||
              rtc_frames_decoded: 0,
 | 
			
		||||
              rtc_frames_dropped: 0,
 | 
			
		||||
              rtc_frames_received: 0,
 | 
			
		||||
              rtc_frames_per_second: 0,
 | 
			
		||||
              rtc_freeze_count: 0,
 | 
			
		||||
              rtc_jitter_sec: 0.0,
 | 
			
		||||
              rtc_keyframes_decoded: 0,
 | 
			
		||||
              rtc_total_freezes_duration_sec: 0.0,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
              const transaction = Sentry.startTransaction({
 | 
			
		||||
                name: 'webrtc-stats',
 | 
			
		||||
              })
 | 
			
		||||
              videoTrackStats.forEach((videoTrackReport) => {
 | 
			
		||||
                if (videoTrackReport.type === 'inbound-rtp') {
 | 
			
		||||
                  // RTC Stream Info
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.framesDecoded',
 | 
			
		||||
                  //   videoTrackReport.framesDecoded,
 | 
			
		||||
                  //   'frame'
 | 
			
		||||
                  // )
 | 
			
		||||
                  transaction.setMeasurement(
 | 
			
		||||
                    'rtcFramesDropped',
 | 
			
		||||
                    videoTrackReport.framesDropped,
 | 
			
		||||
                    ''
 | 
			
		||||
                  )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.framesReceived',
 | 
			
		||||
                  //   videoTrackReport.framesReceived,
 | 
			
		||||
                  //   'frame'
 | 
			
		||||
                  // )
 | 
			
		||||
                  transaction.setMeasurement(
 | 
			
		||||
                    'rtcFramesPerSecond',
 | 
			
		||||
                    videoTrackReport.framesPerSecond,
 | 
			
		||||
                    'fps'
 | 
			
		||||
                  )
 | 
			
		||||
                  transaction.setMeasurement(
 | 
			
		||||
                    'rtcFreezeCount',
 | 
			
		||||
                    videoTrackReport.freezeCount,
 | 
			
		||||
                    ''
 | 
			
		||||
                  )
 | 
			
		||||
                  transaction.setMeasurement(
 | 
			
		||||
                    'rtcJitter',
 | 
			
		||||
                    videoTrackReport.jitter,
 | 
			
		||||
                    'second'
 | 
			
		||||
                  )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.jitterBufferDelay',
 | 
			
		||||
                  //   videoTrackReport.jitterBufferDelay,
 | 
			
		||||
                  //   ''
 | 
			
		||||
                  // )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.jitterBufferEmittedCount',
 | 
			
		||||
                  //   videoTrackReport.jitterBufferEmittedCount,
 | 
			
		||||
                  //   ''
 | 
			
		||||
                  // )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.jitterBufferMinimumDelay',
 | 
			
		||||
                  //   videoTrackReport.jitterBufferMinimumDelay,
 | 
			
		||||
                  //   ''
 | 
			
		||||
                  // )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.jitterBufferTargetDelay',
 | 
			
		||||
                  //   videoTrackReport.jitterBufferTargetDelay,
 | 
			
		||||
                  //   ''
 | 
			
		||||
                  // )
 | 
			
		||||
                  transaction.setMeasurement(
 | 
			
		||||
                    'rtcKeyFramesDecoded',
 | 
			
		||||
                    videoTrackReport.keyFramesDecoded,
 | 
			
		||||
                    ''
 | 
			
		||||
                  )
 | 
			
		||||
                  transaction.setMeasurement(
 | 
			
		||||
                    'rtcTotalFreezesDuration',
 | 
			
		||||
                    videoTrackReport.totalFreezesDuration,
 | 
			
		||||
                    'second'
 | 
			
		||||
                  )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.totalInterFrameDelay',
 | 
			
		||||
                  //   videoTrackReport.totalInterFrameDelay,
 | 
			
		||||
                  //   ''
 | 
			
		||||
                  // )
 | 
			
		||||
                  transaction.setMeasurement(
 | 
			
		||||
                    'rtcTotalPausesDuration',
 | 
			
		||||
                    videoTrackReport.totalPausesDuration,
 | 
			
		||||
                    'second'
 | 
			
		||||
                  )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.totalProcessingDelay',
 | 
			
		||||
                  //   videoTrackReport.totalProcessingDelay,
 | 
			
		||||
                  //   'second'
 | 
			
		||||
                  // )
 | 
			
		||||
                } else if (videoTrackReport.type === 'transport') {
 | 
			
		||||
                  // // Bytes i/o
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.bytesReceived',
 | 
			
		||||
                  //   videoTrackReport.bytesReceived,
 | 
			
		||||
                  //   'byte'
 | 
			
		||||
                  // )
 | 
			
		||||
                  // transaction.setMeasurement(
 | 
			
		||||
                  //   'mediaStreamTrack.bytesSent',
 | 
			
		||||
                  //   videoTrackReport.bytesSent,
 | 
			
		||||
                  //   'byte'
 | 
			
		||||
                  // )
 | 
			
		||||
                }
 | 
			
		||||
              })
 | 
			
		||||
              transaction?.finish()
 | 
			
		||||
            // TODO(paultag): Since we can technically have multiple WebRTC
 | 
			
		||||
            // video tracks (even if the Server doesn't at the moment), we
 | 
			
		||||
            // ought to send stats for every video track(?), and add the stream
 | 
			
		||||
            // ID into it.  This raises the cardinality of collected metrics
 | 
			
		||||
            // when/if we do, but for now, just report the one stream.
 | 
			
		||||
 | 
			
		||||
            videoTrackStats.forEach((videoTrackReport) => {
 | 
			
		||||
              if (videoTrackReport.type === 'inbound-rtp') {
 | 
			
		||||
                client_metrics.rtc_frames_decoded =
 | 
			
		||||
                  videoTrackReport.framesDecoded
 | 
			
		||||
                client_metrics.rtc_frames_dropped =
 | 
			
		||||
                  videoTrackReport.framesDropped
 | 
			
		||||
                client_metrics.rtc_frames_received =
 | 
			
		||||
                  videoTrackReport.framesReceived
 | 
			
		||||
                client_metrics.rtc_frames_per_second =
 | 
			
		||||
                  videoTrackReport.framesPerSecond || 0
 | 
			
		||||
                client_metrics.rtc_freeze_count = videoTrackReport.freezeCount
 | 
			
		||||
                client_metrics.rtc_jitter_sec = videoTrackReport.jitter
 | 
			
		||||
                client_metrics.rtc_keyframes_decoded =
 | 
			
		||||
                  videoTrackReport.keyFramesDecoded
 | 
			
		||||
                client_metrics.rtc_total_freezes_duration_sec =
 | 
			
		||||
                  videoTrackReport.totalFreezesDuration
 | 
			
		||||
              } else if (videoTrackReport.type === 'transport') {
 | 
			
		||||
                // videoTrackReport.bytesReceived,
 | 
			
		||||
                // videoTrackReport.bytesSent,
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
            resolve(client_metrics)
 | 
			
		||||
          })
 | 
			
		||||
        }, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.onNewTrack({
 | 
			
		||||
@ -490,10 +442,6 @@ export class EngineConnection {
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // During startup, we'll track the time from `connect` being called
 | 
			
		||||
    // until the 'done' event fires.
 | 
			
		||||
    let connectionStarted = new Date()
 | 
			
		||||
 | 
			
		||||
    this.pc.addEventListener('datachannel', (event) => {
 | 
			
		||||
      this.unreliableDataChannel = event.channel
 | 
			
		||||
 | 
			
		||||
@ -537,6 +485,7 @@ export class EngineConnection {
 | 
			
		||||
    this.websocket = undefined
 | 
			
		||||
    this.pc = undefined
 | 
			
		||||
    this.unreliableDataChannel = undefined
 | 
			
		||||
    this.webrtcStatsCollector = undefined
 | 
			
		||||
 | 
			
		||||
    this.onClose(this)
 | 
			
		||||
    this.ready = false
 | 
			
		||||
@ -546,6 +495,8 @@ export class EngineConnection {
 | 
			
		||||
export type EngineCommand = Models['WebSocketRequest_type']
 | 
			
		||||
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
 | 
			
		||||
 | 
			
		||||
type CommandTypes = Models['ModelingCmd_type']['type']
 | 
			
		||||
 | 
			
		||||
type UnreliableResponses = Extract<
 | 
			
		||||
  Models['OkModelingCmdResponse_type'],
 | 
			
		||||
  { type: 'highlight_set_entity' }
 | 
			
		||||
@ -687,15 +638,22 @@ export class EngineCommandManager {
 | 
			
		||||
      const resolve = command.resolve
 | 
			
		||||
      this.artifactMap[id] = {
 | 
			
		||||
        type: 'result',
 | 
			
		||||
        range: command.range,
 | 
			
		||||
        commandType: command.commandType,
 | 
			
		||||
        parentId: command.parentId ? command.parentId : undefined,
 | 
			
		||||
        data: modelingResponse,
 | 
			
		||||
      }
 | 
			
		||||
      resolve({
 | 
			
		||||
        id,
 | 
			
		||||
        commandType: command.commandType,
 | 
			
		||||
        range: command.range,
 | 
			
		||||
        data: modelingResponse,
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      this.artifactMap[id] = {
 | 
			
		||||
        type: 'result',
 | 
			
		||||
        commandType: command?.commandType,
 | 
			
		||||
        range: command?.range,
 | 
			
		||||
        data: modelingResponse,
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -747,8 +705,29 @@ export class EngineCommandManager {
 | 
			
		||||
    delete this.unreliableSubscriptions[event][id]
 | 
			
		||||
  }
 | 
			
		||||
  endSession() {
 | 
			
		||||
    // this.websocket?.close()
 | 
			
		||||
    // socket.off('command')
 | 
			
		||||
    // TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
 | 
			
		||||
    // we need to loop over them each individualy because if the engine doesn't recognise a single
 | 
			
		||||
    // id the whole command fails.
 | 
			
		||||
    Object.entries(this.artifactMap).forEach(([id, artifact]) => {
 | 
			
		||||
      const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
 | 
			
		||||
        // 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
 | 
			
		||||
        // however all of the segments in the path are its children so there don't need to be deleted.
 | 
			
		||||
        // this fact is very opaque in the api and docs (as to what should can be deleted).
 | 
			
		||||
        // Using an array is the list is likely to grow.
 | 
			
		||||
        'start_path',
 | 
			
		||||
      ]
 | 
			
		||||
      if (!artifactTypesToDelete.includes(artifact.commandType)) return
 | 
			
		||||
 | 
			
		||||
      const deletCmd: EngineCommand = {
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
        cmd_id: uuidv4(),
 | 
			
		||||
        cmd: {
 | 
			
		||||
          type: 'remove_scene_objects',
 | 
			
		||||
          object_ids: [id],
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
      this.engineConnection?.send(deletCmd)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  cusorsSelected(selections: {
 | 
			
		||||
    otherSelections: Selections['otherSelections']
 | 
			
		||||
@ -801,11 +780,20 @@ export class EngineCommandManager {
 | 
			
		||||
        JSON.stringify(command)
 | 
			
		||||
      )
 | 
			
		||||
      return Promise.resolve()
 | 
			
		||||
    } else if (
 | 
			
		||||
      cmd.type === 'mouse_move' &&
 | 
			
		||||
      this.engineConnection.unreliableDataChannel
 | 
			
		||||
    ) {
 | 
			
		||||
      cmd.sequence = this.outSequence
 | 
			
		||||
      this.outSequence++
 | 
			
		||||
      this.engineConnection?.unreliableDataChannel?.send(
 | 
			
		||||
        JSON.stringify(command)
 | 
			
		||||
      )
 | 
			
		||||
      return Promise.resolve()
 | 
			
		||||
    }
 | 
			
		||||
    console.log('sending command', command)
 | 
			
		||||
    // since it's not mouse drag or highlighting send over TCP and keep track of the command
 | 
			
		||||
    this.engineConnection?.send(command)
 | 
			
		||||
    return this.handlePendingCommand(command.cmd_id)
 | 
			
		||||
    return this.handlePendingCommand(command.cmd_id, command.cmd)
 | 
			
		||||
  }
 | 
			
		||||
  sendModelingCommand({
 | 
			
		||||
    id,
 | 
			
		||||
@ -823,15 +811,35 @@ export class EngineCommandManager {
 | 
			
		||||
      return Promise.resolve()
 | 
			
		||||
    }
 | 
			
		||||
    this.engineConnection?.send(command)
 | 
			
		||||
    return this.handlePendingCommand(id)
 | 
			
		||||
    if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
 | 
			
		||||
      return this.handlePendingCommand(id, command?.cmd, range)
 | 
			
		||||
    } else if (typeof command === 'string') {
 | 
			
		||||
      const parseCommand: EngineCommand = JSON.parse(command)
 | 
			
		||||
      if (parseCommand.type === 'modeling_cmd_req')
 | 
			
		||||
        return this.handlePendingCommand(id, parseCommand?.cmd, range)
 | 
			
		||||
    }
 | 
			
		||||
    throw 'shouldnt reach here'
 | 
			
		||||
  }
 | 
			
		||||
  handlePendingCommand(id: string) {
 | 
			
		||||
  handlePendingCommand(
 | 
			
		||||
    id: string,
 | 
			
		||||
    command: Models['ModelingCmd_type'],
 | 
			
		||||
    range?: SourceRange
 | 
			
		||||
  ) {
 | 
			
		||||
    let resolve: (val: any) => void = () => {}
 | 
			
		||||
    const promise = new Promise((_resolve, reject) => {
 | 
			
		||||
      resolve = _resolve
 | 
			
		||||
    })
 | 
			
		||||
    const getParentId = (): string | undefined => {
 | 
			
		||||
      if (command.type === 'extend_path') {
 | 
			
		||||
        return command.path
 | 
			
		||||
      }
 | 
			
		||||
      // TODO handle other commands that have a parent
 | 
			
		||||
    }
 | 
			
		||||
    this.artifactMap[id] = {
 | 
			
		||||
      range: range || [0, 0],
 | 
			
		||||
      type: 'pending',
 | 
			
		||||
      commandType: command.type,
 | 
			
		||||
      parentId: getParentId(),
 | 
			
		||||
      promise,
 | 
			
		||||
      resolve,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -97,12 +97,12 @@ describe('testing changeSketchArguments', () => {
 | 
			
		||||
  const lineAfterChange = 'lineTo([2, 3], %)'
 | 
			
		||||
  test('changeSketchArguments', async () => {
 | 
			
		||||
    // Enable rotations #152
 | 
			
		||||
    const genCode = (line: string) => `
 | 
			
		||||
const mySketch001 = startSketchAt([0, 0])
 | 
			
		||||
    |> ${line}
 | 
			
		||||
    |> lineTo([0.46, -5.82], %)
 | 
			
		||||
    // |> rx(45, %)
 | 
			
		||||
show(mySketch001)`
 | 
			
		||||
    const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
 | 
			
		||||
  |> ${line}
 | 
			
		||||
  |> lineTo([0.46, -5.82], %)
 | 
			
		||||
// |> rx(45, %)
 | 
			
		||||
show(mySketch001)
 | 
			
		||||
`
 | 
			
		||||
    const code = genCode(lineToChange)
 | 
			
		||||
    const expectedCode = genCode(lineAfterChange)
 | 
			
		||||
    const ast = parser_wasm(code)
 | 
			
		||||
@ -160,13 +160,13 @@ show(mySketch001)`
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
    // Enable rotations #152
 | 
			
		||||
    const expectedCode = `
 | 
			
		||||
const mySketch001 = startSketchAt([0, 0])
 | 
			
		||||
    const expectedCode = `const mySketch001 = startSketchAt([0, 0])
 | 
			
		||||
  // |> rx(45, %)
 | 
			
		||||
  |> lineTo([-1.59, -1.54], %)
 | 
			
		||||
  |> lineTo([0.46, -5.82], %)
 | 
			
		||||
  |> lineTo([2, 3], %)
 | 
			
		||||
show(mySketch001)`
 | 
			
		||||
show(mySketch001)
 | 
			
		||||
`
 | 
			
		||||
    expect(recast(modifiedAst)).toBe(expectedCode)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@ -175,12 +175,12 @@ describe('testing addTagForSketchOnFace', () => {
 | 
			
		||||
  it('needs to be in it', async () => {
 | 
			
		||||
    const originalLine = 'lineTo([-1.59, -1.54], %)'
 | 
			
		||||
    // Enable rotations #152
 | 
			
		||||
    const genCode = (line: string) => `
 | 
			
		||||
  const mySketch001 = startSketchAt([0, 0])
 | 
			
		||||
    // |> rx(45, %)
 | 
			
		||||
    |> ${line}
 | 
			
		||||
    |> lineTo([0.46, -5.82], %)
 | 
			
		||||
  show(mySketch001)`
 | 
			
		||||
    const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0])
 | 
			
		||||
  // |> rx(45, %)
 | 
			
		||||
  |> ${line}
 | 
			
		||||
  |> lineTo([0.46, -5.82], %)
 | 
			
		||||
show(mySketch001)
 | 
			
		||||
`
 | 
			
		||||
    const code = genCode(originalLine)
 | 
			
		||||
    const ast = parser_wasm(code)
 | 
			
		||||
    const programMemory = await enginelessExecutor(ast)
 | 
			
		||||
 | 
			
		||||
@ -59,20 +59,20 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
 | 
			
		||||
    `  |> lineTo({ to: [1, 1], tag: 'abc1' }, %)`,
 | 
			
		||||
    `  |> line({ to: [-2.04, -0.7], tag: 'abc2' }, %)`,
 | 
			
		||||
    `  |> angledLine({`,
 | 
			
		||||
    `      angle: 157,`,
 | 
			
		||||
    `      length: 1.69,`,
 | 
			
		||||
    `      tag: 'abc3'`,
 | 
			
		||||
    `    }, %)`,
 | 
			
		||||
    `       angle: 157,`,
 | 
			
		||||
    `       length: 1.69,`,
 | 
			
		||||
    `       tag: 'abc3'`,
 | 
			
		||||
    `     }, %)`,
 | 
			
		||||
    `  |> angledLineOfXLength({`,
 | 
			
		||||
    `      angle: 217,`,
 | 
			
		||||
    `      length: 0.86,`,
 | 
			
		||||
    `      tag: 'abc4'`,
 | 
			
		||||
    `    }, %)`,
 | 
			
		||||
    `       angle: 217,`,
 | 
			
		||||
    `       length: 0.86,`,
 | 
			
		||||
    `       tag: 'abc4'`,
 | 
			
		||||
    `     }, %)`,
 | 
			
		||||
    `  |> angledLineOfYLength({`,
 | 
			
		||||
    `      angle: 104,`,
 | 
			
		||||
    `      length: 1.58,`,
 | 
			
		||||
    `      tag: 'abc5'`,
 | 
			
		||||
    `    }, %)`,
 | 
			
		||||
    `       angle: 104,`,
 | 
			
		||||
    `       length: 1.58,`,
 | 
			
		||||
    `       tag: 'abc5'`,
 | 
			
		||||
    `     }, %)`,
 | 
			
		||||
    `  |> angledLineToX({ angle: 55, to: -2.89, tag: 'abc6' }, %)`,
 | 
			
		||||
    `  |> angledLineToY({ angle: 330, to: 2.53, tag: 'abc7' }, %)`,
 | 
			
		||||
    `  |> xLine({ length: 1.47, tag: 'abc8' }, %)`,
 | 
			
		||||
@ -144,10 +144,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
 | 
			
		||||
      inputCode: bigExample,
 | 
			
		||||
      callToSwap: [
 | 
			
		||||
        `angledLine({`,
 | 
			
		||||
        `      angle: 157,`,
 | 
			
		||||
        `      length: 1.69,`,
 | 
			
		||||
        `      tag: 'abc3'`,
 | 
			
		||||
        `    }, %)`,
 | 
			
		||||
        `       angle: 157,`,
 | 
			
		||||
        `       length: 1.69,`,
 | 
			
		||||
        `       tag: 'abc3'`,
 | 
			
		||||
        `     }, %)`,
 | 
			
		||||
      ].join('\n'),
 | 
			
		||||
      constraintType: 'horizontal',
 | 
			
		||||
    })
 | 
			
		||||
@ -172,10 +172,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
 | 
			
		||||
      inputCode: bigExample,
 | 
			
		||||
      callToSwap: [
 | 
			
		||||
        `angledLineOfXLength({`,
 | 
			
		||||
        `      angle: 217,`,
 | 
			
		||||
        `      length: 0.86,`,
 | 
			
		||||
        `      tag: 'abc4'`,
 | 
			
		||||
        `    }, %)`,
 | 
			
		||||
        `       angle: 217,`,
 | 
			
		||||
        `       length: 0.86,`,
 | 
			
		||||
        `       tag: 'abc4'`,
 | 
			
		||||
        `     }, %)`,
 | 
			
		||||
      ].join('\n'),
 | 
			
		||||
      constraintType: 'horizontal',
 | 
			
		||||
    })
 | 
			
		||||
@ -201,10 +201,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => {
 | 
			
		||||
      inputCode: bigExample,
 | 
			
		||||
      callToSwap: [
 | 
			
		||||
        `angledLineOfYLength({`,
 | 
			
		||||
        `      angle: 104,`,
 | 
			
		||||
        `      length: 1.58,`,
 | 
			
		||||
        `      tag: 'abc5'`,
 | 
			
		||||
        `    }, %)`,
 | 
			
		||||
        `       angle: 104,`,
 | 
			
		||||
        `       length: 1.58,`,
 | 
			
		||||
        `       tag: 'abc5'`,
 | 
			
		||||
        `     }, %)`,
 | 
			
		||||
      ].join('\n'),
 | 
			
		||||
      constraintType: 'vertical',
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -124,7 +124,8 @@ const part001 = startSketchAt([0, 0])
 | 
			
		||||
  |> yLine(1.04, %) // ln-yLine-free should sub in segLen
 | 
			
		||||
  |> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
 | 
			
		||||
  |> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
 | 
			
		||||
show(part001)`
 | 
			
		||||
show(part001)
 | 
			
		||||
`
 | 
			
		||||
  const expectModifiedScript = `const myVar = 3
 | 
			
		||||
const myVar2 = 5
 | 
			
		||||
const myVar3 = 6
 | 
			
		||||
@ -133,69 +134,70 @@ const myAng2 = 134
 | 
			
		||||
const part001 = startSketchAt([0, 0])
 | 
			
		||||
  |> line({ to: [1, 3.82], tag: 'seg01' }, %) // ln-should-get-tag
 | 
			
		||||
  |> angledLineToX([
 | 
			
		||||
      -angleToMatchLengthX('seg01', myVar, %),
 | 
			
		||||
      myVar
 | 
			
		||||
    ], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
 | 
			
		||||
       -angleToMatchLengthX('seg01', myVar, %),
 | 
			
		||||
       myVar
 | 
			
		||||
     ], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
 | 
			
		||||
  |> angledLineToY([
 | 
			
		||||
      -angleToMatchLengthY('seg01', myVar, %),
 | 
			
		||||
      myVar
 | 
			
		||||
    ], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
 | 
			
		||||
       -angleToMatchLengthY('seg01', myVar, %),
 | 
			
		||||
       myVar
 | 
			
		||||
     ], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
 | 
			
		||||
  |> angledLine([45, segLen('seg01', %)], %) // ln-lineTo-free should become angledLine
 | 
			
		||||
  |> angledLine([45, segLen('seg01', %)], %) // ln-angledLineToX-free should become angledLine
 | 
			
		||||
  |> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineToX-angle should become angledLine
 | 
			
		||||
  |> angledLineToX([
 | 
			
		||||
      angleToMatchLengthX('seg01', myVar2, %),
 | 
			
		||||
      myVar2
 | 
			
		||||
    ], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
 | 
			
		||||
       angleToMatchLengthX('seg01', myVar2, %),
 | 
			
		||||
       myVar2
 | 
			
		||||
     ], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle
 | 
			
		||||
  |> angledLine([-45, segLen('seg01', %)], %) // ln-angledLineToY-free should become angledLine
 | 
			
		||||
  |> angledLine([myAng2, segLen('seg01', %)], %) // ln-angledLineToY-angle should become angledLine
 | 
			
		||||
  |> angledLineToY([
 | 
			
		||||
      angleToMatchLengthY('seg01', myVar3, %),
 | 
			
		||||
      myVar3
 | 
			
		||||
    ], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
 | 
			
		||||
       angleToMatchLengthY('seg01', myVar3, %),
 | 
			
		||||
       myVar3
 | 
			
		||||
     ], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle
 | 
			
		||||
  |> line([
 | 
			
		||||
      min(segLen('seg01', %), myVar),
 | 
			
		||||
      legLen(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-should use legLen for y
 | 
			
		||||
       min(segLen('seg01', %), myVar),
 | 
			
		||||
       legLen(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-should use legLen for y
 | 
			
		||||
  |> line([
 | 
			
		||||
      min(segLen('seg01', %), myVar),
 | 
			
		||||
      -legLen(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-legLen but negative
 | 
			
		||||
       min(segLen('seg01', %), myVar),
 | 
			
		||||
       -legLen(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-legLen but negative
 | 
			
		||||
  |> angledLine([-112, segLen('seg01', %)], %) // ln-should become angledLine
 | 
			
		||||
  |> angledLine([myVar, segLen('seg01', %)], %) // ln-use segLen for secound arg
 | 
			
		||||
  |> angledLine([45, segLen('seg01', %)], %) // ln-segLen again
 | 
			
		||||
  |> angledLine([54, segLen('seg01', %)], %) // ln-should be transformed to angledLine
 | 
			
		||||
  |> angledLineOfXLength([
 | 
			
		||||
      legAngX(segLen('seg01', %), myVar),
 | 
			
		||||
      min(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-should use legAngX to calculate angle
 | 
			
		||||
       legAngX(segLen('seg01', %), myVar),
 | 
			
		||||
       min(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-should use legAngX to calculate angle
 | 
			
		||||
  |> angledLineOfXLength([
 | 
			
		||||
      180 + legAngX(segLen('seg01', %), myVar),
 | 
			
		||||
      min(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-same as above but should have + 180 to match original quadrant
 | 
			
		||||
       180 + legAngX(segLen('seg01', %), myVar),
 | 
			
		||||
       min(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-same as above but should have + 180 to match original quadrant
 | 
			
		||||
  |> line([
 | 
			
		||||
      legLen(segLen('seg01', %), myVar),
 | 
			
		||||
      min(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-legLen again but yRelative
 | 
			
		||||
       legLen(segLen('seg01', %), myVar),
 | 
			
		||||
       min(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-legLen again but yRelative
 | 
			
		||||
  |> line([
 | 
			
		||||
      -legLen(segLen('seg01', %), myVar),
 | 
			
		||||
      min(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-negative legLen yRelative
 | 
			
		||||
       -legLen(segLen('seg01', %), myVar),
 | 
			
		||||
       min(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-negative legLen yRelative
 | 
			
		||||
  |> angledLine([58, segLen('seg01', %)], %) // ln-angledLineOfYLength-free should become angledLine
 | 
			
		||||
  |> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineOfYLength-angle should become angledLine
 | 
			
		||||
  |> angledLineOfXLength([
 | 
			
		||||
      legAngY(segLen('seg01', %), myVar),
 | 
			
		||||
      min(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-angledLineOfYLength-yRelative use legAngY
 | 
			
		||||
       legAngY(segLen('seg01', %), myVar),
 | 
			
		||||
       min(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-angledLineOfYLength-yRelative use legAngY
 | 
			
		||||
  |> angledLineOfXLength([
 | 
			
		||||
      270 + legAngY(segLen('seg01', %), myVar),
 | 
			
		||||
      min(segLen('seg01', %), myVar)
 | 
			
		||||
    ], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
 | 
			
		||||
       270 + legAngY(segLen('seg01', %), myVar),
 | 
			
		||||
       min(segLen('seg01', %), myVar)
 | 
			
		||||
     ], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp
 | 
			
		||||
  |> xLine(segLen('seg01', %), %) // ln-xLine-free should sub in segLen
 | 
			
		||||
  |> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen
 | 
			
		||||
  |> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine
 | 
			
		||||
  |> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
 | 
			
		||||
show(part001)`
 | 
			
		||||
show(part001)
 | 
			
		||||
`
 | 
			
		||||
  it('should transform the ast', async () => {
 | 
			
		||||
    const ast = parser_wasm(inputScript)
 | 
			
		||||
    const selectionRanges: Selections['codeBasedSelections'] = inputScript
 | 
			
		||||
@ -254,7 +256,8 @@ const part001 = startSketchAt([0, 0])
 | 
			
		||||
  |> angledLineToY([223, 7.68], %) // select for vertical constraint 9
 | 
			
		||||
  |> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
 | 
			
		||||
  |> angledLineToY([301, myVar], %) // select for vertical constraint 10
 | 
			
		||||
show(part001)`
 | 
			
		||||
show(part001)
 | 
			
		||||
`
 | 
			
		||||
  it('should transform horizontal lines the ast', async () => {
 | 
			
		||||
    const expectModifiedScript = `const myVar = 2
 | 
			
		||||
const myVar2 = 12
 | 
			
		||||
@ -281,7 +284,8 @@ const part001 = startSketchAt([0, 0])
 | 
			
		||||
  |> angledLineToY([223, 7.68], %) // select for vertical constraint 9
 | 
			
		||||
  |> xLineTo(myVar3, %) // select for horizontal constraint 10
 | 
			
		||||
  |> angledLineToY([301, myVar], %) // select for vertical constraint 10
 | 
			
		||||
show(part001)`
 | 
			
		||||
show(part001)
 | 
			
		||||
`
 | 
			
		||||
    const ast = parser_wasm(inputScript)
 | 
			
		||||
    const selectionRanges: Selections['codeBasedSelections'] = inputScript
 | 
			
		||||
      .split('\n')
 | 
			
		||||
@ -338,7 +342,8 @@ const part001 = startSketchAt([0, 0])
 | 
			
		||||
  |> yLineTo(7.68, %) // select for vertical constraint 9
 | 
			
		||||
  |> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
 | 
			
		||||
  |> yLineTo(myVar, %) // select for vertical constraint 10
 | 
			
		||||
show(part001)`
 | 
			
		||||
show(part001)
 | 
			
		||||
`
 | 
			
		||||
    const ast = parser_wasm(inputScript)
 | 
			
		||||
    const selectionRanges: Selections['codeBasedSelections'] = inputScript
 | 
			
		||||
      .split('\n')
 | 
			
		||||
@ -380,7 +385,8 @@ const part001 = startSketchAt([0, 0])
 | 
			
		||||
  |> line([0.45, 1.46], %) // free
 | 
			
		||||
  |> line([myVar, 0.01], %) // xRelative
 | 
			
		||||
  |> line([0.7, myVar], %) // yRelative
 | 
			
		||||
show(part001)`
 | 
			
		||||
show(part001)
 | 
			
		||||
`
 | 
			
		||||
    it('testing for free to horizontal and vertical distance', async () => {
 | 
			
		||||
      const expectedHorizontalCode = await helperThing(
 | 
			
		||||
        inputScript,
 | 
			
		||||
@ -406,9 +412,9 @@ show(part001)`
 | 
			
		||||
        'setVertDistance'
 | 
			
		||||
      )
 | 
			
		||||
      expect(expectedCode).toContain(`|> lineTo([
 | 
			
		||||
      lastSegX(%) + myVar,
 | 
			
		||||
      segEndY('seg01', %) + 2.93
 | 
			
		||||
    ], %) // xRelative`)
 | 
			
		||||
       lastSegX(%) + myVar,
 | 
			
		||||
       segEndY('seg01', %) + 2.93
 | 
			
		||||
     ], %) // xRelative`)
 | 
			
		||||
    })
 | 
			
		||||
    it('testing for yRelative to horizontal distance', async () => {
 | 
			
		||||
      const expectedCode = await helperThing(
 | 
			
		||||
@ -417,9 +423,9 @@ show(part001)`
 | 
			
		||||
        'setHorzDistance'
 | 
			
		||||
      )
 | 
			
		||||
      expect(expectedCode).toContain(`|> lineTo([
 | 
			
		||||
      segEndX('seg01', %) + 2.6,
 | 
			
		||||
      lastSegY(%) + myVar
 | 
			
		||||
    ], %) // yRelative`)
 | 
			
		||||
       segEndX('seg01', %) + 2.6,
 | 
			
		||||
       lastSegY(%) + myVar
 | 
			
		||||
     ], %) // yRelative`)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -110,7 +110,7 @@ const yi=45`
 | 
			
		||||
      "brace        ')'        from 17  to 18",
 | 
			
		||||
    ])
 | 
			
		||||
    expect(stringSummaryLexer('fn funcName = (param1, param2) => {}')).toEqual([
 | 
			
		||||
      "word         'fn'       from 0   to 2",
 | 
			
		||||
      "keyword      'fn'       from 0   to 2",
 | 
			
		||||
      "whitespace   ' '        from 2   to 3",
 | 
			
		||||
      "word         'funcName' from 3   to 11",
 | 
			
		||||
      "whitespace   ' '        from 11  to 12",
 | 
			
		||||
@ -203,7 +203,7 @@ const yi=45`
 | 
			
		||||
  it('testing array declaration', () => {
 | 
			
		||||
    const result = stringSummaryLexer(`const yo = [1, 2]`)
 | 
			
		||||
    expect(result).toEqual([
 | 
			
		||||
      "word         'const'    from 0   to 5",
 | 
			
		||||
      "keyword      'const'    from 0   to 5",
 | 
			
		||||
      "whitespace   ' '        from 5   to 6",
 | 
			
		||||
      "word         'yo'       from 6   to 8",
 | 
			
		||||
      "whitespace   ' '        from 8   to 9",
 | 
			
		||||
@ -220,7 +220,7 @@ const yi=45`
 | 
			
		||||
  it('testing object declaration', () => {
 | 
			
		||||
    const result = stringSummaryLexer(`const yo = {key: 'value'}`)
 | 
			
		||||
    expect(result).toEqual([
 | 
			
		||||
      "word         'const'    from 0   to 5",
 | 
			
		||||
      "keyword      'const'    from 0   to 5",
 | 
			
		||||
      "whitespace   ' '        from 5   to 6",
 | 
			
		||||
      "word         'yo'       from 6   to 8",
 | 
			
		||||
      "whitespace   ' '        from 8   to 9",
 | 
			
		||||
@ -241,7 +241,7 @@ const prop2 = yo['key']
 | 
			
		||||
const key = 'key'
 | 
			
		||||
const prop3 = yo[key]`)
 | 
			
		||||
    expect(result).toEqual([
 | 
			
		||||
      "word         'const'    from 0   to 5",
 | 
			
		||||
      "keyword      'const'    from 0   to 5",
 | 
			
		||||
      "whitespace   ' '        from 5   to 6",
 | 
			
		||||
      "word         'yo'       from 6   to 8",
 | 
			
		||||
      "whitespace   ' '        from 8   to 9",
 | 
			
		||||
@ -254,7 +254,7 @@ const prop3 = yo[key]`)
 | 
			
		||||
      "string       ''value''  from 17  to 24",
 | 
			
		||||
      "brace        '}'        from 24  to 25",
 | 
			
		||||
      "whitespace   '\n'        from 25  to 26",
 | 
			
		||||
      "word         'const'    from 26  to 31",
 | 
			
		||||
      "keyword      'const'    from 26  to 31",
 | 
			
		||||
      "whitespace   ' '        from 31  to 32",
 | 
			
		||||
      "word         'prop'     from 32  to 36",
 | 
			
		||||
      "whitespace   ' '        from 36  to 37",
 | 
			
		||||
@ -264,7 +264,7 @@ const prop3 = yo[key]`)
 | 
			
		||||
      "period       '.'        from 41  to 42",
 | 
			
		||||
      "word         'key'      from 42  to 45",
 | 
			
		||||
      "whitespace   '\n'        from 45  to 46",
 | 
			
		||||
      "word         'const'    from 46  to 51",
 | 
			
		||||
      "keyword      'const'    from 46  to 51",
 | 
			
		||||
      "whitespace   ' '        from 51  to 52",
 | 
			
		||||
      "word         'prop2'    from 52  to 57",
 | 
			
		||||
      "whitespace   ' '        from 57  to 58",
 | 
			
		||||
@ -275,7 +275,7 @@ const prop3 = yo[key]`)
 | 
			
		||||
      "string       ''key''    from 63  to 68",
 | 
			
		||||
      "brace        ']'        from 68  to 69",
 | 
			
		||||
      "whitespace   '\n'        from 69  to 70",
 | 
			
		||||
      "word         'const'    from 70  to 75",
 | 
			
		||||
      "keyword      'const'    from 70  to 75",
 | 
			
		||||
      "whitespace   ' '        from 75  to 76",
 | 
			
		||||
      "word         'key'      from 76  to 79",
 | 
			
		||||
      "whitespace   ' '        from 79  to 80",
 | 
			
		||||
@ -283,7 +283,7 @@ const prop3 = yo[key]`)
 | 
			
		||||
      "whitespace   ' '        from 81  to 82",
 | 
			
		||||
      "string       ''key''    from 82  to 87",
 | 
			
		||||
      "whitespace   '\n'        from 87  to 88",
 | 
			
		||||
      "word         'const'    from 88  to 93",
 | 
			
		||||
      "keyword      'const'    from 88  to 93",
 | 
			
		||||
      "whitespace   ' '        from 93  to 94",
 | 
			
		||||
      "word         'prop3'    from 94  to 99",
 | 
			
		||||
      "whitespace   ' '        from 99  to 100",
 | 
			
		||||
@ -299,7 +299,7 @@ const prop3 = yo[key]`)
 | 
			
		||||
    const result = stringSummaryLexer(`const yo = 45 // this is a comment
 | 
			
		||||
const yo = 6`)
 | 
			
		||||
    expect(result).toEqual([
 | 
			
		||||
      "word         'const'    from 0   to 5",
 | 
			
		||||
      "keyword      'const'    from 0   to 5",
 | 
			
		||||
      "whitespace   ' '        from 5   to 6",
 | 
			
		||||
      "word         'yo'       from 6   to 8",
 | 
			
		||||
      "whitespace   ' '        from 8   to 9",
 | 
			
		||||
@ -307,9 +307,9 @@ const yo = 6`)
 | 
			
		||||
      "whitespace   ' '        from 10  to 11",
 | 
			
		||||
      "number       '45'       from 11  to 13",
 | 
			
		||||
      "whitespace   ' '        from 13  to 14",
 | 
			
		||||
      "linecomment  '// this is a comment' from 14  to 34",
 | 
			
		||||
      "lineComment  '// this is a comment' from 14  to 34",
 | 
			
		||||
      "whitespace   '\n'        from 34  to 35",
 | 
			
		||||
      "word         'const'    from 35  to 40",
 | 
			
		||||
      "keyword      'const'    from 35  to 40",
 | 
			
		||||
      "whitespace   ' '        from 40  to 41",
 | 
			
		||||
      "word         'yo'       from 41  to 43",
 | 
			
		||||
      "whitespace   ' '        from 43  to 44",
 | 
			
		||||
@ -328,9 +328,9 @@ const yo=45`)
 | 
			
		||||
      "string       ''hi''     from 4   to 8",
 | 
			
		||||
      "brace        ')'        from 8   to 9",
 | 
			
		||||
      "whitespace   '\n'        from 9   to 10",
 | 
			
		||||
      "linecomment  '// comment on a line by itself' from 10  to 40",
 | 
			
		||||
      "lineComment  '// comment on a line by itself' from 10  to 40",
 | 
			
		||||
      "whitespace   '\n'        from 40  to 41",
 | 
			
		||||
      "word         'const'    from 41  to 46",
 | 
			
		||||
      "keyword      'const'    from 41  to 46",
 | 
			
		||||
      "whitespace   ' '        from 46  to 47",
 | 
			
		||||
      "word         'yo'       from 47  to 49",
 | 
			
		||||
      "operator     '='        from 49  to 50",
 | 
			
		||||
@ -342,7 +342,7 @@ const yo=45`)
 | 
			
		||||
const ya = 6 */
 | 
			
		||||
const yi=45`)
 | 
			
		||||
    expect(result).toEqual([
 | 
			
		||||
      "word         'const'    from 0   to 5",
 | 
			
		||||
      "keyword      'const'    from 0   to 5",
 | 
			
		||||
      "whitespace   ' '        from 5   to 6",
 | 
			
		||||
      "word         'yo'       from 6   to 8",
 | 
			
		||||
      "whitespace   ' '        from 8   to 9",
 | 
			
		||||
@ -350,10 +350,10 @@ const yi=45`)
 | 
			
		||||
      "whitespace   ' '        from 10  to 11",
 | 
			
		||||
      "number       '45'       from 11  to 13",
 | 
			
		||||
      "whitespace   ' '        from 13  to 14",
 | 
			
		||||
      `blockcomment '/* this is a comment
 | 
			
		||||
      `blockComment '/* this is a comment
 | 
			
		||||
const ya = 6 */' from 14  to 50`,
 | 
			
		||||
      "whitespace   '\n'        from 50  to 51",
 | 
			
		||||
      "word         'const'    from 51  to 56",
 | 
			
		||||
      "keyword      'const'    from 51  to 56",
 | 
			
		||||
      "whitespace   ' '        from 56  to 57",
 | 
			
		||||
      "word         'yi'       from 57  to 59",
 | 
			
		||||
      "operator     '='        from 59  to 60",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										156
									
								
								src/lib/cameraControls.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/lib/cameraControls.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,156 @@
 | 
			
		||||
const noModifiersPressed = (e: React.MouseEvent) =>
 | 
			
		||||
  !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
 | 
			
		||||
 | 
			
		||||
export type CameraSystem =
 | 
			
		||||
  | 'KittyCAD'
 | 
			
		||||
  | 'OnShape'
 | 
			
		||||
  | 'Trackpad Friendly'
 | 
			
		||||
  | 'Solidworks'
 | 
			
		||||
  | 'NX'
 | 
			
		||||
  | 'Creo'
 | 
			
		||||
  | 'AutoCAD'
 | 
			
		||||
 | 
			
		||||
export const cameraSystems: CameraSystem[] = [
 | 
			
		||||
  'KittyCAD',
 | 
			
		||||
  'OnShape',
 | 
			
		||||
  'Trackpad Friendly',
 | 
			
		||||
  'Solidworks',
 | 
			
		||||
  'NX',
 | 
			
		||||
  'Creo',
 | 
			
		||||
  'AutoCAD',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
interface MouseGuardHandler {
 | 
			
		||||
  description: string
 | 
			
		||||
  callback: (e: React.MouseEvent) => boolean
 | 
			
		||||
  lenientDragStartButton?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface MouseGuardZoomHandler {
 | 
			
		||||
  description: string
 | 
			
		||||
  dragCallback: (e: React.MouseEvent) => boolean
 | 
			
		||||
  scrollCallback: (e: React.MouseEvent) => boolean
 | 
			
		||||
  lenientDragStartButton?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface MouseGuard {
 | 
			
		||||
  pan: MouseGuardHandler
 | 
			
		||||
  zoom: MouseGuardZoomHandler
 | 
			
		||||
  rotate: MouseGuardHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
 | 
			
		||||
  KittyCAD: {
 | 
			
		||||
    pan: {
 | 
			
		||||
      description: 'Right click + Shift + drag or middle click + drag',
 | 
			
		||||
      callback: (e) =>
 | 
			
		||||
        (e.button === 1 && noModifiersPressed(e)) ||
 | 
			
		||||
        (e.button === 2 && e.shiftKey),
 | 
			
		||||
    },
 | 
			
		||||
    zoom: {
 | 
			
		||||
      description: 'Scroll wheel or Right click + Ctrl + drag',
 | 
			
		||||
      dragCallback: (e) => e.button === 2 && e.ctrlKey,
 | 
			
		||||
      scrollCallback: () => true,
 | 
			
		||||
    },
 | 
			
		||||
    rotate: {
 | 
			
		||||
      description: 'Right click + drag',
 | 
			
		||||
      callback: (e) => e.button === 2 && noModifiersPressed(e),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  OnShape: {
 | 
			
		||||
    pan: {
 | 
			
		||||
      description: 'Right click + Ctrl + drag or middle click + drag',
 | 
			
		||||
      callback: (e) =>
 | 
			
		||||
        (e.button === 2 && e.ctrlKey) ||
 | 
			
		||||
        (e.button === 1 && noModifiersPressed(e)),
 | 
			
		||||
    },
 | 
			
		||||
    zoom: {
 | 
			
		||||
      description: 'Scroll wheel',
 | 
			
		||||
      dragCallback: () => false,
 | 
			
		||||
      scrollCallback: () => true,
 | 
			
		||||
    },
 | 
			
		||||
    rotate: {
 | 
			
		||||
      description: 'Right click + drag',
 | 
			
		||||
      callback: (e) => e.button === 2 && noModifiersPressed(e),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Trackpad Friendly': {
 | 
			
		||||
    pan: {
 | 
			
		||||
      description: 'Left click + Alt + Shift + drag or middle click + drag',
 | 
			
		||||
      callback: (e) =>
 | 
			
		||||
        (e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
 | 
			
		||||
        (e.button === 1 && noModifiersPressed(e)),
 | 
			
		||||
    },
 | 
			
		||||
    zoom: {
 | 
			
		||||
      description: 'Scroll wheel or Left click + Alt + OS + drag',
 | 
			
		||||
      dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
 | 
			
		||||
      scrollCallback: () => true,
 | 
			
		||||
    },
 | 
			
		||||
    rotate: {
 | 
			
		||||
      description: 'Left click + Alt + drag',
 | 
			
		||||
      callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
 | 
			
		||||
      lenientDragStartButton: 0,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Solidworks: {
 | 
			
		||||
    pan: {
 | 
			
		||||
      description: 'Right click + Ctrl + drag',
 | 
			
		||||
      callback: (e) => e.button === 2 && e.ctrlKey,
 | 
			
		||||
      lenientDragStartButton: 2,
 | 
			
		||||
    },
 | 
			
		||||
    zoom: {
 | 
			
		||||
      description: 'Scroll wheel or Middle click + Shift + drag',
 | 
			
		||||
      dragCallback: (e) => e.button === 1 && e.shiftKey,
 | 
			
		||||
      scrollCallback: () => true,
 | 
			
		||||
    },
 | 
			
		||||
    rotate: {
 | 
			
		||||
      description: 'Middle click + drag',
 | 
			
		||||
      callback: (e) => e.button === 1 && noModifiersPressed(e),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  NX: {
 | 
			
		||||
    pan: {
 | 
			
		||||
      description: 'Middle click + Shift + drag',
 | 
			
		||||
      callback: (e) => e.button === 1 && e.shiftKey,
 | 
			
		||||
    },
 | 
			
		||||
    zoom: {
 | 
			
		||||
      description: 'Scroll wheel or Middle click + Ctrl + drag',
 | 
			
		||||
      dragCallback: (e) => e.button === 1 && e.ctrlKey,
 | 
			
		||||
      scrollCallback: () => true,
 | 
			
		||||
    },
 | 
			
		||||
    rotate: {
 | 
			
		||||
      description: 'Middle click + drag',
 | 
			
		||||
      callback: (e) => e.button === 1 && noModifiersPressed(e),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Creo: {
 | 
			
		||||
    pan: {
 | 
			
		||||
      description: 'Middle click + Shift + drag',
 | 
			
		||||
      callback: (e) => e.button === 1 && e.shiftKey,
 | 
			
		||||
    },
 | 
			
		||||
    zoom: {
 | 
			
		||||
      description: 'Scroll wheel or Middle click + Ctrl + drag',
 | 
			
		||||
      dragCallback: (e) => e.button === 1 && e.ctrlKey,
 | 
			
		||||
      scrollCallback: () => true,
 | 
			
		||||
    },
 | 
			
		||||
    rotate: {
 | 
			
		||||
      description: 'Middle click + drag',
 | 
			
		||||
      callback: (e) => e.button === 1 && noModifiersPressed(e),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  AutoCAD: {
 | 
			
		||||
    pan: {
 | 
			
		||||
      description: 'Middle click + drag',
 | 
			
		||||
      callback: (e) => e.button === 1 && noModifiersPressed(e),
 | 
			
		||||
    },
 | 
			
		||||
    zoom: {
 | 
			
		||||
      description: 'Scroll wheel',
 | 
			
		||||
      dragCallback: () => false,
 | 
			
		||||
      scrollCallback: () => true,
 | 
			
		||||
    },
 | 
			
		||||
    rotate: {
 | 
			
		||||
      description: 'Middle click + Shift + drag',
 | 
			
		||||
      callback: (e) => e.button === 1 && e.shiftKey,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
@ -39,7 +39,6 @@ class MockEngineCommandManager {
 | 
			
		||||
    if (commandStr === undefined) {
 | 
			
		||||
      throw new Error('commandStr is undefined')
 | 
			
		||||
    }
 | 
			
		||||
    console.log('sendModelingCommandFromWasm', id, rangeStr, commandStr)
 | 
			
		||||
    const command: EngineCommand = JSON.parse(commandStr)
 | 
			
		||||
    const range: SourceRange = JSON.parse(rangeStr)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,27 @@ export function throttle<T>(
 | 
			
		||||
  return throttled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 defferExecution<T>(func: (args: T) => any, wait: number) {
 | 
			
		||||
  let timeout: ReturnType<typeof setTimeout> | null
 | 
			
		||||
  let latestArgs: T
 | 
			
		||||
 | 
			
		||||
  function later() {
 | 
			
		||||
    timeout = null
 | 
			
		||||
    func(latestArgs)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function deffered(args: T) {
 | 
			
		||||
    latestArgs = args
 | 
			
		||||
    if (timeout) {
 | 
			
		||||
      clearTimeout(timeout)
 | 
			
		||||
    }
 | 
			
		||||
    timeout = setTimeout(later, wait)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return deffered
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNormalisedCoordinates({
 | 
			
		||||
  clientX,
 | 
			
		||||
  clientY,
 | 
			
		||||
 | 
			
		||||
@ -118,16 +118,14 @@ async function getUser(context: UserContext) {
 | 
			
		||||
  if (!context.token && '__TAURI__' in window) throw 'not log in'
 | 
			
		||||
  if (context.token) headers['Authorization'] = `Bearer ${context.token}`
 | 
			
		||||
  if (SKIP_AUTH) return LOCAL_USER
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await fetch(url, {
 | 
			
		||||
      method: 'GET',
 | 
			
		||||
      credentials: 'include',
 | 
			
		||||
      headers,
 | 
			
		||||
    })
 | 
			
		||||
    const user = await response.json()
 | 
			
		||||
    if ('error_code' in user) throw new Error(user.message)
 | 
			
		||||
    return user
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e)
 | 
			
		||||
  }
 | 
			
		||||
  const response = await fetch(url, {
 | 
			
		||||
    method: 'GET',
 | 
			
		||||
    credentials: 'include',
 | 
			
		||||
    headers,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const user = await response.json()
 | 
			
		||||
  if ('error_code' in user) throw new Error(user.message)
 | 
			
		||||
 | 
			
		||||
  return user
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,54 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { BaseUnit, baseUnitsUnion } from '../useStore'
 | 
			
		||||
import { CommandBarMeta } from '../lib/commands'
 | 
			
		||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
 | 
			
		||||
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
 | 
			
		||||
 | 
			
		||||
export enum UnitSystem {
 | 
			
		||||
  Imperial = 'imperial',
 | 
			
		||||
  Metric = 'metric',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const baseUnits = {
 | 
			
		||||
  imperial: ['in', 'ft'],
 | 
			
		||||
  metric: ['mm', 'cm', 'm'],
 | 
			
		||||
} as const
 | 
			
		||||
 | 
			
		||||
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
 | 
			
		||||
 | 
			
		||||
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
 | 
			
		||||
 | 
			
		||||
export type Toggle = 'On' | 'Off'
 | 
			
		||||
 | 
			
		||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
 | 
			
		||||
 | 
			
		||||
export const settingsCommandBarMeta: CommandBarMeta = {
 | 
			
		||||
  'Set Theme': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Change the app theme',
 | 
			
		||||
  'Set Base Unit': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Set your default base unit',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'theme',
 | 
			
		||||
        name: 'baseUnit',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        defaultValue: 'theme',
 | 
			
		||||
        options: Object.values(Themes).map((v) => ({ name: v })) as {
 | 
			
		||||
          name: string
 | 
			
		||||
        }[],
 | 
			
		||||
        defaultValue: 'baseUnit',
 | 
			
		||||
        options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Camera Controls': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Set your camera controls',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'cameraControls',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        defaultValue: 'cameraControls',
 | 
			
		||||
        options: Object.values(cameraSystems).map((v) => ({ name: v })),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Default Directory': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Set Default Project Name': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Set a new default project name',
 | 
			
		||||
    hide: 'web',
 | 
			
		||||
@ -37,9 +62,33 @@ export const settingsCommandBarMeta: CommandBarMeta = {
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Default Directory': {
 | 
			
		||||
  'Set Onboarding Status': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Set Text Wrapping': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Set whether text in the editor wraps',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'textWrapping',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        defaultValue: 'textWrapping',
 | 
			
		||||
        options: [{ name: 'On' }, { name: 'Off' }],
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Theme': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Change the app theme',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'theme',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        defaultValue: 'theme',
 | 
			
		||||
        options: Object.values(Themes).map((v): { name: string } => ({
 | 
			
		||||
          name: v,
 | 
			
		||||
        })),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Unit System': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Set your default unit system',
 | 
			
		||||
    args: [
 | 
			
		||||
@ -51,20 +100,6 @@ export const settingsCommandBarMeta: CommandBarMeta = {
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Base Unit': {
 | 
			
		||||
    displayValue: (args: string[]) => 'Set your default base unit',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'baseUnit',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        defaultValue: 'baseUnit',
 | 
			
		||||
        options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Set Onboarding Status': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const settingsMachine = createMachine(
 | 
			
		||||
@ -73,35 +108,34 @@ export const settingsMachine = createMachine(
 | 
			
		||||
    id: 'Settings',
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
    context: {
 | 
			
		||||
      theme: Themes.System,
 | 
			
		||||
      defaultProjectName: '',
 | 
			
		||||
      unitSystem: UnitSystem.Imperial,
 | 
			
		||||
      baseUnit: 'in' as BaseUnit,
 | 
			
		||||
      cameraControls: 'KittyCAD' as CameraSystem,
 | 
			
		||||
      defaultDirectory: '',
 | 
			
		||||
      showDebugPanel: false,
 | 
			
		||||
      defaultProjectName: DEFAULT_PROJECT_NAME,
 | 
			
		||||
      onboardingStatus: '',
 | 
			
		||||
      showDebugPanel: false,
 | 
			
		||||
      textWrapping: 'On' as Toggle,
 | 
			
		||||
      theme: Themes.System,
 | 
			
		||||
      unitSystem: UnitSystem.Imperial,
 | 
			
		||||
    },
 | 
			
		||||
    initial: 'idle',
 | 
			
		||||
    states: {
 | 
			
		||||
      idle: {
 | 
			
		||||
        entry: ['setThemeClass'],
 | 
			
		||||
        on: {
 | 
			
		||||
          'Set Theme': {
 | 
			
		||||
          'Set Base Unit': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                theme: (_, event) => event.data.theme,
 | 
			
		||||
              }),
 | 
			
		||||
              assign({ baseUnit: (_, event) => event.data.baseUnit }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
              'setThemeClass',
 | 
			
		||||
            ],
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
          'Set Default Project Name': {
 | 
			
		||||
          'Set Camera Controls': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                defaultProjectName: (_, event) => event.data.defaultProjectName,
 | 
			
		||||
                cameraControls: (_, event) => event.data.cameraControls,
 | 
			
		||||
              }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
@ -120,12 +154,11 @@ export const settingsMachine = createMachine(
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
          'Set Unit System': {
 | 
			
		||||
          'Set Default Project Name': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                unitSystem: (_, event) => event.data.unitSystem,
 | 
			
		||||
                baseUnit: (_, event) =>
 | 
			
		||||
                  event.data.unitSystem === 'imperial' ? 'in' : 'mm',
 | 
			
		||||
                defaultProjectName: (_, event) =>
 | 
			
		||||
                  event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
 | 
			
		||||
              }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
@ -133,9 +166,46 @@ export const settingsMachine = createMachine(
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
          'Set Base Unit': {
 | 
			
		||||
          'Set Onboarding Status': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({ baseUnit: (_, event) => event.data.baseUnit }),
 | 
			
		||||
              assign({
 | 
			
		||||
                onboardingStatus: (_, event) => event.data.onboardingStatus,
 | 
			
		||||
              }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
            ],
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
          'Set Text Wrapping': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                textWrapping: (_, event) => event.data.textWrapping,
 | 
			
		||||
              }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
            ],
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
          'Set Theme': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                theme: (_, event) => event.data.theme,
 | 
			
		||||
              }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
              'setThemeClass',
 | 
			
		||||
            ],
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
          'Set Unit System': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                unitSystem: (_, event) => event.data.unitSystem,
 | 
			
		||||
                baseUnit: (_, event) =>
 | 
			
		||||
                  event.data.unitSystem === 'imperial' ? 'in' : 'mm',
 | 
			
		||||
              }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
              'toastSuccess',
 | 
			
		||||
            ],
 | 
			
		||||
@ -155,34 +225,29 @@ export const settingsMachine = createMachine(
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
          'Set Onboarding Status': {
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                onboardingStatus: (_, event) => event.data.onboardingStatus,
 | 
			
		||||
              }),
 | 
			
		||||
              'persistSettings',
 | 
			
		||||
            ],
 | 
			
		||||
            target: 'idle',
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as
 | 
			
		||||
        | { type: 'Set Theme'; data: { theme: Themes } }
 | 
			
		||||
        | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Set Camera Controls'
 | 
			
		||||
            data: { cameraControls: CameraSystem }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Set Default Directory'; data: { defaultDirectory: string } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Set Default Project Name'
 | 
			
		||||
            data: { defaultProjectName: string }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Set Default Directory'; data: { defaultDirectory: string } }
 | 
			
		||||
        | { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
 | 
			
		||||
        | { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
 | 
			
		||||
        | { type: 'Set Theme'; data: { theme: Themes } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Set Unit System'
 | 
			
		||||
            data: { unitSystem: UnitSystem }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
 | 
			
		||||
        | { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
 | 
			
		||||
        | { type: 'Toggle Debug Panel' },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -15,25 +15,31 @@ export interface Typegen0 {
 | 
			
		||||
  eventsCausingActions: {
 | 
			
		||||
    persistSettings:
 | 
			
		||||
      | 'Set Base Unit'
 | 
			
		||||
      | 'Set Camera Controls'
 | 
			
		||||
      | 'Set Default Directory'
 | 
			
		||||
      | 'Set Default Project Name'
 | 
			
		||||
      | 'Set Onboarding Status'
 | 
			
		||||
      | 'Set Text Wrapping'
 | 
			
		||||
      | 'Set Theme'
 | 
			
		||||
      | 'Set Unit System'
 | 
			
		||||
      | 'Toggle Debug Panel'
 | 
			
		||||
    setThemeClass:
 | 
			
		||||
      | 'Set Base Unit'
 | 
			
		||||
      | 'Set Camera Controls'
 | 
			
		||||
      | 'Set Default Directory'
 | 
			
		||||
      | 'Set Default Project Name'
 | 
			
		||||
      | 'Set Onboarding Status'
 | 
			
		||||
      | 'Set Text Wrapping'
 | 
			
		||||
      | 'Set Theme'
 | 
			
		||||
      | 'Set Unit System'
 | 
			
		||||
      | 'Toggle Debug Panel'
 | 
			
		||||
      | 'xstate.init'
 | 
			
		||||
    toastSuccess:
 | 
			
		||||
      | 'Set Base Unit'
 | 
			
		||||
      | 'Set Camera Controls'
 | 
			
		||||
      | 'Set Default Directory'
 | 
			
		||||
      | 'Set Default Project Name'
 | 
			
		||||
      | 'Set Text Wrapping'
 | 
			
		||||
      | 'Set Theme'
 | 
			
		||||
      | 'Set Unit System'
 | 
			
		||||
      | 'Toggle Debug Panel'
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ import {
 | 
			
		||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
 | 
			
		||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
 | 
			
		||||
 | 
			
		||||
// This route only opens in the Tauri desktop context for now,
 | 
			
		||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
 | 
			
		||||
@ -38,6 +39,7 @@ const Home = () => {
 | 
			
		||||
  const {
 | 
			
		||||
    settings: {
 | 
			
		||||
      context: { defaultDirectory, defaultProjectName },
 | 
			
		||||
      send: sendToSettings,
 | 
			
		||||
    },
 | 
			
		||||
  } = useGlobalStateContext()
 | 
			
		||||
 | 
			
		||||
@ -71,16 +73,33 @@ const Home = () => {
 | 
			
		||||
        context: ContextFrom<typeof homeMachine>,
 | 
			
		||||
        event: EventFrom<typeof homeMachine, 'Create project'>
 | 
			
		||||
      ) => {
 | 
			
		||||
        let name =
 | 
			
		||||
        let name = (
 | 
			
		||||
          event.data && 'name' in event.data
 | 
			
		||||
            ? event.data.name
 | 
			
		||||
            : defaultProjectName
 | 
			
		||||
        ).trim()
 | 
			
		||||
        let shouldUpdateDefaultProjectName = false
 | 
			
		||||
 | 
			
		||||
        // If there is no default project name, flag it to be set to the default
 | 
			
		||||
        if (!name) {
 | 
			
		||||
          name = DEFAULT_PROJECT_NAME
 | 
			
		||||
          shouldUpdateDefaultProjectName = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (doesProjectNameNeedInterpolated(name)) {
 | 
			
		||||
          const nextIndex = await getNextProjectIndex(name, projects)
 | 
			
		||||
          name = interpolateProjectNameWithIndex(name, nextIndex)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await createNewProject(context.defaultDirectory + '/' + name)
 | 
			
		||||
 | 
			
		||||
        if (shouldUpdateDefaultProjectName) {
 | 
			
		||||
          sendToSettings({
 | 
			
		||||
            type: 'Set Default Project Name',
 | 
			
		||||
            data: { defaultProjectName: DEFAULT_PROJECT_NAME },
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return `Successfully created "${name}"`
 | 
			
		||||
      },
 | 
			
		||||
      renameProject: async (
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.'
 | 
			
		||||
import { useStore } from '../../useStore'
 | 
			
		||||
 | 
			
		||||
export default function Units() {
 | 
			
		||||
  const { isMouseDownInStream } = useStore((s) => ({
 | 
			
		||||
    isMouseDownInStream: s.isMouseDownInStream,
 | 
			
		||||
  const { buttonDownInStream } = useStore((s) => ({
 | 
			
		||||
    buttonDownInStream: s.buttonDownInStream,
 | 
			
		||||
  }))
 | 
			
		||||
  const dismiss = useDismiss()
 | 
			
		||||
  const next = useNextClick(onboardingPaths.SKETCHING)
 | 
			
		||||
@ -15,7 +15,7 @@ export default function Units() {
 | 
			
		||||
      <div
 | 
			
		||||
        className={
 | 
			
		||||
          'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' +
 | 
			
		||||
          (isMouseDownInStream ? '' : ' pointer-events-auto')
 | 
			
		||||
          (buttonDownInStream ? '' : ' pointer-events-auto')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <h1 className="text-2xl font-bold">Camera</h1>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { BaseUnit, baseUnits } from '../../useStore'
 | 
			
		||||
import { BaseUnit, baseUnits } from '../../machines/settingsMachine'
 | 
			
		||||
import { ActionButton } from '../../components/ActionButton'
 | 
			
		||||
import { SettingsSection } from '../Settings'
 | 
			
		||||
import { Toggle } from '../../components/Toggle/Toggle'
 | 
			
		||||
 | 
			
		||||
@ -6,13 +6,22 @@ import {
 | 
			
		||||
import { ActionButton } from '../components/ActionButton'
 | 
			
		||||
import { AppHeader } from '../components/AppHeader'
 | 
			
		||||
import { open } from '@tauri-apps/api/dialog'
 | 
			
		||||
import { BaseUnit, baseUnits } from '../useStore'
 | 
			
		||||
import {
 | 
			
		||||
  BaseUnit,
 | 
			
		||||
  DEFAULT_PROJECT_NAME,
 | 
			
		||||
  baseUnits,
 | 
			
		||||
} from '../machines/settingsMachine'
 | 
			
		||||
import { Toggle } from '../components/Toggle/Toggle'
 | 
			
		||||
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import { IndexLoaderData, paths } from '../Router'
 | 
			
		||||
import { Themes } from '../lib/theme'
 | 
			
		||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
 | 
			
		||||
import {
 | 
			
		||||
  CameraSystem,
 | 
			
		||||
  cameraSystems,
 | 
			
		||||
  cameraMouseDragGuards,
 | 
			
		||||
} from 'lib/cameraControls'
 | 
			
		||||
import { UnitSystem } from 'machines/settingsMachine'
 | 
			
		||||
 | 
			
		||||
export const Settings = () => {
 | 
			
		||||
@ -25,12 +34,13 @@ export const Settings = () => {
 | 
			
		||||
      send,
 | 
			
		||||
      state: {
 | 
			
		||||
        context: {
 | 
			
		||||
          baseUnit,
 | 
			
		||||
          cameraControls,
 | 
			
		||||
          defaultDirectory,
 | 
			
		||||
          defaultProjectName,
 | 
			
		||||
          showDebugPanel,
 | 
			
		||||
          defaultDirectory,
 | 
			
		||||
          unitSystem,
 | 
			
		||||
          baseUnit,
 | 
			
		||||
          theme,
 | 
			
		||||
          unitSystem,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@ -82,6 +92,42 @@ export const Settings = () => {
 | 
			
		||||
          , and start a discussion if you don't see it! Your feedback will help
 | 
			
		||||
          us prioritize what to build next.
 | 
			
		||||
        </p>
 | 
			
		||||
        <SettingsSection
 | 
			
		||||
          title="Camera Controls"
 | 
			
		||||
          description="How you want to control the camera in the 3D view"
 | 
			
		||||
        >
 | 
			
		||||
          <select
 | 
			
		||||
            id="camera-controls"
 | 
			
		||||
            className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
 | 
			
		||||
            value={cameraControls}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              send({
 | 
			
		||||
                type: 'Set Camera Controls',
 | 
			
		||||
                data: { cameraControls: e.target.value as CameraSystem },
 | 
			
		||||
              })
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {cameraSystems.map((program) => (
 | 
			
		||||
              <option key={program} value={program}>
 | 
			
		||||
                {program}
 | 
			
		||||
              </option>
 | 
			
		||||
            ))}
 | 
			
		||||
          </select>
 | 
			
		||||
          <ul className="text-sm my-2 mx-4 leading-relaxed">
 | 
			
		||||
            <li>
 | 
			
		||||
              <strong>Pan:</strong>{' '}
 | 
			
		||||
              {cameraMouseDragGuards[cameraControls].pan.description}
 | 
			
		||||
            </li>
 | 
			
		||||
            <li>
 | 
			
		||||
              <strong>Zoom:</strong>{' '}
 | 
			
		||||
              {cameraMouseDragGuards[cameraControls].zoom.description}
 | 
			
		||||
            </li>
 | 
			
		||||
            <li>
 | 
			
		||||
              <strong>Rotate:</strong>{' '}
 | 
			
		||||
              {cameraMouseDragGuards[cameraControls].rotate.description}
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </SettingsSection>
 | 
			
		||||
        {(window as any).__TAURI__ && (
 | 
			
		||||
          <>
 | 
			
		||||
            <SettingsSection
 | 
			
		||||
@ -118,10 +164,14 @@ export const Settings = () => {
 | 
			
		||||
                className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
 | 
			
		||||
                defaultValue={defaultProjectName}
 | 
			
		||||
                onBlur={(e) => {
 | 
			
		||||
                  const newValue = e.target.value.trim() || DEFAULT_PROJECT_NAME
 | 
			
		||||
                  send({
 | 
			
		||||
                    type: 'Set Default Project Name',
 | 
			
		||||
                    data: { defaultProjectName: e.target.value },
 | 
			
		||||
                    data: {
 | 
			
		||||
                      defaultProjectName: newValue,
 | 
			
		||||
                    },
 | 
			
		||||
                  })
 | 
			
		||||
                  e.target.value = newValue
 | 
			
		||||
                }}
 | 
			
		||||
                autoCapitalize="off"
 | 
			
		||||
                autoComplete="off"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										396
									
								
								src/useStore.ts
									
									
									
									
									
								
							
							
						
						
									
										396
									
								
								src/useStore.ts
									
									
									
									
									
								
							@ -19,6 +19,7 @@ import {
 | 
			
		||||
  EngineCommandManager,
 | 
			
		||||
} from './lang/std/engineConnection'
 | 
			
		||||
import { KCLError } from './lang/errors'
 | 
			
		||||
import { defferExecution } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
export type Selection = {
 | 
			
		||||
  type: 'default' | 'line-end' | 'line-mid'
 | 
			
		||||
@ -94,16 +95,13 @@ export type GuiModes =
 | 
			
		||||
      position: Position
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export const baseUnits = {
 | 
			
		||||
  imperial: ['in', 'ft'],
 | 
			
		||||
  metric: ['mm', 'cm', 'm'],
 | 
			
		||||
} as const
 | 
			
		||||
 | 
			
		||||
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
 | 
			
		||||
 | 
			
		||||
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
 | 
			
		||||
 | 
			
		||||
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
 | 
			
		||||
export type PaneType =
 | 
			
		||||
  | 'code'
 | 
			
		||||
  | 'variables'
 | 
			
		||||
  | 'debug'
 | 
			
		||||
  | 'kclErrors'
 | 
			
		||||
  | 'logs'
 | 
			
		||||
  | 'lspMessages'
 | 
			
		||||
 | 
			
		||||
export interface StoreState {
 | 
			
		||||
  editorView: EditorView | null
 | 
			
		||||
@ -135,7 +133,9 @@ export interface StoreState {
 | 
			
		||||
  ) => void
 | 
			
		||||
  updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
 | 
			
		||||
  code: string
 | 
			
		||||
  defferedCode: string
 | 
			
		||||
  setCode: (code: string) => void
 | 
			
		||||
  defferedSetCode: (code: string) => void
 | 
			
		||||
  formatCode: () => void
 | 
			
		||||
  errorState: {
 | 
			
		||||
    isError: boolean
 | 
			
		||||
@ -158,12 +158,12 @@ export interface StoreState {
 | 
			
		||||
  setMediaStream: (mediaStream: MediaStream) => void
 | 
			
		||||
  isStreamReady: boolean
 | 
			
		||||
  setIsStreamReady: (isStreamReady: boolean) => void
 | 
			
		||||
  isMouseDownInStream: boolean
 | 
			
		||||
  setIsMouseDownInStream: (isMouseDownInStream: boolean) => void
 | 
			
		||||
  isLSPServerReady: boolean
 | 
			
		||||
  setIsLSPServerReady: (isLSPServerReady: boolean) => void
 | 
			
		||||
  buttonDownInStream: number | undefined
 | 
			
		||||
  setButtonDownInStream: (buttonDownInStream: number | undefined) => void
 | 
			
		||||
  didDragInStream: boolean
 | 
			
		||||
  setDidDragInStream: (didDragInStream: boolean) => void
 | 
			
		||||
  cmdId?: string
 | 
			
		||||
  setCmdId: (cmdId: string) => void
 | 
			
		||||
  fileId: string
 | 
			
		||||
  setFileId: (fileId: string) => void
 | 
			
		||||
  streamDimensions: { streamWidth: number; streamHeight: number }
 | 
			
		||||
@ -171,6 +171,8 @@ export interface StoreState {
 | 
			
		||||
    streamWidth: number
 | 
			
		||||
    streamHeight: number
 | 
			
		||||
  }) => void
 | 
			
		||||
  isExecuting: boolean
 | 
			
		||||
  setIsExecuting: (isExecuting: boolean) => void
 | 
			
		||||
 | 
			
		||||
  showHomeMenu: boolean
 | 
			
		||||
  setHomeShowMenu: (showMenu: boolean) => void
 | 
			
		||||
@ -189,193 +191,207 @@ let pendingAstUpdates: number[] = []
 | 
			
		||||
 | 
			
		||||
export const useStore = create<StoreState>()(
 | 
			
		||||
  persist(
 | 
			
		||||
    (set, get) => ({
 | 
			
		||||
      editorView: null,
 | 
			
		||||
      setEditorView: (editorView) => {
 | 
			
		||||
        set({ editorView })
 | 
			
		||||
      },
 | 
			
		||||
      highlightRange: [0, 0],
 | 
			
		||||
      setHighlightRange: (selection) => {
 | 
			
		||||
        set({ highlightRange: selection })
 | 
			
		||||
        const editorView = get().editorView
 | 
			
		||||
        if (editorView) {
 | 
			
		||||
          editorView.dispatch({ effects: addLineHighlight.of(selection) })
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      setCursor: (selections) => {
 | 
			
		||||
        const { editorView } = get()
 | 
			
		||||
        if (!editorView) return
 | 
			
		||||
        const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
 | 
			
		||||
        const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
 | 
			
		||||
        set({ selectionRangeTypeMap })
 | 
			
		||||
        selections.codeBasedSelections.forEach(({ range, type }) => {
 | 
			
		||||
          if (range?.[1]) {
 | 
			
		||||
            ranges.push(EditorSelection.cursor(range[1]))
 | 
			
		||||
            selectionRangeTypeMap[range[1]] = type
 | 
			
		||||
    (set, get) => {
 | 
			
		||||
      const setDefferedCode = defferExecution(
 | 
			
		||||
        (code: string) => set({ defferedCode: code }),
 | 
			
		||||
        600
 | 
			
		||||
      )
 | 
			
		||||
      return {
 | 
			
		||||
        editorView: null,
 | 
			
		||||
        setEditorView: (editorView) => {
 | 
			
		||||
          set({ editorView })
 | 
			
		||||
        },
 | 
			
		||||
        highlightRange: [0, 0],
 | 
			
		||||
        setHighlightRange: (selection) => {
 | 
			
		||||
          set({ highlightRange: selection })
 | 
			
		||||
          const editorView = get().editorView
 | 
			
		||||
          if (editorView) {
 | 
			
		||||
            editorView.dispatch({ effects: addLineHighlight.of(selection) })
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          editorView.dispatch({
 | 
			
		||||
            selection: EditorSelection.create(
 | 
			
		||||
              ranges,
 | 
			
		||||
              selections.codeBasedSelections.length - 1
 | 
			
		||||
            ),
 | 
			
		||||
        },
 | 
			
		||||
        setCursor: (selections) => {
 | 
			
		||||
          const { editorView } = get()
 | 
			
		||||
          if (!editorView) return
 | 
			
		||||
          const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
 | 
			
		||||
          const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
 | 
			
		||||
          set({ selectionRangeTypeMap })
 | 
			
		||||
          selections.codeBasedSelections.forEach(({ range, type }) => {
 | 
			
		||||
            if (range?.[1]) {
 | 
			
		||||
              ranges.push(EditorSelection.cursor(range[1]))
 | 
			
		||||
              selectionRangeTypeMap[range[1]] = type
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
      setCursor2: (codeSelections) => {
 | 
			
		||||
        const currestSelections = get().selectionRanges
 | 
			
		||||
        const code = get().code
 | 
			
		||||
        if (!codeSelections) {
 | 
			
		||||
          get().setCursor({
 | 
			
		||||
            otherSelections: currestSelections.otherSelections,
 | 
			
		||||
            codeBasedSelections: [
 | 
			
		||||
              { range: [0, code.length - 1], type: 'default' },
 | 
			
		||||
            ],
 | 
			
		||||
          })
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        const selections: Selections = {
 | 
			
		||||
          ...currestSelections,
 | 
			
		||||
          codeBasedSelections: get().isShiftDown
 | 
			
		||||
            ? [...currestSelections.codeBasedSelections, codeSelections]
 | 
			
		||||
            : [codeSelections],
 | 
			
		||||
        }
 | 
			
		||||
        get().setCursor(selections)
 | 
			
		||||
      },
 | 
			
		||||
      selectionRangeTypeMap: {},
 | 
			
		||||
      selectionRanges: {
 | 
			
		||||
        otherSelections: [],
 | 
			
		||||
        codeBasedSelections: [],
 | 
			
		||||
      },
 | 
			
		||||
      setSelectionRanges: (selectionRanges) =>
 | 
			
		||||
        set({ selectionRanges, selectionRangeTypeMap: {} }),
 | 
			
		||||
      guiMode: { mode: 'default' },
 | 
			
		||||
      lastGuiMode: { mode: 'default' },
 | 
			
		||||
      setGuiMode: (guiMode) => {
 | 
			
		||||
        set({ guiMode })
 | 
			
		||||
      },
 | 
			
		||||
      logs: [],
 | 
			
		||||
      addLog: (log) => {
 | 
			
		||||
        if (Array.isArray(log)) {
 | 
			
		||||
          const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
 | 
			
		||||
          set((state) => ({ logs: [...state.logs, cleanLog] }))
 | 
			
		||||
        } else {
 | 
			
		||||
          set((state) => ({ logs: [...state.logs, log] }))
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      resetLogs: () => {
 | 
			
		||||
        set({ logs: [] })
 | 
			
		||||
      },
 | 
			
		||||
      kclErrors: [],
 | 
			
		||||
      addKCLError: (e) => {
 | 
			
		||||
        set((state) => ({ kclErrors: [...state.kclErrors, e] }))
 | 
			
		||||
      },
 | 
			
		||||
      resetKCLErrors: () => {
 | 
			
		||||
        set({ kclErrors: [] })
 | 
			
		||||
      },
 | 
			
		||||
      ast: null,
 | 
			
		||||
      setAst: (ast) => {
 | 
			
		||||
        set({ ast })
 | 
			
		||||
      },
 | 
			
		||||
      updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
 | 
			
		||||
        const newCode = recast(ast)
 | 
			
		||||
        const astWithUpdatedSource = parser_wasm(newCode)
 | 
			
		||||
        callBack(astWithUpdatedSource)
 | 
			
		||||
 | 
			
		||||
        set({ ast: astWithUpdatedSource, code: newCode })
 | 
			
		||||
        if (focusPath) {
 | 
			
		||||
          const { node } = getNodeFromPath<any>(astWithUpdatedSource, focusPath)
 | 
			
		||||
          const { start, end } = node
 | 
			
		||||
          if (!start || !end) return
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            get().setCursor({
 | 
			
		||||
              codeBasedSelections: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'default',
 | 
			
		||||
                  range: [start, end],
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
              otherSelections: [],
 | 
			
		||||
            editorView.dispatch({
 | 
			
		||||
              selection: EditorSelection.create(
 | 
			
		||||
                ranges,
 | 
			
		||||
                selections.codeBasedSelections.length - 1
 | 
			
		||||
              ),
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      updateAstAsync: async (ast, focusPath) => {
 | 
			
		||||
        // clear any pending updates
 | 
			
		||||
        pendingAstUpdates.forEach((id) => clearTimeout(id))
 | 
			
		||||
        pendingAstUpdates = []
 | 
			
		||||
        // setup a new update
 | 
			
		||||
        pendingAstUpdates.push(
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            get().updateAst(ast, { focusPath })
 | 
			
		||||
          }, 100) as unknown as number
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      code: '',
 | 
			
		||||
      setCode: (code) => {
 | 
			
		||||
        set({ code })
 | 
			
		||||
      },
 | 
			
		||||
      formatCode: async () => {
 | 
			
		||||
        const code = get().code
 | 
			
		||||
        const ast = parser_wasm(code)
 | 
			
		||||
        const newCode = recast(ast)
 | 
			
		||||
        set({ code: newCode, ast })
 | 
			
		||||
      },
 | 
			
		||||
      errorState: {
 | 
			
		||||
        isError: false,
 | 
			
		||||
        error: '',
 | 
			
		||||
      },
 | 
			
		||||
      setError: (error = '') => {
 | 
			
		||||
        set({ errorState: { isError: !!error, error } })
 | 
			
		||||
      },
 | 
			
		||||
      programMemory: { root: {}, pendingMemory: {} },
 | 
			
		||||
      setProgramMemory: (programMemory) => set({ programMemory }),
 | 
			
		||||
      isShiftDown: false,
 | 
			
		||||
      setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
 | 
			
		||||
      artifactMap: {},
 | 
			
		||||
      sourceRangeMap: {},
 | 
			
		||||
      setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
 | 
			
		||||
      setEngineCommandManager: (engineCommandManager) =>
 | 
			
		||||
        set({ engineCommandManager }),
 | 
			
		||||
      setMediaStream: (mediaStream) => set({ mediaStream }),
 | 
			
		||||
      isStreamReady: false,
 | 
			
		||||
      setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
 | 
			
		||||
      isMouseDownInStream: false,
 | 
			
		||||
      setIsMouseDownInStream: (isMouseDownInStream) => {
 | 
			
		||||
        set({ isMouseDownInStream })
 | 
			
		||||
      },
 | 
			
		||||
      didDragInStream: false,
 | 
			
		||||
      setDidDragInStream: (didDragInStream) => {
 | 
			
		||||
        set({ didDragInStream })
 | 
			
		||||
      },
 | 
			
		||||
      // For stream event handling
 | 
			
		||||
      cmdId: undefined,
 | 
			
		||||
      setCmdId: (cmdId) => set({ cmdId }),
 | 
			
		||||
      fileId: '',
 | 
			
		||||
      setFileId: (fileId) => set({ fileId }),
 | 
			
		||||
      streamDimensions: { streamWidth: 1280, streamHeight: 720 },
 | 
			
		||||
      setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
 | 
			
		||||
        },
 | 
			
		||||
        setCursor2: (codeSelections) => {
 | 
			
		||||
          const currestSelections = get().selectionRanges
 | 
			
		||||
          const code = get().code
 | 
			
		||||
          if (!codeSelections) {
 | 
			
		||||
            get().setCursor({
 | 
			
		||||
              otherSelections: currestSelections.otherSelections,
 | 
			
		||||
              codeBasedSelections: [
 | 
			
		||||
                { range: [0, code.length - 1], type: 'default' },
 | 
			
		||||
              ],
 | 
			
		||||
            })
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
          const selections: Selections = {
 | 
			
		||||
            ...currestSelections,
 | 
			
		||||
            codeBasedSelections: get().isShiftDown
 | 
			
		||||
              ? [...currestSelections.codeBasedSelections, codeSelections]
 | 
			
		||||
              : [codeSelections],
 | 
			
		||||
          }
 | 
			
		||||
          get().setCursor(selections)
 | 
			
		||||
        },
 | 
			
		||||
        selectionRangeTypeMap: {},
 | 
			
		||||
        selectionRanges: {
 | 
			
		||||
          otherSelections: [],
 | 
			
		||||
          codeBasedSelections: [],
 | 
			
		||||
        },
 | 
			
		||||
        setSelectionRanges: (selectionRanges) =>
 | 
			
		||||
          set({ selectionRanges, selectionRangeTypeMap: {} }),
 | 
			
		||||
        guiMode: { mode: 'default' },
 | 
			
		||||
        lastGuiMode: { mode: 'default' },
 | 
			
		||||
        setGuiMode: (guiMode) => {
 | 
			
		||||
          set({ guiMode })
 | 
			
		||||
        },
 | 
			
		||||
        logs: [],
 | 
			
		||||
        addLog: (log) => {
 | 
			
		||||
          if (Array.isArray(log)) {
 | 
			
		||||
            const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
 | 
			
		||||
            set((state) => ({ logs: [...state.logs, cleanLog] }))
 | 
			
		||||
          } else {
 | 
			
		||||
            set((state) => ({ logs: [...state.logs, log] }))
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        resetLogs: () => {
 | 
			
		||||
          set({ logs: [] })
 | 
			
		||||
        },
 | 
			
		||||
        kclErrors: [],
 | 
			
		||||
        addKCLError: (e) => {
 | 
			
		||||
          set((state) => ({ kclErrors: [...state.kclErrors, e] }))
 | 
			
		||||
        },
 | 
			
		||||
        resetKCLErrors: () => {
 | 
			
		||||
          set({ kclErrors: [] })
 | 
			
		||||
        },
 | 
			
		||||
        ast: null,
 | 
			
		||||
        setAst: (ast) => {
 | 
			
		||||
          set({ ast })
 | 
			
		||||
        },
 | 
			
		||||
        updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
 | 
			
		||||
          const newCode = recast(ast)
 | 
			
		||||
          const astWithUpdatedSource = parser_wasm(newCode)
 | 
			
		||||
          callBack(astWithUpdatedSource)
 | 
			
		||||
 | 
			
		||||
      // tauri specific app settings
 | 
			
		||||
      defaultDir: {
 | 
			
		||||
        dir: '',
 | 
			
		||||
      },
 | 
			
		||||
      isBannerDismissed: false,
 | 
			
		||||
      setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
 | 
			
		||||
      openPanes: ['code'],
 | 
			
		||||
      setOpenPanes: (openPanes) => set({ openPanes }),
 | 
			
		||||
      showHomeMenu: true,
 | 
			
		||||
      setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
 | 
			
		||||
      homeMenuItems: [],
 | 
			
		||||
      setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
 | 
			
		||||
    }),
 | 
			
		||||
          set({ ast: astWithUpdatedSource, code: newCode })
 | 
			
		||||
          if (focusPath) {
 | 
			
		||||
            const { node } = getNodeFromPath<any>(
 | 
			
		||||
              astWithUpdatedSource,
 | 
			
		||||
              focusPath
 | 
			
		||||
            )
 | 
			
		||||
            const { start, end } = node
 | 
			
		||||
            if (!start || !end) return
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              get().setCursor({
 | 
			
		||||
                codeBasedSelections: [
 | 
			
		||||
                  {
 | 
			
		||||
                    type: 'default',
 | 
			
		||||
                    range: [start, end],
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
                otherSelections: [],
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        updateAstAsync: async (ast, focusPath) => {
 | 
			
		||||
          // clear any pending updates
 | 
			
		||||
          pendingAstUpdates.forEach((id) => clearTimeout(id))
 | 
			
		||||
          pendingAstUpdates = []
 | 
			
		||||
          // setup a new update
 | 
			
		||||
          pendingAstUpdates.push(
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              get().updateAst(ast, { focusPath })
 | 
			
		||||
            }, 100) as unknown as number
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
        code: '',
 | 
			
		||||
        defferedCode: '',
 | 
			
		||||
        setCode: (code) => set({ code, defferedCode: code }),
 | 
			
		||||
        defferedSetCode: (code) => {
 | 
			
		||||
          set({ code })
 | 
			
		||||
          setDefferedCode(code)
 | 
			
		||||
        },
 | 
			
		||||
        formatCode: async () => {
 | 
			
		||||
          const code = get().code
 | 
			
		||||
          const ast = parser_wasm(code)
 | 
			
		||||
          const newCode = recast(ast)
 | 
			
		||||
          set({ code: newCode, ast })
 | 
			
		||||
        },
 | 
			
		||||
        errorState: {
 | 
			
		||||
          isError: false,
 | 
			
		||||
          error: '',
 | 
			
		||||
        },
 | 
			
		||||
        setError: (error = '') => {
 | 
			
		||||
          set({ errorState: { isError: !!error, error } })
 | 
			
		||||
        },
 | 
			
		||||
        programMemory: { root: {}, pendingMemory: {} },
 | 
			
		||||
        setProgramMemory: (programMemory) => set({ programMemory }),
 | 
			
		||||
        isShiftDown: false,
 | 
			
		||||
        setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
 | 
			
		||||
        artifactMap: {},
 | 
			
		||||
        sourceRangeMap: {},
 | 
			
		||||
        setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
 | 
			
		||||
        setEngineCommandManager: (engineCommandManager) =>
 | 
			
		||||
          set({ engineCommandManager }),
 | 
			
		||||
        setMediaStream: (mediaStream) => set({ mediaStream }),
 | 
			
		||||
        isStreamReady: false,
 | 
			
		||||
        setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
 | 
			
		||||
        isLSPServerReady: false,
 | 
			
		||||
        setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
 | 
			
		||||
        buttonDownInStream: undefined,
 | 
			
		||||
        setButtonDownInStream: (buttonDownInStream) => {
 | 
			
		||||
          set({ buttonDownInStream })
 | 
			
		||||
        },
 | 
			
		||||
        didDragInStream: false,
 | 
			
		||||
        setDidDragInStream: (didDragInStream) => {
 | 
			
		||||
          set({ didDragInStream })
 | 
			
		||||
        },
 | 
			
		||||
        // For stream event handling
 | 
			
		||||
        fileId: '',
 | 
			
		||||
        setFileId: (fileId) => set({ fileId }),
 | 
			
		||||
        streamDimensions: { streamWidth: 1280, streamHeight: 720 },
 | 
			
		||||
        setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
 | 
			
		||||
        isExecuting: false,
 | 
			
		||||
        setIsExecuting: (isExecuting) => set({ isExecuting }),
 | 
			
		||||
 | 
			
		||||
        // tauri specific app settings
 | 
			
		||||
        defaultDir: {
 | 
			
		||||
          dir: '',
 | 
			
		||||
        },
 | 
			
		||||
        isBannerDismissed: false,
 | 
			
		||||
        setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
 | 
			
		||||
        openPanes: ['code'],
 | 
			
		||||
        setOpenPanes: (openPanes) => set({ openPanes }),
 | 
			
		||||
        showHomeMenu: true,
 | 
			
		||||
        setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
 | 
			
		||||
        homeMenuItems: [],
 | 
			
		||||
        setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'store',
 | 
			
		||||
      partialize: (state) =>
 | 
			
		||||
        Object.fromEntries(
 | 
			
		||||
          Object.entries(state).filter(([key]) =>
 | 
			
		||||
            ['code', 'openPanes'].includes(key)
 | 
			
		||||
            ['code', 'defferedCode', 'openPanes'].includes(key)
 | 
			
		||||
          )
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										319
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										319
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -63,6 +63,54 @@ dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "anstream"
 | 
			
		||||
version = "0.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anstyle",
 | 
			
		||||
 "anstyle-parse",
 | 
			
		||||
 "anstyle-query",
 | 
			
		||||
 "anstyle-wincon",
 | 
			
		||||
 "colorchoice",
 | 
			
		||||
 "utf8parse",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "anstyle"
 | 
			
		||||
version = "1.0.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "anstyle-parse"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "utf8parse",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "anstyle-query"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "anstyle-wincon"
 | 
			
		||||
version = "2.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anstyle",
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "anyhow"
 | 
			
		||||
version = "1.0.75"
 | 
			
		||||
@ -78,6 +126,22 @@ version = "1.6.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "async-codec-lite"
 | 
			
		||||
version = "0.0.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2527c30e3972d8ff366b353125dae828c4252a154dbe6063684f6c5e014760a3"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "futures-io",
 | 
			
		||||
 "futures-sink",
 | 
			
		||||
 "log",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "async-trait"
 | 
			
		||||
version = "0.1.73"
 | 
			
		||||
@ -100,6 +164,18 @@ dependencies = [
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "auto_impl"
 | 
			
		||||
version = "1.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro-error",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 1.0.109",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "autocfg"
 | 
			
		||||
version = "1.1.0"
 | 
			
		||||
@ -268,8 +344,8 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "atty",
 | 
			
		||||
 "bitflags 1.3.2",
 | 
			
		||||
 "clap_derive",
 | 
			
		||||
 "clap_lex",
 | 
			
		||||
 "clap_derive 3.2.25",
 | 
			
		||||
 "clap_lex 0.2.4",
 | 
			
		||||
 "indexmap 1.9.3",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "strsim",
 | 
			
		||||
@ -278,6 +354,30 @@ dependencies = [
 | 
			
		||||
 "unicase",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap"
 | 
			
		||||
version = "4.4.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "clap_builder",
 | 
			
		||||
 "clap_derive 4.4.2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap_builder"
 | 
			
		||||
version = "4.4.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anstream",
 | 
			
		||||
 "anstyle",
 | 
			
		||||
 "clap_lex 0.5.1",
 | 
			
		||||
 "strsim",
 | 
			
		||||
 "unicase",
 | 
			
		||||
 "unicode-width",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap_derive"
 | 
			
		||||
version = "3.2.25"
 | 
			
		||||
@ -291,6 +391,18 @@ dependencies = [
 | 
			
		||||
 "syn 1.0.109",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap_derive"
 | 
			
		||||
version = "4.4.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "heck",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap_lex"
 | 
			
		||||
version = "0.2.4"
 | 
			
		||||
@ -300,6 +412,18 @@ dependencies = [
 | 
			
		||||
 "os_str_bytes",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "clap_lex"
 | 
			
		||||
version = "0.5.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "colorchoice"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "colored"
 | 
			
		||||
version = "2.0.4"
 | 
			
		||||
@ -387,6 +511,19 @@ dependencies = [
 | 
			
		||||
 "typenum",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dashmap"
 | 
			
		||||
version = "5.5.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "hashbrown 0.14.0",
 | 
			
		||||
 "lock_api",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "parking_lot_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "data-encoding"
 | 
			
		||||
version = "2.4.0"
 | 
			
		||||
@ -401,7 +538,7 @@ checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "derive-docs"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
version = "0.1.3"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "convert_case",
 | 
			
		||||
 "expectorate",
 | 
			
		||||
@ -416,9 +553,9 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "derive-docs"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
version = "0.1.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "075291fd1d6d70a886078f7b1c132a160559ceb9a0fe143177872d40ea587906"
 | 
			
		||||
checksum = "5fe5c5ea065cfabc5a7c5e8ed616e369fbf108c4be01e0e5609bc9846a732664"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "convert_case",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
@ -920,6 +1057,15 @@ dependencies = [
 | 
			
		||||
 "either",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "itertools"
 | 
			
		||||
version = "0.11.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "either",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "itoa"
 | 
			
		||||
version = "1.0.9"
 | 
			
		||||
@ -948,13 +1094,16 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "kcl-lib"
 | 
			
		||||
version = "0.1.10"
 | 
			
		||||
version = "0.1.26"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "bson",
 | 
			
		||||
 "derive-docs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
 "clap 4.4.2",
 | 
			
		||||
 "dashmap",
 | 
			
		||||
 "derive-docs 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
 "expectorate",
 | 
			
		||||
 "futures",
 | 
			
		||||
 "itertools 0.11.0",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "kittycad",
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
@ -968,6 +1117,7 @@ dependencies = [
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-tungstenite",
 | 
			
		||||
 "tower-lsp",
 | 
			
		||||
 "ts-rs-json-value",
 | 
			
		||||
 "uuid",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
@ -976,16 +1126,16 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "kittycad"
 | 
			
		||||
version = "0.2.23"
 | 
			
		||||
version = "0.2.25"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b8b33e5df8f82b97e5f5af94ff1400ae37449d0f5f1bb79acedf17cf2193680f"
 | 
			
		||||
checksum = "d9cf962b1e81a0b4eb923a727e761b40672cbacc7f5f0b75e13579d346352bc7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "base64 0.21.2",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "data-encoding",
 | 
			
		||||
 "itertools",
 | 
			
		||||
 "itertools 0.10.5",
 | 
			
		||||
 "parse-display",
 | 
			
		||||
 "phonenumber",
 | 
			
		||||
 "schemars",
 | 
			
		||||
@ -1049,6 +1199,19 @@ dependencies = [
 | 
			
		||||
 "linked-hash-map",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lsp-types"
 | 
			
		||||
version = "0.94.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bitflags 1.3.2",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "serde_repr",
 | 
			
		||||
 "url",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "memchr"
 | 
			
		||||
version = "2.5.0"
 | 
			
		||||
@ -1201,7 +1364,7 @@ dependencies = [
 | 
			
		||||
 "Inflector",
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "clap",
 | 
			
		||||
 "clap 3.2.25",
 | 
			
		||||
 "data-encoding",
 | 
			
		||||
 "format_serde_error",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
@ -1332,7 +1495,7 @@ dependencies = [
 | 
			
		||||
 "bincode",
 | 
			
		||||
 "either",
 | 
			
		||||
 "fnv",
 | 
			
		||||
 "itertools",
 | 
			
		||||
 "itertools 0.10.5",
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "nom",
 | 
			
		||||
 "quick-xml",
 | 
			
		||||
@ -1343,6 +1506,26 @@ dependencies = [
 | 
			
		||||
 "thiserror",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pin-project"
 | 
			
		||||
version = "1.1.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "pin-project-internal",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pin-project-internal"
 | 
			
		||||
version = "1.1.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pin-project-lite"
 | 
			
		||||
version = "0.2.13"
 | 
			
		||||
@ -1848,6 +2031,17 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_repr"
 | 
			
		||||
version = "0.1.16"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_tokenstream"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
@ -1940,9 +2134,9 @@ checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "slog-async"
 | 
			
		||||
version = "2.7.0"
 | 
			
		||||
version = "2.8.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe"
 | 
			
		||||
checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "crossbeam-channel",
 | 
			
		||||
 "slog",
 | 
			
		||||
@ -2320,6 +2514,61 @@ dependencies = [
 | 
			
		||||
 "walkdir",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tower"
 | 
			
		||||
version = "0.4.13"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "pin-project",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "tower-layer",
 | 
			
		||||
 "tower-service",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tower-layer"
 | 
			
		||||
version = "0.3.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tower-lsp"
 | 
			
		||||
version = "0.20.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "async-codec-lite",
 | 
			
		||||
 "async-trait",
 | 
			
		||||
 "auto_impl",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "dashmap",
 | 
			
		||||
 "futures",
 | 
			
		||||
 "httparse",
 | 
			
		||||
 "lsp-types",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-util",
 | 
			
		||||
 "tower",
 | 
			
		||||
 "tower-lsp-macros",
 | 
			
		||||
 "tracing",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tower-lsp-macros"
 | 
			
		||||
version = "0.9.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tower-service"
 | 
			
		||||
version = "0.3.2"
 | 
			
		||||
@ -2334,9 +2583,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "pin-project-lite",
 | 
			
		||||
 "tracing-attributes",
 | 
			
		||||
 "tracing-core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-attributes"
 | 
			
		||||
version = "0.1.26"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tracing-core"
 | 
			
		||||
version = "0.1.31"
 | 
			
		||||
@ -2363,10 +2624,11 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ts-rs-json-value"
 | 
			
		||||
version = "7.0.0"
 | 
			
		||||
version = "7.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b66d07e64e1e39d693819307757ad16878ff2be1f26d6fc2137c4e23bc0c0545"
 | 
			
		||||
checksum = "f7a6c8eccea9e885ef26336d58ef9ae48b22d7ae3e503422af1902240616d1f6"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "schemars",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "ts-rs-macros",
 | 
			
		||||
@ -2490,6 +2752,12 @@ version = "0.7.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "utf8parse"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "uuid"
 | 
			
		||||
version = "1.4.1"
 | 
			
		||||
@ -2570,6 +2838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
@ -2609,12 +2878,30 @@ name = "wasm-lib"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bson",
 | 
			
		||||
 "futures",
 | 
			
		||||
 "gloo-utils",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "kcl-lib",
 | 
			
		||||
 "kittycad",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tower-lsp",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "wasm-bindgen-futures",
 | 
			
		||||
 "wasm-streams",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "wasm-streams"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "wasm-bindgen-futures",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
 | 
			
		||||
@ -11,11 +11,27 @@ crate-type = ["cdylib"]
 | 
			
		||||
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
 | 
			
		||||
gloo-utils = "0.2.0"
 | 
			
		||||
kcl-lib = { path = "kcl" }
 | 
			
		||||
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
 | 
			
		||||
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
 | 
			
		||||
serde_json = "1.0.93"
 | 
			
		||||
wasm-bindgen = "0.2.87"
 | 
			
		||||
wasm-bindgen-futures = "0.4.37"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
 | 
			
		||||
futures = "0.3.28"
 | 
			
		||||
js-sys = "0.3.64"
 | 
			
		||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
 | 
			
		||||
wasm-bindgen-futures = { version = "0.4.37", features = ["futures-core-03-stream"] }
 | 
			
		||||
wasm-streams = "0.3.0"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
 | 
			
		||||
version = "0.3.57"
 | 
			
		||||
features = [
 | 
			
		||||
  "console",
 | 
			
		||||
  "HtmlTextAreaElement",
 | 
			
		||||
  "ReadableStream",
 | 
			
		||||
  "WritableStream",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[profile.release]
 | 
			
		||||
panic = "abort"
 | 
			
		||||
debug = true
 | 
			
		||||
@ -23,5 +39,5 @@ debug = true
 | 
			
		||||
[workspace]
 | 
			
		||||
members = [
 | 
			
		||||
	"derive-docs",
 | 
			
		||||
	"kcl"
 | 
			
		||||
	"kcl",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "derive-docs"
 | 
			
		||||
description = "A tool for generating documentation from Rust derive macros"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
version = "0.1.3"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
license = "MIT"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -195,7 +195,9 @@ fn do_stdlib_inner(
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        }
 | 
			
		||||
        .trim_start_matches('_')
 | 
			
		||||
        .to_string();
 | 
			
		||||
 | 
			
		||||
        let ty = match arg {
 | 
			
		||||
            syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
 | 
			
		||||
@ -247,15 +249,21 @@ fn do_stdlib_inner(
 | 
			
		||||
        .replace("-> ", "")
 | 
			
		||||
        .replace("Result < ", "")
 | 
			
		||||
        .replace(", KclError >", "");
 | 
			
		||||
    let ret_ty_string = ret_ty_string.trim().to_string();
 | 
			
		||||
    let ret_ty_ident = format_ident!("{}", ret_ty_string);
 | 
			
		||||
    let ret_ty_string = clean_type(&ret_ty_string);
 | 
			
		||||
    let return_type = quote! {
 | 
			
		||||
        #docs_crate::StdLibFnArg {
 | 
			
		||||
            name: "".to_string(),
 | 
			
		||||
            type_: #ret_ty_string.to_string(),
 | 
			
		||||
            schema: #ret_ty_ident::json_schema(&mut generator),
 | 
			
		||||
            required: true,
 | 
			
		||||
    let return_type = if !ret_ty_string.is_empty() {
 | 
			
		||||
        let ret_ty_string = ret_ty_string.trim().to_string();
 | 
			
		||||
        let ret_ty_ident = format_ident!("{}", ret_ty_string);
 | 
			
		||||
        let ret_ty_string = clean_type(&ret_ty_string);
 | 
			
		||||
        quote! {
 | 
			
		||||
            Some(#docs_crate::StdLibFnArg {
 | 
			
		||||
                name: "".to_string(),
 | 
			
		||||
                type_: #ret_ty_string.to_string(),
 | 
			
		||||
                schema: #ret_ty_ident::json_schema(&mut generator),
 | 
			
		||||
                required: true,
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        quote! {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -275,6 +283,8 @@ fn do_stdlib_inner(
 | 
			
		||||
        // ... a struct type called `#name_ident` that has no members
 | 
			
		||||
        #[allow(non_camel_case_types, missing_docs)]
 | 
			
		||||
        #description_doc_comment
 | 
			
		||||
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars::JsonSchema, ts_rs::TS)]
 | 
			
		||||
        #[ts(export)]
 | 
			
		||||
        pub(crate) struct #name_ident {}
 | 
			
		||||
        // ... a constant of type `#name` whose identifier is also #name_ident
 | 
			
		||||
        #[allow(non_upper_case_globals, missing_docs)]
 | 
			
		||||
@ -307,7 +317,7 @@ fn do_stdlib_inner(
 | 
			
		||||
                vec![#(#arg_types),*]
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fn return_value(&self) -> #docs_crate::StdLibFnArg {
 | 
			
		||||
            fn return_value(&self) -> Option<#docs_crate::StdLibFnArg> {
 | 
			
		||||
                let mut settings = schemars::gen::SchemaSettings::openapi3();
 | 
			
		||||
                settings.inline_subschemas = true;
 | 
			
		||||
                let mut generator = schemars::gen::SchemaGenerator::new(settings);
 | 
			
		||||
@ -326,6 +336,10 @@ fn do_stdlib_inner(
 | 
			
		||||
            fn std_lib_fn(&self) -> crate::std::StdFn {
 | 
			
		||||
                #fn_name_ident
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fn clone_box(&self) -> Box<dyn #docs_crate::StdLibFn> {
 | 
			
		||||
                Box::new(self.clone())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #item
 | 
			
		||||
@ -529,4 +543,25 @@ mod tests {
 | 
			
		||||
        assert!(errors.is_empty());
 | 
			
		||||
        expectorate::assert_contents("tests/min.gen", &openapitor::types::get_text_fmt(&item).unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_stdlib_show() {
 | 
			
		||||
        let (item, errors) = do_stdlib(
 | 
			
		||||
            quote! {
 | 
			
		||||
                name = "show",
 | 
			
		||||
            },
 | 
			
		||||
            quote! {
 | 
			
		||||
                fn inner_show(
 | 
			
		||||
                    /// The args to do shit to.
 | 
			
		||||
                    _args: Vec<f64>
 | 
			
		||||
                ) {
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap();
 | 
			
		||||
        let _expected = quote! {};
 | 
			
		||||
 | 
			
		||||
        assert!(errors.is_empty());
 | 
			
		||||
        expectorate::assert_contents("tests/show.gen", &openapitor::types::get_text_fmt(&item).unwrap());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
#[allow(non_camel_case_types, missing_docs)]
 | 
			
		||||
#[doc = "Std lib function: lineTo"]
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub(crate) struct LineTo {}
 | 
			
		||||
 | 
			
		||||
#[allow(non_upper_case_globals, missing_docs)]
 | 
			
		||||
@ -42,16 +44,16 @@ impl crate::docs::StdLibFn for LineTo {
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn return_value(&self) -> crate::docs::StdLibFnArg {
 | 
			
		||||
    fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
 | 
			
		||||
        let mut settings = schemars::gen::SchemaSettings::openapi3();
 | 
			
		||||
        settings.inline_subschemas = true;
 | 
			
		||||
        let mut generator = schemars::gen::SchemaGenerator::new(settings);
 | 
			
		||||
        crate::docs::StdLibFnArg {
 | 
			
		||||
        Some(crate::docs::StdLibFnArg {
 | 
			
		||||
            name: "".to_string(),
 | 
			
		||||
            type_: "SketchGroup".to_string(),
 | 
			
		||||
            schema: SketchGroup::json_schema(&mut generator),
 | 
			
		||||
            required: true,
 | 
			
		||||
        }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn unpublished(&self) -> bool {
 | 
			
		||||
@ -65,6 +67,10 @@ impl crate::docs::StdLibFn for LineTo {
 | 
			
		||||
    fn std_lib_fn(&self) -> crate::std::StdFn {
 | 
			
		||||
        line_to
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
 | 
			
		||||
        Box::new(self.clone())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn inner_line_to(
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
#[allow(non_camel_case_types, missing_docs)]
 | 
			
		||||
#[doc = "Std lib function: min"]
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub(crate) struct Min {}
 | 
			
		||||
 | 
			
		||||
#[allow(non_upper_case_globals, missing_docs)]
 | 
			
		||||
@ -34,16 +36,16 @@ impl crate::docs::StdLibFn for Min {
 | 
			
		||||
        }]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn return_value(&self) -> crate::docs::StdLibFnArg {
 | 
			
		||||
    fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
 | 
			
		||||
        let mut settings = schemars::gen::SchemaSettings::openapi3();
 | 
			
		||||
        settings.inline_subschemas = true;
 | 
			
		||||
        let mut generator = schemars::gen::SchemaGenerator::new(settings);
 | 
			
		||||
        crate::docs::StdLibFnArg {
 | 
			
		||||
        Some(crate::docs::StdLibFnArg {
 | 
			
		||||
            name: "".to_string(),
 | 
			
		||||
            type_: "number".to_string(),
 | 
			
		||||
            schema: f64::json_schema(&mut generator),
 | 
			
		||||
            required: true,
 | 
			
		||||
        }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn unpublished(&self) -> bool {
 | 
			
		||||
@ -57,6 +59,10 @@ impl crate::docs::StdLibFn for Min {
 | 
			
		||||
    fn std_lib_fn(&self) -> crate::std::StdFn {
 | 
			
		||||
        min
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
 | 
			
		||||
        Box::new(self.clone())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn inner_min(#[doc = r" The args to do shit to."] args: Vec<f64>) -> f64 {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										63
									
								
								src/wasm-lib/derive-docs/tests/show.gen
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/wasm-lib/derive-docs/tests/show.gen
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
			
		||||
#[allow(non_camel_case_types, missing_docs)]
 | 
			
		||||
#[doc = "Std lib function: show"]
 | 
			
		||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub(crate) struct Show {}
 | 
			
		||||
 | 
			
		||||
#[allow(non_upper_case_globals, missing_docs)]
 | 
			
		||||
#[doc = "Std lib function: show"]
 | 
			
		||||
pub(crate) const Show: Show = Show {};
 | 
			
		||||
impl crate::docs::StdLibFn for Show {
 | 
			
		||||
    fn name(&self) -> String {
 | 
			
		||||
        "show".to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn summary(&self) -> String {
 | 
			
		||||
        "".to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn description(&self) -> String {
 | 
			
		||||
        "".to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn tags(&self) -> Vec<String> {
 | 
			
		||||
        vec![]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
 | 
			
		||||
        let mut settings = schemars::gen::SchemaSettings::openapi3();
 | 
			
		||||
        settings.inline_subschemas = true;
 | 
			
		||||
        let mut generator = schemars::gen::SchemaGenerator::new(settings);
 | 
			
		||||
        vec![crate::docs::StdLibFnArg {
 | 
			
		||||
            name: "args".to_string(),
 | 
			
		||||
            type_: "[number]".to_string(),
 | 
			
		||||
            schema: <Vec<f64>>::json_schema(&mut generator),
 | 
			
		||||
            required: true,
 | 
			
		||||
        }]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
 | 
			
		||||
        let mut settings = schemars::gen::SchemaSettings::openapi3();
 | 
			
		||||
        settings.inline_subschemas = true;
 | 
			
		||||
        let mut generator = schemars::gen::SchemaGenerator::new(settings);
 | 
			
		||||
        None
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn unpublished(&self) -> bool {
 | 
			
		||||
        false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn deprecated(&self) -> bool {
 | 
			
		||||
        false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn std_lib_fn(&self) -> crate::std::StdFn {
 | 
			
		||||
        show
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
 | 
			
		||||
        Box::new(self.clone())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn inner_show(#[doc = r" The args to do shit to."] _args: Vec<f64>) {}
 | 
			
		||||
@ -1,30 +1,34 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "kcl-lib"
 | 
			
		||||
description = "KittyCAD Language"
 | 
			
		||||
version = "0.1.10"
 | 
			
		||||
version = "0.1.26"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
license = "MIT"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
anyhow = "1.0.75"
 | 
			
		||||
derive-docs = { version = "0.1.0" }
 | 
			
		||||
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
 | 
			
		||||
anyhow = { version = "1.0.75", features = ["backtrace"] }
 | 
			
		||||
clap = { version = "4.4.2", features = ["cargo", "derive", "env", "unicode"] }
 | 
			
		||||
dashmap = "5.5.3"
 | 
			
		||||
derive-docs = { version = "0.1.3" }
 | 
			
		||||
#derive-docs = { path = "../derive-docs" }
 | 
			
		||||
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
 | 
			
		||||
lazy_static = "1.4.0"
 | 
			
		||||
parse-display = "0.8.2"
 | 
			
		||||
regex = "1.7.1"
 | 
			
		||||
schemars = { version = "0.8", features = ["url", "uuid1"] }
 | 
			
		||||
schemars = { version = "0.8", features = ["impl_json_schema", "url", "uuid1"] }
 | 
			
		||||
serde = {version = "1.0.152", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.93"
 | 
			
		||||
thiserror = "1.0.47"
 | 
			
		||||
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "uuid-impl"] }
 | 
			
		||||
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
 | 
			
		||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
 | 
			
		||||
wasm-bindgen = "0.2.87"
 | 
			
		||||
wasm-bindgen-futures = "0.4.37"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
 | 
			
		||||
js-sys = { version = "0.3.64" }
 | 
			
		||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
 | 
			
		||||
wasm-bindgen = "0.2.87"
 | 
			
		||||
wasm-bindgen-futures = "0.4.37"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 | 
			
		||||
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
 | 
			
		||||
@ -32,6 +36,7 @@ futures = { version = "0.3.28" }
 | 
			
		||||
reqwest = { version = "0.11.20", default-features = false }
 | 
			
		||||
tokio = { version = "1.32.0", features = ["full"] }
 | 
			
		||||
tokio-tungstenite = { version = "0.20.0", features = ["rustls-tls-native-roots"] }
 | 
			
		||||
tower-lsp = { version = "0.20.0", features = ["proposed"] }
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
default = ["engine"]
 | 
			
		||||
@ -43,5 +48,6 @@ debug = true
 | 
			
		||||
 | 
			
		||||
[dev-dependencies]
 | 
			
		||||
expectorate = "1.0.7"
 | 
			
		||||
itertools = "0.11.0"
 | 
			
		||||
pretty_assertions = "1.4.0"
 | 
			
		||||
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								src/wasm-lib/kcl/fuzz/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/wasm-lib/kcl/fuzz/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
target
 | 
			
		||||
corpus
 | 
			
		||||
artifacts
 | 
			
		||||
coverage
 | 
			
		||||
							
								
								
									
										2218
									
								
								src/wasm-lib/kcl/fuzz/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2218
									
								
								src/wasm-lib/kcl/fuzz/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								src/wasm-lib/kcl/fuzz/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/wasm-lib/kcl/fuzz/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "kcl-lib-fuzz"
 | 
			
		||||
version = "0.0.0"
 | 
			
		||||
publish = false
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[package.metadata]
 | 
			
		||||
cargo-fuzz = true
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
libfuzzer-sys = "0.4"
 | 
			
		||||
 | 
			
		||||
[dependencies.kcl-lib]
 | 
			
		||||
path = ".."
 | 
			
		||||
 | 
			
		||||
# Prevent this from interfering with workspaces
 | 
			
		||||
[workspace]
 | 
			
		||||
members = ["."]
 | 
			
		||||
 | 
			
		||||
[profile.release]
 | 
			
		||||
debug = 1
 | 
			
		||||
 | 
			
		||||
[[bin]]
 | 
			
		||||
name = "parser"
 | 
			
		||||
path = "fuzz_targets/parser.rs"
 | 
			
		||||
test = false
 | 
			
		||||
doc = false
 | 
			
		||||
							
								
								
									
										14
									
								
								src/wasm-lib/kcl/fuzz/fuzz_targets/parser.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/wasm-lib/kcl/fuzz/fuzz_targets/parser.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
#![no_main]
 | 
			
		||||
#[macro_use]
 | 
			
		||||
extern crate libfuzzer_sys;
 | 
			
		||||
extern crate kcl_lib;
 | 
			
		||||
 | 
			
		||||
fuzz_target!(|data: &[u8]| {
 | 
			
		||||
    if let Ok(s) = std::str::from_utf8(data) {
 | 
			
		||||
        let tokens = kcl_lib::tokeniser::lexer(s);
 | 
			
		||||
        let parser = kcl_lib::parser::Parser::new(tokens);
 | 
			
		||||
        if let Ok(_) = parser.ast() {
 | 
			
		||||
            println!("OK");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,11 +1,18 @@
 | 
			
		||||
//! Functions for generating docs for our stdlib functions.
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use schemars::JsonSchema;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tower_lsp::lsp_types::{
 | 
			
		||||
    CompletionItem, CompletionItemKind, CompletionItemLabelDetails, Documentation, InsertTextFormat, MarkupContent,
 | 
			
		||||
    MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::std::Primitive;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct StdLibFnData {
 | 
			
		||||
    /// The name of the function.
 | 
			
		||||
    pub name: String,
 | 
			
		||||
@ -18,7 +25,7 @@ pub struct StdLibFnData {
 | 
			
		||||
    /// The args of the function.
 | 
			
		||||
    pub args: Vec<StdLibFnArg>,
 | 
			
		||||
    /// The return value of the function.
 | 
			
		||||
    pub return_value: StdLibFnArg,
 | 
			
		||||
    pub return_value: Option<StdLibFnArg>,
 | 
			
		||||
    /// If the function is unpublished.
 | 
			
		||||
    pub unpublished: bool,
 | 
			
		||||
    /// If the function is deprecated.
 | 
			
		||||
@ -26,7 +33,9 @@ pub struct StdLibFnData {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// This struct defines a single argument to a stdlib function.
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct StdLibFnArg {
 | 
			
		||||
    /// The name of the argument.
 | 
			
		||||
    pub name: String,
 | 
			
		||||
@ -41,23 +50,36 @@ pub struct StdLibFnArg {
 | 
			
		||||
impl StdLibFnArg {
 | 
			
		||||
    #[allow(dead_code)]
 | 
			
		||||
    pub fn get_type_string(&self) -> Result<(String, bool)> {
 | 
			
		||||
        get_type_string_from_schema(&self.schema)
 | 
			
		||||
        get_type_string_from_schema(&self.schema.clone())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[allow(dead_code)]
 | 
			
		||||
    pub fn get_autocomplete_string(&self) -> Result<String> {
 | 
			
		||||
        get_autocomplete_string_from_schema(&self.schema)
 | 
			
		||||
        get_autocomplete_string_from_schema(&self.schema.clone())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[allow(dead_code)]
 | 
			
		||||
    pub fn description(&self) -> Option<String> {
 | 
			
		||||
        get_description_string_from_schema(&self.schema)
 | 
			
		||||
        get_description_string_from_schema(&self.schema.clone())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<StdLibFnArg> for ParameterInformation {
 | 
			
		||||
    fn from(arg: StdLibFnArg) -> Self {
 | 
			
		||||
        ParameterInformation {
 | 
			
		||||
            label: ParameterLabel::Simple(arg.name.to_string()),
 | 
			
		||||
            documentation: arg.description().map(|description| {
 | 
			
		||||
                Documentation::MarkupContent(MarkupContent {
 | 
			
		||||
                    kind: MarkupKind::Markdown,
 | 
			
		||||
                    value: description,
 | 
			
		||||
                })
 | 
			
		||||
            }),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// This trait defines functions called upon stdlib functions to generate
 | 
			
		||||
/// documentation for them.
 | 
			
		||||
pub trait StdLibFn {
 | 
			
		||||
pub trait StdLibFn: std::fmt::Debug + Send + Sync {
 | 
			
		||||
    /// The name of the function.
 | 
			
		||||
    fn name(&self) -> String;
 | 
			
		||||
 | 
			
		||||
@ -74,7 +96,7 @@ pub trait StdLibFn {
 | 
			
		||||
    fn args(&self) -> Vec<StdLibFnArg>;
 | 
			
		||||
 | 
			
		||||
    /// The return value of the function.
 | 
			
		||||
    fn return_value(&self) -> StdLibFnArg;
 | 
			
		||||
    fn return_value(&self) -> Option<StdLibFnArg>;
 | 
			
		||||
 | 
			
		||||
    /// If the function is unpublished.
 | 
			
		||||
    fn unpublished(&self) -> bool;
 | 
			
		||||
@ -85,6 +107,9 @@ pub trait StdLibFn {
 | 
			
		||||
    /// The function itself.
 | 
			
		||||
    fn std_lib_fn(&self) -> crate::std::StdFn;
 | 
			
		||||
 | 
			
		||||
    /// Helper function to clone the boxed trait object.
 | 
			
		||||
    fn clone_box(&self) -> Box<dyn StdLibFn>;
 | 
			
		||||
 | 
			
		||||
    /// Return a JSON struct representing the function.
 | 
			
		||||
    fn to_json(&self) -> Result<StdLibFnData> {
 | 
			
		||||
        Ok(StdLibFnData {
 | 
			
		||||
@ -108,11 +133,139 @@ pub trait StdLibFn {
 | 
			
		||||
            }
 | 
			
		||||
            signature.push_str(&format!("{}: {}", arg.name, arg.type_));
 | 
			
		||||
        }
 | 
			
		||||
        signature.push_str(") -> ");
 | 
			
		||||
        signature.push_str(&self.return_value().type_);
 | 
			
		||||
        signature.push(')');
 | 
			
		||||
        if let Some(return_value) = self.return_value() {
 | 
			
		||||
            signature.push_str(&format!(" -> {}", return_value.type_));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        signature
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn to_completion_item(&self) -> CompletionItem {
 | 
			
		||||
        CompletionItem {
 | 
			
		||||
            label: self.name(),
 | 
			
		||||
            label_details: Some(CompletionItemLabelDetails {
 | 
			
		||||
                detail: Some(self.fn_signature().replace(&self.name(), "")),
 | 
			
		||||
                description: None,
 | 
			
		||||
            }),
 | 
			
		||||
            kind: Some(CompletionItemKind::FUNCTION),
 | 
			
		||||
            detail: None,
 | 
			
		||||
            documentation: Some(Documentation::MarkupContent(MarkupContent {
 | 
			
		||||
                kind: MarkupKind::Markdown,
 | 
			
		||||
                value: if !self.description().is_empty() {
 | 
			
		||||
                    format!("{}\n\n{}", self.summary(), self.description())
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.summary()
 | 
			
		||||
                },
 | 
			
		||||
            })),
 | 
			
		||||
            deprecated: Some(self.deprecated()),
 | 
			
		||||
            preselect: None,
 | 
			
		||||
            sort_text: None,
 | 
			
		||||
            filter_text: None,
 | 
			
		||||
            insert_text: Some(format!(
 | 
			
		||||
                "{}({})",
 | 
			
		||||
                self.name(),
 | 
			
		||||
                self.args()
 | 
			
		||||
                    .iter()
 | 
			
		||||
                    .enumerate()
 | 
			
		||||
                    // It is okay to unwrap here since in the `kcl-lib` tests, we would have caught
 | 
			
		||||
                    // any errors in the `self`'s signature.
 | 
			
		||||
                    .map(|(index, item)| {
 | 
			
		||||
                        let format = item.get_autocomplete_string().unwrap();
 | 
			
		||||
                        if item.type_ == "SketchGroup" || item.type_ == "ExtrudeGroup" {
 | 
			
		||||
                            format!("${{{}:{}}}", index + 1, "%")
 | 
			
		||||
                        } else {
 | 
			
		||||
                            format!("${{{}:{}}}", index + 1, format)
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                    .collect::<Vec<_>>()
 | 
			
		||||
                    .join(",")
 | 
			
		||||
            )),
 | 
			
		||||
            insert_text_format: Some(InsertTextFormat::SNIPPET),
 | 
			
		||||
            insert_text_mode: None,
 | 
			
		||||
            text_edit: None,
 | 
			
		||||
            additional_text_edits: None,
 | 
			
		||||
            command: None,
 | 
			
		||||
            commit_characters: None,
 | 
			
		||||
            data: None,
 | 
			
		||||
            tags: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn to_signature_help(&self) -> SignatureHelp {
 | 
			
		||||
        // Fill this in based on the current positon of the cursor.
 | 
			
		||||
        let active_parameter = None;
 | 
			
		||||
 | 
			
		||||
        SignatureHelp {
 | 
			
		||||
            signatures: vec![SignatureInformation {
 | 
			
		||||
                label: self.name(),
 | 
			
		||||
                documentation: Some(Documentation::MarkupContent(MarkupContent {
 | 
			
		||||
                    kind: MarkupKind::Markdown,
 | 
			
		||||
                    value: if !self.description().is_empty() {
 | 
			
		||||
                        format!("{}\n\n{}", self.summary(), self.description())
 | 
			
		||||
                    } else {
 | 
			
		||||
                        self.summary()
 | 
			
		||||
                    },
 | 
			
		||||
                })),
 | 
			
		||||
                parameters: Some(self.args().into_iter().map(|arg| arg.into()).collect()),
 | 
			
		||||
                active_parameter,
 | 
			
		||||
            }],
 | 
			
		||||
            active_signature: Some(0),
 | 
			
		||||
            active_parameter,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl JsonSchema for dyn StdLibFn {
 | 
			
		||||
    fn schema_name() -> String {
 | 
			
		||||
        "StdLibFn".to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
 | 
			
		||||
        gen.subschema_for::<StdLibFnData>()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Serialize for Box<dyn StdLibFn> {
 | 
			
		||||
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
 | 
			
		||||
        self.to_json().unwrap().serialize(serializer)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'de> Deserialize<'de> for Box<dyn StdLibFn> {
 | 
			
		||||
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
 | 
			
		||||
        let data = StdLibFnData::deserialize(deserializer)?;
 | 
			
		||||
        let stdlib = crate::std::StdLib::new();
 | 
			
		||||
        let stdlib_fn = stdlib
 | 
			
		||||
            .get(&data.name)
 | 
			
		||||
            .ok_or_else(|| serde::de::Error::custom(format!("StdLibFn {} not found", data.name)))?;
 | 
			
		||||
        Ok(stdlib_fn)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ts_rs::TS for dyn StdLibFn {
 | 
			
		||||
    const EXPORT_TO: Option<&'static str> = Some("bindings/StdLibFnData");
 | 
			
		||||
 | 
			
		||||
    fn name() -> String {
 | 
			
		||||
        "StdLibFnData".to_string()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn dependencies() -> Vec<ts_rs::Dependency>
 | 
			
		||||
    where
 | 
			
		||||
        Self: 'static,
 | 
			
		||||
    {
 | 
			
		||||
        StdLibFnData::dependencies()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn transparent() -> bool {
 | 
			
		||||
        StdLibFnData::transparent()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Clone for Box<dyn StdLibFn> {
 | 
			
		||||
    fn clone(&self) -> Box<dyn StdLibFn> {
 | 
			
		||||
        self.clone_box()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> {
 | 
			
		||||
@ -152,11 +305,7 @@ pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<
 | 
			
		||||
                    if let Some(description) = get_description_string_from_schema(prop) {
 | 
			
		||||
                        fn_docs.push_str(&format!("\t// {}\n", description));
 | 
			
		||||
                    }
 | 
			
		||||
                    fn_docs.push_str(&format!(
 | 
			
		||||
                        "\t\"{}\": {},\n",
 | 
			
		||||
                        prop_name,
 | 
			
		||||
                        get_type_string_from_schema(prop)?.0,
 | 
			
		||||
                    ));
 | 
			
		||||
                    fn_docs.push_str(&format!("\t{}: {},\n", prop_name, get_type_string_from_schema(prop)?.0,));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                fn_docs.push('}');
 | 
			
		||||
@ -234,7 +383,7 @@ pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) ->
 | 
			
		||||
                        fn_docs.push_str(&format!("\t// {}\n", description));
 | 
			
		||||
                    }
 | 
			
		||||
                    fn_docs.push_str(&format!(
 | 
			
		||||
                        "\t\"{}\": {},\n",
 | 
			
		||||
                        "\t{}: {},\n",
 | 
			
		||||
                        prop_name,
 | 
			
		||||
                        get_autocomplete_string_from_schema(prop)?,
 | 
			
		||||
                    ));
 | 
			
		||||
@ -282,3 +431,93 @@ pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) ->
 | 
			
		||||
        schemars::schema::Schema::Bool(_) => Ok(Primitive::Bool.to_string()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn completion_item_from_enum_schema(
 | 
			
		||||
    schema: &schemars::schema::Schema,
 | 
			
		||||
    kind: CompletionItemKind,
 | 
			
		||||
) -> Result<CompletionItem> {
 | 
			
		||||
    // Get the docs for the schema.
 | 
			
		||||
    let description = get_description_string_from_schema(schema).unwrap_or_default();
 | 
			
		||||
    let schemars::schema::Schema::Object(o) = schema else {
 | 
			
		||||
        anyhow::bail!("expected object schema: {:#?}", schema);
 | 
			
		||||
    };
 | 
			
		||||
    let Some(enum_values) = o.enum_values.as_ref() else {
 | 
			
		||||
        anyhow::bail!("expected enum values: {:#?}", o);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if enum_values.len() > 1 {
 | 
			
		||||
        anyhow::bail!("expected only one enum value: {:#?}", o);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if enum_values.is_empty() {
 | 
			
		||||
        anyhow::bail!("expected at least one enum value: {:#?}", o);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let label = enum_values[0].to_string();
 | 
			
		||||
 | 
			
		||||
    Ok(CompletionItem {
 | 
			
		||||
        label,
 | 
			
		||||
        label_details: None,
 | 
			
		||||
        kind: Some(kind),
 | 
			
		||||
        detail: Some(description.to_string()),
 | 
			
		||||
        documentation: Some(Documentation::MarkupContent(MarkupContent {
 | 
			
		||||
            kind: MarkupKind::Markdown,
 | 
			
		||||
            value: description.to_string(),
 | 
			
		||||
        })),
 | 
			
		||||
        deprecated: Some(false),
 | 
			
		||||
        preselect: None,
 | 
			
		||||
        sort_text: None,
 | 
			
		||||
        filter_text: None,
 | 
			
		||||
        insert_text: None,
 | 
			
		||||
        insert_text_format: None,
 | 
			
		||||
        insert_text_mode: None,
 | 
			
		||||
        text_edit: None,
 | 
			
		||||
        additional_text_edits: None,
 | 
			
		||||
        command: None,
 | 
			
		||||
        commit_characters: None,
 | 
			
		||||
        data: None,
 | 
			
		||||
        tags: None,
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use pretty_assertions::assert_eq;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_serialize_function() {
 | 
			
		||||
        let some_function = crate::abstract_syntax_tree_types::Function::StdLib {
 | 
			
		||||
            func: Box::new(crate::std::sketch::Line),
 | 
			
		||||
        };
 | 
			
		||||
        let serialized = serde_json::to_string(&some_function).unwrap();
 | 
			
		||||
        assert!(serialized.contains(r#"{"type":"StdLib""#));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_deserialize_function() {
 | 
			
		||||
        let some_function_string = r#"{"type":"StdLib","func":{"name":"line","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}}"#;
 | 
			
		||||
        let some_function: crate::abstract_syntax_tree_types::Function =
 | 
			
		||||
            serde_json::from_str(some_function_string).unwrap();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            some_function,
 | 
			
		||||
            crate::abstract_syntax_tree_types::Function::StdLib {
 | 
			
		||||
                func: Box::new(crate::std::sketch::Line),
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_deserialize_function_show() {
 | 
			
		||||
        let some_function_string = r#"{"type":"StdLib","func":{"name":"show","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false}}"#;
 | 
			
		||||
        let some_function: crate::abstract_syntax_tree_types::Function =
 | 
			
		||||
            serde_json::from_str(some_function_string).unwrap();
 | 
			
		||||
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            some_function,
 | 
			
		||||
            crate::abstract_syntax_tree_types::Function::StdLib {
 | 
			
		||||
                func: Box::new(crate::std::Show),
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@ impl EngineConnection {
 | 
			
		||||
 | 
			
		||||
                        if let Some(msg) = ws_resp.resp {
 | 
			
		||||
                            match msg {
 | 
			
		||||
                                OkWebSocketResponseData::MetricsRequest {} => {
 | 
			
		||||
                                    // @paultag todo
 | 
			
		||||
                                }
 | 
			
		||||
                                OkWebSocketResponseData::IceServerInfo { ice_servers } => {
 | 
			
		||||
                                    println!("got ice server info: {:?}", ice_servers);
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,8 @@
 | 
			
		||||
//! Functions for managing engine communications.
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
#[cfg(not(test))]
 | 
			
		||||
#[cfg(feature = "engine")]
 | 
			
		||||
use wasm_bindgen::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
@ -32,19 +35,18 @@ pub mod conn_mock;
 | 
			
		||||
#[cfg(not(test))]
 | 
			
		||||
pub use conn_mock::EngineConnection;
 | 
			
		||||
 | 
			
		||||
use crate::executor::SourceRange;
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
#[cfg(not(test))]
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
pub struct EngineManager {
 | 
			
		||||
    connection: EngineConnection,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
#[cfg(not(test))]
 | 
			
		||||
#[cfg(feature = "engine")]
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
impl EngineManager {
 | 
			
		||||
    #[cfg(target_arch = "wasm32")]
 | 
			
		||||
    #[cfg(not(test))]
 | 
			
		||||
    #[cfg(feature = "engine")]
 | 
			
		||||
    #[wasm_bindgen(constructor)]
 | 
			
		||||
    pub async fn new(manager: conn_wasm::EngineCommandManager) -> EngineManager {
 | 
			
		||||
        EngineManager {
 | 
			
		||||
@ -57,7 +59,7 @@ impl EngineManager {
 | 
			
		||||
        let id = uuid::Uuid::parse_str(id_str).map_err(|e| e.to_string())?;
 | 
			
		||||
        let cmd = serde_json::from_str(cmd_str).map_err(|e| e.to_string())?;
 | 
			
		||||
        self.connection
 | 
			
		||||
            .send_modeling_cmd(id, SourceRange::default(), cmd)
 | 
			
		||||
            .send_modeling_cmd(id, crate::executor::SourceRange::default(), cmd)
 | 
			
		||||
            .map_err(String::from)?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,8 @@
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
 | 
			
		||||
 | 
			
		||||
use crate::executor::SourceRange;
 | 
			
		||||
 | 
			
		||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
@ -29,7 +32,7 @@ pub enum KclError {
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub struct KclErrorDetails {
 | 
			
		||||
    #[serde(rename = "sourceRanges")]
 | 
			
		||||
    pub source_ranges: Vec<crate::executor::SourceRange>,
 | 
			
		||||
    pub source_ranges: Vec<SourceRange>,
 | 
			
		||||
    #[serde(rename = "msg")]
 | 
			
		||||
    pub message: String,
 | 
			
		||||
}
 | 
			
		||||
@ -61,6 +64,37 @@ impl KclError {
 | 
			
		||||
 | 
			
		||||
        (format!("{}: {}", type_, message), line, column)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn source_ranges(&self) -> Vec<SourceRange> {
 | 
			
		||||
        match &self {
 | 
			
		||||
            KclError::Syntax(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::Semantic(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::Type(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::Unimplemented(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::Unexpected(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::ValueAlreadyDefined(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::UndefinedValue(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::InvalidExpression(e) => e.source_ranges.clone(),
 | 
			
		||||
            KclError::Engine(e) => e.source_ranges.clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
 | 
			
		||||
        let (message, _, _) = self.get_message_line_column(code);
 | 
			
		||||
        let source_ranges = self.source_ranges();
 | 
			
		||||
 | 
			
		||||
        Diagnostic {
 | 
			
		||||
            range: source_ranges.first().map(|r| r.to_lsp_range(code)).unwrap_or_default(),
 | 
			
		||||
            severity: Some(DiagnosticSeverity::ERROR),
 | 
			
		||||
            code: None,
 | 
			
		||||
            // TODO: this is neat we can pass a URL to a help page here for this specific error.
 | 
			
		||||
            code_description: None,
 | 
			
		||||
            source: Some("kcl".to_string()),
 | 
			
		||||
            message,
 | 
			
		||||
            related_information: None,
 | 
			
		||||
            tags: None,
 | 
			
		||||
            data: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// This is different than to_string() in that it will serialize the Error
 | 
			
		||||
 | 
			
		||||
@ -5,9 +5,10 @@ use std::collections::HashMap;
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use schemars::JsonSchema;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    abstract_syntax_tree_types::{BodyItem, FunctionExpression, Value},
 | 
			
		||||
    abstract_syntax_tree_types::{BodyItem, Function, FunctionExpression, Value},
 | 
			
		||||
    engine::EngineConnection,
 | 
			
		||||
    errors::{KclError, KclErrorDetails},
 | 
			
		||||
};
 | 
			
		||||
@ -281,10 +282,65 @@ pub struct Position(pub [f64; 3]);
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub struct Rotation(pub [f64; 4]);
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
 | 
			
		||||
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub struct SourceRange(pub [usize; 2]);
 | 
			
		||||
 | 
			
		||||
impl SourceRange {
 | 
			
		||||
    /// Create a new source range.
 | 
			
		||||
    pub fn new(start: usize, end: usize) -> Self {
 | 
			
		||||
        Self([start, end])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the start of the range.
 | 
			
		||||
    pub fn start(&self) -> usize {
 | 
			
		||||
        self.0[0]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the end of the range.
 | 
			
		||||
    pub fn end(&self) -> usize {
 | 
			
		||||
        self.0[1]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Check if the range contains a position.
 | 
			
		||||
    pub fn contains(&self, pos: usize) -> bool {
 | 
			
		||||
        pos >= self.start() && pos <= self.end()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn start_to_lsp_position(&self, code: &str) -> LspPosition {
 | 
			
		||||
        // Calculate the line and column of the error from the source range.
 | 
			
		||||
        // Lines are zero indexed in vscode so we need to subtract 1.
 | 
			
		||||
        let mut line = code[..self.start()].lines().count();
 | 
			
		||||
        if line > 0 {
 | 
			
		||||
            line = line.saturating_sub(1);
 | 
			
		||||
        }
 | 
			
		||||
        let column = code[..self.start()].lines().last().map(|l| l.len()).unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        LspPosition {
 | 
			
		||||
            line: line as u32,
 | 
			
		||||
            character: column as u32,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn end_to_lsp_position(&self, code: &str) -> LspPosition {
 | 
			
		||||
        // Calculate the line and column of the error from the source range.
 | 
			
		||||
        // Lines are zero indexed in vscode so we need to subtract 1.
 | 
			
		||||
        let line = code[..self.end()].lines().count() - 1;
 | 
			
		||||
        let column = code[..self.end()].lines().last().map(|l| l.len()).unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        LspPosition {
 | 
			
		||||
            line: line as u32,
 | 
			
		||||
            character: column as u32,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn to_lsp_range(&self, code: &str) -> LspRange {
 | 
			
		||||
        let start = self.start_to_lsp_position(code);
 | 
			
		||||
        let end = self.end_to_lsp_position(code);
 | 
			
		||||
        LspRange { start, end }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub struct Point2d {
 | 
			
		||||
@ -509,7 +565,6 @@ pub fn execute(
 | 
			
		||||
    engine: &mut EngineConnection,
 | 
			
		||||
) -> Result<ProgramMemory, KclError> {
 | 
			
		||||
    let mut pipe_info = PipeInfo::default();
 | 
			
		||||
    let stdlib = crate::std::StdLib::new();
 | 
			
		||||
 | 
			
		||||
    // Iterate over the body of the program.
 | 
			
		||||
    for statement in &program.body {
 | 
			
		||||
@ -529,7 +584,8 @@ pub fn execute(
 | 
			
		||||
                            _ => (),
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if fn_name == "show" {
 | 
			
		||||
                    let _show_fn = Box::new(crate::std::Show);
 | 
			
		||||
                    if let Function::StdLib { func: _show_fn } = &call_expr.function {
 | 
			
		||||
                        if options != BodyType::Root {
 | 
			
		||||
                            return Err(KclError::Semantic(KclErrorDetails {
 | 
			
		||||
                                message: "Cannot call show outside of a root".to_string(),
 | 
			
		||||
@ -539,7 +595,9 @@ pub fn execute(
 | 
			
		||||
 | 
			
		||||
                        memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
 | 
			
		||||
                    } else if let Some(func) = memory.clone().root.get(&fn_name) {
 | 
			
		||||
                        func.call_fn(&args, memory, engine)?;
 | 
			
		||||
                        let result = func.call_fn(&args, memory, engine)?;
 | 
			
		||||
 | 
			
		||||
                        memory.return_ = result;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return Err(KclError::Semantic(KclErrorDetails {
 | 
			
		||||
                            message: format!("No such name {} defined", fn_name),
 | 
			
		||||
@ -563,7 +621,7 @@ pub fn execute(
 | 
			
		||||
                            memory.add(&var_name, value.clone(), source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::BinaryExpression(binary_expression) => {
 | 
			
		||||
                            let result = binary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
 | 
			
		||||
                            let result = binary_expression.get_result(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                            memory.add(&var_name, result, source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::FunctionExpression(function_expression) => {
 | 
			
		||||
@ -586,7 +644,7 @@ pub fn execute(
 | 
			
		||||
                                        for (index, param) in function_expression.params.iter().enumerate() {
 | 
			
		||||
                                            fn_memory.add(
 | 
			
		||||
                                                ¶m.name,
 | 
			
		||||
                                                args.clone().get(index).unwrap().clone(),
 | 
			
		||||
                                                args.get(index).unwrap().clone(),
 | 
			
		||||
                                                param.into(),
 | 
			
		||||
                                            )?;
 | 
			
		||||
                                        }
 | 
			
		||||
@ -600,11 +658,11 @@ pub fn execute(
 | 
			
		||||
                            )?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::CallExpression(call_expression) => {
 | 
			
		||||
                            let result = call_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
 | 
			
		||||
                            let result = call_expression.execute(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                            memory.add(&var_name, result, source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::PipeExpression(pipe_expression) => {
 | 
			
		||||
                            let result = pipe_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
 | 
			
		||||
                            let result = pipe_expression.get_result(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                            memory.add(&var_name, result, source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::PipeSubstitution(pipe_substitution) => {
 | 
			
		||||
@ -617,11 +675,11 @@ pub fn execute(
 | 
			
		||||
                            }));
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::ArrayExpression(array_expression) => {
 | 
			
		||||
                            let result = array_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
 | 
			
		||||
                            let result = array_expression.execute(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                            memory.add(&var_name, result, source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::ObjectExpression(object_expression) => {
 | 
			
		||||
                            let result = object_expression.execute(memory, &mut pipe_info, &stdlib, engine)?;
 | 
			
		||||
                            let result = object_expression.execute(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                            memory.add(&var_name, result, source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::MemberExpression(member_expression) => {
 | 
			
		||||
@ -629,7 +687,7 @@ pub fn execute(
 | 
			
		||||
                            memory.add(&var_name, result, source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::UnaryExpression(unary_expression) => {
 | 
			
		||||
                            let result = unary_expression.get_result(memory, &mut pipe_info, &stdlib, engine)?;
 | 
			
		||||
                            let result = unary_expression.get_result(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                            memory.add(&var_name, result, source_range)?;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@ -637,14 +695,42 @@ pub fn execute(
 | 
			
		||||
            }
 | 
			
		||||
            BodyItem::ReturnStatement(return_statement) => match &return_statement.argument {
 | 
			
		||||
                Value::BinaryExpression(bin_expr) => {
 | 
			
		||||
                    let result = bin_expr.get_result(memory, &mut pipe_info, &stdlib, engine)?;
 | 
			
		||||
                    let result = bin_expr.get_result(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(result));
 | 
			
		||||
                }
 | 
			
		||||
                Value::UnaryExpression(unary_expr) => {
 | 
			
		||||
                    let result = unary_expr.get_result(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(result));
 | 
			
		||||
                }
 | 
			
		||||
                Value::Identifier(identifier) => {
 | 
			
		||||
                    let value = memory.get(&identifier.name, identifier.into())?.clone();
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(value));
 | 
			
		||||
                }
 | 
			
		||||
                _ => (),
 | 
			
		||||
                Value::Literal(literal) => {
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(literal.into()));
 | 
			
		||||
                }
 | 
			
		||||
                Value::ArrayExpression(array_expr) => {
 | 
			
		||||
                    let result = array_expr.execute(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(result));
 | 
			
		||||
                }
 | 
			
		||||
                Value::ObjectExpression(obj_expr) => {
 | 
			
		||||
                    let result = obj_expr.execute(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(result));
 | 
			
		||||
                }
 | 
			
		||||
                Value::CallExpression(call_expr) => {
 | 
			
		||||
                    let result = call_expr.execute(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(result));
 | 
			
		||||
                }
 | 
			
		||||
                Value::MemberExpression(member_expr) => {
 | 
			
		||||
                    let result = member_expr.get_result(memory)?;
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(result));
 | 
			
		||||
                }
 | 
			
		||||
                Value::PipeExpression(pipe_expr) => {
 | 
			
		||||
                    let result = pipe_expr.get_result(memory, &mut pipe_info, engine)?;
 | 
			
		||||
                    memory.return_ = Some(ProgramReturn::Value(result));
 | 
			
		||||
                }
 | 
			
		||||
                Value::PipeSubstitution(_) => {}
 | 
			
		||||
                Value::FunctionExpression(_) => {}
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -660,7 +746,8 @@ mod tests {
 | 
			
		||||
 | 
			
		||||
    pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
 | 
			
		||||
        let tokens = crate::tokeniser::lexer(code);
 | 
			
		||||
        let program = crate::parser::abstract_syntax_tree(&tokens)?;
 | 
			
		||||
        let parser = crate::parser::Parser::new(tokens);
 | 
			
		||||
        let program = parser.ast()?;
 | 
			
		||||
        let mut mem: ProgramMemory = Default::default();
 | 
			
		||||
        let mut engine = EngineConnection::new().await?;
 | 
			
		||||
        let memory = execute(program, &mut mem, BodyType::Root, &mut engine)?;
 | 
			
		||||
@ -777,4 +864,138 @@ show(part001)"#;
 | 
			
		||||
 | 
			
		||||
        parse_execute(ast).await.unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_execute_with_inline_comment() {
 | 
			
		||||
        let ast = r#"const baseThick = 1
 | 
			
		||||
const armAngle = 60
 | 
			
		||||
 | 
			
		||||
const baseThickHalf = baseThick / 2
 | 
			
		||||
const halfArmAngle = armAngle / 2
 | 
			
		||||
 | 
			
		||||
const arrExpShouldNotBeIncluded = [1, 2, 3]
 | 
			
		||||
const objExpShouldNotBeIncluded = { a: 1, b: 2, c: 3 }
 | 
			
		||||
 | 
			
		||||
const part001 = startSketchAt([0, 0])
 | 
			
		||||
  |> yLineTo(1, %)
 | 
			
		||||
  |> xLine(3.84, %) // selection-range-7ish-before-this
 | 
			
		||||
 | 
			
		||||
const variableBelowShouldNotBeIncluded = 3
 | 
			
		||||
 | 
			
		||||
show(part001)"#;
 | 
			
		||||
 | 
			
		||||
        parse_execute(ast).await.unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_execute_with_function_literal_in_pipe() {
 | 
			
		||||
        let ast = r#"const w = 20
 | 
			
		||||
const l = 8
 | 
			
		||||
const h = 10
 | 
			
		||||
 | 
			
		||||
fn thing = () => {
 | 
			
		||||
  return -8
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const firstExtrude = startSketchAt([0,0])
 | 
			
		||||
  |> line([0, l], %)
 | 
			
		||||
  |> line([w, 0], %)
 | 
			
		||||
  |> line([0, thing()], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(h, %)
 | 
			
		||||
 | 
			
		||||
show(firstExtrude)"#;
 | 
			
		||||
 | 
			
		||||
        parse_execute(ast).await.unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_execute_with_function_unary_in_pipe() {
 | 
			
		||||
        let ast = r#"const w = 20
 | 
			
		||||
const l = 8
 | 
			
		||||
const h = 10
 | 
			
		||||
 | 
			
		||||
fn thing = (x) => {
 | 
			
		||||
  return -x
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const firstExtrude = startSketchAt([0,0])
 | 
			
		||||
  |> line([0, l], %)
 | 
			
		||||
  |> line([w, 0], %)
 | 
			
		||||
  |> line([0, thing(8)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(h, %)
 | 
			
		||||
 | 
			
		||||
show(firstExtrude)"#;
 | 
			
		||||
 | 
			
		||||
        parse_execute(ast).await.unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_execute_with_function_array_in_pipe() {
 | 
			
		||||
        let ast = r#"const w = 20
 | 
			
		||||
const l = 8
 | 
			
		||||
const h = 10
 | 
			
		||||
 | 
			
		||||
fn thing = (x) => {
 | 
			
		||||
  return [0, -x]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const firstExtrude = startSketchAt([0,0])
 | 
			
		||||
  |> line([0, l], %)
 | 
			
		||||
  |> line([w, 0], %)
 | 
			
		||||
  |> line(thing(8), %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(h, %)
 | 
			
		||||
 | 
			
		||||
show(firstExtrude)"#;
 | 
			
		||||
 | 
			
		||||
        parse_execute(ast).await.unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_execute_with_function_call_in_pipe() {
 | 
			
		||||
        let ast = r#"const w = 20
 | 
			
		||||
const l = 8
 | 
			
		||||
const h = 10
 | 
			
		||||
 | 
			
		||||
fn other_thing = (y) => {
 | 
			
		||||
  return -y
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn thing = (x) => {
 | 
			
		||||
  return other_thing(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const firstExtrude = startSketchAt([0,0])
 | 
			
		||||
  |> line([0, l], %)
 | 
			
		||||
  |> line([w, 0], %)
 | 
			
		||||
  |> line([0, thing(8)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
  |> extrude(h, %)
 | 
			
		||||
 | 
			
		||||
show(firstExtrude)"#;
 | 
			
		||||
 | 
			
		||||
        parse_execute(ast).await.unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_execute_with_function_sketch() {
 | 
			
		||||
        let ast = r#"const box = (h, l, w) => {
 | 
			
		||||
 const myBox = startSketchAt([0,0])
 | 
			
		||||
    |> line([0, l], %)
 | 
			
		||||
    |> line([w, 0], %)
 | 
			
		||||
    |> line([0, -l], %)
 | 
			
		||||
    |> close(%)
 | 
			
		||||
    |> extrude(h, %)
 | 
			
		||||
 | 
			
		||||
  return myBox
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const fnBox = box(3, 6, 10)
 | 
			
		||||
 | 
			
		||||
show(fnBox)"#;
 | 
			
		||||
 | 
			
		||||
        parse_execute(ast).await.unwrap();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,6 @@ pub mod errors;
 | 
			
		||||
pub mod executor;
 | 
			
		||||
pub mod math_parser;
 | 
			
		||||
pub mod parser;
 | 
			
		||||
pub mod recast;
 | 
			
		||||
pub mod server;
 | 
			
		||||
pub mod std;
 | 
			
		||||
pub mod tokeniser;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,347 +0,0 @@
 | 
			
		||||
//! Generates source code from the AST.
 | 
			
		||||
//! The inverse of parsing (which generates an AST from the source code)
 | 
			
		||||
 | 
			
		||||
use crate::abstract_syntax_tree_types::{
 | 
			
		||||
    ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, FunctionExpression, Literal,
 | 
			
		||||
    LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression, PipeExpression, Program, UnaryExpression,
 | 
			
		||||
    Value,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
fn recast_literal(literal: Literal) -> String {
 | 
			
		||||
    if let serde_json::Value::String(value) = literal.value {
 | 
			
		||||
        let quote = if literal.raw.trim().starts_with('"') { '"' } else { '\'' };
 | 
			
		||||
        format!("{}{}{}", quote, value, quote)
 | 
			
		||||
    } else {
 | 
			
		||||
        literal.value.to_string()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn precedence(operator: &str) -> u8 {
 | 
			
		||||
    match operator {
 | 
			
		||||
        "+" | "-" => 11,
 | 
			
		||||
        "*" | "/" | "%" => 12,
 | 
			
		||||
        _ => 0,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_binary_expression(expression: BinaryExpression) -> String {
 | 
			
		||||
    let maybe_wrap_it = |a: String, doit: bool| -> String {
 | 
			
		||||
        if doit {
 | 
			
		||||
            format!("({})", a)
 | 
			
		||||
        } else {
 | 
			
		||||
            a
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let should_wrap_right = match expression.right.clone() {
 | 
			
		||||
        BinaryPart::BinaryExpression(bin_exp) => {
 | 
			
		||||
            precedence(&expression.operator) > precedence(&bin_exp.operator) || expression.operator == "-"
 | 
			
		||||
        }
 | 
			
		||||
        _ => false,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let should_wrap_left = match expression.left.clone() {
 | 
			
		||||
        BinaryPart::BinaryExpression(bin_exp) => precedence(&expression.operator) > precedence(&bin_exp.operator),
 | 
			
		||||
        _ => false,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    format!(
 | 
			
		||||
        "{} {} {}",
 | 
			
		||||
        maybe_wrap_it(recast_binary_part(expression.left), should_wrap_left),
 | 
			
		||||
        expression.operator,
 | 
			
		||||
        maybe_wrap_it(recast_binary_part(expression.right), should_wrap_right)
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_binary_part(part: BinaryPart) -> String {
 | 
			
		||||
    match part {
 | 
			
		||||
        BinaryPart::Literal(literal) => recast_literal(*literal),
 | 
			
		||||
        BinaryPart::Identifier(identifier) => identifier.name,
 | 
			
		||||
        BinaryPart::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
 | 
			
		||||
        BinaryPart::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
 | 
			
		||||
        _ => String::new(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_value(node: Value, _indentation: String, is_in_pipe_expression: bool) -> String {
 | 
			
		||||
    let indentation = _indentation + if is_in_pipe_expression { "  " } else { "" };
 | 
			
		||||
    match node {
 | 
			
		||||
        Value::BinaryExpression(bin_exp) => recast_binary_expression(*bin_exp),
 | 
			
		||||
        Value::ArrayExpression(array_exp) => recast_array_expression(&array_exp, &indentation),
 | 
			
		||||
        Value::ObjectExpression(ref obj_exp) => recast_object_expression(obj_exp, &indentation, is_in_pipe_expression),
 | 
			
		||||
        Value::MemberExpression(mem_exp) => recast_member_expression(*mem_exp),
 | 
			
		||||
        Value::Literal(literal) => recast_literal(*literal),
 | 
			
		||||
        Value::FunctionExpression(func_exp) => recast_function(*func_exp),
 | 
			
		||||
        Value::CallExpression(call_exp) => recast_call_expression(&call_exp, &indentation, is_in_pipe_expression),
 | 
			
		||||
        Value::Identifier(ident) => ident.name,
 | 
			
		||||
        Value::PipeExpression(pipe_exp) => recast_pipe_expression(&pipe_exp),
 | 
			
		||||
        Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
 | 
			
		||||
        _ => String::new(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_array_expression(expression: &ArrayExpression, indentation: &str) -> String {
 | 
			
		||||
    let flat_recast = format!(
 | 
			
		||||
        "[{}]",
 | 
			
		||||
        expression
 | 
			
		||||
            .elements
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|el| recast_value(el.clone(), String::new(), false))
 | 
			
		||||
            .collect::<Vec<String>>()
 | 
			
		||||
            .join(", ")
 | 
			
		||||
    );
 | 
			
		||||
    let max_array_length = 40;
 | 
			
		||||
    if flat_recast.len() > max_array_length {
 | 
			
		||||
        let _indentation = indentation.to_string() + "  ";
 | 
			
		||||
        format!(
 | 
			
		||||
            "[\n{}{}\n{}]",
 | 
			
		||||
            _indentation,
 | 
			
		||||
            expression
 | 
			
		||||
                .elements
 | 
			
		||||
                .iter()
 | 
			
		||||
                .map(|el| recast_value(el.clone(), _indentation.clone(), false))
 | 
			
		||||
                .collect::<Vec<String>>()
 | 
			
		||||
                .join(format!(",\n{}", _indentation).as_str()),
 | 
			
		||||
            indentation
 | 
			
		||||
        )
 | 
			
		||||
    } else {
 | 
			
		||||
        flat_recast
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_object_expression(expression: &ObjectExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
 | 
			
		||||
    let flat_recast = format!(
 | 
			
		||||
        "{{ {} }}",
 | 
			
		||||
        expression
 | 
			
		||||
            .properties
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|prop| {
 | 
			
		||||
                format!(
 | 
			
		||||
                    "{}: {}",
 | 
			
		||||
                    prop.key.name,
 | 
			
		||||
                    recast_value(prop.value.clone(), String::new(), false)
 | 
			
		||||
                )
 | 
			
		||||
            })
 | 
			
		||||
            .collect::<Vec<String>>()
 | 
			
		||||
            .join(", ")
 | 
			
		||||
    );
 | 
			
		||||
    let max_array_length = 40;
 | 
			
		||||
    if flat_recast.len() > max_array_length {
 | 
			
		||||
        let _indentation = indentation.to_owned() + "  ";
 | 
			
		||||
        format!(
 | 
			
		||||
            "{{\n{}{}\n{}}}",
 | 
			
		||||
            _indentation,
 | 
			
		||||
            expression
 | 
			
		||||
                .properties
 | 
			
		||||
                .iter()
 | 
			
		||||
                .map(|prop| {
 | 
			
		||||
                    format!(
 | 
			
		||||
                        "{}: {}",
 | 
			
		||||
                        prop.key.name,
 | 
			
		||||
                        recast_value(prop.value.clone(), _indentation.clone(), is_in_pipe_expression)
 | 
			
		||||
                    )
 | 
			
		||||
                })
 | 
			
		||||
                .collect::<Vec<String>>()
 | 
			
		||||
                .join(format!(",\n{}", _indentation).as_str()),
 | 
			
		||||
            if is_in_pipe_expression { "    " } else { "" }
 | 
			
		||||
        )
 | 
			
		||||
    } else {
 | 
			
		||||
        flat_recast
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_call_expression(expression: &CallExpression, indentation: &str, is_in_pipe_expression: bool) -> String {
 | 
			
		||||
    format!(
 | 
			
		||||
        "{}({})",
 | 
			
		||||
        expression.callee.name,
 | 
			
		||||
        expression
 | 
			
		||||
            .arguments
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|arg| recast_argument(arg.clone(), indentation, is_in_pipe_expression))
 | 
			
		||||
            .collect::<Vec<String>>()
 | 
			
		||||
            .join(", ")
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_argument(argument: Value, indentation: &str, is_in_pipe_expression: bool) -> String {
 | 
			
		||||
    match argument {
 | 
			
		||||
        Value::Literal(literal) => recast_literal(*literal),
 | 
			
		||||
        Value::Identifier(identifier) => identifier.name,
 | 
			
		||||
        Value::BinaryExpression(binary_exp) => recast_binary_expression(*binary_exp),
 | 
			
		||||
        Value::ArrayExpression(array_exp) => recast_array_expression(&array_exp, indentation),
 | 
			
		||||
        Value::ObjectExpression(object_exp) => {
 | 
			
		||||
            recast_object_expression(&object_exp, indentation, is_in_pipe_expression)
 | 
			
		||||
        }
 | 
			
		||||
        Value::CallExpression(call_exp) => recast_call_expression(&call_exp, indentation, is_in_pipe_expression),
 | 
			
		||||
        Value::FunctionExpression(function_exp) => recast_function(*function_exp),
 | 
			
		||||
        Value::PipeSubstitution(_) => "%".to_string(),
 | 
			
		||||
        Value::UnaryExpression(unary_exp) => recast_unary_expression(*unary_exp),
 | 
			
		||||
        _ => String::new(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_member_expression(expression: MemberExpression) -> String {
 | 
			
		||||
    let key_str = match expression.property {
 | 
			
		||||
        LiteralIdentifier::Identifier(identifier) => {
 | 
			
		||||
            if expression.computed {
 | 
			
		||||
                format!("[{}]", &(*identifier.name))
 | 
			
		||||
            } else {
 | 
			
		||||
                format!(".{}", &(*identifier.name))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    match expression.object {
 | 
			
		||||
        MemberObject::MemberExpression(member_exp) => recast_member_expression(*member_exp) + key_str.as_str(),
 | 
			
		||||
        MemberObject::Identifier(identifier) => identifier.name + key_str.as_str(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_pipe_expression(expression: &PipeExpression) -> String {
 | 
			
		||||
    expression
 | 
			
		||||
        .body
 | 
			
		||||
        .iter()
 | 
			
		||||
        .enumerate()
 | 
			
		||||
        .map(|(index, statement)| {
 | 
			
		||||
            let mut indentation = "  ".to_string();
 | 
			
		||||
            let mut maybe_line_break = "\n".to_string();
 | 
			
		||||
            let mut str = recast_value(statement.clone(), indentation.clone(), true);
 | 
			
		||||
            let non_code_meta = expression.non_code_meta.clone();
 | 
			
		||||
            if let Some(non_code_meta_value) = non_code_meta.none_code_nodes.get(&index) {
 | 
			
		||||
                if non_code_meta_value.value != " " {
 | 
			
		||||
                    str += non_code_meta_value.value.as_str();
 | 
			
		||||
                    indentation = String::new();
 | 
			
		||||
                    maybe_line_break = String::new();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if index != expression.body.len() - 1 {
 | 
			
		||||
                str += maybe_line_break.as_str();
 | 
			
		||||
                str += indentation.as_str();
 | 
			
		||||
                str += "|> ".to_string().as_str();
 | 
			
		||||
            }
 | 
			
		||||
            str
 | 
			
		||||
        })
 | 
			
		||||
        .collect::<String>()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn recast_unary_expression(expression: UnaryExpression) -> String {
 | 
			
		||||
    let bin_part_val = match expression.argument {
 | 
			
		||||
        BinaryPart::Literal(literal) => Value::Literal(literal),
 | 
			
		||||
        BinaryPart::Identifier(identifier) => Value::Identifier(identifier),
 | 
			
		||||
        BinaryPart::BinaryExpression(binary_expression) => Value::BinaryExpression(binary_expression),
 | 
			
		||||
        BinaryPart::CallExpression(call_expression) => Value::CallExpression(call_expression),
 | 
			
		||||
        BinaryPart::UnaryExpression(unary_expression) => Value::UnaryExpression(unary_expression),
 | 
			
		||||
    };
 | 
			
		||||
    format!(
 | 
			
		||||
        "{}{}",
 | 
			
		||||
        expression.operator,
 | 
			
		||||
        recast_value(bin_part_val, String::new(), false)
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn recast(ast: &Program, indentation: &str, is_with_block: bool) -> String {
 | 
			
		||||
    ast.body
 | 
			
		||||
        .iter()
 | 
			
		||||
        .map(|statement| match statement.clone() {
 | 
			
		||||
            BodyItem::ExpressionStatement(expression_statement) => match expression_statement.expression {
 | 
			
		||||
                Value::BinaryExpression(binary_expression) => recast_binary_expression(*binary_expression),
 | 
			
		||||
                Value::ArrayExpression(array_expression) => recast_array_expression(&array_expression, ""),
 | 
			
		||||
                Value::ObjectExpression(object_expression) => recast_object_expression(&object_expression, "", false),
 | 
			
		||||
                Value::CallExpression(call_expression) => recast_call_expression(&call_expression, "", false),
 | 
			
		||||
                _ => "Expression".to_string(),
 | 
			
		||||
            },
 | 
			
		||||
            BodyItem::VariableDeclaration(variable_declaration) => variable_declaration
 | 
			
		||||
                .declarations
 | 
			
		||||
                .iter()
 | 
			
		||||
                .map(|declaration| {
 | 
			
		||||
                    format!(
 | 
			
		||||
                        "{} {} = {}",
 | 
			
		||||
                        variable_declaration.kind,
 | 
			
		||||
                        declaration.id.name,
 | 
			
		||||
                        recast_value(declaration.init.clone(), String::new(), false)
 | 
			
		||||
                    )
 | 
			
		||||
                })
 | 
			
		||||
                .collect::<String>(),
 | 
			
		||||
            BodyItem::ReturnStatement(return_statement) => {
 | 
			
		||||
                format!("return {}", recast_argument(return_statement.argument, "", false))
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        .enumerate()
 | 
			
		||||
        .map(|(index, recast_str)| {
 | 
			
		||||
            let is_legit_custom_whitespace_or_comment = |str: String| str != " " && str != "\n" && str != "  ";
 | 
			
		||||
 | 
			
		||||
            // determine the value of startString
 | 
			
		||||
            let last_white_space_or_comment = if index > 0 {
 | 
			
		||||
                let tmp = if let Some(non_code_node) = ast.non_code_meta.none_code_nodes.get(&(index - 1)) {
 | 
			
		||||
                    non_code_node.value.clone()
 | 
			
		||||
                } else {
 | 
			
		||||
                    " ".to_string()
 | 
			
		||||
                };
 | 
			
		||||
                tmp
 | 
			
		||||
            } else {
 | 
			
		||||
                " ".to_string()
 | 
			
		||||
            };
 | 
			
		||||
            // indentation of this line will be covered by the previous if we're using a custom whitespace or comment
 | 
			
		||||
            let mut start_string = if is_legit_custom_whitespace_or_comment(last_white_space_or_comment) {
 | 
			
		||||
                String::new()
 | 
			
		||||
            } else {
 | 
			
		||||
                indentation.to_owned()
 | 
			
		||||
            };
 | 
			
		||||
            if index == 0 {
 | 
			
		||||
                if let Some(start) = ast.non_code_meta.start.clone() {
 | 
			
		||||
                    start_string = start.value;
 | 
			
		||||
                } else {
 | 
			
		||||
                    start_string = indentation.to_owned();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if start_string.ends_with('\n') {
 | 
			
		||||
                start_string += indentation;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // determine the value of endString
 | 
			
		||||
            let maybe_line_break: String = if index == ast.body.len() - 1 && !is_with_block {
 | 
			
		||||
                String::new()
 | 
			
		||||
            } else {
 | 
			
		||||
                "\n".to_string()
 | 
			
		||||
            };
 | 
			
		||||
            let mut custom_white_space_or_comment = match ast.non_code_meta.none_code_nodes.get(&index) {
 | 
			
		||||
                Some(custom_white_space_or_comment) => custom_white_space_or_comment.value.clone(),
 | 
			
		||||
                None => String::new(),
 | 
			
		||||
            };
 | 
			
		||||
            if !is_legit_custom_whitespace_or_comment(custom_white_space_or_comment.clone()) {
 | 
			
		||||
                custom_white_space_or_comment = String::new();
 | 
			
		||||
            }
 | 
			
		||||
            let end_string = if custom_white_space_or_comment.is_empty() {
 | 
			
		||||
                maybe_line_break
 | 
			
		||||
            } else {
 | 
			
		||||
                custom_white_space_or_comment
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            format!("{}{}{}", start_string, recast_str, end_string)
 | 
			
		||||
        })
 | 
			
		||||
        .collect::<String>()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn recast_function(expression: FunctionExpression) -> String {
 | 
			
		||||
    format!(
 | 
			
		||||
        "({}) => {{{}}}",
 | 
			
		||||
        expression
 | 
			
		||||
            .params
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|param| param.name.clone())
 | 
			
		||||
            .collect::<Vec<String>>()
 | 
			
		||||
            .join(", "),
 | 
			
		||||
        recast(
 | 
			
		||||
            &Program {
 | 
			
		||||
                start: expression.body.start,
 | 
			
		||||
                end: expression.body.start,
 | 
			
		||||
                body: expression.body.body,
 | 
			
		||||
                non_code_meta: expression.body.non_code_meta
 | 
			
		||||
            },
 | 
			
		||||
            "",
 | 
			
		||||
            true
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										672
									
								
								src/wasm-lib/kcl/src/server/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										672
									
								
								src/wasm-lib/kcl/src/server/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,672 @@
 | 
			
		||||
//! Functions for the `kcl` lsp server.
 | 
			
		||||
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use dashmap::DashMap;
 | 
			
		||||
use tower_lsp::{jsonrpc::Result as RpcResult, lsp_types::*, Client, LanguageServer};
 | 
			
		||||
 | 
			
		||||
use crate::{abstract_syntax_tree_types::VariableKind, executor::SourceRange, parser::PIPE_OPERATOR};
 | 
			
		||||
 | 
			
		||||
/// A subcommand for running the server.
 | 
			
		||||
#[derive(Parser, Clone, Debug)]
 | 
			
		||||
pub struct Server {
 | 
			
		||||
    /// Port that the server should listen
 | 
			
		||||
    #[clap(long, default_value = "8080")]
 | 
			
		||||
    pub socket: i32,
 | 
			
		||||
 | 
			
		||||
    /// Listen over stdin and stdout instead of a tcp socket.
 | 
			
		||||
    #[clap(short, long, default_value = "false")]
 | 
			
		||||
    pub stdio: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The lsp server backend.
 | 
			
		||||
pub struct Backend {
 | 
			
		||||
    /// The client for the backend.
 | 
			
		||||
    pub client: Client,
 | 
			
		||||
    /// The stdlib completions for the language.
 | 
			
		||||
    pub stdlib_completions: HashMap<String, CompletionItem>,
 | 
			
		||||
    /// The stdlib signatures for the language.
 | 
			
		||||
    pub stdlib_signatures: HashMap<String, SignatureHelp>,
 | 
			
		||||
    /// The types of tokens the server supports.
 | 
			
		||||
    pub token_types: Vec<SemanticTokenType>,
 | 
			
		||||
    /// Token maps.
 | 
			
		||||
    pub token_map: DashMap<String, Vec<crate::tokeniser::Token>>,
 | 
			
		||||
    /// AST maps.
 | 
			
		||||
    pub ast_map: DashMap<String, crate::abstract_syntax_tree_types::Program>,
 | 
			
		||||
    /// Current code.
 | 
			
		||||
    pub current_code_map: DashMap<String, String>,
 | 
			
		||||
    /// Diagnostics.
 | 
			
		||||
    pub diagnostics_map: DashMap<String, DocumentDiagnosticReport>,
 | 
			
		||||
    /// Symbols map.
 | 
			
		||||
    pub symbols_map: DashMap<String, Vec<DocumentSymbol>>,
 | 
			
		||||
    /// Semantic tokens map.
 | 
			
		||||
    pub semantic_tokens_map: DashMap<String, Vec<SemanticToken>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Backend {
 | 
			
		||||
    fn get_semantic_token_type_index(&self, token_type: SemanticTokenType) -> Option<usize> {
 | 
			
		||||
        self.token_types.iter().position(|x| *x == token_type)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn on_change(&self, params: TextDocumentItem) {
 | 
			
		||||
        // Lets update the tokens.
 | 
			
		||||
        self.current_code_map
 | 
			
		||||
            .insert(params.uri.to_string(), params.text.clone());
 | 
			
		||||
        let tokens = crate::tokeniser::lexer(¶ms.text);
 | 
			
		||||
        self.token_map.insert(params.uri.to_string(), tokens.clone());
 | 
			
		||||
 | 
			
		||||
        // Update the semantic tokens map.
 | 
			
		||||
        let mut semantic_tokens = vec![];
 | 
			
		||||
        let mut last_position = Position::new(0, 0);
 | 
			
		||||
        for token in &tokens {
 | 
			
		||||
            let Ok(mut token_type) = SemanticTokenType::try_from(token.token_type) else {
 | 
			
		||||
                // We continue here because not all tokens can be converted this way, we will get
 | 
			
		||||
                // the rest from the ast.
 | 
			
		||||
                continue;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if token.token_type == crate::tokeniser::TokenType::Word
 | 
			
		||||
                && self.stdlib_completions.contains_key(&token.value)
 | 
			
		||||
            {
 | 
			
		||||
                // This is a stdlib function.
 | 
			
		||||
                token_type = SemanticTokenType::FUNCTION;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let token_type_index = match self.get_semantic_token_type_index(token_type.clone()) {
 | 
			
		||||
                Some(index) => index,
 | 
			
		||||
                // This is actually bad this should not fail.
 | 
			
		||||
                // TODO: ensure we never get here.
 | 
			
		||||
                None => {
 | 
			
		||||
                    self.client
 | 
			
		||||
                        .log_message(
 | 
			
		||||
                            MessageType::INFO,
 | 
			
		||||
                            format!("token type `{:?}` not accounted for", token_type),
 | 
			
		||||
                        )
 | 
			
		||||
                        .await;
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let source_range: SourceRange = token.clone().into();
 | 
			
		||||
            let position = source_range.start_to_lsp_position(¶ms.text);
 | 
			
		||||
 | 
			
		||||
            let semantic_token = SemanticToken {
 | 
			
		||||
                delta_line: position.line - last_position.line,
 | 
			
		||||
                delta_start: if position.line != last_position.line {
 | 
			
		||||
                    position.character
 | 
			
		||||
                } else {
 | 
			
		||||
                    position.character - last_position.character
 | 
			
		||||
                },
 | 
			
		||||
                length: token.value.len() as u32,
 | 
			
		||||
                token_type: token_type_index as u32,
 | 
			
		||||
                token_modifiers_bitset: 0,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            semantic_tokens.push(semantic_token);
 | 
			
		||||
 | 
			
		||||
            last_position = position;
 | 
			
		||||
        }
 | 
			
		||||
        self.semantic_tokens_map.insert(params.uri.to_string(), semantic_tokens);
 | 
			
		||||
 | 
			
		||||
        // Lets update the ast.
 | 
			
		||||
        let parser = crate::parser::Parser::new(tokens);
 | 
			
		||||
        let result = parser.ast();
 | 
			
		||||
        let ast = match result {
 | 
			
		||||
            Ok(ast) => ast,
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                let diagnostic = e.to_lsp_diagnostic(¶ms.text);
 | 
			
		||||
                // We got errors, update the diagnostics.
 | 
			
		||||
                self.diagnostics_map.insert(
 | 
			
		||||
                    params.uri.to_string(),
 | 
			
		||||
                    DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
 | 
			
		||||
                        related_documents: None,
 | 
			
		||||
                        full_document_diagnostic_report: FullDocumentDiagnosticReport {
 | 
			
		||||
                            result_id: None,
 | 
			
		||||
                            items: vec![diagnostic.clone()],
 | 
			
		||||
                        },
 | 
			
		||||
                    }),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Publish the diagnostic.
 | 
			
		||||
                // If the client supports it.
 | 
			
		||||
                self.client
 | 
			
		||||
                    .publish_diagnostics(params.uri, vec![diagnostic], None)
 | 
			
		||||
                    .await;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Update the symbols map.
 | 
			
		||||
        self.symbols_map
 | 
			
		||||
            .insert(params.uri.to_string(), ast.get_lsp_symbols(¶ms.text));
 | 
			
		||||
 | 
			
		||||
        self.ast_map.insert(params.uri.to_string(), ast);
 | 
			
		||||
        // Lets update the diagnostics, since we got no errors.
 | 
			
		||||
        self.diagnostics_map.insert(
 | 
			
		||||
            params.uri.to_string(),
 | 
			
		||||
            DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
 | 
			
		||||
                related_documents: None,
 | 
			
		||||
                full_document_diagnostic_report: FullDocumentDiagnosticReport {
 | 
			
		||||
                    result_id: None,
 | 
			
		||||
                    items: vec![],
 | 
			
		||||
                },
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Publish the diagnostic, we reset it here so the client knows the code compiles now.
 | 
			
		||||
        // If the client supports it.
 | 
			
		||||
        self.client.publish_diagnostics(params.uri.clone(), vec![], None).await;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec<CompletionItem> {
 | 
			
		||||
        let mut completions = vec![];
 | 
			
		||||
 | 
			
		||||
        let ast = match self.ast_map.get(file_name) {
 | 
			
		||||
            Some(ast) => ast,
 | 
			
		||||
            None => return completions,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        for item in &ast.body {
 | 
			
		||||
            match item {
 | 
			
		||||
                crate::abstract_syntax_tree_types::BodyItem::ExpressionStatement(_) => continue,
 | 
			
		||||
                crate::abstract_syntax_tree_types::BodyItem::ReturnStatement(_) => continue,
 | 
			
		||||
                crate::abstract_syntax_tree_types::BodyItem::VariableDeclaration(variable) => {
 | 
			
		||||
                    // We only want to complete variables.
 | 
			
		||||
                    for declaration in &variable.declarations {
 | 
			
		||||
                        completions.push(CompletionItem {
 | 
			
		||||
                            label: declaration.id.name.to_string(),
 | 
			
		||||
                            label_details: None,
 | 
			
		||||
                            kind: Some(match variable.kind {
 | 
			
		||||
                                crate::abstract_syntax_tree_types::VariableKind::Let => CompletionItemKind::VARIABLE,
 | 
			
		||||
                                crate::abstract_syntax_tree_types::VariableKind::Const => CompletionItemKind::CONSTANT,
 | 
			
		||||
                                crate::abstract_syntax_tree_types::VariableKind::Var => CompletionItemKind::VARIABLE,
 | 
			
		||||
                                crate::abstract_syntax_tree_types::VariableKind::Fn => CompletionItemKind::FUNCTION,
 | 
			
		||||
                            }),
 | 
			
		||||
                            detail: Some(variable.kind.to_string()),
 | 
			
		||||
                            documentation: None,
 | 
			
		||||
                            deprecated: None,
 | 
			
		||||
                            preselect: None,
 | 
			
		||||
                            sort_text: None,
 | 
			
		||||
                            filter_text: None,
 | 
			
		||||
                            insert_text: None,
 | 
			
		||||
                            insert_text_format: None,
 | 
			
		||||
                            insert_text_mode: None,
 | 
			
		||||
                            text_edit: None,
 | 
			
		||||
                            additional_text_edits: None,
 | 
			
		||||
                            command: None,
 | 
			
		||||
                            commit_characters: None,
 | 
			
		||||
                            data: None,
 | 
			
		||||
                            tags: None,
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        completions
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tower_lsp::async_trait]
 | 
			
		||||
impl LanguageServer for Backend {
 | 
			
		||||
    async fn initialize(&self, params: InitializeParams) -> RpcResult<InitializeResult> {
 | 
			
		||||
        self.client
 | 
			
		||||
            .log_message(MessageType::INFO, format!("initialize: {:?}", params))
 | 
			
		||||
            .await;
 | 
			
		||||
 | 
			
		||||
        Ok(InitializeResult {
 | 
			
		||||
            capabilities: ServerCapabilities {
 | 
			
		||||
                completion_provider: Some(CompletionOptions {
 | 
			
		||||
                    resolve_provider: Some(false),
 | 
			
		||||
                    trigger_characters: Some(vec![".".to_string()]),
 | 
			
		||||
                    work_done_progress_options: Default::default(),
 | 
			
		||||
                    all_commit_characters: None,
 | 
			
		||||
                    ..Default::default()
 | 
			
		||||
                }),
 | 
			
		||||
                diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
 | 
			
		||||
                    ..Default::default()
 | 
			
		||||
                })),
 | 
			
		||||
                document_formatting_provider: Some(OneOf::Left(true)),
 | 
			
		||||
                document_symbol_provider: Some(OneOf::Left(true)),
 | 
			
		||||
                hover_provider: Some(HoverProviderCapability::Simple(true)),
 | 
			
		||||
                inlay_hint_provider: Some(OneOf::Left(true)),
 | 
			
		||||
                rename_provider: Some(OneOf::Left(true)),
 | 
			
		||||
                semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
 | 
			
		||||
                    SemanticTokensRegistrationOptions {
 | 
			
		||||
                        text_document_registration_options: {
 | 
			
		||||
                            TextDocumentRegistrationOptions {
 | 
			
		||||
                                document_selector: Some(vec![DocumentFilter {
 | 
			
		||||
                                    language: Some("kcl".to_string()),
 | 
			
		||||
                                    scheme: Some("file".to_string()),
 | 
			
		||||
                                    pattern: None,
 | 
			
		||||
                                }]),
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        semantic_tokens_options: SemanticTokensOptions {
 | 
			
		||||
                            work_done_progress_options: WorkDoneProgressOptions::default(),
 | 
			
		||||
                            legend: SemanticTokensLegend {
 | 
			
		||||
                                token_types: self.token_types.clone(),
 | 
			
		||||
                                token_modifiers: vec![],
 | 
			
		||||
                            },
 | 
			
		||||
                            range: Some(false),
 | 
			
		||||
                            full: Some(SemanticTokensFullOptions::Bool(true)),
 | 
			
		||||
                        },
 | 
			
		||||
                        static_registration_options: StaticRegistrationOptions::default(),
 | 
			
		||||
                    },
 | 
			
		||||
                )),
 | 
			
		||||
                signature_help_provider: Some(SignatureHelpOptions {
 | 
			
		||||
                    trigger_characters: None,
 | 
			
		||||
                    retrigger_characters: None,
 | 
			
		||||
                    ..Default::default()
 | 
			
		||||
                }),
 | 
			
		||||
                text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
 | 
			
		||||
                    open_close: Some(true),
 | 
			
		||||
                    change: Some(TextDocumentSyncKind::FULL),
 | 
			
		||||
                    ..Default::default()
 | 
			
		||||
                })),
 | 
			
		||||
                workspace: Some(WorkspaceServerCapabilities {
 | 
			
		||||
                    workspace_folders: Some(WorkspaceFoldersServerCapabilities {
 | 
			
		||||
                        supported: Some(true),
 | 
			
		||||
                        change_notifications: Some(OneOf::Left(true)),
 | 
			
		||||
                    }),
 | 
			
		||||
                    file_operations: None,
 | 
			
		||||
                }),
 | 
			
		||||
                ..Default::default()
 | 
			
		||||
            },
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn initialized(&self, params: InitializedParams) {
 | 
			
		||||
        self.client
 | 
			
		||||
            .log_message(MessageType::INFO, format!("initialized: {:?}", params))
 | 
			
		||||
            .await;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn shutdown(&self) -> RpcResult<()> {
 | 
			
		||||
        self.client.log_message(MessageType::INFO, "shutdown".to_string()).await;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
 | 
			
		||||
        self.client
 | 
			
		||||
            .log_message(MessageType::INFO, "workspace folders changed!")
 | 
			
		||||
            .await;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
 | 
			
		||||
        self.client
 | 
			
		||||
            .log_message(MessageType::INFO, "configuration changed!")
 | 
			
		||||
            .await;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {
 | 
			
		||||
        self.client
 | 
			
		||||
            .log_message(MessageType::INFO, "watched files have changed!")
 | 
			
		||||
            .await;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn did_open(&self, params: DidOpenTextDocumentParams) {
 | 
			
		||||
        self.on_change(TextDocumentItem {
 | 
			
		||||
            uri: params.text_document.uri,
 | 
			
		||||
            text: params.text_document.text,
 | 
			
		||||
            version: params.text_document.version,
 | 
			
		||||
            language_id: params.text_document.language_id,
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn did_change(&self, mut params: DidChangeTextDocumentParams) {
 | 
			
		||||
        self.on_change(TextDocumentItem {
 | 
			
		||||
            uri: params.text_document.uri,
 | 
			
		||||
            text: std::mem::take(&mut params.content_changes[0].text),
 | 
			
		||||
            version: params.text_document.version,
 | 
			
		||||
            language_id: Default::default(),
 | 
			
		||||
        })
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn did_save(&self, params: DidSaveTextDocumentParams) {
 | 
			
		||||
        if let Some(text) = params.text {
 | 
			
		||||
            self.on_change(TextDocumentItem {
 | 
			
		||||
                uri: params.text_document.uri,
 | 
			
		||||
                text,
 | 
			
		||||
                version: Default::default(),
 | 
			
		||||
                language_id: Default::default(),
 | 
			
		||||
            })
 | 
			
		||||
            .await
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn did_close(&self, _: DidCloseTextDocumentParams) {
 | 
			
		||||
        self.client.log_message(MessageType::INFO, "file closed!").await;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {
 | 
			
		||||
        let filename = params.text_document_position_params.text_document.uri.to_string();
 | 
			
		||||
 | 
			
		||||
        let Some(current_code) = self.current_code_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code);
 | 
			
		||||
 | 
			
		||||
        // Let's iterate over the AST and find the node that contains the cursor.
 | 
			
		||||
        let Some(ast) = self.ast_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Some(value) = ast.get_value_for_position(pos) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        match hover {
 | 
			
		||||
            crate::abstract_syntax_tree_types::Hover::Function { name, range } => {
 | 
			
		||||
                // Get the docs for this function.
 | 
			
		||||
                let Some(completion) = self.stdlib_completions.get(&name) else {
 | 
			
		||||
                    return Ok(None);
 | 
			
		||||
                };
 | 
			
		||||
                let Some(docs) = &completion.documentation else {
 | 
			
		||||
                    return Ok(None);
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                let docs = match docs {
 | 
			
		||||
                    Documentation::String(docs) => docs,
 | 
			
		||||
                    Documentation::MarkupContent(MarkupContent { value, .. }) => value,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                let Some(label_details) = &completion.label_details else {
 | 
			
		||||
                    return Ok(None);
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                Ok(Some(Hover {
 | 
			
		||||
                    contents: HoverContents::Markup(MarkupContent {
 | 
			
		||||
                        kind: MarkupKind::Markdown,
 | 
			
		||||
                        value: format!(
 | 
			
		||||
                            "```{}{}```\n{}",
 | 
			
		||||
                            name,
 | 
			
		||||
                            label_details.detail.clone().unwrap_or_default(),
 | 
			
		||||
                            docs
 | 
			
		||||
                        ),
 | 
			
		||||
                    }),
 | 
			
		||||
                    range: Some(range),
 | 
			
		||||
                }))
 | 
			
		||||
            }
 | 
			
		||||
            crate::abstract_syntax_tree_types::Hover::Signature { .. } => Ok(None),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
 | 
			
		||||
        let mut completions = vec![CompletionItem {
 | 
			
		||||
            label: PIPE_OPERATOR.to_string(),
 | 
			
		||||
            label_details: None,
 | 
			
		||||
            kind: Some(CompletionItemKind::OPERATOR),
 | 
			
		||||
            detail: Some("A pipe operator.".to_string()),
 | 
			
		||||
            documentation: Some(Documentation::MarkupContent(MarkupContent {
 | 
			
		||||
                kind: MarkupKind::Markdown,
 | 
			
		||||
                value: "A pipe operator.".to_string(),
 | 
			
		||||
            })),
 | 
			
		||||
            deprecated: Some(false),
 | 
			
		||||
            preselect: None,
 | 
			
		||||
            sort_text: None,
 | 
			
		||||
            filter_text: None,
 | 
			
		||||
            insert_text: Some("|> ".to_string()),
 | 
			
		||||
            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
 | 
			
		||||
            insert_text_mode: None,
 | 
			
		||||
            text_edit: None,
 | 
			
		||||
            additional_text_edits: None,
 | 
			
		||||
            command: None,
 | 
			
		||||
            commit_characters: None,
 | 
			
		||||
            data: None,
 | 
			
		||||
            tags: None,
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
        completions.extend(self.stdlib_completions.values().cloned());
 | 
			
		||||
 | 
			
		||||
        // Get our variables from our AST to include in our completions.
 | 
			
		||||
        completions.extend(
 | 
			
		||||
            self.completions_get_variables_from_ast(params.text_document_position.text_document.uri.as_ref())
 | 
			
		||||
                .await,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Ok(Some(CompletionResponse::Array(completions)))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn diagnostic(&self, params: DocumentDiagnosticParams) -> RpcResult<DocumentDiagnosticReportResult> {
 | 
			
		||||
        let filename = params.text_document.uri.to_string();
 | 
			
		||||
 | 
			
		||||
        // Get the current diagnostics for this file.
 | 
			
		||||
        let Some(diagnostic) = self.diagnostics_map.get(&filename) else {
 | 
			
		||||
            // Send an empty report.
 | 
			
		||||
            return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
 | 
			
		||||
                RelatedFullDocumentDiagnosticReport {
 | 
			
		||||
                    related_documents: None,
 | 
			
		||||
                    full_document_diagnostic_report: FullDocumentDiagnosticReport {
 | 
			
		||||
                        result_id: None,
 | 
			
		||||
                        items: vec![],
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            )));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Ok(DocumentDiagnosticReportResult::Report(diagnostic.clone()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn signature_help(&self, params: SignatureHelpParams) -> RpcResult<Option<SignatureHelp>> {
 | 
			
		||||
        let filename = params.text_document_position_params.text_document.uri.to_string();
 | 
			
		||||
 | 
			
		||||
        let Some(current_code) = self.current_code_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let pos = position_to_char_index(params.text_document_position_params.position, ¤t_code);
 | 
			
		||||
 | 
			
		||||
        // Let's iterate over the AST and find the node that contains the cursor.
 | 
			
		||||
        let Some(ast) = self.ast_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Some(value) = ast.get_value_for_position(pos) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let Some(hover) = value.get_hover_value_for_position(pos, ¤t_code) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        match hover {
 | 
			
		||||
            crate::abstract_syntax_tree_types::Hover::Function { name, range: _ } => {
 | 
			
		||||
                // Get the docs for this function.
 | 
			
		||||
                let Some(signature) = self.stdlib_signatures.get(&name) else {
 | 
			
		||||
                    return Ok(None);
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                Ok(Some(signature.clone()))
 | 
			
		||||
            }
 | 
			
		||||
            crate::abstract_syntax_tree_types::Hover::Signature {
 | 
			
		||||
                name,
 | 
			
		||||
                parameter_index,
 | 
			
		||||
                range: _,
 | 
			
		||||
            } => {
 | 
			
		||||
                let Some(signature) = self.stdlib_signatures.get(&name) else {
 | 
			
		||||
                    return Ok(None);
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                let mut signature = signature.clone();
 | 
			
		||||
 | 
			
		||||
                signature.active_parameter = Some(parameter_index);
 | 
			
		||||
 | 
			
		||||
                Ok(Some(signature.clone()))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn inlay_hint(&self, _params: InlayHintParams) -> RpcResult<Option<Vec<InlayHint>>> {
 | 
			
		||||
        // TODO: do this
 | 
			
		||||
 | 
			
		||||
        Ok(None)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn semantic_tokens_full(&self, params: SemanticTokensParams) -> RpcResult<Option<SemanticTokensResult>> {
 | 
			
		||||
        let filename = params.text_document.uri.to_string();
 | 
			
		||||
 | 
			
		||||
        let Some(semantic_tokens) = self.semantic_tokens_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
 | 
			
		||||
            result_id: None,
 | 
			
		||||
            data: semantic_tokens.clone(),
 | 
			
		||||
        })))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn document_symbol(&self, params: DocumentSymbolParams) -> RpcResult<Option<DocumentSymbolResponse>> {
 | 
			
		||||
        let filename = params.text_document.uri.to_string();
 | 
			
		||||
 | 
			
		||||
        let Some(symbols) = self.symbols_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Ok(Some(DocumentSymbolResponse::Nested(symbols.clone())))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn formatting(&self, params: DocumentFormattingParams) -> RpcResult<Option<Vec<TextEdit>>> {
 | 
			
		||||
        let filename = params.text_document.uri.to_string();
 | 
			
		||||
 | 
			
		||||
        let Some(current_code) = self.current_code_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Parse the ast.
 | 
			
		||||
        // I don't know if we need to do this again since it should be updated in the context.
 | 
			
		||||
        // But I figure better safe than sorry since this will write back out to the file.
 | 
			
		||||
        let tokens = crate::tokeniser::lexer(¤t_code);
 | 
			
		||||
        let parser = crate::parser::Parser::new(tokens);
 | 
			
		||||
        let Ok(ast) = parser.ast() else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
        // Now recast it.
 | 
			
		||||
        let recast = ast.recast(
 | 
			
		||||
            &crate::abstract_syntax_tree_types::FormatOptions {
 | 
			
		||||
                tab_size: params.options.tab_size as usize,
 | 
			
		||||
                insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
 | 
			
		||||
                use_tabs: !params.options.insert_spaces,
 | 
			
		||||
            },
 | 
			
		||||
            0,
 | 
			
		||||
        );
 | 
			
		||||
        let source_range = SourceRange([0, current_code.len() - 1]);
 | 
			
		||||
        let range = source_range.to_lsp_range(¤t_code);
 | 
			
		||||
        Ok(Some(vec![TextEdit {
 | 
			
		||||
            new_text: recast,
 | 
			
		||||
            range,
 | 
			
		||||
        }]))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
 | 
			
		||||
        let filename = params.text_document_position.text_document.uri.to_string();
 | 
			
		||||
 | 
			
		||||
        let Some(current_code) = self.current_code_map.get(&filename) else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Parse the ast.
 | 
			
		||||
        // I don't know if we need to do this again since it should be updated in the context.
 | 
			
		||||
        // But I figure better safe than sorry since this will write back out to the file.
 | 
			
		||||
        let tokens = crate::tokeniser::lexer(¤t_code);
 | 
			
		||||
        let parser = crate::parser::Parser::new(tokens);
 | 
			
		||||
        let Ok(mut ast) = parser.ast() else {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Let's convert the position to a character index.
 | 
			
		||||
        let pos = position_to_char_index(params.text_document_position.position, ¤t_code);
 | 
			
		||||
        // Now let's perform the rename on the ast.
 | 
			
		||||
        ast.rename_symbol(¶ms.new_name, pos);
 | 
			
		||||
        // Now recast it.
 | 
			
		||||
        let recast = ast.recast(&Default::default(), 0);
 | 
			
		||||
        let source_range = SourceRange([0, current_code.len() - 1]);
 | 
			
		||||
        let range = source_range.to_lsp_range(¤t_code);
 | 
			
		||||
        Ok(Some(WorkspaceEdit {
 | 
			
		||||
            changes: Some(HashMap::from([(
 | 
			
		||||
                params.text_document_position.text_document.uri,
 | 
			
		||||
                vec![TextEdit {
 | 
			
		||||
                    new_text: recast,
 | 
			
		||||
                    range,
 | 
			
		||||
                }],
 | 
			
		||||
            )])),
 | 
			
		||||
            document_changes: None,
 | 
			
		||||
            change_annotations: None,
 | 
			
		||||
        }))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get completions from our stdlib.
 | 
			
		||||
pub fn get_completions_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, CompletionItem>> {
 | 
			
		||||
    let mut completions = HashMap::new();
 | 
			
		||||
 | 
			
		||||
    for internal_fn in stdlib.fns.values() {
 | 
			
		||||
        completions.insert(internal_fn.name(), internal_fn.to_completion_item());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let variable_kinds = VariableKind::to_completion_items()?;
 | 
			
		||||
    for variable_kind in variable_kinds {
 | 
			
		||||
        completions.insert(variable_kind.label.clone(), variable_kind);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(completions)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get signatures from our stdlib.
 | 
			
		||||
pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMap<String, SignatureHelp>> {
 | 
			
		||||
    let mut signatures = HashMap::new();
 | 
			
		||||
 | 
			
		||||
    for internal_fn in stdlib.fns.values() {
 | 
			
		||||
        signatures.insert(internal_fn.name(), internal_fn.to_signature_help());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let show = SignatureHelp {
 | 
			
		||||
        signatures: vec![SignatureInformation {
 | 
			
		||||
            label: "show".to_string(),
 | 
			
		||||
            documentation: Some(Documentation::MarkupContent(MarkupContent {
 | 
			
		||||
                kind: MarkupKind::PlainText,
 | 
			
		||||
                value: "Show a model.".to_string(),
 | 
			
		||||
            })),
 | 
			
		||||
            parameters: Some(vec![ParameterInformation {
 | 
			
		||||
                label: ParameterLabel::Simple("sg: SketchGroup".to_string()),
 | 
			
		||||
                documentation: Some(Documentation::MarkupContent(MarkupContent {
 | 
			
		||||
                    kind: MarkupKind::PlainText,
 | 
			
		||||
                    value: "A sketch group.".to_string(),
 | 
			
		||||
                })),
 | 
			
		||||
            }]),
 | 
			
		||||
            active_parameter: None,
 | 
			
		||||
        }],
 | 
			
		||||
        active_signature: Some(0),
 | 
			
		||||
        active_parameter: None,
 | 
			
		||||
    };
 | 
			
		||||
    signatures.insert("show".to_string(), show);
 | 
			
		||||
 | 
			
		||||
    Ok(signatures)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Convert a position to a character index from the start of the file.
 | 
			
		||||
fn position_to_char_index(position: Position, code: &str) -> usize {
 | 
			
		||||
    // Get the character position from the start of the file.
 | 
			
		||||
    let mut char_position = 0;
 | 
			
		||||
    for (index, line) in code.lines().enumerate() {
 | 
			
		||||
        if index == position.line as usize {
 | 
			
		||||
            char_position += position.character as usize;
 | 
			
		||||
            break;
 | 
			
		||||
        } else {
 | 
			
		||||
            char_position += line.len() + 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    char_position
 | 
			
		||||
}
 | 
			
		||||
@ -5,9 +5,6 @@ pub mod segment;
 | 
			
		||||
pub mod sketch;
 | 
			
		||||
pub mod utils;
 | 
			
		||||
 | 
			
		||||
// TODO: Something that would be nice is if we could generate docs for Kcl based on the
 | 
			
		||||
// actual stdlib functions below.
 | 
			
		||||
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
@ -23,18 +20,17 @@ use crate::{
 | 
			
		||||
    executor::{ExtrudeGroup, MemoryItem, Metadata, SketchGroup, SourceRange},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub type FnMap = HashMap<String, StdFn>;
 | 
			
		||||
pub type StdFn = fn(&mut Args) -> Result<MemoryItem, KclError>;
 | 
			
		||||
pub type FnMap = HashMap<String, StdFn>;
 | 
			
		||||
 | 
			
		||||
pub struct StdLib {
 | 
			
		||||
    pub internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>>,
 | 
			
		||||
 | 
			
		||||
    pub fns: FnMap,
 | 
			
		||||
    pub fns: HashMap<String, Box<(dyn crate::docs::StdLibFn)>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl StdLib {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        let internal_fn_names: Vec<Box<(dyn crate::docs::StdLibFn)>> = vec![
 | 
			
		||||
        let internal_fns: Vec<Box<(dyn crate::docs::StdLibFn)>> = vec![
 | 
			
		||||
            Box::new(Show),
 | 
			
		||||
            Box::new(Min),
 | 
			
		||||
            Box::new(LegLen),
 | 
			
		||||
            Box::new(LegAngX),
 | 
			
		||||
@ -68,11 +64,15 @@ impl StdLib {
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        let mut fns = HashMap::new();
 | 
			
		||||
        for internal_fn_name in &internal_fn_names {
 | 
			
		||||
            fns.insert(internal_fn_name.name().to_string(), internal_fn_name.std_lib_fn());
 | 
			
		||||
        for internal_fn in &internal_fns {
 | 
			
		||||
            fns.insert(internal_fn.name().to_string(), internal_fn.clone());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self { internal_fn_names, fns }
 | 
			
		||||
        Self { fns }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn get(&self, name: &str) -> Option<Box<dyn crate::docs::StdLibFn>> {
 | 
			
		||||
        self.fns.get(name).cloned()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -407,7 +407,6 @@ impl<'a> Args<'a> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Returns the minimum of the given arguments.
 | 
			
		||||
/// TODO fix min
 | 
			
		||||
pub fn min(args: &mut Args) -> Result<MemoryItem, KclError> {
 | 
			
		||||
    let nums = args.get_number_array()?;
 | 
			
		||||
    let result = inner_min(nums);
 | 
			
		||||
@ -430,6 +429,21 @@ fn inner_min(args: Vec<f64>) -> f64 {
 | 
			
		||||
    min
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Render a model.
 | 
			
		||||
// This never actually gets called so this is fine.
 | 
			
		||||
pub fn show(args: &mut Args) -> Result<MemoryItem, KclError> {
 | 
			
		||||
    let sketch_group = args.get_sketch_group()?;
 | 
			
		||||
    inner_show(sketch_group);
 | 
			
		||||
 | 
			
		||||
    args.make_user_val_from_f64(0.0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Render a model.
 | 
			
		||||
#[stdlib {
 | 
			
		||||
    name = "show",
 | 
			
		||||
}]
 | 
			
		||||
fn inner_show(_sketch: SketchGroup) {}
 | 
			
		||||
 | 
			
		||||
/// Returns the length of the given leg.
 | 
			
		||||
pub fn leg_length(args: &mut Args) -> Result<MemoryItem, KclError> {
 | 
			
		||||
    let (hypotenuse, leg) = args.get_hypotenuse_leg()?;
 | 
			
		||||
@ -493,6 +507,7 @@ pub enum Primitive {
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use crate::std::StdLib;
 | 
			
		||||
    use itertools::Itertools;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_generate_stdlib_markdown_docs() {
 | 
			
		||||
@ -508,7 +523,8 @@ mod tests {
 | 
			
		||||
 | 
			
		||||
        buf.push_str("* [Functions](#functions)\n");
 | 
			
		||||
 | 
			
		||||
        for internal_fn in &stdlib.internal_fn_names {
 | 
			
		||||
        for key in stdlib.fns.keys().sorted() {
 | 
			
		||||
            let internal_fn = stdlib.fns.get(key).unwrap();
 | 
			
		||||
            if internal_fn.unpublished() || internal_fn.deprecated() {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
@ -520,7 +536,8 @@ mod tests {
 | 
			
		||||
 | 
			
		||||
        buf.push_str("## Functions\n\n");
 | 
			
		||||
 | 
			
		||||
        for internal_fn in &stdlib.internal_fn_names {
 | 
			
		||||
        for key in stdlib.fns.keys().sorted() {
 | 
			
		||||
            let internal_fn = stdlib.fns.get(key).unwrap();
 | 
			
		||||
            if internal_fn.unpublished() {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
@ -555,17 +572,18 @@ mod tests {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fn_docs.push_str("\n#### Returns\n\n");
 | 
			
		||||
            let return_type = internal_fn.return_value();
 | 
			
		||||
            if let Some(description) = return_type.description() {
 | 
			
		||||
                fn_docs.push_str(&format!("* `{}` - {}\n", return_type.type_, description));
 | 
			
		||||
            } else {
 | 
			
		||||
                fn_docs.push_str(&format!("* `{}`\n", return_type.type_));
 | 
			
		||||
            }
 | 
			
		||||
            if let Some(return_type) = internal_fn.return_value() {
 | 
			
		||||
                fn_docs.push_str("\n#### Returns\n\n");
 | 
			
		||||
                if let Some(description) = return_type.description() {
 | 
			
		||||
                    fn_docs.push_str(&format!("* `{}` - {}\n", return_type.type_, description));
 | 
			
		||||
                } else {
 | 
			
		||||
                    fn_docs.push_str(&format!("* `{}`\n", return_type.type_));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            let (format, should_be_indented) = return_type.get_type_string().unwrap();
 | 
			
		||||
            if should_be_indented {
 | 
			
		||||
                fn_docs.push_str(&format!("```\n{}\n```\n", format));
 | 
			
		||||
                let (format, should_be_indented) = return_type.get_type_string().unwrap();
 | 
			
		||||
                if should_be_indented {
 | 
			
		||||
                    fn_docs.push_str(&format!("```\n{}\n```\n", format));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fn_docs.push_str("\n\n\n");
 | 
			
		||||
@ -582,7 +600,8 @@ mod tests {
 | 
			
		||||
 | 
			
		||||
        let mut json_data = vec![];
 | 
			
		||||
 | 
			
		||||
        for internal_fn in &stdlib.internal_fn_names {
 | 
			
		||||
        for key in stdlib.fns.keys().sorted() {
 | 
			
		||||
            let internal_fn = stdlib.fns.get(key).unwrap();
 | 
			
		||||
            json_data.push(internal_fn.to_json().unwrap());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -382,6 +382,20 @@ fn inner_angled_line(
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    args.send_modeling_cmd(
 | 
			
		||||
        id,
 | 
			
		||||
        ModelingCmd::ExtendPath {
 | 
			
		||||
            path: sketch_group.id,
 | 
			
		||||
            segment: kittycad::types::PathSegment::Line {
 | 
			
		||||
                end: Point3D {
 | 
			
		||||
                    x: to[0],
 | 
			
		||||
                    y: to[1],
 | 
			
		||||
                    z: 0.0,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    let mut new_sketch_group = sketch_group.clone();
 | 
			
		||||
    new_sketch_group.value.push(current_path);
 | 
			
		||||
    Ok(new_sketch_group)
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,110 @@
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS)]
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use parse_display::{Display, FromStr};
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use schemars::JsonSchema;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tower_lsp::lsp_types::SemanticTokenType;
 | 
			
		||||
 | 
			
		||||
/// The types of tokens.
 | 
			
		||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema, FromStr, Display)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(rename_all = "lowercase")]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
#[display(style = "camelCase")]
 | 
			
		||||
pub enum TokenType {
 | 
			
		||||
    /// A number.
 | 
			
		||||
    Number,
 | 
			
		||||
    /// A word.
 | 
			
		||||
    Word,
 | 
			
		||||
    /// An operator.
 | 
			
		||||
    Operator,
 | 
			
		||||
    /// A string.
 | 
			
		||||
    String,
 | 
			
		||||
    /// A keyword.
 | 
			
		||||
    Keyword,
 | 
			
		||||
    /// A brace.
 | 
			
		||||
    Brace,
 | 
			
		||||
    /// Whitespace.
 | 
			
		||||
    Whitespace,
 | 
			
		||||
    /// A comma.
 | 
			
		||||
    Comma,
 | 
			
		||||
    /// A colon.
 | 
			
		||||
    Colon,
 | 
			
		||||
    /// A period.
 | 
			
		||||
    Period,
 | 
			
		||||
    /// A line comment.
 | 
			
		||||
    LineComment,
 | 
			
		||||
    /// A block comment.
 | 
			
		||||
    BlockComment,
 | 
			
		||||
    /// A function name.
 | 
			
		||||
    Function,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<TokenType> for SemanticTokenType {
 | 
			
		||||
    type Error = anyhow::Error;
 | 
			
		||||
    fn try_from(token_type: TokenType) -> Result<Self> {
 | 
			
		||||
        Ok(match token_type {
 | 
			
		||||
            TokenType::Number => Self::NUMBER,
 | 
			
		||||
            TokenType::Word => Self::VARIABLE,
 | 
			
		||||
            TokenType::Keyword => Self::KEYWORD,
 | 
			
		||||
            TokenType::Operator => Self::OPERATOR,
 | 
			
		||||
            TokenType::String => Self::STRING,
 | 
			
		||||
            TokenType::LineComment => Self::COMMENT,
 | 
			
		||||
            TokenType::BlockComment => Self::COMMENT,
 | 
			
		||||
            TokenType::Function => Self::FUNCTION,
 | 
			
		||||
            TokenType::Whitespace | TokenType::Brace | TokenType::Comma | TokenType::Colon | TokenType::Period => {
 | 
			
		||||
                anyhow::bail!("unsupported token type: {:?}", token_type)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TokenType {
 | 
			
		||||
    // This is for the lsp server.
 | 
			
		||||
    pub fn to_semantic_token_types() -> Result<Vec<SemanticTokenType>> {
 | 
			
		||||
        let mut settings = schemars::gen::SchemaSettings::openapi3();
 | 
			
		||||
        settings.inline_subschemas = true;
 | 
			
		||||
        let mut generator = schemars::gen::SchemaGenerator::new(settings);
 | 
			
		||||
 | 
			
		||||
        let schema = TokenType::json_schema(&mut generator);
 | 
			
		||||
        let schemars::schema::Schema::Object(o) = &schema else {
 | 
			
		||||
            anyhow::bail!("expected object schema: {:#?}", schema);
 | 
			
		||||
        };
 | 
			
		||||
        let Some(subschemas) = &o.subschemas else {
 | 
			
		||||
            anyhow::bail!("expected subschemas: {:#?}", schema);
 | 
			
		||||
        };
 | 
			
		||||
        let Some(one_ofs) = &subschemas.one_of else {
 | 
			
		||||
            anyhow::bail!("expected one_of: {:#?}", schema);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut semantic_tokens = vec![];
 | 
			
		||||
        for one_of in one_ofs {
 | 
			
		||||
            let schemars::schema::Schema::Object(o) = one_of else {
 | 
			
		||||
                anyhow::bail!("expected object one_of: {:#?}", one_of);
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let Some(enum_values) = o.enum_values.as_ref() else {
 | 
			
		||||
                anyhow::bail!("expected enum values: {:#?}", o);
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if enum_values.len() > 1 {
 | 
			
		||||
                anyhow::bail!("expected only one enum value: {:#?}", o);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if enum_values.is_empty() {
 | 
			
		||||
                anyhow::bail!("expected at least one enum value: {:#?}", o);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let label = TokenType::from_str(&enum_values[0].to_string().replace('"', ""))?;
 | 
			
		||||
            if let Ok(semantic_token_type) = SemanticTokenType::try_from(label) {
 | 
			
		||||
                semantic_tokens.push(semantic_token_type);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(semantic_tokens)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, ts_rs::TS)]
 | 
			
		||||
@ -45,8 +133,11 @@ lazy_static! {
 | 
			
		||||
    static ref NUMBER: Regex = Regex::new(r"^-?\d+(\.\d+)?").unwrap();
 | 
			
		||||
    static ref WHITESPACE: Regex = Regex::new(r"\s+").unwrap();
 | 
			
		||||
    static ref WORD: Regex = Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*").unwrap();
 | 
			
		||||
    static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
 | 
			
		||||
    // TODO: these should be generated using our struct types for these.
 | 
			
		||||
    static ref KEYWORD: Regex =
 | 
			
		||||
        Regex::new(r"^(if|else|for|while|return|break|continue|fn|let|true|false|nil|and|or|not|var|const)\b").unwrap();
 | 
			
		||||
    static ref OPERATOR: Regex = Regex::new(r"^(>=|<=|==|=>|!= |\|>|\*|\+|-|/|%|=|<|>|\||\^)").unwrap();
 | 
			
		||||
    static ref STRING: Regex = Regex::new(r#"^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'"#).unwrap();
 | 
			
		||||
    static ref BLOCK_START: Regex = Regex::new(r"^\{").unwrap();
 | 
			
		||||
    static ref BLOCK_END: Regex = Regex::new(r"^\}").unwrap();
 | 
			
		||||
    static ref PARAN_START: Regex = Regex::new(r"^\(").unwrap();
 | 
			
		||||
@ -69,6 +160,9 @@ fn is_whitespace(character: &str) -> bool {
 | 
			
		||||
fn is_word(character: &str) -> bool {
 | 
			
		||||
    WORD.is_match(character)
 | 
			
		||||
}
 | 
			
		||||
fn is_keyword(character: &str) -> bool {
 | 
			
		||||
    KEYWORD.is_match(character)
 | 
			
		||||
}
 | 
			
		||||
fn is_string(character: &str) -> bool {
 | 
			
		||||
    match STRING.find(character) {
 | 
			
		||||
        Some(m) => m.start() == 0,
 | 
			
		||||
@ -112,8 +206,8 @@ fn is_block_comment(character: &str) -> bool {
 | 
			
		||||
    BLOCKCOMMENT.is_match(character)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn match_first(str: &str, regex: &Regex) -> Option<String> {
 | 
			
		||||
    regex.find(str).map(|the_match| the_match.as_str().to_string())
 | 
			
		||||
fn match_first(s: &str, regex: &Regex) -> Option<String> {
 | 
			
		||||
    regex.find(s).map(|the_match| the_match.as_str().to_string())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
 | 
			
		||||
@ -125,8 +219,8 @@ fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
 | 
			
		||||
    let str_from_index = &str[start_index..];
 | 
			
		||||
fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
 | 
			
		||||
    let str_from_index = &s.chars().skip(start_index).collect::<String>();
 | 
			
		||||
    if is_string(str_from_index) {
 | 
			
		||||
        return Some(make_token(
 | 
			
		||||
            TokenType::String,
 | 
			
		||||
@ -216,6 +310,13 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
 | 
			
		||||
            start_index,
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
    if is_keyword(str_from_index) {
 | 
			
		||||
        return Some(make_token(
 | 
			
		||||
            TokenType::Keyword,
 | 
			
		||||
            &match_first(str_from_index, &KEYWORD)?,
 | 
			
		||||
            start_index,
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
    if is_word(str_from_index) {
 | 
			
		||||
        return Some(make_token(
 | 
			
		||||
            TokenType::Word,
 | 
			
		||||
@ -247,21 +348,22 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn lexer(str: &str) -> Vec<Token> {
 | 
			
		||||
    fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
 | 
			
		||||
        if current_index >= str.len() {
 | 
			
		||||
            return previous_tokens;
 | 
			
		||||
        }
 | 
			
		||||
        let token = return_token_at_index(str, current_index);
 | 
			
		||||
        let Some(token) = token else {
 | 
			
		||||
            return recursively_tokenise(str, current_index + 1, previous_tokens);
 | 
			
		||||
        };
 | 
			
		||||
        let mut new_tokens = previous_tokens;
 | 
			
		||||
        let token_length = token.value.len();
 | 
			
		||||
        new_tokens.push(token);
 | 
			
		||||
        recursively_tokenise(str, current_index + token_length, new_tokens)
 | 
			
		||||
fn recursively_tokenise(s: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
 | 
			
		||||
    if current_index >= s.len() {
 | 
			
		||||
        return previous_tokens;
 | 
			
		||||
    }
 | 
			
		||||
    recursively_tokenise(str, 0, Vec::new())
 | 
			
		||||
    let token = return_token_at_index(s, current_index);
 | 
			
		||||
    let Some(token) = token else {
 | 
			
		||||
        return recursively_tokenise(s, current_index + 1, previous_tokens);
 | 
			
		||||
    };
 | 
			
		||||
    let mut new_tokens = previous_tokens;
 | 
			
		||||
    let token_length = token.value.len();
 | 
			
		||||
    new_tokens.push(token);
 | 
			
		||||
    recursively_tokenise(s, current_index + token_length, new_tokens)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn lexer(s: &str) -> Vec<Token> {
 | 
			
		||||
    recursively_tokenise(s, 0, Vec::new())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
@ -330,6 +432,7 @@ mod tests {
 | 
			
		||||
        assert!(!is_string(" \"a\""));
 | 
			
		||||
        assert!(!is_string("5\"a\""));
 | 
			
		||||
        assert!(!is_string("a + 'str'"));
 | 
			
		||||
        assert!(is_string("'c'"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
@ -453,14 +556,20 @@ mod tests {
 | 
			
		||||
        assert!(!is_block_comment("5 + 5"));
 | 
			
		||||
        assert!(!is_block_comment("5/* + 5"));
 | 
			
		||||
        assert!(!is_block_comment(" /* + 5"));
 | 
			
		||||
        assert!(!is_block_comment(
 | 
			
		||||
            r#"  /* and
 | 
			
		||||
   here
 | 
			
		||||
   */
 | 
			
		||||
   "#
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn make_token_test() {
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            make_token(TokenType::Word, "const", 56),
 | 
			
		||||
            make_token(TokenType::Keyword, "const", 56),
 | 
			
		||||
            Token {
 | 
			
		||||
                token_type: TokenType::Word,
 | 
			
		||||
                token_type: TokenType::Keyword,
 | 
			
		||||
                value: "const".to_string(),
 | 
			
		||||
                start: 56,
 | 
			
		||||
                end: 61,
 | 
			
		||||
@ -473,7 +582,7 @@ mod tests {
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            return_token_at_index("const", 0),
 | 
			
		||||
            Some(Token {
 | 
			
		||||
                token_type: TokenType::Word,
 | 
			
		||||
                token_type: TokenType::Keyword,
 | 
			
		||||
                value: "const".to_string(),
 | 
			
		||||
                start: 0,
 | 
			
		||||
                end: 5,
 | 
			
		||||
@ -496,7 +605,7 @@ mod tests {
 | 
			
		||||
            lexer("const a=5"),
 | 
			
		||||
            vec![
 | 
			
		||||
                Token {
 | 
			
		||||
                    token_type: TokenType::Word,
 | 
			
		||||
                    token_type: TokenType::Keyword,
 | 
			
		||||
                    value: "const".to_string(),
 | 
			
		||||
                    start: 0,
 | 
			
		||||
                    end: 5,
 | 
			
		||||
@ -587,4 +696,11 @@ mod tests {
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We have this as a test so we can ensure it never panics with an unwrap in the server.
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_token_type_to_semantic_token_type() {
 | 
			
		||||
        let semantic_types = TokenType::to_semantic_token_types().unwrap();
 | 
			
		||||
        assert!(!semantic_types.is_empty());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,12 @@
 | 
			
		||||
//! Wasm bindings for `kcl`.
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
use futures::stream::TryStreamExt;
 | 
			
		||||
use gloo_utils::format::JsValueSerdeExt;
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
use kcl_lib::server::{get_completions_from_stdlib, get_signatures_from_stdlib, Backend};
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
use tower_lsp::{LspService, Server};
 | 
			
		||||
use wasm_bindgen::prelude::*;
 | 
			
		||||
 | 
			
		||||
// wasm_bindgen wrapper for execute
 | 
			
		||||
@ -55,7 +61,8 @@ pub fn lexer_js(js: &str) -> Result<JsValue, JsError> {
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
pub fn parse_js(js: &str) -> Result<JsValue, String> {
 | 
			
		||||
    let tokens = kcl_lib::tokeniser::lexer(js);
 | 
			
		||||
    let program = kcl_lib::parser::abstract_syntax_tree(&tokens).map_err(String::from)?;
 | 
			
		||||
    let parser = kcl_lib::parser::Parser::new(tokens);
 | 
			
		||||
    let program = parser.ast().map_err(String::from)?;
 | 
			
		||||
    // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
 | 
			
		||||
    // gloo-serialize crate instead.
 | 
			
		||||
    JsValue::from_serde(&program).map_err(|e| e.to_string())
 | 
			
		||||
@ -69,6 +76,81 @@ pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
 | 
			
		||||
    let program: kcl_lib::abstract_syntax_tree_types::Program =
 | 
			
		||||
        serde_json::from_str(json_str).map_err(JsError::from)?;
 | 
			
		||||
 | 
			
		||||
    let result = kcl_lib::recast::recast(&program, "", false);
 | 
			
		||||
    // Use the default options until we integrate into the UI the ability to change them.
 | 
			
		||||
    let result = program.recast(&Default::default(), 0);
 | 
			
		||||
    Ok(JsValue::from_serde(&result)?)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
pub struct ServerConfig {
 | 
			
		||||
    into_server: js_sys::AsyncIterator,
 | 
			
		||||
    from_server: web_sys::WritableStream,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
impl ServerConfig {
 | 
			
		||||
    #[wasm_bindgen(constructor)]
 | 
			
		||||
    pub fn new(into_server: js_sys::AsyncIterator, from_server: web_sys::WritableStream) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            into_server,
 | 
			
		||||
            from_server,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Run the `kcl` lsp server.
 | 
			
		||||
//
 | 
			
		||||
// NOTE: we don't use web_sys::ReadableStream for input here because on the
 | 
			
		||||
// browser side we need to use a ReadableByteStreamController to construct it
 | 
			
		||||
// and so far only Chromium-based browsers support that functionality.
 | 
			
		||||
 | 
			
		||||
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
 | 
			
		||||
#[cfg(target_arch = "wasm32")]
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
pub async fn lsp_run(config: ServerConfig) -> Result<(), JsValue> {
 | 
			
		||||
    let ServerConfig {
 | 
			
		||||
        into_server,
 | 
			
		||||
        from_server,
 | 
			
		||||
    } = config;
 | 
			
		||||
 | 
			
		||||
    let stdlib = kcl_lib::std::StdLib::new();
 | 
			
		||||
    let stdlib_completions = get_completions_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
 | 
			
		||||
    let stdlib_signatures = get_signatures_from_stdlib(&stdlib).map_err(|e| e.to_string())?;
 | 
			
		||||
    // We can unwrap here because we know the tokeniser is valid, since
 | 
			
		||||
    // we have a test for it.
 | 
			
		||||
    let token_types = kcl_lib::tokeniser::TokenType::to_semantic_token_types().unwrap();
 | 
			
		||||
 | 
			
		||||
    let (service, socket) = LspService::new(|client| Backend {
 | 
			
		||||
        client,
 | 
			
		||||
        stdlib_completions,
 | 
			
		||||
        stdlib_signatures,
 | 
			
		||||
        token_types,
 | 
			
		||||
        token_map: Default::default(),
 | 
			
		||||
        ast_map: Default::default(),
 | 
			
		||||
        current_code_map: Default::default(),
 | 
			
		||||
        diagnostics_map: Default::default(),
 | 
			
		||||
        symbols_map: Default::default(),
 | 
			
		||||
        semantic_tokens_map: Default::default(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let input = wasm_bindgen_futures::stream::JsStream::from(into_server);
 | 
			
		||||
    let input = input
 | 
			
		||||
        .map_ok(|value| {
 | 
			
		||||
            value
 | 
			
		||||
                .dyn_into::<js_sys::Uint8Array>()
 | 
			
		||||
                .expect("could not cast stream item to Uint8Array")
 | 
			
		||||
                .to_vec()
 | 
			
		||||
        })
 | 
			
		||||
        .map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
 | 
			
		||||
        .into_async_read();
 | 
			
		||||
 | 
			
		||||
    let output = wasm_bindgen::JsCast::unchecked_into::<wasm_streams::writable::sys::WritableStream>(from_server);
 | 
			
		||||
    let output = wasm_streams::WritableStream::from_raw(output);
 | 
			
		||||
    let output = output.try_into_async_write().map_err(|err| err.0)?;
 | 
			
		||||
 | 
			
		||||
    Server::new(input, output, socket).serve(service).await;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										521
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										521
									
								
								yarn.lock
									
									
									
									
									
								
							@ -1157,7 +1157,7 @@
 | 
			
		||||
    "@babel/helper-validator-identifier" "^7.22.5"
 | 
			
		||||
    to-fast-properties "^2.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.3.2", "@codemirror/autocomplete@^6.7.1":
 | 
			
		||||
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.9.0":
 | 
			
		||||
  version "6.9.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz#1a1e63122288b8f8e1e9d7aff2eb39a83e04d8a9"
 | 
			
		||||
  integrity sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==
 | 
			
		||||
@ -1177,222 +1177,7 @@
 | 
			
		||||
    "@codemirror/view" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-angular@^0.1.0":
 | 
			
		||||
  version "0.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-angular/-/lang-angular-0.1.2.tgz#a3f565297842ad60caf2a0bf6f6137c13d19a666"
 | 
			
		||||
  integrity sha512-Nq7lmx9SU+JyoaRcs6SaJs7uAmW2W06HpgJVQYeZptVGNWDzDvzhjwVb/ZuG1rwTlOocY4Y9GwNOBuKCeJbKtw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/lang-html" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-javascript" "^6.1.2"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.3.3"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-cpp@^6.0.0":
 | 
			
		||||
  version "6.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz#076c98340c3beabde016d7d83e08eebe17254ef9"
 | 
			
		||||
  integrity sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/cpp" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-css@^6.0.0", "@codemirror/lang-css@^6.2.0":
 | 
			
		||||
  version "6.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.2.1.tgz#5dc0a43b8e3c31f6af7aabd55ff07fe9aef2a227"
 | 
			
		||||
  integrity sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.2"
 | 
			
		||||
    "@lezer/css" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-html@^6.0.0", "@codemirror/lang-html@^6.4.0":
 | 
			
		||||
  version "6.4.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.5.tgz#4cf014da02624a8a4365ef6c8e343f35afa0c784"
 | 
			
		||||
  integrity sha512-dUCSxkIw2G+chaUfw3Gfu5kkN83vJQN8gfQDp9iEHsIZluMJA0YJveT12zg/28BJx+uPsbQ6VimKCgx3oJrZxA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-css" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-javascript" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.4.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@codemirror/view" "^6.2.2"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/css" "^1.1.0"
 | 
			
		||||
    "@lezer/html" "^1.3.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-java@^6.0.0":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz#03bd06334da7c8feb9dff6db01ac6d85bd2e48bb"
 | 
			
		||||
  integrity sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/java" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-javascript@^6.0.0", "@codemirror/lang-javascript@^6.1.0", "@codemirror/lang-javascript@^6.1.2":
 | 
			
		||||
  version "6.1.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz#19065ad32db7b3797829eca01b8d9c69da5fd0d6"
 | 
			
		||||
  integrity sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.6.0"
 | 
			
		||||
    "@codemirror/lint" "^6.0.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@codemirror/view" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/javascript" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-json@^6.0.0":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz#0a0be701a5619c4b0f8991f9b5e95fe33f462330"
 | 
			
		||||
  integrity sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/json" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-less@^6.0.0", "@codemirror/lang-less@^6.0.1":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-less/-/lang-less-6.0.1.tgz#fef10e8dbcd07055b815c3928233a05a8549181e"
 | 
			
		||||
  integrity sha512-ABcsKBjLbyPZwPR5gePpc8jEKCQrFF4pby2WlMVdmJOOr7OWwwyz8DZonPx/cKDE00hfoSLc8F7yAcn/d6+rTQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/lang-css" "^6.2.0"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-lezer@^6.0.0":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-lezer/-/lang-lezer-6.0.1.tgz#16a5909ab8ab4a23e9b214476413dc92a3191780"
 | 
			
		||||
  integrity sha512-WHwjI7OqKFBEfkunohweqA5B/jIlxaZso6Nl3weVckz8EafYbPZldQEKSDb4QQ9H9BUkle4PVELP4sftKoA0uQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/lezer" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.1.0":
 | 
			
		||||
  version "6.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.2.0.tgz#d391d1314911da522bf4cc4edb15ff6b3eb66979"
 | 
			
		||||
  integrity sha512-deKegEQVzfBAcLPqsJEa+IxotqPVwWZi90UOEvQbfa01NTAw8jNinrykuYPTULGUj+gha0ZG2HBsn4s5d64Qrg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.7.1"
 | 
			
		||||
    "@codemirror/lang-html" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.3.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@codemirror/view" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/markdown" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-php@^6.0.0":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-php/-/lang-php-6.0.1.tgz#fa34cc75562178325861a5731f79bd621f57ffaa"
 | 
			
		||||
  integrity sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/lang-html" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/php" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-python@^6.0.0", "@codemirror/lang-python@^6.1.0":
 | 
			
		||||
  version "6.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-python/-/lang-python-6.1.3.tgz#47b8d9fb42eb4482317843e519c6c211accacb62"
 | 
			
		||||
  integrity sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.3.2"
 | 
			
		||||
    "@codemirror/language" "^6.8.0"
 | 
			
		||||
    "@lezer/python" "^1.1.4"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-rust@^6.0.0":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz#d6829fc7baa39a15bcd174a41a9e0a1bf7cf6ba8"
 | 
			
		||||
  integrity sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/rust" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-sass@^6.0.0", "@codemirror/lang-sass@^6.0.1":
 | 
			
		||||
  version "6.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz#38c1b0a1326cc9f5cb2741d2cd51cfbcd7abc0b2"
 | 
			
		||||
  integrity sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/lang-css" "^6.2.0"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.2"
 | 
			
		||||
    "@lezer/sass" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-sql@^6.0.0", "@codemirror/lang-sql@^6.4.0":
 | 
			
		||||
  version "6.5.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-sql/-/lang-sql-6.5.3.tgz#e530f735f432afb7287c0e9bdd00496b8ae654ff"
 | 
			
		||||
  integrity sha512-3M+0LgBN/H4ukfdX2E/6LnsCyOyas9jd+39c4DQu92ihlllE76arLM0RRBHR6IV0sVzpJq+wTcDgahwWtbQthg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-vue@^0.1.1":
 | 
			
		||||
  version "0.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-vue/-/lang-vue-0.1.2.tgz#50aec87b93ba8a6b0742a24cbab566b3989ee6ca"
 | 
			
		||||
  integrity sha512-D4YrefiRBAr+CfEIM4S3yvGSbYW+N69mttIfGMEf7diHpRbmygDxS+R/5xSqjgtkY6VO6qmUrre1GkRcWeZa9A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/lang-html" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-javascript" "^6.1.2"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.3.1"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-wast@^6.0.0":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-wast/-/lang-wast-6.0.1.tgz#c15bec84548a5e9b0a43fa69fb63631d087d6047"
 | 
			
		||||
  integrity sha512-sQLsqhRjl2MWG3rxZysX+2XAyed48KhLBHLgq9xcKxIJu3npH/G+BIXW5NM5mHeDUjG0jcGh9BcjP0NfMStuzA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lang-xml@^6.0.0":
 | 
			
		||||
  version "6.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz#66f75390bf8013fd8645db9cdd0b1d177e0777a4"
 | 
			
		||||
  integrity sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.4.0"
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/xml" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/language-data@^6.0.0":
 | 
			
		||||
  version "6.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/language-data/-/language-data-6.3.1.tgz#795ec09e04260868070296241363d70f4060bb36"
 | 
			
		||||
  integrity sha512-p6jhJmvhGe1TG1EGNhwH7nFWWFSTJ8NDKnB2fVx5g3t+PpO0+63R7GJNxjS0TmmH3cdMxZbzejsik+rlEh1EyQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/lang-angular" "^0.1.0"
 | 
			
		||||
    "@codemirror/lang-cpp" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-css" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-html" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-java" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-javascript" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-json" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-less" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-markdown" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-php" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-python" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-rust" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-sass" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-sql" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-vue" "^0.1.1"
 | 
			
		||||
    "@codemirror/lang-wast" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-xml" "^6.0.0"
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@codemirror/legacy-modes" "^6.1.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/language@^6.0.0", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0":
 | 
			
		||||
"@codemirror/language@^6.0.0":
 | 
			
		||||
  version "6.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.8.0.tgz#f2d7eea6b338c25593d800f2293b062d9f9856db"
 | 
			
		||||
  integrity sha512-r1paAyWOZkfY0RaYEZj3Kul+MiQTEbDvYqf8gPGaRvNneHXCmfSaAVFjwRUPlgxS8yflMxw2CTu6uCMp8R8A2g==
 | 
			
		||||
@ -1404,13 +1189,6 @@
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
    style-mod "^4.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/legacy-modes@^6.0.0", "@codemirror/legacy-modes@^6.1.0":
 | 
			
		||||
  version "6.3.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/legacy-modes/-/legacy-modes-6.3.3.tgz#d7827c76c9533efdc76f7d0a0fc866f5acd4b764"
 | 
			
		||||
  integrity sha512-X0Z48odJ0KIoh/HY8Ltz75/4tDYc9msQf1E/2trlxFaFFhgjpVHjZ/BCXe1Lk7s4Gd67LL/CeEEHNI+xHOiESg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/lint@^6.0.0":
 | 
			
		||||
  version "6.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.4.0.tgz#3507e937aa9415ef0831ff04734ef0e736e75014"
 | 
			
		||||
@ -1444,7 +1222,7 @@
 | 
			
		||||
    "@codemirror/view" "^6.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@codemirror/view@^6.0.0", "@codemirror/view@^6.2.2", "@codemirror/view@^6.6.0":
 | 
			
		||||
"@codemirror/view@^6.0.0", "@codemirror/view@^6.6.0":
 | 
			
		||||
  version "6.16.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.16.0.tgz#047001b8dd04e104776c476e45ee9c4eed9f99fa"
 | 
			
		||||
  integrity sha512-1Z2HkvkC3KR/oEZVuW9Ivmp8TWLzGEd8T8TA04TTwPvqogfkHBdYSlflytDOqmkUxM2d1ywTg7X2dU5mC+SXvg==
 | 
			
		||||
@ -1752,37 +1530,21 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
 | 
			
		||||
  integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
 | 
			
		||||
 | 
			
		||||
"@kittycad/lib@^0.0.35":
 | 
			
		||||
  version "0.0.35"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.35.tgz#bde8868048f9fd53f8309e7308aeba622898b935"
 | 
			
		||||
  integrity sha512-qM8AyP2QUlDfPWNxb1Fs/Pq9AebGVDN1OHjByxbGomKCy0jFdN2TsyDdhQH/CAZGfBCgPEfr5bq6rkUBGSXcNw==
 | 
			
		||||
"@kittycad/lib@^0.0.37":
 | 
			
		||||
  version "0.0.37"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.37.tgz#ec4f6c4fb5d06402a19339f3374036b6582d2265"
 | 
			
		||||
  integrity sha512-P8p9FeLV79/0Lfd0RioBta1drzhmpROnU4YV38+zsAA4LhibQCTjeekRkxVvHztGumPxz9pPsAeeLJyuz2RWKQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    node-fetch "3.3.2"
 | 
			
		||||
    openapi-types "^12.0.0"
 | 
			
		||||
    ts-node "^10.9.1"
 | 
			
		||||
    tslib "~2.4"
 | 
			
		||||
 | 
			
		||||
"@lezer/common@^1.0.0", "@lezer/common@^1.0.2":
 | 
			
		||||
"@lezer/common@^1.0.0":
 | 
			
		||||
  version "1.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.0.3.tgz#1808f70e2b0a7b1fdcbaf5c074723d2d4ed1e4c5"
 | 
			
		||||
  integrity sha512-JH4wAXCgUOcCGNekQPLhVeUtIqjH0yPBs7vvUdSjyQama9618IOKFJwkv2kcqdhF0my8hQEgCTEJU0GIgnahvA==
 | 
			
		||||
 | 
			
		||||
"@lezer/cpp@^1.0.0":
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/cpp/-/cpp-1.1.1.tgz#ac0261f48dc3651bfea13fdaeff35f04c9011a7f"
 | 
			
		||||
  integrity sha512-eS1M3L3U2mDowoFVPG7tEp01SWu9/68Nx3HEBgLJVn3N9ku7g5S7WdFv0jzmcTipAyONYfZJ+7x4WRkfdB2Ung==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/css@^1.0.0", "@lezer/css@^1.1.0":
 | 
			
		||||
  version "1.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.1.3.tgz#605495b00fd8a122088becf196a93744cbe817fc"
 | 
			
		||||
  integrity sha512-SjSM4pkQnQdJDVc80LYzEaMiNy9txsFbI7HsMgeVF28NdLaAdHNtQ+kB/QqDUzRBV/75NTXjJ/R5IdC8QQGxMg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
 | 
			
		||||
  version "1.1.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.1.6.tgz#87e56468c0f43c2a8b3dc7f0b7c2804b34901556"
 | 
			
		||||
@ -1790,117 +1552,21 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/html@^1.3.0":
 | 
			
		||||
  version "1.3.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.6.tgz#26a2a17da4e0f91835e36db9ccd025b2ed8d33f7"
 | 
			
		||||
  integrity sha512-Kk9HJARZTc0bAnMQUqbtuhFVsB4AnteR2BFUWfZV7L/x1H0aAKz6YabrfJ2gk/BEgjh9L3hg5O4y2IDZRBdzuQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/java@^1.0.0":
 | 
			
		||||
  version "1.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/java/-/java-1.0.4.tgz#f31f5af4bfc40475dc886f0e3e2d291889b87d25"
 | 
			
		||||
  integrity sha512-POc53LHf2AuNeRXjqZbXNu88GKj0KZTjjSx0L7tYeXlrEHF+3NAQx+dEwKVuCbkl0ZMtpRy2VsDYOV7KKV0oyg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/javascript@^1.0.0":
 | 
			
		||||
  version "1.4.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.5.tgz#4ab56dbcbff3e58ef331294a549903a5dd8d154a"
 | 
			
		||||
  integrity sha512-FmBUHz8K1V22DgjTd6SrIG9owbzOYZ1t3rY6vGEmw+e2RVBd7sqjM8uXEVRFmfxKFn1Mx2ABJehHjrN3G2ZpmA==
 | 
			
		||||
"@lezer/javascript@^1.4.7":
 | 
			
		||||
  version "1.4.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.7.tgz#4ebcce2db6043c07fbe827188c07cb001bc7fe37"
 | 
			
		||||
  integrity sha512-OVWlK0YEi7HM+9JRWtRkir8qvcg0/kVYg2TAMHlVtl6DU1C9yK1waEOLBMztZsV/axRJxsqfJKhzYz+bxZme5g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.1.3"
 | 
			
		||||
    "@lezer/lr" "^1.3.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/json@^1.0.0":
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/json/-/json-1.0.1.tgz#3bf5641f3d1408ec31a5f9b29e4e96c6e3a232e6"
 | 
			
		||||
  integrity sha512-nkVC27qiEZEjySbi6gQRuMwa2sDu2PtfjSgz0A4QF81QyRGm3kb2YRzLcOPcTEtmcwvrX/cej7mlhbwViA4WJw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/lezer@^1.0.0":
 | 
			
		||||
  version "1.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/lezer/-/lezer-1.1.2.tgz#c2bf13d505ad193d9b8f6cdc1b0f9c71aa6abd98"
 | 
			
		||||
  integrity sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/lr@^1.0.0", "@lezer/lr@^1.1.0", "@lezer/lr@^1.3.0", "@lezer/lr@^1.3.1", "@lezer/lr@^1.3.3":
 | 
			
		||||
"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0":
 | 
			
		||||
  version "1.3.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.9.tgz#cb299816d1c58efcca23ebbeb70bb4204fdd001b"
 | 
			
		||||
  integrity sha512-XPz6dzuTHlnsbA5M2DZgjflNQ+9Hi5Swhic0RULdp3oOs3rh6bqGZolosVqN/fQIT8uNiepzINJDnS39oweTHQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/markdown@^1.0.0":
 | 
			
		||||
  version "1.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/markdown/-/markdown-1.1.0.tgz#5cee104ef353a3442ecee023ff1912826fac8658"
 | 
			
		||||
  integrity sha512-JYOI6Lkqbl83semCANkO3CKbKc0pONwinyagBufWBm+k4yhIcqfCF8B8fpEpvJLmIy7CAfwiq7dQ/PzUZA340g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/common" "^1.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/php@^1.0.0":
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/php/-/php-1.0.1.tgz#4496b58c980ca710c0433fd743d27e9964fd74ea"
 | 
			
		||||
  integrity sha512-aqdCQJOXJ66De22vzdwnuC502hIaG9EnPK2rSi+ebXyUd+j7GAX1mRjWZOVOmf3GST1YUfUCu6WXDiEgDGOVwA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.1.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/python@^1.1.4":
 | 
			
		||||
  version "1.1.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/python/-/python-1.1.8.tgz#fe8d03d6cbc95a1d5625cffd30d78018ee816633"
 | 
			
		||||
  integrity sha512-1T/XsmeF57ijrjpC0Zmrf9YeO5mn2zC1XeSNrOnc0KB+6PgxJ5m7kWKt0CnwyS74oHQXbJxUUL+QDQJR26c1Gw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/rust@^1.0.0":
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/rust/-/rust-1.0.1.tgz#ac2d7263fe22527e621bb5623929ba6d6c3a29ea"
 | 
			
		||||
  integrity sha512-j+ToFKM6Wpglv3OQ4ebHYdYIMT2dh0ziCCV0rTf47AWiHOVhR0WjaKrBq+yuvDQNEhr5sxPxVI7+naJIgpqcsQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/sass@^1.0.0":
 | 
			
		||||
  version "1.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/sass/-/sass-1.0.3.tgz#17e5d27e40979bc8b4aec8d05df0d01f745aedb8"
 | 
			
		||||
  integrity sha512-n4l2nVOB7gWiGU/Cg2IVxpt2Ic9Hgfgy/7gk+p/XJibAsPXs0lSbsfGwQgwsAw9B/euYo3oS6lEFr9WytoqcZg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@lezer/xml@^1.0.0":
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@lezer/xml/-/xml-1.0.2.tgz#5c934602d1d3565fdaf04e93b534c8b94f4df2d1"
 | 
			
		||||
  integrity sha512-dlngsWceOtQBMuBPw5wtHpaxdPJ71aVntqjbpGkFtWsp4WtQmCnuTjQGocviymydN6M18fhj6UQX3oiEtSuY7w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@nextjournal/lang-clojure@^1.0.0":
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@nextjournal/lang-clojure/-/lang-clojure-1.0.0.tgz#0efbd594769e606eea532758519a239f0d38959d"
 | 
			
		||||
  integrity sha512-gOCV71XrYD0DhwGoPMWZmZ0r92/lIHsqQu9QWdpZYYBwiChNwMO4sbVMP7eTuAqffFB2BTtCSC+1skSH9d3bNg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@nextjournal/lezer-clojure" "1.0.0"
 | 
			
		||||
 | 
			
		||||
"@nextjournal/lezer-clojure@1.0.0":
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@nextjournal/lezer-clojure/-/lezer-clojure-1.0.0.tgz#0e7ff75f8d0fabed36d26b9f6b5f00d8a9f385e6"
 | 
			
		||||
  integrity sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
 | 
			
		||||
  version "5.1.1-v1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
 | 
			
		||||
@ -1929,6 +1595,16 @@
 | 
			
		||||
    "@nodelib/fs.scandir" "2.1.5"
 | 
			
		||||
    fastq "^1.6.0"
 | 
			
		||||
 | 
			
		||||
"@open-rpc/client-js@^1.8.1":
 | 
			
		||||
  version "1.8.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@open-rpc/client-js/-/client-js-1.8.1.tgz#73b5a5bf237f24b14c3c89205b1fca3aea213213"
 | 
			
		||||
  integrity sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    isomorphic-fetch "^3.0.0"
 | 
			
		||||
    isomorphic-ws "^5.0.0"
 | 
			
		||||
    strict-event-emitter-types "^2.0.0"
 | 
			
		||||
    ws "^7.0.0"
 | 
			
		||||
 | 
			
		||||
"@react-hook/latest@^1.0.2":
 | 
			
		||||
  version "1.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@react-hook/latest/-/latest-1.0.3.tgz#c2d1d0b0af8b69ec6e2b3a2412ba0768ac82db80"
 | 
			
		||||
@ -1953,26 +1629,6 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
 | 
			
		||||
  integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
 | 
			
		||||
 | 
			
		||||
"@replit/codemirror-lang-csharp@^6.1.0":
 | 
			
		||||
  version "6.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.1.0.tgz#3f3087fe0938f35fcf2012357f364d22755508c7"
 | 
			
		||||
  integrity sha512-Dtyk9WVrdPPgkgTp8MUX9HyXd87O7UZnFrE647gjHUZY8p0UN+z0m6dPfk6rJMsTTvMcl7YbDUykxfeqB6EQOQ==
 | 
			
		||||
 | 
			
		||||
"@replit/codemirror-lang-nix@^6.0.1":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz#d87af4ce9eb2cf30fdd64c9be0cb576783331217"
 | 
			
		||||
  integrity sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==
 | 
			
		||||
 | 
			
		||||
"@replit/codemirror-lang-solidity@^6.0.1":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@replit/codemirror-lang-solidity/-/codemirror-lang-solidity-6.0.1.tgz#c7e5ace087f9fa1a2c55b5b62f6bd0b064706a71"
 | 
			
		||||
  integrity sha512-kDnak0xZelGmvzJwKTpMTl6gYSfFq9hnxrkbLaMV0CARq/MFvDQJmcmYon/k8uZqXy6DfzewKDV8tx9kY2WUZg==
 | 
			
		||||
 | 
			
		||||
"@replit/codemirror-lang-svelte@^6.0.0":
 | 
			
		||||
  version "6.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz#a9d36a2c762280db66809190f0d68fa43befe0d9"
 | 
			
		||||
  integrity sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==
 | 
			
		||||
 | 
			
		||||
"@rollup/pluginutils@^4.2.1":
 | 
			
		||||
  version "4.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
 | 
			
		||||
@ -2171,6 +1827,13 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.12.5"
 | 
			
		||||
 | 
			
		||||
"@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.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
 | 
			
		||||
@ -2213,6 +1876,11 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d"
 | 
			
		||||
  integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==
 | 
			
		||||
 | 
			
		||||
"@types/debounce@^1.2.1":
 | 
			
		||||
  version "1.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.1.tgz#79b65710bc8b6d44094d286aecf38e44f9627852"
 | 
			
		||||
  integrity sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==
 | 
			
		||||
 | 
			
		||||
"@types/eslint@^8.4.5":
 | 
			
		||||
  version "8.44.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.1.tgz#d1811559bb6bcd1a76009e3f7883034b78a0415e"
 | 
			
		||||
@ -2453,10 +2121,10 @@
 | 
			
		||||
    "@typescript-eslint/types" "5.62.0"
 | 
			
		||||
    eslint-visitor-keys "^3.3.0"
 | 
			
		||||
 | 
			
		||||
"@uiw/codemirror-extensions-basic-setup@4.21.9":
 | 
			
		||||
  version "4.21.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.21.9.tgz#e886c6e6ad477bc0943691b9572958c81a2beab3"
 | 
			
		||||
  integrity sha512-TQT6aF8brxZpFnk/K4fm/K/9k9eF3PMav/KKjHlYrGUT8BTNk/qL+ximLtIzvTUhmBFchjM1lrqSJdvpVom7/w==
 | 
			
		||||
"@uiw/codemirror-extensions-basic-setup@4.21.13":
 | 
			
		||||
  version "4.21.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.21.13.tgz#d7bcebf1906157bafde2d097dd6b63bcc772f54c"
 | 
			
		||||
  integrity sha512-5ObHaBqPV00xBVleDFehzPfOQvek5dPM7YLdPHJUE9bumeSflIWJb55n0Zg/w1rsuU0Lt/Q6WJUh4X6VGR1FVw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/autocomplete" "^6.0.0"
 | 
			
		||||
    "@codemirror/commands" "^6.0.0"
 | 
			
		||||
@ -2466,48 +2134,16 @@
 | 
			
		||||
    "@codemirror/state" "^6.0.0"
 | 
			
		||||
    "@codemirror/view" "^6.0.0"
 | 
			
		||||
 | 
			
		||||
"@uiw/codemirror-extensions-langs@^4.21.9":
 | 
			
		||||
  version "4.21.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.21.9.tgz#0cb18bb1a15ce272c8aa9613dc0b11d84eaefacb"
 | 
			
		||||
  integrity sha512-s1VT1rss0iyvrtRl7BZtC5H7U5uQtCKTaD8wxjQrgZz5un9wHVvy9twU97aJGQR0FwbKWqK8/1iiICRJTRCoZA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/lang-angular" "^0.1.0"
 | 
			
		||||
    "@codemirror/lang-cpp" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-css" "^6.2.0"
 | 
			
		||||
    "@codemirror/lang-html" "^6.4.0"
 | 
			
		||||
    "@codemirror/lang-java" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-javascript" "^6.1.0"
 | 
			
		||||
    "@codemirror/lang-json" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-less" "^6.0.1"
 | 
			
		||||
    "@codemirror/lang-lezer" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-markdown" "^6.1.0"
 | 
			
		||||
    "@codemirror/lang-php" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-python" "^6.1.0"
 | 
			
		||||
    "@codemirror/lang-rust" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-sass" "^6.0.1"
 | 
			
		||||
    "@codemirror/lang-sql" "^6.4.0"
 | 
			
		||||
    "@codemirror/lang-vue" "^0.1.1"
 | 
			
		||||
    "@codemirror/lang-wast" "^6.0.0"
 | 
			
		||||
    "@codemirror/lang-xml" "^6.0.0"
 | 
			
		||||
    "@codemirror/language-data" "^6.0.0"
 | 
			
		||||
    "@codemirror/legacy-modes" "^6.0.0"
 | 
			
		||||
    "@nextjournal/lang-clojure" "^1.0.0"
 | 
			
		||||
    "@replit/codemirror-lang-csharp" "^6.1.0"
 | 
			
		||||
    "@replit/codemirror-lang-nix" "^6.0.1"
 | 
			
		||||
    "@replit/codemirror-lang-solidity" "^6.0.1"
 | 
			
		||||
    "@replit/codemirror-lang-svelte" "^6.0.0"
 | 
			
		||||
    codemirror-lang-mermaid "^0.2.1"
 | 
			
		||||
 | 
			
		||||
"@uiw/react-codemirror@^4.15.1":
 | 
			
		||||
  version "4.21.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.21.9.tgz#74393955d159a7d452731e61957773ae053c65b8"
 | 
			
		||||
  integrity sha512-aeLegPz2iCvqJjhzXp2WUMqpMZDqxsTnF3rX9kGRlfY6vQLsrjoctj0cQ29uxEtFYJChOVjtCOtnQUlyIuNAHQ==
 | 
			
		||||
"@uiw/react-codemirror@^4.21.13":
 | 
			
		||||
  version "4.21.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.21.13.tgz#b6e44cbccef70c1ff13bc905b46edc5bc3363dcc"
 | 
			
		||||
  integrity sha512-kNX8jLeoDrF2CDa5lsey0MXjBXN3JP00z6AQTTP58mHvlE7Rf03QJSs7bNwwco+3kpwREifFJjnwRe+Y3Gmwtw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.18.6"
 | 
			
		||||
    "@codemirror/commands" "^6.1.0"
 | 
			
		||||
    "@codemirror/state" "^6.1.1"
 | 
			
		||||
    "@codemirror/theme-one-dark" "^6.0.0"
 | 
			
		||||
    "@uiw/codemirror-extensions-basic-setup" "4.21.9"
 | 
			
		||||
    "@uiw/codemirror-extensions-basic-setup" "4.21.13"
 | 
			
		||||
    codemirror "^6.0.0"
 | 
			
		||||
 | 
			
		||||
"@vitejs/plugin-react@^4.0.3":
 | 
			
		||||
@ -3027,15 +2663,6 @@ client-only@^0.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
 | 
			
		||||
  integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
 | 
			
		||||
 | 
			
		||||
codemirror-lang-mermaid@^0.2.1:
 | 
			
		||||
  version "0.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/codemirror-lang-mermaid/-/codemirror-lang-mermaid-0.2.2.tgz#f7f6622c08f6ac459a7ce11632f9b5097b3da106"
 | 
			
		||||
  integrity sha512-AqSzkQgfWsjBbifio3dy/zDj6WXEw4g52Mq6bltIWLMWryWWRMpFwjQSlHtCGOol1FENYObUF5KI4ofiv8bjXA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@codemirror/language" "^6.0.0"
 | 
			
		||||
    "@lezer/highlight" "^1.0.0"
 | 
			
		||||
    "@lezer/lr" "^1.0.0"
 | 
			
		||||
 | 
			
		||||
codemirror@^6.0.0:
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
 | 
			
		||||
@ -4372,6 +3999,19 @@ isexe@^2.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
 | 
			
		||||
  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 | 
			
		||||
 | 
			
		||||
isomorphic-fetch@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4"
 | 
			
		||||
  integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    node-fetch "^2.6.1"
 | 
			
		||||
    whatwg-fetch "^3.4.1"
 | 
			
		||||
 | 
			
		||||
isomorphic-ws@^5.0.0:
 | 
			
		||||
  version "5.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf"
 | 
			
		||||
  integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==
 | 
			
		||||
 | 
			
		||||
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
 | 
			
		||||
  version "3.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
 | 
			
		||||
@ -4498,6 +4138,11 @@ json-parse-even-better-errors@^2.3.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
 | 
			
		||||
  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 | 
			
		||||
 | 
			
		||||
json-rpc-2.0@^1.6.0:
 | 
			
		||||
  version "1.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/json-rpc-2.0/-/json-rpc-2.0-1.6.0.tgz#60770ca98f663376126af7335ed2d30164691c89"
 | 
			
		||||
  integrity sha512-+pKxaoIqnA5VjXmZiAI1+CkFG7mHLg+dhtliOe/mp1P5Gdn8P5kE/Xxp2CUBwnGL7pfw6gC8zWTWekhSnKzHFA==
 | 
			
		||||
 | 
			
		||||
json-schema-traverse@^0.4.1:
 | 
			
		||||
  version "0.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
 | 
			
		||||
@ -4784,6 +4429,13 @@ node-fetch@3.3.2:
 | 
			
		||||
    fetch-blob "^3.1.4"
 | 
			
		||||
    formdata-polyfill "^4.0.10"
 | 
			
		||||
 | 
			
		||||
node-fetch@^2.6.1:
 | 
			
		||||
  version "2.7.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
 | 
			
		||||
  integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    whatwg-url "^5.0.0"
 | 
			
		||||
 | 
			
		||||
node-fetch@^2.6.12:
 | 
			
		||||
  version "2.6.12"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
 | 
			
		||||
@ -5541,6 +5193,11 @@ stop-iteration-iterator@^1.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    internal-slot "^1.0.4"
 | 
			
		||||
 | 
			
		||||
strict-event-emitter-types@^2.0.0:
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f"
 | 
			
		||||
  integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==
 | 
			
		||||
 | 
			
		||||
string-natural-compare@^3.0.1:
 | 
			
		||||
  version "3.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
 | 
			
		||||
@ -5827,7 +5484,7 @@ tslib@^2.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
 | 
			
		||||
  integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==
 | 
			
		||||
 | 
			
		||||
"tslib@^2.4.1 || ^1.9.3":
 | 
			
		||||
tslib@^2.3.0, "tslib@^2.4.1 || ^1.9.3":
 | 
			
		||||
  version "2.6.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
 | 
			
		||||
  integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
 | 
			
		||||
@ -6083,6 +5740,24 @@ vitest@^0.34.1:
 | 
			
		||||
    vite-node "0.34.1"
 | 
			
		||||
    why-is-node-running "^2.2.2"
 | 
			
		||||
 | 
			
		||||
vscode-jsonrpc@8.1.0, vscode-jsonrpc@^8.1.0:
 | 
			
		||||
  version "8.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94"
 | 
			
		||||
  integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==
 | 
			
		||||
 | 
			
		||||
vscode-languageserver-protocol@^3.17.3:
 | 
			
		||||
  version "3.17.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz#6d0d54da093f0c0ee3060b81612cce0f11060d57"
 | 
			
		||||
  integrity sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    vscode-jsonrpc "8.1.0"
 | 
			
		||||
    vscode-languageserver-types "3.17.3"
 | 
			
		||||
 | 
			
		||||
vscode-languageserver-types@3.17.3:
 | 
			
		||||
  version "3.17.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64"
 | 
			
		||||
  integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==
 | 
			
		||||
 | 
			
		||||
w3c-keyname@^2.2.4:
 | 
			
		||||
  version "2.2.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
 | 
			
		||||
@ -6129,6 +5804,11 @@ whatwg-encoding@^2.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    iconv-lite "0.6.3"
 | 
			
		||||
 | 
			
		||||
whatwg-fetch@^3.4.1:
 | 
			
		||||
  version "3.6.18"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.18.tgz#2f640cdee315abced7daeaed2309abd1e44e62d4"
 | 
			
		||||
  integrity sha512-ltN7j66EneWn5TFDO4L9inYC1D+Czsxlrw2SalgjMmEMkLfA5SIZxEFdE6QtHFiiM6Q7WL32c7AkI3w6yxM84Q==
 | 
			
		||||
 | 
			
		||||
whatwg-mimetype@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
 | 
			
		||||
@ -6194,6 +5874,11 @@ wrappy@1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 | 
			
		||||
  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 | 
			
		||||
 | 
			
		||||
ws@^7.0.0:
 | 
			
		||||
  version "7.5.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
 | 
			
		||||
  integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
 | 
			
		||||
 | 
			
		||||
ws@^8.13.0:
 | 
			
		||||
  version "8.13.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user