Compare commits

..

1 Commits

Author SHA1 Message Date
062f9a1ad8 Nicer error messages for unknown tokens 2023-11-01 17:36:39 -05:00
29 changed files with 308 additions and 332 deletions

View File

@ -12,13 +12,9 @@ on:
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
env:
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && contains(github.event.pull_request.title, 'Cut release v') }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-latest'
@ -50,20 +46,6 @@ jobs:
- run: yarn tsc
check-typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-latest
steps:
@ -120,7 +102,7 @@ jobs:
build-test-apps:
needs: [prepare-json-files]
needs: [check-format, build-test-web, prepare-json-files, check-types]
runs-on: ${{ matrix.os }}
strategy:
matrix:
@ -137,7 +119,7 @@ jobs:
cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
- name: Install ubuntu system dependencies
- name: install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest'
run: >
sudo apt-get update &&
@ -157,10 +139,10 @@ jobs:
- run: yarn install
- name: Setup Rust
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
@ -169,27 +151,24 @@ jobs:
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
- name: wasm prep
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
npx wasm-pack build --target web --out-dir pkg
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Fix format
run: yarn fmt
- name: Install Universal target (MacOS only)
- name: install apple silicon target mac
if: matrix.os == 'macos-latest'
run: |
rustup target add aarch64-apple-darwin
- name: Prepare certificate and variables (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
- name: Prepare Windows certificate and variables
if: matrix.os == 'windows-latest'
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
@ -203,8 +182,8 @@ jobs:
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
- name: Setup Windows certicate with SSM KSP
if: matrix.os == 'windows-latest'
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
@ -214,17 +193,8 @@ jobs:
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug)
- name: Build and sign the app for the current platform
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'false' }}
with:
includeRelease: false
includeDebug: true
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
- name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
@ -234,35 +204,34 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
with:
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
- uses: actions/upload-artifact@v3
env:
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
- name: Install tauri-driver for e2e tests (linux only)
- name: Install tauri-driver for e2e tests
if: matrix.os == 'ubuntu-latest'
uses: actions-rs/cargo@v1
with:
command: install
args: tauri-driver
- name: Run e2e tests (linux only)
- name: Run e2e tests
if: matrix.os == 'ubuntu-latest'
run: xvfb-run yarn test:e2e
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
typos: # Edit .codespellrc to change this CI job's configuration.
runs-on: ubuntu-latest
steps:
- name: Check for typos
uses: codespell-project/actions-codespell@v2
publish-apps-release:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
needs: [build-test-web, prepare-json-files, build-test-apps]
env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}

View File

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

View File

