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/* | ||||
|  | ||||
							
								
								
									
										73
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										73
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,4 +1,4 @@ | ||||
| name: CI  | ||||
| name: CI | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
| @ -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 | ||||
| @ -177,15 +204,15 @@ jobs: | ||||
|         uses: google-github-actions/setup-gcloud@v1.1.1 | ||||
|         with: | ||||
|           project_id: kittycadapi | ||||
|        | ||||
|  | ||||
|       - name: Upload release files to public bucket | ||||
|         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 }}  | ||||
|        | ||||
|           destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }} | ||||
|  | ||||
|       - name: Upload update endpoint to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v1.0.3 | ||||
|         with: | ||||
| @ -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 = ` | ||||
| @ -255,7 +260,7 @@ const mySk1 = startSketchAt([0, 0]) | ||||
|   // comment here | ||||
|   |> lineTo({ to: [0, 1], tag: 'myTag' }, %) | ||||
|   |> lineTo([1, 1], %) /* and | ||||
|   here  | ||||
|   here | ||||
|   */ | ||||
|   // a comment between pipe expression statements | ||||
|   |> rx(90, %) | ||||
| @ -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