@ -8,7 +8,7 @@
},
"package": {
"productName": "kittycad-modeling",
"version": "0.11.2"
"version": "0.11.1"
},
"tauri": {
"allowlist": {
@ -72,13 +72,23 @@
},
"resources": [],
"shortDescription": "",
"targets": "all"
"targets": "all",
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
}
},
"security": {
"csp": null
},
"updater": {
"active": false
"active": true,
"endpoints": [
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
},
"windows": [
{

View File

@ -1,22 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"updater": {
"active": true,
"endpoints": [
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
},
"bundle": {
"identifier": "io.kittycad.modeling-app",
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
}
}
}
}

View File

@ -1,4 +1,4 @@
import { useCallback, MouseEventHandler } from 'react'
import { useEffect, useCallback, MouseEventHandler } from 'react'
import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid'
import { PaneType, useStore } from './useStore'
@ -19,6 +19,7 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import { useHotkeys } from 'react-hotkeys-hook'
import { getNormalisedCoordinates } from './lib/utils'
import { isTauri } from './lib/isTauri'
import { useLoaderData } from 'react-router-dom'
import { IndexLoaderData } from './Router'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
@ -30,10 +31,11 @@ import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { engineCommandManager } from './lang/std/engineConnection'
import { kclManager } from 'lang/KclSinglton'
import { useModelingContext } from 'hooks/useModelingContext'
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
const { code: loadedCode, project, file } = useLoaderData() as IndexLoaderData
useHotKeyListener()
const {
@ -80,6 +82,26 @@ export function App() {
? 'opacity-40'
: ''
// Use file code loaded from disk
// on mount, and overwrite any locally-stored code
useEffect(() => {
if (isTauri() && loadedCode !== null) {
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.setCodeAndExecute(loadedCode)
} else {
// Otherwise, just set the code and wait for the connection to complete
kclManager.setCode(loadedCode)
}
}
return () => {
// Clear code on unmount if in desktop app
if (isTauri()) {
kclManager.setCode('')
}
}
}, [loadedCode])
useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => {
@ -226,7 +248,7 @@ export function App() {
<Stream className="absolute inset-0 z-0" />
{showDebugPanel && (
<DebugPanel
title="Debug (AST Explorer)"
title="Debug"
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +

View File

@ -42,7 +42,7 @@ import CommandBarProvider from 'components/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider, kclManager } from 'lang/KclSinglton'
import { KclContextProvider } from 'lang/KclSinglton'
import FileMachineProvider from 'components/FileMachineProvider'
import { sep } from '@tauri-apps/api/path'
@ -207,7 +207,6 @@ const router = createBrowserRouter(
projectPath + sep + PROJECT_ENTRYPOINT
)
const children = await readDir(projectPath, { recursive: true })
kclManager.setCodeAndExecute(code, false)
return {
code,

View File

@ -1,5 +1,5 @@
import { useEffect, useState, useRef } from 'react'
import { parse, BinaryPart, Value } from '../lang/wasm'
import { parse, BinaryPart, Value, executor } from '../lang/wasm'
import {
createIdentifier,
createLiteral,
@ -10,7 +10,6 @@ import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSinglton'
import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'useStore'
export const AvailableVars = ({
onVarClick,
@ -131,29 +130,27 @@ export function useCalc({
if (!programMemory || !selectionRange) return
const varInfo = findAllPreviousVariables(
kclManager.ast,
kclManager.programMemory,
programMemory,
selectionRange
)
setAvailableVarInfo(varInfo)
}, [kclManager.ast, kclManager.programMemory, selectionRange])
}, [kclManager.ast, programMemory, selectionRange])
useEffect(() => {
try {
const code = `const __result__ = ${value}`
const code = `const __result__ = ${value}\nshow(__result__)`
const ast = parse(code)
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
executeAst({
executor(
ast,
_programMem,
engineCommandManager,
defaultPlanes: kclManager.defaultPlanes,
useFakeExecutor: true,
programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory)
),
}).then(({ programMemory }) => {
kclManager.defaultPlanes
).then((programMemory) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
@ -170,7 +167,7 @@ export function useCalc({
setCalcResult('NAN')
setValueNode(null)
}
}, [value, availableVarInfo])
}, [value])
return {
valueNode,
@ -215,10 +212,7 @@ export const CreateNewVariable = ({
}) => {
return (
<>
<label
htmlFor="create-new-variable"
className="block mt-3 font-mono text-gray-900"
>
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
Create new variable
</label>
<div className="mt-1 flex gap-2 items-center">
@ -229,7 +223,6 @@ export const CreateNewVariable = ({
onChange={(e) => {
setShouldCreateVariable(e.target.checked)
}}
className="bg-white text-gray-900"
/>
)}
<input

View File

@ -1,7 +1,29 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { v4 as uuidv4 } from 'uuid'
import { EngineCommand } from '../lang/std/engineConnection'
import { useState } from 'react'
import { ActionButton } from '../components/ActionButton'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import { isReducedMotion } from 'lang/util'
import { AstExplorer } from './AstExplorer'
import { engineCommandManager } from '../lang/std/engineConnection'
type SketchModeCmd = Extract<
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
{ type: 'default_camera_enable_sketch_mode' }
>
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
const [sketchModeCmd, setSketchModeCmd] = useState<SketchModeCmd>({
type: 'default_camera_enable_sketch_mode',
origin: { x: 0, y: 0, z: 0 },
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
distance_to_plane: 100,
ortho: true,
animated: !isReducedMotion(),
})
if (!sketchModeCmd) return null
return (
<CollapsiblePanel
{...props}
@ -12,6 +34,67 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
>
<section className="p-4 flex flex-col gap-4">
<Xyz
onChange={setSketchModeCmd}
pointKey="origin"
data={sketchModeCmd}
/>
<Xyz
onChange={setSketchModeCmd}
pointKey="x_axis"
data={sketchModeCmd}
/>
<Xyz
onChange={setSketchModeCmd}
pointKey="y_axis"
data={sketchModeCmd}
/>
<div className="flex">
<div className="pr-4">distance_to_plane</div>
<input
className="w-16 dark:bg-chalkboard-90"
type="number"
value={sketchModeCmd.distance_to_plane}
onChange={({ target }) => {
setSketchModeCmd({
...sketchModeCmd,
distance_to_plane: Number(target.value),
})
}}
/>
<div className="pr-4">ortho</div>
<input
className="w-16"
type="checkbox"
checked={sketchModeCmd.ortho}
onChange={(a) =>
setSketchModeCmd({
...sketchModeCmd,
ortho: a.target.checked,
})
}
/>
</div>
<ActionButton
Element="button"
onClick={() => {
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: sketchModeCmd,
cmd_id: uuidv4(),
})
}}
className="hover:border-succeed-50"
icon={{
icon: faCheck,
bgClassName:
'bg-succeed-80 group-hover:bg-succeed-70 hover:bg-succeed-70',
iconClassName:
'text-succeed-20 group-hover:text-succeed-10 hover:text-succeed-10',
}}
>
Send sketch mode command
</ActionButton>
<div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer />
</div>
@ -19,3 +102,41 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
</CollapsiblePanel>
)
}
const Xyz = ({
pointKey,
data,
onChange,
}: {
pointKey: 'origin' | 'y_axis' | 'x_axis'
data: SketchModeCmd
onChange: (a: SketchModeCmd) => void
}) => {
if (!data) return null
return (
<div className="flex">
<div className="pr-4">{pointKey}</div>
{Object.entries(data[pointKey]).map(([axis, val]) => {
return (
<div key={axis} className="flex">
<div className="w-4">{axis}</div>
<input
className="w-16 dark:bg-chalkboard-90"
type="number"
value={val}
onChange={({ target }) => {
onChange({
...data,
[pointKey]: {
...data[pointKey],
[axis]: Number(target.value),
},
})
}}
/>
</div>
)
})}
</div>
)
}

View File

@ -163,6 +163,7 @@ const FileTreeItem = ({
function openFile() {
if (fileOrDir.children !== undefined) return // Don't open directories
kclManager.setCode('')
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
closePanel()
}

View File

@ -147,7 +147,7 @@ export const ModelingMachineProvider = ({
engineCommandManager.artifactMap[sketchEnginePathId] = {
type: 'result',
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
commandType: 'start_path',
commandType: 'extend_path',
data: null,
raw: {} as any,
}

View File

@ -108,7 +108,7 @@ export const SetAngleLengthModal = ({
</label>
<div className="mt-1 flex">
<button
className="border border-gray-300 px-2 text-gray-900"
className="border border-gray-300 px-2"
onClick={() => setSign(-sign)}
>
{sign > 0 ? '+' : '-'}
@ -118,7 +118,7 @@ export const SetAngleLengthModal = ({
type="text"
name="val"
id="val"
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 text-gray-900"
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"
value={value}
onChange={(e) => {
setValue(e.target.value)

View File

@ -87,7 +87,7 @@ export const GetInfoModal = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white/90 p-6 text-left align-middle shadow-xl transition-all">
<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"
@ -109,7 +109,7 @@ export const GetInfoModal = ({
</label>
<div className="mt-1 flex">
<button
className="border border-gray-400 px-2 mr-1 text-gray-900"
className="border border-gray-300 px-2 mr-1"
onClick={() => setSign(-sign)}
>
{sign > 0 ? '+' : '-'}
@ -119,7 +119,7 @@ export const GetInfoModal = ({
name="val"
id="val"
ref={inputRef}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
value={value}
onChange={(e) => {
setValue(e.target.value)
@ -139,7 +139,7 @@ export const GetInfoModal = ({
name="segName"
id="segName"
disabled={!isSegNameEditable}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
value={segName}
onChange={(e) => {
setSegName(e.target.value)

View File

@ -14,7 +14,7 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib'
import { getNodeFromPath } from 'lang/queryAst'
import { VariableDeclarator, recast, CallExpression } from 'lang/wasm'
import { VariableDeclarator, recast, parse, CallExpression } from 'lang/wasm'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager, useKclContext } from 'lang/KclSinglton'
@ -342,8 +342,7 @@ export const Stream = ({ className = '' }) => {
// update artifact map ranges now that we have updated the ast.
code = recast(modded.modifiedAst)
const astWithCurrentRanges = kclManager.safeParse(code)
if (!astWithCurrentRanges) return
const astWithCurrentRanges = parse(code)
const updateNode = getNodeFromPath<CallExpression>(
astWithCurrentRanges,
modded.pathToNode

View File

@ -17,7 +17,15 @@ import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/lsp/language'
import { EditorView, lineHighlightField } from 'editor/highlightextension'
import { isTauri } from 'lib/isTauri'
import { useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs'
import { toast } from 'react-hot-toast'
import {
EditorView,
addLineHighlight,
lineHighlightField,
} from 'editor/highlightextension'
import { roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors'
import { CSSRuleObject } from 'tailwindcss/types/config'
@ -42,6 +50,7 @@ export const TextEditor = ({
}: {
theme: Themes.Light | Themes.Dark
}) => {
const pathParams = useParams()
const { editorView, isLSPServerReady, setEditorView, setIsLSPServerReady } =
useStore((s) => ({
editorView: s.editorView,
@ -104,6 +113,18 @@ export const TextEditor = ({
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (newCode: string) => {
kclManager.setCodeAndExecute(newCode)
if (isTauri() && pathParams.id) {
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
writeTextFile(pathParams.id, newCode).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) {

View File

@ -32,7 +32,6 @@ export function useEngineConnectionSubscriptions() {
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: async (engineEvent) => {
if (!context.sketchEnginePathId) return
const event = await getEventForSelectWithPoint(engineEvent, {
sketchEnginePathId: context.sketchEnginePathId,
})

View File

@ -26,6 +26,10 @@ export function useSetupEngineManager(
const hasSetNonZeroDimensions = useRef<boolean>(false)
useEffect(() => {
kclManager.executeCode()
}, [])
useLayoutEffect(() => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.

View File

@ -18,11 +18,7 @@ import { bracket } from 'lib/exampleKcl'
import { createContext, useContext, useEffect, useState } from 'react'
import { getNodeFromPath } from './queryAst'
import { IndexLoaderData } from 'Router'
import { Params, useLoaderData } from 'react-router-dom'
import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/api/fs'
import { toast } from 'react-hot-toast'
import { useParams } from 'react-router-dom'
import { useLoaderData } from 'react-router-dom'
const PERSIST_CODE_TOKEN = 'persistCode'
@ -45,20 +41,10 @@ class KclManager {
private _kclErrors: KCLError[] = []
private _isExecuting = false
private _wasmInitFailed = true
private _params: Params<string> = {}
engineCommandManager: EngineCommandManager
private _defferer = deferExecution((code: string) => {
const ast = this.safeParse(code)
if (!ast) return
try {
const fmtAndStringify = (ast: Program) =>
JSON.stringify(parse(recast(ast)))
const isAstTheSame = fmtAndStringify(ast) === fmtAndStringify(this._ast)
if (isAstTheSame) return
} catch (e) {
console.error(e)
}
const ast = parse(code)
this.executeAst(ast)
}, 600)
@ -85,21 +71,6 @@ class KclManager {
set code(code) {
this._code = code
this._codeCallBack(code)
if (isTauri()) {
setTimeout(() => {
// Wait one event loop to give a chance for params to be set
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
this._params.id &&
writeTextFile(this._params.id, code).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')
})
})
} else {
localStorage.setItem(PERSIST_CODE_TOKEN, code)
}
}
get programMemory() {
@ -146,17 +117,8 @@ class KclManager {
this._wasmInitFailedCallback(wasmInitFailed)
}
setParams(params: Params<string>) {
this._params = params
}
constructor(engineCommandManager: EngineCommandManager) {
this.engineCommandManager = engineCommandManager
if (isTauri()) {
this.code = ''
return
}
const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN)
// TODO #819 remove zustand persistence logic in a few months
// short term migration, shouldn't make a difference for tauri app users
@ -168,7 +130,6 @@ class KclManager {
zustandStore.state.code = ''
localStorage.setItem('store', JSON.stringify(zustandStore))
} else if (storedCode === null) {
console.log('stored brack thing')
this.code = bracket
} else {
this.code = storedCode
@ -203,21 +164,6 @@ class KclManager {
this._executeCallback = callback
}
safeParse(code: string): Program | null {
try {
const ast = parse(code)
this.kclErrors = []
return ast
} catch (e) {
console.error('error parsing code', e)
if (e instanceof KCLError) {
this.kclErrors = [e]
if (e.msg === 'file is empty') engineCommandManager.endSession()
}
return null
}
}
async ensureWasmInit() {
try {
await initPromise
@ -243,15 +189,15 @@ class KclManager {
this._programMemory = programMemory
this._ast = { ...ast }
if (updateCode) {
this.code = recast(ast)
this._code = recast(ast)
this._codeCallBack(this._code)
}
this._executeCallback()
}
async executeAstMock(ast: Program = this._ast, updateCode = false) {
await this.ensureWasmInit()
const newCode = recast(ast)
const newAst = this.safeParse(newCode)
if (!newAst) return
const newAst = parse(newCode)
await this?.engineCommandManager?.waitForReady
if (updateCode) {
this.setCode(recast(ast))
@ -287,17 +233,13 @@ class KclManager {
this.ast = ast
if (code) this.code = code
}
setCode(code: string, shouldWriteFile = true) {
if (shouldWriteFile) {
// use the normal code setter
this.code = code
return
}
setCode(code: string) {
this._code = code
this._codeCallBack(code)
localStorage.setItem(PERSIST_CODE_TOKEN, code)
}
setCodeAndExecute(code: string, shouldWriteFile = true) {
this.setCode(code, shouldWriteFile)
setCodeAndExecute(code: string) {
this.setCode(code)
if (code.trim()) {
this._defferer(code)
return
@ -318,9 +260,7 @@ class KclManager {
this.engineCommandManager.endSession()
}
format() {
const ast = this.safeParse(this.code)
if (!ast) return
this.code = recast(ast)
this.code = recast(parse(kclManager.code))
}
// There's overlapping responsibility between updateAst and executeAst.
// updateAst was added as it was used a lot before xState migration so makes the port easier.
@ -330,13 +270,15 @@ class KclManager {
execute: boolean,
optionalParams?: {
focusPath?: PathToNode
callBack?: (ast: Program) => void
}
): Promise<Selections | null> {
const newCode = recast(ast)
const astWithUpdatedSource = this.safeParse(newCode)
if (!astWithUpdatedSource) return null
const astWithUpdatedSource = parse(newCode)
optionalParams?.callBack?.(astWithUpdatedSource)
let returnVal: Selections | null = null
this.code = newCode
if (optionalParams?.focusPath) {
const { node } = getNodeFromPath<any>(
astWithUpdatedSource,
@ -357,12 +299,12 @@ class KclManager {
if (execute) {
// Call execute on the set ast.
await this.executeAst(astWithUpdatedSource, true)
await this.executeAst(astWithUpdatedSource)
} else {
// When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor
// instead.
await this.executeAstMock(astWithUpdatedSource, true)
await this.executeAstMock(astWithUpdatedSource)
}
return returnVal
}
@ -427,11 +369,6 @@ export function KclContextProvider({
setWasmInitFailed,
})
}, [])
const params = useParams()
useEffect(() => {
kclManager.setParams(params)
}, [params])
return (
<KclContext.Provider
value={{

View File

@ -25,13 +25,6 @@ export class KCLLexicalError extends KCLError {
}
}
export class KCLInternalError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('internal', msg, sourceRanges)
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
}
}
export class KCLSyntaxError extends KCLError {
constructor(msg: string, sourceRanges: [number, number][]) {
super('syntax', msg, sourceRanges)

View File

@ -889,7 +889,6 @@ export class EngineCommandManager {
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
// we need to loop over them each individually because if the engine doesn't recognise a single
// id the whole command fails.
const artifactsToDelete: any = {}
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,
@ -899,9 +898,7 @@ export class EngineCommandManager {
'start_path',
]
if (!artifactTypesToDelete.includes(artifact.commandType)) return
artifactsToDelete[id] = artifact
})
Object.keys(artifactsToDelete).forEach((id) => {
const deletCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),

View File

@ -26,4 +26,5 @@ const bracket = startSketchOn('XY')
|> close(%)
|> extrude(width, %)
show(bracket)
`

View File

@ -9,6 +9,7 @@ import { documentDir, homeDir, sep } from '@tauri-apps/api/path'
import { isTauri } from './isTauri'
import { ProjectWithEntryPointMetadata } from '../Router'
import { metadata } from 'tauri-plugin-fs-extra-api'
import { bracket } from './exampleKcl'
const PROJECT_FOLDER = 'kittycad-modeling-projects'
export const FILE_EXT = '.kcl'
@ -210,8 +211,7 @@ export function sortProject(project: FileEntry[]): FileEntry[] {
// Creates a new file in the default directory with the default project name
// Returns the path to the new file
export async function createNewProject(
path: string,
initCode = ''
path: string
): Promise<ProjectWithEntryPointMetadata> {
if (!isTauri) {
throw new Error('createNewProject() can only be called from a Tauri app')
@ -225,12 +225,10 @@ export async function createNewProject(
})
}
await writeTextFile(path + sep + PROJECT_ENTRYPOINT, initCode).catch(
(err) => {
console.error('Error creating new file:', err)
throw err
}
)
await writeTextFile(path + sep + PROJECT_ENTRYPOINT, bracket).catch((err) => {
console.error('Error creating new file:', err)
throw err
})
const m = await metadata(path)

View File

@ -43,10 +43,7 @@ function OnboardingWithNewFile() {
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newFile = await createNewProject(
defaultDirectory + sep + name,
bracket
)
const newFile = await createNewProject(defaultDirectory + sep + name)
navigate(
`${paths.FILE}/${encodeURIComponent(
newFile.path + sep + PROJECT_ENTRYPOINT
@ -82,7 +79,7 @@ function OnboardingWithNewFile() {
<ActionButton
Element="button"
onClick={() => {
kclManager.setCodeAndExecute(bracket)
kclManager.setCode(bracket)
next()
}}
icon={{ icon: faArrowRight }}
@ -121,7 +118,7 @@ function OnboardingWithNewFile() {
Element="button"
onClick={() => {
createAndOpenNewProject()
kclManager.setCode(bracket, false)
kclManager.setCode(bracket)
dismiss()
}}
icon={{ icon: faArrowRight }}

View File

@ -32,7 +32,6 @@ import {
} from 'lib/tauriFS'
import { ONBOARDING_PROJECT_NAME } from './Onboarding'
import { sep } from '@tauri-apps/api/path'
import { bracket } from 'lib/exampleKcl'
export const Settings = () => {
const loaderData =
@ -97,10 +96,7 @@ export const Settings = () => {
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newFile = await createNewProject(
defaultDirectory + sep + name,
bracket
)
const newFile = await createNewProject(defaultDirectory + sep + name)
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
}

View File

@ -253,13 +253,11 @@ export async function executeAst({
engineCommandManager,
defaultPlanes,
useFakeExecutor = false,
programMemoryOverride,
}: {
ast: Program
engineCommandManager: EngineCommandManager
defaultPlanes: DefaultPlanes
useFakeExecutor?: boolean
programMemoryOverride?: ProgramMemory
}): Promise<{
logs: string[]
errors: KCLError[]
@ -271,13 +269,10 @@ export async function executeAst({
engineCommandManager.startNewSession()
}
const programMemory = await (useFakeExecutor
? enginelessExecutor(
ast,
programMemoryOverride || {
root: defaultProgramMemory,
return: null,
}
)
? enginelessExecutor(ast, {
root: defaultProgramMemory,
return: null,
})
: _executor(
ast,
{

View File

@ -28,8 +28,6 @@ pub enum KclError {
InvalidExpression(KclErrorDetails),
#[error("engine: {0:?}")]
Engine(KclErrorDetails),
#[error("internal error, please report to KittyCAD team: {0:?}")]
Internal(KclErrorDetails),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
@ -44,8 +42,21 @@ pub struct KclErrorDetails {
impl KclError {
/// Get the error message, line and column from the error and input code.
pub fn get_message_line_column(&self, input: &str) -> (String, Option<usize>, Option<usize>) {
let (type_, source_range, message) = match &self {
KclError::Lexical(e) => ("lexical", e.source_ranges.clone(), e.message.clone()),
KclError::Syntax(e) => ("syntax", e.source_ranges.clone(), e.message.clone()),
KclError::Semantic(e) => ("semantic", e.source_ranges.clone(), e.message.clone()),
KclError::Type(e) => ("type", e.source_ranges.clone(), e.message.clone()),
KclError::Unimplemented(e) => ("unimplemented", e.source_ranges.clone(), e.message.clone()),
KclError::Unexpected(e) => ("unexpected", e.source_ranges.clone(), e.message.clone()),
KclError::ValueAlreadyDefined(e) => ("value already defined", e.source_ranges.clone(), e.message.clone()),
KclError::UndefinedValue(e) => ("undefined value", e.source_ranges.clone(), e.message.clone()),
KclError::InvalidExpression(e) => ("invalid expression", e.source_ranges.clone(), e.message.clone()),
KclError::Engine(e) => ("engine", e.source_ranges.clone(), e.message.clone()),
};
// Calculate the line and column of the error from the source range.
let (line, column) = if let Some(range) = self.source_ranges().first() {
let (line, column) = if let Some(range) = source_range.first() {
let line = input[..range.0[0]].lines().count();
let column = input[..range.0[0]].lines().last().map(|l| l.len()).unwrap_or_default();
@ -54,23 +65,7 @@ impl KclError {
(None, None)
};
(format!("{}: {}", self.error_type(), self.message()), line, column)
}
pub fn error_type(&self) -> &'static str {
match self {
KclError::Lexical(_) => "lexical",
KclError::Syntax(_) => "syntax",
KclError::Semantic(_) => "semantic",
KclError::Type(_) => "type",
KclError::Unimplemented(_) => "unimplemented",
KclError::Unexpected(_) => "unexpected",
KclError::ValueAlreadyDefined(_) => "value already defined",
KclError::UndefinedValue(_) => "undefined value",
KclError::InvalidExpression(_) => "invalid expression",
KclError::Engine(_) => "engine",
KclError::Internal(_) => "internal",
}
(format!("{}: {}", type_, message), line, column)
}
pub fn source_ranges(&self) -> Vec<SourceRange> {
@ -85,7 +80,6 @@ impl KclError {
KclError::UndefinedValue(e) => e.source_ranges.clone(),
KclError::InvalidExpression(e) => e.source_ranges.clone(),
KclError::Engine(e) => e.source_ranges.clone(),
KclError::Internal(e) => e.source_ranges.clone(),
}
}
@ -102,7 +96,6 @@ impl KclError {
KclError::UndefinedValue(e) => &e.message,
KclError::InvalidExpression(e) => &e.message,
KclError::Engine(e) => &e.message,
KclError::Internal(e) => &e.message,
}
}

View File

@ -1,33 +1,20 @@
use crate::{
ast::types::{BinaryExpression, BinaryOperator, BinaryPart},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
};
use crate::ast::types::{BinaryExpression, BinaryOperator, BinaryPart};
/// Parses a list of tokens (in infix order, i.e. as the user typed them)
/// into a binary expression tree.
pub fn parse(infix_tokens: Vec<BinaryExpressionToken>) -> Result<BinaryExpression, KclError> {
pub fn parse(infix_tokens: Vec<BinaryExpressionToken>) -> BinaryExpression {
let rpn = postfix(infix_tokens);
evaluate(rpn)
}
/// Parses a list of tokens (in postfix order) into a binary expression tree.
fn evaluate(rpn: Vec<BinaryExpressionToken>) -> Result<BinaryExpression, KclError> {
let source_ranges = source_range(&rpn);
let mut operand_stack: Vec<BinaryPart> = Vec::new();
let e = KclError::Internal(KclErrorDetails {
source_ranges,
message: "error parsing binary math expressions".to_owned(),
});
fn evaluate(rpn: Vec<BinaryExpressionToken>) -> BinaryExpression {
let mut operand_stack = Vec::new();
for item in rpn {
let expr = match item {
BinaryExpressionToken::Operator(operator) => {
let Some(right) = operand_stack.pop() else {
return Err(e);
};
let Some(left) = operand_stack.pop() else {
return Err(e);
};
let right: BinaryPart = operand_stack.pop().unwrap();
let left = operand_stack.pop().unwrap();
BinaryPart::BinaryExpression(Box::new(BinaryExpression {
start: left.start(),
end: right.end(),
@ -40,26 +27,10 @@ fn evaluate(rpn: Vec<BinaryExpressionToken>) -> Result<BinaryExpression, KclErro
};
operand_stack.push(expr)
}
if let Some(BinaryPart::BinaryExpression(expr)) = operand_stack.pop() {
Ok(*expr)
if let BinaryPart::BinaryExpression(expr) = operand_stack.pop().unwrap() {
*expr
} else {
// If this branch is used, the evaluation algorithm has a bug and must be fixed.
// This is a programmer error, not a user error.
Err(e)
}
}
fn source_range(tokens: &[BinaryExpressionToken]) -> Vec<SourceRange> {
let sources: Vec<_> = tokens
.iter()
.filter_map(|op| match op {
BinaryExpressionToken::Operator(_) => None,
BinaryExpressionToken::Operand(o) => Some((o.start(), o.end())),
})
.collect();
match (sources.first(), sources.last()) {
(Some((start, _)), Some((_, end))) => vec![SourceRange([*start, *end])],
_ => Vec::new(),
panic!("Last expression was not a binary expression")
}
}
@ -75,16 +46,15 @@ fn postfix(infix: Vec<BinaryExpressionToken>) -> Vec<BinaryExpressionToken> {
// there is an operator o2 at the top of the operator stack which is not a left parenthesis,
// and (o2 has greater precedence than o1 or (o1 and o2 have the same precedence and o1 is left-associative))
// )
// pop o2 from the operator stack into the output queue
while let Some(o2) = operator_stack.pop() {
if (o2.precedence() > o1.precedence())
|| o1.precedence() == o2.precedence() && o1.associativity().is_left()
{
output.push(BinaryExpressionToken::Operator(o2));
} else {
operator_stack.push(o2);
break;
}
while operator_stack
.last()
.map(|o2| {
(o2.precedence() > o1.precedence())
|| o1.precedence() == o2.precedence() && o1.associativity().is_left()
})
.unwrap_or(false)
{
output.push(BinaryExpressionToken::Operator(operator_stack.pop().unwrap()));
}
operator_stack.push(o1);
}
@ -157,7 +127,8 @@ mod tests {
];
for infix_input in tests {
let rpn = postfix(infix_input);
let _tree = evaluate(rpn).unwrap();
let tree = evaluate(rpn);
dbg!(tree);
}
}
}

View File

@ -1039,8 +1039,7 @@ fn binary_expression(i: TokenSlice) -> PResult<BinaryExpression> {
// Pass the token slice into the specialized math parser, for things like
// precedence and converting infix operations to an AST.
let expr = super::math::parse(tokens).map_err(|e| ErrMode::Backtrack(e.into()))?;
Ok(expr)
Ok(super::math::parse(tokens))
}
fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> {
@ -2215,10 +2214,9 @@ const secondExtrude = startSketchOn('XY')
let tokens = crate::token::lexer(">!");
let parser = crate::parser::Parser::new(tokens);
let err = parser.ast().unwrap_err();
assert_eq!(
err.to_string(),
r#"lexical: KclErrorDetails { source_ranges: [SourceRange([1, 2])], message: "found unknown token '!'" }"#
);
// TODO: Better errors when program cannot tokenize.
// https://github.com/KittyCAD/modeling-app/issues/696
assert!(err.to_string().contains("found unknown token"));
}
#[test]
@ -2274,9 +2272,11 @@ z(-[["#,
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
// TODO: Better errors when program cannot tokenize.
// https://github.com/KittyCAD/modeling-app/issues/696
assert_eq!(
result.err().unwrap().to_string(),
r#"lexical: KclErrorDetails { source_ranges: [SourceRange([6, 7])], message: "found unknown token '#'" }"#
r##"lexical: KclErrorDetails { source_ranges: [SourceRange([6, 7])], message: "found unknown token '#'" }"##
);
}
@ -2286,9 +2286,11 @@ z(-[["#,
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
assert!(result.is_err());
// TODO: Better errors when program cannot tokenize.
// https://github.com/KittyCAD/modeling-app/issues/696
assert_eq!(
result.err().unwrap().to_string(),
r#"lexical: KclErrorDetails { source_ranges: [SourceRange([25, 26]), SourceRange([26, 27])], message: "found unknown tokens [#, #]" }"#
r##"lexical: KclErrorDetails { source_ranges: [SourceRange([25, 26]), SourceRange([26, 27])], message: "found unknown tokens [#, #]" }"##
);
}
@ -2574,23 +2576,6 @@ thing(false)
);
}
#[test]
fn random_words_fail() {
let test_program = r#"const part001 = startSketchOn('-XZ')
|> startProfileAt([8.53, 11.8], %)
asdasd asdasd
|> line([11.12, -14.82], %)
|> line([-13.27, -6.98], %)
|> line([-5.09, 12.33], %)
asdasd
"#;
let tokens = crate::token::lexer(test_program);
let parser = crate::parser::Parser::new(tokens);
let result = parser.ast();
let e = result.unwrap_err();
eprintln!("{e:?}")
}
#[test]
fn test_member_expression_sketch_group() {
let some_program_string = r#"fn cube = (pos, scale) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -2,14 +2,11 @@ const os = require('os')
const path = require('path')
const { spawn } = require('child_process')
// keep track of the `tauri-driver` child process
let tauriDriver
const mode = process.env.MODE
const application =
process.env.E2E_APPLICATION || `./src-tauri/target/${mode}/kittycad-modeling`
process.env.E2E_APPLICATION || './src-tauri/target/release/kittycad-modeling'
exports.config = {
port: 4444,