Compare commits
37 Commits
achalmers/
...
kcl-test-s
Author | SHA1 | Date | |
---|---|---|---|
062fae1e54 | |||
d7660e221c | |||
938e27adac | |||
17b9af2416 | |||
64f0f5b773 | |||
f452f9bf00 | |||
97705234c6 | |||
30dfc167d3 | |||
d8105627c0 | |||
6b7fac3642 | |||
35805916aa | |||
4a4400e979 | |||
efd1f288b9 | |||
0337ab9cff | |||
f0dda692f6 | |||
2ce0c59d08 | |||
393b43d485 | |||
4fbcde8773 | |||
12d444fa69 | |||
683b4488af | |||
e1c1e07046 | |||
984420c155 | |||
7bad60dfa3 | |||
aaca88220c | |||
360384e8c8 | |||
ab2ad1313f | |||
897205acc2 | |||
862ca1124e | |||
d9981d9d7b | |||
8df0581831 | |||
54e6358df1 | |||
daf20a978d | |||
8e64798dda | |||
a1ceb4fa47 | |||
2db8d13051 | |||
aceb8052e2 | |||
62fae1e93b |
6
.github/dependabot.yml
vendored
@ -8,21 +8,21 @@ updates:
|
||||
- package-ecosystem: 'npm' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- franknoirot
|
||||
- irev-dev
|
||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src/wasm-lib/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
interval: 'weekly'
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
|
13
.github/workflows/build-test-publish-apps.yml
vendored
@ -85,7 +85,7 @@ jobs:
|
||||
- name: Prepare electron-builder.yml file for updater test
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
run: |
|
||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
|
||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
@ -181,6 +181,7 @@ jobs:
|
||||
- name: Build the app (release)
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
env:
|
||||
PUBLISH_FOR_PULL_REQUEST: true
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
@ -360,17 +361,17 @@ jobs:
|
||||
run: "ls -R out"
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.6'
|
||||
uses: 'google-github-actions/auth@v2.1.7'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@v2.1.0
|
||||
uses: google-github-actions/setup-gcloud@v2.1.2
|
||||
with:
|
||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||
with:
|
||||
path: out
|
||||
glob: 'Zoo*'
|
||||
@ -378,7 +379,7 @@ jobs:
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||
with:
|
||||
path: out
|
||||
glob: 'latest*'
|
||||
@ -386,7 +387,7 @@ jobs:
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
25
.github/workflows/static-analysis.yml
vendored
@ -81,6 +81,31 @@ jobs:
|
||||
- name: Run codespell
|
||||
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
|
||||
|
||||
yarn-unit-test-kcl-samples:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
- run: yarn build:wasm
|
||||
|
||||
- run: yarn simpleserver:bg
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
|
||||
- name: Install Chromium Browser
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
run: yarn playwright install chromium --with-deps
|
||||
|
||||
- name: run unit tests for kcl samples
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
run: yarn test:unit:kcl-samples
|
||||
env:
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
yarn-unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -84,9 +84,13 @@ layout: manual
|
||||
* [`rem`](kcl/rem)
|
||||
* [`revolve`](kcl/revolve)
|
||||
* [`segAng`](kcl/segAng)
|
||||
* [`segEnd`](kcl/segEnd)
|
||||
* [`segEndX`](kcl/segEndX)
|
||||
* [`segEndY`](kcl/segEndY)
|
||||
* [`segLen`](kcl/segLen)
|
||||
* [`segStart`](kcl/segStart)
|
||||
* [`segStartX`](kcl/segStartX)
|
||||
* [`segStartY`](kcl/segStartY)
|
||||
* [`shell`](kcl/shell)
|
||||
* [`sin`](kcl/sin)
|
||||
* [`sqrt`](kcl/sqrt)
|
||||
|
53
docs/kcl/segEnd.md
Normal file
56
docs/kcl/segStart.md
Normal file
43
docs/kcl/segStartX.md
Normal file
44
docs/kcl/segStartY.md
Normal file
4166
docs/kcl/std.json
@ -632,16 +632,18 @@ test.describe('Editor tests', () => {
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// this test might be brittle as we add and remove functions
|
||||
// but should also be easy to update.
|
||||
// tests clicking on an option, selection the first option
|
||||
// and arrowing down to an option
|
||||
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type('sketch001 = start')
|
||||
|
||||
// expect there to be six auto complete options
|
||||
await expect(page.locator('.cm-completionLabel')).toHaveCount(8)
|
||||
// expect there to be some auto complete options
|
||||
// exact number depends on the KCL stdlib, so let's just check it's > 0 for now.
|
||||
await expect(async () => {
|
||||
const children = await page.locator('.cm-completionLabel').count()
|
||||
expect(children).toBeGreaterThan(0)
|
||||
}).toPass()
|
||||
// this makes sure we can accept a completion with click
|
||||
await page.getByText('startSketchOn').click()
|
||||
await page.keyboard.type("'XZ'")
|
||||
|
@ -26,10 +26,6 @@ test.describe('integrations tests', () => {
|
||||
'Creating a new file or switching file while in sketchMode should exit sketchMode',
|
||||
{ tag: '@electron' },
|
||||
async ({ tronApp, homePage, scene, editor, toolbar }) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'windows times out will waiting for the execution indicator?'
|
||||
)
|
||||
await tronApp.initialise({
|
||||
fixtures: { homePage, scene, editor, toolbar },
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -55,7 +51,6 @@ test.describe('integrations tests', () => {
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.openProject('test-sample')
|
||||
// windows times out here, hence the skip above
|
||||
await scene.waitForExecutionDone()
|
||||
})
|
||||
await test.step('enter sketch mode', async () => {
|
||||
@ -71,10 +66,13 @@ test.describe('integrations tests', () => {
|
||||
await toolbar.editSketch()
|
||||
await expect(toolbar.exitSketchBtn).toBeVisible()
|
||||
})
|
||||
|
||||
const fileName = 'Untitled.kcl'
|
||||
await test.step('check sketch mode is exited when creating new file', async () => {
|
||||
await toolbar.fileTreeBtn.click()
|
||||
await toolbar.expectFileTreeState(['main.kcl'])
|
||||
await toolbar.createFile({ wait: true })
|
||||
|
||||
await toolbar.createFile({ fileName, waitForToastToDisappear: true })
|
||||
|
||||
// check we're out of sketch mode
|
||||
await expect(toolbar.exitSketchBtn).not.toBeVisible()
|
||||
@ -93,10 +91,10 @@ test.describe('integrations tests', () => {
|
||||
})
|
||||
await toolbar.editSketch()
|
||||
await expect(toolbar.exitSketchBtn).toBeVisible()
|
||||
await toolbar.expectFileTreeState(['main.kcl', 'Untitled.kcl'])
|
||||
await toolbar.expectFileTreeState(['main.kcl', fileName])
|
||||
})
|
||||
await test.step('check sketch mode is exited when opening a different file', async () => {
|
||||
await toolbar.openFile('untitled.kcl', { wait: false })
|
||||
await toolbar.openFile(fileName, { wait: false })
|
||||
|
||||
// check we're out of sketch mode
|
||||
await expect(toolbar.exitSketchBtn).not.toBeVisible()
|
||||
|
@ -195,7 +195,7 @@ export class SceneFixture {
|
||||
}
|
||||
|
||||
waitForExecutionDone = async () => {
|
||||
await expect(this.exeIndicator).toBeVisible()
|
||||
await expect(this.exeIndicator).toBeVisible({ timeout: 30000 })
|
||||
}
|
||||
|
||||
expectPixelColor = async (
|
||||
|
@ -16,6 +16,7 @@ export class ToolbarFixture {
|
||||
fileCreateToast!: Locator
|
||||
filePane!: Locator
|
||||
exeIndicator!: Locator
|
||||
treeInputField!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
@ -31,6 +32,7 @@ export class ToolbarFixture {
|
||||
this.editSketchBtn = page.getByText('Edit Sketch')
|
||||
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
||||
this.createFileBtn = page.getByTestId('create-file-button')
|
||||
this.treeInputField = page.getByTestId('tree-input-field')
|
||||
|
||||
this.filePane = page.locator('#files-pane')
|
||||
this.fileCreateToast = page.getByText('Successfully created')
|
||||
@ -59,10 +61,15 @@ export class ToolbarFixture {
|
||||
expectFileTreeState = async (expected: string[]) => {
|
||||
await expect.poll(this._serialiseFileTree).toEqual(expected)
|
||||
}
|
||||
createFile = async ({ wait }: { wait: boolean } = { wait: false }) => {
|
||||
createFile = async (args: {
|
||||
fileName: string
|
||||
waitForToastToDisappear: boolean
|
||||
}) => {
|
||||
await this.createFileBtn.click()
|
||||
await this.treeInputField.fill(args.fileName)
|
||||
await this.treeInputField.press('Enter')
|
||||
await expect(this.fileCreateToast).toBeVisible()
|
||||
if (wait) {
|
||||
if (args.waitForToastToDisappear) {
|
||||
await this.fileCreateToast.waitFor({ state: 'detached' })
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export const isErrorWhitelisted = (exception: Error) => {
|
||||
{
|
||||
name: '"{"kind"',
|
||||
message:
|
||||
'"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}"',
|
||||
'"engine","sourceRanges":[[0,0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}"',
|
||||
stack: '',
|
||||
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
|
||||
project: 'Google Chrome',
|
||||
@ -156,8 +156,8 @@ export const isErrorWhitelisted = (exception: Error) => {
|
||||
{
|
||||
name: 'Unhandled Promise Rejection',
|
||||
message:
|
||||
'{"kind":"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}',
|
||||
stack: `Unhandled Promise Rejection: {"kind":"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: \`JsValue(undefined)\`"}
|
||||
'{"kind":"engine","sourceRanges":[[0,0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}',
|
||||
stack: `Unhandled Promise Rejection: {"kind":"engine","sourceRanges":[[0,0,0]],"msg":"Failed to get string from response from engine: \`JsValue(undefined)\`"}
|
||||
at unknown (http://localhost:3000/src/lang/std/engineConnection.ts:1245:26)`,
|
||||
foundInSpec:
|
||||
'e2e/playwright/onboarding-tests.spec.ts Click through each onboarding step',
|
||||
@ -253,7 +253,7 @@ export const isErrorWhitelisted = (exception: Error) => {
|
||||
{
|
||||
name: '{"kind"',
|
||||
stack: ``,
|
||||
message: `engine","sourceRanges":[[0,0]],"msg":"Failed to wait for promise from engine: JsValue(\\"Force interrupt, executionIsStale, new AST requested\\")"}`,
|
||||
message: `engine","sourceRanges":[[0,0,0]],"msg":"Failed to wait for promise from engine: JsValue(\\"Force interrupt, executionIsStale, new AST requested\\")"}`,
|
||||
project: 'Google Chrome',
|
||||
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
|
||||
},
|
||||
|
@ -1669,7 +1669,8 @@ test(
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
// Flaky
|
||||
test.fixme(
|
||||
'Original project name persist after onboarding',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
1
interface.d.ts
vendored
@ -78,6 +78,7 @@ export interface IElectronAPI {
|
||||
) => Electron.IpcRenderer
|
||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||
appRestart: () => void
|
||||
getArgvParsed: () => any
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
25
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zoo-modeling-app",
|
||||
"version": "0.26.2",
|
||||
"version": "0.26.3",
|
||||
"private": true,
|
||||
"productName": "Zoo Modeling App",
|
||||
"author": {
|
||||
@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/language": "^6.10.2",
|
||||
"@codemirror/language": "^6.10.3",
|
||||
"@codemirror/lint": "^6.8.1",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
@ -40,7 +40,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.3.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
@ -60,12 +60,13 @@
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"three": "^0.166.1",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuid": "^9.0.1",
|
||||
"uuid": "^11.0.2",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xstate": "^5.17.4"
|
||||
"xstate": "^5.17.4",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
@ -105,7 +106,8 @@
|
||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||
"test-setup": "yarn install && yarn build:wasm",
|
||||
"test": "vitest --mode development",
|
||||
"test:unit": "vitest run --mode development",
|
||||
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
||||
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
||||
"test:playwright:browser:chrome": "playwright test --project='Google Chrome' --config=playwright.ci.config.ts --grep-invert='@snapshot|@electron'",
|
||||
"test:playwright:browser:chrome:windows": "playwright test --project=\"Google Chrome\" --config=playwright.ci.config.ts --grep-invert=\"@snapshot|@electron|@skipWin\"",
|
||||
"test:playwright:browser:chrome:ubuntu": "playwright test --project='Google Chrome' --config=playwright.ci.config.ts --grep-invert='@snapshot|@electron|@skipLinux'",
|
||||
@ -117,7 +119,8 @@
|
||||
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipWin",
|
||||
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipMacos",
|
||||
"test:playwright:electron:ubuntu:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipLinux",
|
||||
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000"
|
||||
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
|
||||
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -144,8 +147,8 @@
|
||||
"@electron-forge/maker-deb": "^7.4.0",
|
||||
"@electron-forge/maker-rpm": "^7.4.0",
|
||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||
"@electron-forge/maker-wix": "^7.4.0",
|
||||
"@electron-forge/maker-zip": "^7.4.0",
|
||||
"@electron-forge/maker-wix": "^7.5.0",
|
||||
"@electron-forge/maker-zip": "^7.5.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
||||
"@electron-forge/plugin-vite": "^7.4.0",
|
||||
@ -171,7 +174,7 @@
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
@ -187,7 +190,7 @@
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"happy-dom": "^14.3.10",
|
||||
"happy-dom": "^15.10.2",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^9.1.5",
|
||||
"kill-port": "^2.0.1",
|
||||
|
@ -22,6 +22,10 @@ import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from 'react-router-dom'
|
||||
import { ErrorPage } from './components/ErrorPage'
|
||||
import { Settings } from './routes/Settings'
|
||||
import { Telemetry } from './routes/Telemetry'
|
||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||
import SignIn from './routes/SignIn'
|
||||
import { Auth } from './Auth'
|
||||
@ -28,6 +29,7 @@ import {
|
||||
homeLoader,
|
||||
onboardingRedirectLoader,
|
||||
settingsLoader,
|
||||
telemetryLoader,
|
||||
} from 'lib/routeLoaders'
|
||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||
@ -43,6 +45,7 @@ import { coreDump } from 'lang/wasm'
|
||||
import { useMemo } from 'react'
|
||||
import { AppStateProvider } from 'AppState'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { RouteProvider } from 'components/RouteProvider'
|
||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
@ -56,19 +59,21 @@ const router = createRouter([
|
||||
* inefficient re-renders, use the react profiler to see. */
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
<RouteProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
<ProjectsContextProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</RouteProvider>
|
||||
</CommandBarProvider>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
@ -124,6 +129,16 @@ const router = createRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: PATHS.FILE + 'TELEMETRY',
|
||||
loader: telemetryLoader,
|
||||
children: [
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||
element: <Telemetry />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -149,6 +164,11 @@ const router = createRouter([
|
||||
loader: settingsLoader,
|
||||
element: <Settings />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||
loader: telemetryLoader,
|
||||
element: <Telemetry />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
12
src/commandLineArgs.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import yargs from 'yargs'
|
||||
import { hideBin } from 'yargs/helpers'
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.option('telemetry', {
|
||||
alias: 't',
|
||||
type: 'boolean',
|
||||
description: 'Writes startup telemetry to file on disk.',
|
||||
})
|
||||
.parse()
|
||||
|
||||
export default argv
|
@ -1161,6 +1161,29 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
stopwatch: (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.95705 5.99046C7.05643 6.44935 6.33654 7.19809 5.91336 8.11602C5.49019 9.03396 5.38838 10.0676 5.62434 11.0505C5.8603 12.0334 6.42029 12.9081 7.21408 13.5339C8.00787 14.1597 8.98922 14.5 10 14.5C11.0108 14.5 11.9921 14.1597 12.7859 13.5339C13.5797 12.9082 14.1397 12.0334 14.3757 11.0505C14.6116 10.0676 14.5098 9.03396 14.0866 8.11603C13.6635 7.19809 12.9436 6.44935 12.043 5.99046L12.497 5.09946C13.5977 5.66032 14.4776 6.57544 14.9948 7.69737C15.512 8.81929 15.6364 10.0827 15.348 11.2839C15.0596 12.4852 14.3752 13.5544 13.405 14.3192C12.4348 15.0841 11.2354 15.5 10 15.5C8.7646 15.5 7.56517 15.0841 6.59499 14.3192C5.6248 13.5544 4.94037 12.4852 4.65197 11.2839C4.36357 10.0827 4.488 8.81929 5.00522 7.69736C5.52243 6.57544 6.40231 5.66032 7.50306 5.09946L7.95705 5.99046Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M10 5.5V4M10 4H8M10 4H12" stroke="currentColor" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.8536 7.85356L10.3536 10.3536C10.1583 10.5488 9.84171 10.5488 9.64645 10.3536C9.45118 10.1583 9.45118 9.84172 9.64645 9.64645L12.1464 7.14645L12.8536 7.85356Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
} as const
|
||||
|
||||
export type CustomIconName = keyof typeof CustomIconMap
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
KclSamplesManifestItem,
|
||||
} from 'lib/getKclSamplesManifest'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { markOnce } from 'lib/performance'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -54,6 +55,7 @@ export const FileMachineProvider = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
markOnce('code/didLoadFile')
|
||||
async function fetchKclSamples() {
|
||||
setKclSamples(await getKclSamplesManifest())
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ import { Dispatch, useCallback, useRef, useState } from 'react'
|
||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
import { Disclosure } from '@headlessui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronRight, faPencil } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import styles from './FileTree.module.css'
|
||||
import { sortProject } from 'lib/desktopFS'
|
||||
import { sortFilesAndDirectories } from 'lib/desktopFS'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
@ -27,6 +27,36 @@ function getIndentationCSS(level: number) {
|
||||
return `calc(1rem * ${level + 1})`
|
||||
}
|
||||
|
||||
function TreeEntryInput(props: {
|
||||
level: number
|
||||
onSubmit: (value: string) => void
|
||||
}) {
|
||||
const [value, setValue] = useState('')
|
||||
const onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Enter') return
|
||||
props.onSubmit(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span className="sr-only">Entry input</span>
|
||||
<input
|
||||
data-testid="tree-input-field"
|
||||
type="text"
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
||||
onBlur={() => props.onSubmit(value)}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyPress={onKeyPress}
|
||||
style={{ paddingInlineStart: getIndentationCSS(props.level) }}
|
||||
value={value}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function RenameForm({
|
||||
fileOrDir,
|
||||
onSubmit,
|
||||
@ -113,23 +143,44 @@ function DeleteFileTreeItemDialog({
|
||||
}
|
||||
|
||||
const FileTreeItem = ({
|
||||
parentDir,
|
||||
project,
|
||||
currentFile,
|
||||
lastDirectoryClicked,
|
||||
fileOrDir,
|
||||
onNavigateToFile,
|
||||
onClickDirectory,
|
||||
onCreateFile,
|
||||
onCreateFolder,
|
||||
newTreeEntry,
|
||||
level = 0,
|
||||
treeSelection,
|
||||
setTreeSelection,
|
||||
}: {
|
||||
parentDir: FileEntry | undefined
|
||||
project?: IndexLoaderData['project']
|
||||
currentFile?: IndexLoaderData['file']
|
||||
lastDirectoryClicked?: FileEntry
|
||||
fileOrDir: FileEntry
|
||||
onNavigateToFile?: () => void
|
||||
onClickDirectory: (
|
||||
open: boolean,
|
||||
path: FileEntry,
|
||||
parentDir: FileEntry | undefined
|
||||
) => void
|
||||
onCreateFile: (name: string) => void
|
||||
onCreateFolder: (name: string) => void
|
||||
newTreeEntry: TreeEntry
|
||||
level?: number
|
||||
treeSelection: FileEntry | undefined
|
||||
setTreeSelection: Dispatch<React.SetStateAction<FileEntry | undefined>>
|
||||
}) => {
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { onFileOpen, onFileClose } = useLspContext()
|
||||
const navigate = useNavigate()
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
const isCurrentFile = fileOrDir.path === currentFile?.path
|
||||
const isFileOrDirHighlighted = treeSelection?.path === fileOrDir?.path
|
||||
const itemRef = useRef(null)
|
||||
|
||||
// Since every file or directory gets its own FileTreeItem, we can do this.
|
||||
@ -156,6 +207,10 @@ const FileTreeItem = ({
|
||||
[fileOrDir.path]
|
||||
)
|
||||
|
||||
const showNewTreeEntry =
|
||||
newTreeEntry !== undefined &&
|
||||
fileOrDir.path === fileContext.selectedDirectory.path
|
||||
|
||||
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
|
||||
const removeCurrentItemFromRenaming = useCallback(
|
||||
() =>
|
||||
@ -179,13 +234,6 @@ const FileTreeItem = ({
|
||||
})
|
||||
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
|
||||
|
||||
const clickDirectory = () => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
|
||||
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
|
||||
if (e.metaKey && e.key === 'Backspace') {
|
||||
// Open confirmation dialog
|
||||
@ -199,6 +247,8 @@ const FileTreeItem = ({
|
||||
}
|
||||
|
||||
async function handleClick() {
|
||||
setTreeSelection(fileOrDir)
|
||||
|
||||
if (fileOrDir.children !== null) return // Don't open directories
|
||||
|
||||
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
||||
@ -220,16 +270,19 @@ const FileTreeItem = ({
|
||||
// Open kcl files
|
||||
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
}
|
||||
|
||||
onNavigateToFile?.()
|
||||
}
|
||||
|
||||
// The below handles both the "root" of all directories and all subs. It's
|
||||
// why some code is duplicated.
|
||||
return (
|
||||
<div className="contents" data-testid="file-tree-item" ref={itemRef}>
|
||||
{fileOrDir.children === null ? (
|
||||
<li
|
||||
className={
|
||||
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
|
||||
(isCurrentFile
|
||||
(isFileOrDirHighlighted || isCurrentFile
|
||||
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
|
||||
: '')
|
||||
}
|
||||
@ -266,14 +319,13 @@ const FileTreeItem = ({
|
||||
<Disclosure.Button
|
||||
className={
|
||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
||||
(fileContext.selectedDirectory.path.includes(fileOrDir.path)
|
||||
? ' ui-open:bg-primary/10'
|
||||
: '')
|
||||
(isFileOrDirHighlighted ? ' ui-open:bg-primary/10' : '')
|
||||
}
|
||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onClickCapture={clickDirectory}
|
||||
onFocusCapture={clickDirectory}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickDirectory(open, fileOrDir, parentDir)
|
||||
}}
|
||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
@ -315,35 +367,69 @@ const FileTreeItem = ({
|
||||
>
|
||||
<ul
|
||||
className="m-0 p-0"
|
||||
onClickCapture={(e) => {
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
directory: fileOrDir,
|
||||
})
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickDirectory(open, fileOrDir, parentDir)
|
||||
}}
|
||||
onFocusCapture={(e) =>
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
directory: fileOrDir,
|
||||
})
|
||||
}
|
||||
>
|
||||
{fileOrDir.children?.map((child) => (
|
||||
<FileTreeItem
|
||||
fileOrDir={child}
|
||||
project={project}
|
||||
currentFile={currentFile}
|
||||
onNavigateToFile={onNavigateToFile}
|
||||
level={level + 1}
|
||||
key={level + '-' + child.path}
|
||||
/>
|
||||
))}
|
||||
{showNewTreeEntry && (
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{
|
||||
paddingInlineStart: getIndentationCSS(level + 1),
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPencil}
|
||||
className="inline-block mr-2 m-0 p-0 w-2 h-2"
|
||||
/>
|
||||
<TreeEntryInput
|
||||
level={-1}
|
||||
onSubmit={(value: string) =>
|
||||
newTreeEntry === 'file'
|
||||
? onCreateFile(value)
|
||||
: onCreateFolder(value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{sortFilesAndDirectories(fileOrDir.children || []).map(
|
||||
(child) => (
|
||||
<FileTreeItem
|
||||
parentDir={fileOrDir}
|
||||
fileOrDir={child}
|
||||
project={project}
|
||||
currentFile={currentFile}
|
||||
onCreateFile={onCreateFile}
|
||||
onCreateFolder={onCreateFolder}
|
||||
newTreeEntry={newTreeEntry}
|
||||
lastDirectoryClicked={lastDirectoryClicked}
|
||||
onClickDirectory={onClickDirectory}
|
||||
onNavigateToFile={onNavigateToFile}
|
||||
level={level + 1}
|
||||
key={level + '-' + child.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{!showNewTreeEntry && fileOrDir.children?.length === 0 && (
|
||||
<div
|
||||
className="flex items-center text-chalkboard-50"
|
||||
style={{
|
||||
paddingInlineStart: getIndentationCSS(level + 1),
|
||||
}}
|
||||
>
|
||||
<div>No files</div>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
|
||||
{isConfirmingDelete && (
|
||||
<DeleteFileTreeItemDialog
|
||||
fileOrDir={fileOrDir}
|
||||
@ -407,27 +493,15 @@ interface FileTreeProps {
|
||||
) => void
|
||||
}
|
||||
|
||||
export const FileTreeMenu = () => {
|
||||
const { send } = useFileContext()
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
|
||||
function createFile() {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: false, shouldSetToRename: true },
|
||||
})
|
||||
modelingSend({ type: 'Cancel' })
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: true, shouldSetToRename: true },
|
||||
})
|
||||
}
|
||||
|
||||
useHotkeyWrapper(['mod + n'], createFile)
|
||||
useHotkeyWrapper(['mod + shift + n'], createFolder)
|
||||
export const FileTreeMenu = ({
|
||||
onCreateFile,
|
||||
onCreateFolder,
|
||||
}: {
|
||||
onCreateFile: () => void
|
||||
onCreateFolder: () => void
|
||||
}) => {
|
||||
useHotkeyWrapper(['mod + n'], onCreateFile)
|
||||
useHotkeyWrapper(['mod + shift + n'], onCreateFolder)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -440,7 +514,7 @@ export const FileTreeMenu = () => {
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={createFile}
|
||||
onClick={onCreateFile}
|
||||
>
|
||||
<Tooltip position="bottom-right" delay={750}>
|
||||
Create file
|
||||
@ -456,7 +530,7 @@ export const FileTreeMenu = () => {
|
||||
bgClassName: 'bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||
onClick={createFolder}
|
||||
onClick={onCreateFolder}
|
||||
>
|
||||
<Tooltip position="bottom-right" delay={750}>
|
||||
Create folder
|
||||
@ -466,30 +540,110 @@ export const FileTreeMenu = () => {
|
||||
)
|
||||
}
|
||||
|
||||
type TreeEntry = 'file' | 'folder' | undefined
|
||||
|
||||
export const useFileTreeOperations = () => {
|
||||
const { send } = useFileContext()
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
|
||||
// As long as this is undefined, a new "file tree entry prompt" is not shown.
|
||||
const [newTreeEntry, setNewTreeEntry] = useState<TreeEntry>(undefined)
|
||||
|
||||
function createFile(args: { dryRun: boolean; name?: string }) {
|
||||
if (args.dryRun) {
|
||||
setNewTreeEntry('file')
|
||||
return
|
||||
}
|
||||
|
||||
// Clear so that the entry prompt goes away.
|
||||
setNewTreeEntry(undefined)
|
||||
|
||||
if (!args.name) return
|
||||
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: args.name, makeDir: false, shouldSetToRename: false },
|
||||
})
|
||||
modelingSend({ type: 'Cancel' })
|
||||
}
|
||||
|
||||
function createFolder(args: { dryRun: boolean; name?: string }) {
|
||||
if (args.dryRun) {
|
||||
setNewTreeEntry('folder')
|
||||
return
|
||||
}
|
||||
|
||||
setNewTreeEntry(undefined)
|
||||
|
||||
if (!args.name) return
|
||||
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: args.name, makeDir: true, shouldSetToRename: false },
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
createFile,
|
||||
createFolder,
|
||||
newTreeEntry,
|
||||
}
|
||||
}
|
||||
|
||||
export const FileTree = ({
|
||||
className = '',
|
||||
onNavigateToFile: closePanel,
|
||||
}: FileTreeProps) => {
|
||||
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||
<FileTreeMenu />
|
||||
<FileTreeMenu
|
||||
onCreateFile={() => createFile({ dryRun: true })}
|
||||
onCreateFolder={() => createFolder({ dryRun: true })}
|
||||
/>
|
||||
</div>
|
||||
<FileTreeInner onNavigateToFile={closePanel} />
|
||||
<FileTreeInner
|
||||
onNavigateToFile={closePanel}
|
||||
newTreeEntry={newTreeEntry}
|
||||
onCreateFile={(name: string) => createFile({ dryRun: false, name })}
|
||||
onCreateFolder={(name: string) => createFolder({ dryRun: false, name })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FileTreeInner = ({
|
||||
onNavigateToFile,
|
||||
onCreateFile,
|
||||
onCreateFolder,
|
||||
newTreeEntry,
|
||||
}: {
|
||||
onCreateFile: (name: string) => void
|
||||
onCreateFolder: (name: string) => void
|
||||
newTreeEntry: TreeEntry
|
||||
onNavigateToFile?: () => void
|
||||
}) => {
|
||||
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { send: fileSend, context: fileContext } = useFileContext()
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
|
||||
const [lastDirectoryClicked, setLastDirectoryClicked] = useState<
|
||||
FileEntry | undefined
|
||||
>(undefined)
|
||||
|
||||
const [treeSelection, setTreeSelection] = useState<FileEntry | undefined>(
|
||||
loaderData.file
|
||||
)
|
||||
|
||||
const onNavigateToFile_ = () => {
|
||||
// Reset modeling state when navigating to a new file
|
||||
onNavigateToFile?.()
|
||||
modelingSend({ type: 'Cancel' })
|
||||
}
|
||||
|
||||
// Refresh the file tree when there are changes.
|
||||
useFileSystemWatcher(
|
||||
async (eventType, path) => {
|
||||
@ -513,33 +667,81 @@ export const FileTreeInner = ({
|
||||
)
|
||||
)
|
||||
|
||||
const clickDirectory = () => {
|
||||
const onTreeEntryInputSubmit = (value: string) => {
|
||||
if (newTreeEntry === 'file') {
|
||||
onCreateFile(value)
|
||||
onNavigateToFile_()
|
||||
} else {
|
||||
onCreateFolder(value)
|
||||
}
|
||||
}
|
||||
|
||||
const onClickDirectory = (
|
||||
open_: boolean,
|
||||
fileOrDir: FileEntry,
|
||||
parentDir: FileEntry | undefined
|
||||
) => {
|
||||
// open true is closed... it's broken. Save me. I've inverted it here for
|
||||
// sanity.
|
||||
const open = !open_
|
||||
|
||||
const target = open ? fileOrDir : parentDir
|
||||
|
||||
// We're at the root, can't select anything further
|
||||
if (!target) return
|
||||
|
||||
setTreeSelection(target)
|
||||
setLastDirectoryClicked(target)
|
||||
fileSend({
|
||||
type: 'Set selected directory',
|
||||
directory: fileContext.project,
|
||||
directory: target,
|
||||
})
|
||||
}
|
||||
|
||||
const showNewTreeEntry =
|
||||
newTreeEntry !== undefined &&
|
||||
fileContext.selectedDirectory.path === loaderData.project?.path
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overflow-auto pb-12 absolute inset-0"
|
||||
data-testid="file-pane-scroll-container"
|
||||
>
|
||||
<ul className="m-0 p-0 text-sm" onClickCapture={clickDirectory}>
|
||||
{sortProject(fileContext.project?.children || []).map((fileOrDir) => (
|
||||
<FileTreeItem
|
||||
project={fileContext.project}
|
||||
currentFile={loaderData?.file}
|
||||
fileOrDir={fileOrDir}
|
||||
onNavigateToFile={() => {
|
||||
// Reset modeling state when navigating to a new file
|
||||
modelingSend({ type: 'Cancel' })
|
||||
onNavigateToFile?.()
|
||||
}}
|
||||
key={fileOrDir.path}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="overflow-auto pb-12 absolute inset-0"
|
||||
data-testid="file-pane-scroll-container"
|
||||
>
|
||||
<ul className="m-0 p-0 text-sm">
|
||||
{showNewTreeEntry && (
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ paddingInlineStart: getIndentationCSS(0) }}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPencil}
|
||||
className="inline-block mr-2 m-0 p-0 w-2 h-2"
|
||||
/>
|
||||
<TreeEntryInput level={-1} onSubmit={onTreeEntryInputSubmit} />
|
||||
</div>
|
||||
)}
|
||||
{sortFilesAndDirectories(fileContext.project?.children || []).map(
|
||||
(fileOrDir) => (
|
||||
<FileTreeItem
|
||||
parentDir={fileContext.project}
|
||||
project={fileContext.project}
|
||||
currentFile={loaderData?.file}
|
||||
lastDirectoryClicked={lastDirectoryClicked}
|
||||
fileOrDir={fileOrDir}
|
||||
onCreateFile={onCreateFile}
|
||||
onCreateFolder={onCreateFolder}
|
||||
newTreeEntry={newTreeEntry}
|
||||
onClickDirectory={onClickDirectory}
|
||||
onNavigateToFile={onNavigateToFile_}
|
||||
key={fileOrDir.path}
|
||||
treeSelection={treeSelection}
|
||||
setTreeSelection={setTreeSelection}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -96,6 +96,23 @@ export function LowerRightControls({
|
||||
Report a bug
|
||||
</Tooltip>
|
||||
</a>
|
||||
<Link
|
||||
to={
|
||||
location.pathname.includes(PATHS.FILE)
|
||||
? filePath + PATHS.TELEMETRY + '?tab=project'
|
||||
: PATHS.HOME + PATHS.TELEMETRY
|
||||
}
|
||||
data-testid="telemetry-link"
|
||||
>
|
||||
<CustomIcon
|
||||
name="stopwatch"
|
||||
className={`w-5 h-5 ${linkOverrideClassName}`}
|
||||
/>
|
||||
<span className="sr-only">Telemetry</span>
|
||||
<Tooltip position="top" contentClassName="text-xs">
|
||||
Telemetry
|
||||
</Tooltip>
|
||||
</Link>
|
||||
<Link
|
||||
to={
|
||||
location.pathname.includes(PATHS.FILE)
|
||||
|
@ -158,36 +158,39 @@ export const ModelingMachineProvider = ({
|
||||
'enable copilot': () => {
|
||||
editorManager.setCopilotEnabled(true)
|
||||
},
|
||||
'sketch exit execute': ({ context: { store } }) => {
|
||||
;(async () => {
|
||||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'sketch_mode_disable' },
|
||||
})
|
||||
// tsc reports this typing as perfectly fine, but eslint is complaining.
|
||||
// It's actually nonsensical, so I'm quieting.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
'sketch exit execute': async ({
|
||||
context: { store },
|
||||
}): Promise<void> => {
|
||||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'sketch_mode_disable' },
|
||||
})
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
|
||||
if (cameraProjection.current === 'perspective') {
|
||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||
}
|
||||
if (cameraProjection.current === 'perspective') {
|
||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||
}
|
||||
|
||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||
|
||||
store.videoElement?.pause()
|
||||
store.videoElement?.pause()
|
||||
|
||||
kclManager
|
||||
.executeCode()
|
||||
.then(() => {
|
||||
if (engineCommandManager.engineConnection?.idleMode) return
|
||||
return kclManager
|
||||
.executeCode()
|
||||
.then(() => {
|
||||
if (engineCommandManager.engineConnection?.idleMode) return
|
||||
|
||||
store.videoElement?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e)
|
||||
})
|
||||
store.videoElement?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e)
|
||||
})
|
||||
.catch(reportRejection)
|
||||
})().catch(reportRejection)
|
||||
})
|
||||
.catch(reportRejection)
|
||||
},
|
||||
'Set mouse state': assign(({ context, event }) => {
|
||||
if (event.type !== 'Set mouse state') return {}
|
||||
|
@ -48,7 +48,7 @@ export const ModelingPaneHeader = ({
|
||||
bgClassName: 'bg-transparent dark:bg-transparent',
|
||||
}}
|
||||
className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 !outline-none"
|
||||
onClick={onClose}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<Tooltip position="bottom-right" delay={750}>
|
||||
Close
|
||||
@ -59,14 +59,12 @@ export const ModelingPaneHeader = ({
|
||||
}
|
||||
|
||||
export const ModelingPane = ({
|
||||
title,
|
||||
icon,
|
||||
id,
|
||||
children,
|
||||
className,
|
||||
Menu,
|
||||
detailsTestId,
|
||||
onClose,
|
||||
title,
|
||||
...props
|
||||
}: ModelingPaneProps) => {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
@ -78,6 +76,7 @@ export const ModelingPane = ({
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
title={title && typeof title === 'string' ? title : ''}
|
||||
data-testid={detailsTestId}
|
||||
id={id}
|
||||
className={
|
||||
@ -88,14 +87,7 @@ export const ModelingPane = ({
|
||||
(className || '')
|
||||
}
|
||||
>
|
||||
<ModelingPaneHeader
|
||||
id={id}
|
||||
icon={icon}
|
||||
title={title}
|
||||
Menu={Menu}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<div className="relative w-full">{children}</div>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
@ -5,16 +5,18 @@ import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||
|
||||
export const DebugPane = () => {
|
||||
return (
|
||||
<section
|
||||
data-testid="debug-panel"
|
||||
className="absolute inset-0 p-2 box-border overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<EngineCommands />
|
||||
<CamDebugSettings />
|
||||
<AstExplorer />
|
||||
<DebugFeatureTree />
|
||||
</div>
|
||||
</section>
|
||||
<div className="relative">
|
||||
<section
|
||||
data-testid="debug-panel"
|
||||
className="absolute inset-0 p-2 box-border overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<EngineCommands />
|
||||
<CamDebugSettings />
|
||||
<AstExplorer />
|
||||
<DebugFeatureTree />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
|
||||
import { lineHighlightField } from 'editor/highlightextension'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { onMouseDragMakeANewNumber, onMouseDragRegex } from 'lib/utils'
|
||||
import {
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
@ -129,7 +129,9 @@ export const KclEditorPane = () => {
|
||||
closeBrackets(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
syntaxHighlighting(defaultHighlightStyle, {
|
||||
fallback: true,
|
||||
}),
|
||||
rectangularSelection(),
|
||||
dropCursor(),
|
||||
interact({
|
||||
@ -137,29 +139,12 @@ export const KclEditorPane = () => {
|
||||
// a rule for a number dragger
|
||||
{
|
||||
// the regexp matching the value
|
||||
regexp: /-?\b\d+\.?\d*\b/g,
|
||||
regexp: onMouseDragRegex,
|
||||
// set cursor to "ew-resize" on hover
|
||||
cursor: 'ew-resize',
|
||||
// change number value based on mouse X movement on drag
|
||||
onDrag: (text, setText, e) => {
|
||||
const multiplier =
|
||||
e.shiftKey && e.metaKey
|
||||
? 0.01
|
||||
: e.metaKey
|
||||
? 0.1
|
||||
: e.shiftKey
|
||||
? 10
|
||||
: 1
|
||||
|
||||
const delta = e.movementX * multiplier
|
||||
|
||||
const newVal = roundOff(
|
||||
Number(text) + delta,
|
||||
multiplier === 0.01 ? 2 : multiplier === 0.1 ? 1 : 0
|
||||
)
|
||||
|
||||
if (isNaN(newVal)) return
|
||||
setText(newVal.toString())
|
||||
onMouseDragMakeANewNumber(text, setText, e)
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -174,27 +159,31 @@ export const KclEditorPane = () => {
|
||||
const initialCode = useRef(codeManager.code)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
|
||||
>
|
||||
<CodeEditor
|
||||
initialDocValue={initialCode.current}
|
||||
extensions={editorExtensions}
|
||||
theme={theme}
|
||||
onCreateEditor={(_editorView) => {
|
||||
if (_editorView === null) return
|
||||
<div className="relative">
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className={
|
||||
'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')
|
||||
}
|
||||
>
|
||||
<CodeEditor
|
||||
initialDocValue={initialCode.current}
|
||||
extensions={editorExtensions}
|
||||
theme={theme}
|
||||
onCreateEditor={(_editorView) => {
|
||||
if (_editorView === null) return
|
||||
|
||||
editorManager.setEditorView(_editorView)
|
||||
editorManager.setEditorView(_editorView)
|
||||
|
||||
// On first load of this component, ensure we show the current errors
|
||||
// in the editor.
|
||||
// Make sure we don't add them twice.
|
||||
if (diagnosticCount(_editorView.state) === 0) {
|
||||
kclManager.setDiagnosticsForCurrentErrors()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
// On first load of this component, ensure we show the current errors
|
||||
// in the editor.
|
||||
// Make sure we don't add them twice.
|
||||
if (diagnosticCount(_editorView.state) === 0) {
|
||||
kclManager.setDiagnosticsForCurrentErrors()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -43,14 +43,14 @@ describe('processMemory', () => {
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
faceId: expect.any(String),
|
||||
sourceRange: [170, 194],
|
||||
sourceRange: [170, 194, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
faceId: expect.any(String),
|
||||
sourceRange: [202, 230],
|
||||
sourceRange: [202, 230, 0],
|
||||
},
|
||||
],
|
||||
theSketch: [
|
||||
|
@ -2,11 +2,17 @@ import { IconDefinition, faBugSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
||||
import { ModelingPaneHeader } from 'components/ModelingSidebar/ModelingPane'
|
||||
import { MouseEventHandler, ReactNode } from 'react'
|
||||
import { MemoryPane, MemoryPaneMenu } from './MemoryPane'
|
||||
import { LogsPane } from './LoggingPanes'
|
||||
import { DebugPane } from './DebugPane'
|
||||
import { FileTreeInner, FileTreeMenu, FileTreeRoot } from 'components/FileTree'
|
||||
import {
|
||||
FileTreeInner,
|
||||
FileTreeMenu,
|
||||
FileTreeRoot,
|
||||
useFileTreeOperations,
|
||||
} from 'components/FileTree'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
import { ContextFrom } from 'xstate'
|
||||
@ -38,20 +44,19 @@ interface PaneCallbackProps {
|
||||
|
||||
export type SidebarPane = {
|
||||
id: SidebarType
|
||||
title: ReactNode
|
||||
sidebarName?: string
|
||||
sidebarName: string
|
||||
icon: CustomIconName | IconDefinition
|
||||
keybinding: string
|
||||
Content: ReactNode | React.FC
|
||||
Menu?: ReactNode | React.FC
|
||||
Content: React.FC<{ id: SidebarType; onClose: () => void }>
|
||||
hide?: boolean | ((props: PaneCallbackProps) => boolean)
|
||||
showBadge?: BadgeInfo
|
||||
}
|
||||
|
||||
export type SidebarAction = {
|
||||
id: string
|
||||
title: ReactNode
|
||||
sidebarName: string
|
||||
icon: CustomIconName
|
||||
title: ReactNode
|
||||
iconClassName?: string // Just until we get rid of FontAwesome icons
|
||||
keybinding: string
|
||||
action: () => void
|
||||
@ -59,14 +64,30 @@ export type SidebarAction = {
|
||||
disable?: () => string | undefined
|
||||
}
|
||||
|
||||
// For now a lot of icons are the same but the reality is they could totally
|
||||
// be different, like an icon based on some data for the pane, or the icon
|
||||
// changes to be a spinning loader on loading.
|
||||
|
||||
export const sidebarPanes: SidebarPane[] = [
|
||||
{
|
||||
id: 'code',
|
||||
title: 'KCL Code',
|
||||
icon: 'code',
|
||||
Content: KclEditorPane,
|
||||
sidebarName: 'KCL Code',
|
||||
Content: (props: { id: SidebarType; onClose: () => void }) => {
|
||||
return (
|
||||
<>
|
||||
<ModelingPaneHeader
|
||||
id={props.id}
|
||||
icon="code"
|
||||
title="KCL Code"
|
||||
Menu={<KclEditorMenu />}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
<KclEditorPane />
|
||||
</>
|
||||
)
|
||||
},
|
||||
keybinding: 'Shift + C',
|
||||
Menu: KclEditorMenu,
|
||||
showBadge: {
|
||||
value: ({ kclContext }) => {
|
||||
return kclContext.errors.length
|
||||
@ -79,34 +100,96 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
title: <FileTreeRoot />,
|
||||
sidebarName: 'Project Files',
|
||||
icon: 'folder',
|
||||
Content: FileTreeInner,
|
||||
sidebarName: 'Project Files',
|
||||
Content: (props: { id: SidebarType; onClose: () => void }) => {
|
||||
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModelingPaneHeader
|
||||
id={props.id}
|
||||
icon="folder"
|
||||
title={<FileTreeRoot />}
|
||||
Menu={
|
||||
<FileTreeMenu
|
||||
onCreateFile={() => createFile({ dryRun: true })}
|
||||
onCreateFolder={() => createFolder({ dryRun: true })}
|
||||
/>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
<FileTreeInner
|
||||
onCreateFile={(name: string) => createFile({ dryRun: false, name })}
|
||||
onCreateFolder={(name: string) =>
|
||||
createFolder({ dryRun: false, name })
|
||||
}
|
||||
newTreeEntry={newTreeEntry}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
keybinding: 'Shift + F',
|
||||
Menu: FileTreeMenu,
|
||||
hide: ({ platform }) => platform === 'web',
|
||||
},
|
||||
{
|
||||
id: 'variables',
|
||||
title: 'Variables',
|
||||
icon: 'make-variable',
|
||||
Content: MemoryPane,
|
||||
Menu: MemoryPaneMenu,
|
||||
sidebarName: 'Variables',
|
||||
Content: (props: { id: SidebarType; onClose: () => void }) => {
|
||||
return (
|
||||
<>
|
||||
<ModelingPaneHeader
|
||||
id={props.id}
|
||||
icon="make-variable"
|
||||
title="Variables"
|
||||
Menu={<MemoryPaneMenu />}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
<MemoryPane />
|
||||
</>
|
||||
)
|
||||
},
|
||||
keybinding: 'Shift + V',
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
title: 'Logs',
|
||||
icon: 'logs',
|
||||
Content: LogsPane,
|
||||
sidebarName: 'Logs',
|
||||
Content: (props: { id: SidebarType; onClose: () => void }) => {
|
||||
return (
|
||||
<>
|
||||
<ModelingPaneHeader
|
||||
id={props.id}
|
||||
icon="logs"
|
||||
title="Logs"
|
||||
Menu={null}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
<LogsPane />
|
||||
</>
|
||||
)
|
||||
},
|
||||
keybinding: 'Shift + L',
|
||||
},
|
||||
{
|
||||
id: 'debug',
|
||||
title: 'Debug',
|
||||
icon: faBugSlash,
|
||||
Content: DebugPane,
|
||||
sidebarName: 'Debug',
|
||||
Content: (props: { id: SidebarType; onClose: () => void }) => {
|
||||
return (
|
||||
<>
|
||||
<ModelingPaneHeader
|
||||
id={props.id}
|
||||
icon={faBugSlash}
|
||||
title="Debug"
|
||||
Menu={null}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
<DebugPane />
|
||||
</>
|
||||
)
|
||||
},
|
||||
keybinding: 'Shift + D',
|
||||
hide: ({ settings }) => !settings.modeling.showDebugPanel.current,
|
||||
},
|
||||
|
@ -1,11 +0,0 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
row-gap: 0.25rem;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
padding-block: 1px;
|
||||
max-width: 100%;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
@ -5,14 +5,12 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
ReactNode,
|
||||
useContext,
|
||||
} from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import styles from './ModelingSidebar.module.css'
|
||||
import { ModelingPane } from './ModelingPane'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
@ -62,6 +60,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
{
|
||||
id: 'export',
|
||||
title: 'Export part',
|
||||
sidebarName: 'Export part',
|
||||
icon: 'floppyDiskArrow',
|
||||
keybinding: 'Ctrl + Shift + E',
|
||||
action: () =>
|
||||
@ -73,6 +72,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
{
|
||||
id: 'make',
|
||||
title: 'Make part',
|
||||
sidebarName: 'Make part',
|
||||
icon: 'printer3d',
|
||||
keybinding: 'Ctrl + Shift + M',
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
@ -182,7 +182,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
bottomRight: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div id="app-sidebar" className={styles.grid + ' flex-1'}>
|
||||
<div id="app-sidebar" className="flex flex-row h-full">
|
||||
<ul
|
||||
className={
|
||||
(context.store?.openPanes.length === 0 ? 'rounded-r ' : '') +
|
||||
@ -220,7 +220,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
key={action.id}
|
||||
paneConfig={{
|
||||
id: action.id,
|
||||
title: action.title,
|
||||
sidebarName: action.sidebarName,
|
||||
icon: action.icon,
|
||||
keybinding: action.keybinding,
|
||||
iconClassName: action.iconClassName,
|
||||
@ -237,10 +237,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
<ul
|
||||
id="pane-section"
|
||||
className={
|
||||
'ml-[-1px] col-start-2 col-span-1 flex flex-col gap-2 ' +
|
||||
(context.store?.openPanes.length >= 1
|
||||
? `row-start-1 row-end-3`
|
||||
: `hidden`)
|
||||
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
|
||||
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
|
||||
}
|
||||
>
|
||||
{filteredPanes
|
||||
@ -249,13 +247,15 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
<ModelingPane
|
||||
key={pane.id}
|
||||
icon={pane.icon}
|
||||
title={pane.sidebarName}
|
||||
onClose={() => {}}
|
||||
id={`${pane.id}-pane`}
|
||||
title={pane.title}
|
||||
Menu={pane.Menu}
|
||||
onClose={() => togglePane(pane.id)}
|
||||
>
|
||||
{pane.Content instanceof Function ? (
|
||||
<pane.Content />
|
||||
<pane.Content
|
||||
id={pane.id}
|
||||
onClose={() => togglePane(pane.id)}
|
||||
/>
|
||||
) : (
|
||||
pane.Content
|
||||
)}
|
||||
@ -271,8 +271,7 @@ interface ModelingPaneButtonProps
|
||||
extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
paneConfig: {
|
||||
id: string
|
||||
title: ReactNode
|
||||
sidebarName?: string
|
||||
sidebarName: string
|
||||
icon: CustomIconName | IconDefinition
|
||||
keybinding: string
|
||||
iconClassName?: string
|
||||
@ -301,10 +300,7 @@ function ModelingPaneButton({
|
||||
<button
|
||||
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
||||
onClick={onClick}
|
||||
name={
|
||||
paneConfig.sidebarName ??
|
||||
(typeof paneConfig.title === 'string' ? paneConfig.title : '')
|
||||
}
|
||||
name={paneConfig.sidebarName}
|
||||
data-testid={paneConfig.id + '-pane-button'}
|
||||
disabled={disabledText !== undefined}
|
||||
aria-disabled={disabledText !== undefined}
|
||||
@ -320,7 +316,7 @@ function ModelingPaneButton({
|
||||
}
|
||||
/>
|
||||
<span className="sr-only">
|
||||
{paneConfig.sidebarName ?? paneConfig.title}
|
||||
{paneConfig.sidebarName}
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
<Tooltip
|
||||
@ -329,7 +325,7 @@ function ModelingPaneButton({
|
||||
hoverOnly
|
||||
>
|
||||
<span className="flex-1">
|
||||
{paneConfig.sidebarName ?? paneConfig.title}
|
||||
{paneConfig.sidebarName}
|
||||
{disabledText !== undefined ? ` (${disabledText})` : ''}
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
|
33
src/components/RouteProvider.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useEffect, useState, createContext, ReactNode } from 'react'
|
||||
import { useNavigation, useLocation } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { markOnce } from 'lib/performance'
|
||||
|
||||
export const RouteProviderContext = createContext({})
|
||||
|
||||
export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
const [first, setFirstState] = useState(true)
|
||||
const navigation = useNavigation()
|
||||
const location = useLocation()
|
||||
useEffect(() => {
|
||||
// On initialization, the react-router-dom does not send a 'loading' state event.
|
||||
// it sends an idle event first.
|
||||
const pathname = first ? location.pathname : navigation.location?.pathname
|
||||
const isHome = pathname === PATHS.HOME
|
||||
const isFile =
|
||||
pathname?.includes(PATHS.FILE) &&
|
||||
pathname?.substring(pathname?.length - 4) === '.kcl'
|
||||
if (isHome) {
|
||||
markOnce('code/willLoadHome')
|
||||
} else if (isFile) {
|
||||
markOnce('code/willLoadFile')
|
||||
}
|
||||
setFirstState(false)
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<RouteProviderContext.Provider value={{}}>
|
||||
{children}
|
||||
</RouteProviderContext.Provider>
|
||||
)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { trap } from 'lib/trap'
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
@ -42,6 +42,7 @@ import { getAppSettingsFilePath } from 'lib/desktop'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -288,6 +289,44 @@ export const SettingsAuthProviderBase = ({
|
||||
settingsWithCommandConfigs,
|
||||
])
|
||||
|
||||
// Due to the route provider, i've moved this to the SettingsAuthProvider instead of CommandBarProvider
|
||||
// This will register the commands to route to Telemetry, Home, and Settings.
|
||||
useEffect(() => {
|
||||
const filePath =
|
||||
PATHS.FILE +
|
||||
'/' +
|
||||
encodeURIComponent(loadedProject?.file?.path || BROWSER_PATH)
|
||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||
createRouteCommands(navigate, location, filePath)
|
||||
commandBarSend({
|
||||
type: 'Remove commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteHomeCommand,
|
||||
RouteSettingsCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
if (location.pathname === PATHS.HOME) {
|
||||
commandBarSend({
|
||||
type: 'Add commands',
|
||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||
})
|
||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||
commandBarSend({
|
||||
type: 'Add commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteSettingsCommand,
|
||||
RouteHomeCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [location])
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
// This is only done if the theme setting is set to 'system'.
|
||||
// It can't be done in XState (in an invoked callback, for example)
|
||||
|
72
src/components/TelemetryExplorer.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { getMarks } from 'lib/performance'
|
||||
|
||||
import {
|
||||
printDeltaTotal,
|
||||
printInvocationCount,
|
||||
printMarkDownTable,
|
||||
printRawMarks,
|
||||
} from 'lib/telemetry'
|
||||
|
||||
export function TelemetryExplorer() {
|
||||
const marks = getMarks()
|
||||
const markdownTable = printMarkDownTable(marks)
|
||||
const rawMarks = printRawMarks(marks)
|
||||
const deltaTotalTable = printDeltaTotal(marks)
|
||||
const invocationCount = printInvocationCount(marks)
|
||||
// TODO data-telemetry-type
|
||||
// TODO data-telemetry-name
|
||||
return (
|
||||
<div>
|
||||
<h1 className="pb-4">Marks</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{marks.map((mark, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{JSON.stringify(mark, null, 2)}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Startup Performance</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{markdownTable.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Delta and Totals</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{deltaTotalTable.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Raw Marks</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{rawMarks.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<h1 className="pb-4">Invocation Count</h1>
|
||||
<div className="max-w-xl max-h-64 overflow-auto select-all">
|
||||
{invocationCount.map((line, index) => {
|
||||
return (
|
||||
<pre className="text-xs" key={index}>
|
||||
<code key={index}>{line}</code>
|
||||
</pre>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { modelingMachine, ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
@ -12,6 +13,7 @@ import {
|
||||
setDiagnosticsEffect,
|
||||
} from '@codemirror/lint'
|
||||
import { StateFrom } from 'xstate'
|
||||
import { markOnce } from 'lib/performance'
|
||||
|
||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
||||
@ -59,6 +61,48 @@ export default class EditorManager {
|
||||
|
||||
setEditorView(editorView: EditorView) {
|
||||
this._editorView = editorView
|
||||
this.overrideTreeHighlighterUpdateForPerformanceTracking()
|
||||
}
|
||||
|
||||
overrideTreeHighlighterUpdateForPerformanceTracking() {
|
||||
// @ts-ignore
|
||||
this._editorView?.plugins.forEach((e) => {
|
||||
let sawATreeDiff = false
|
||||
|
||||
// we cannot use <>.constructor.name since it will get destroyed
|
||||
// when packaging the application.
|
||||
const isTreeHighlightPlugin =
|
||||
e.value.hasOwnProperty('tree') &&
|
||||
e.value.hasOwnProperty('decoratedTo') &&
|
||||
e.value.hasOwnProperty('decorations')
|
||||
|
||||
if (isTreeHighlightPlugin) {
|
||||
let originalUpdate = e.value.update
|
||||
// @ts-ignore
|
||||
function performanceTrackingUpdate(args) {
|
||||
/**
|
||||
* TreeHighlighter.update will be called multiple times on start up.
|
||||
* We do not want to track the highlight performance of an empty update.
|
||||
* mark the syntax highlight one time when the new tree comes in with the
|
||||
* initial code
|
||||
*/
|
||||
const treeIsDifferent =
|
||||
// @ts-ignore
|
||||
!sawATreeDiff && this.tree !== syntaxTree(args.state)
|
||||
if (treeIsDifferent && !sawATreeDiff) {
|
||||
markOnce('code/willSyntaxHighlight')
|
||||
}
|
||||
// Call the original function
|
||||
// @ts-ignore
|
||||
originalUpdate.apply(this, [args])
|
||||
if (treeIsDifferent && !sawATreeDiff) {
|
||||
markOnce('code/didSyntaxHighlight')
|
||||
sawATreeDiff = true
|
||||
}
|
||||
}
|
||||
e.value.update = performanceTrackingUpdate
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get editorView(): EditorView | null {
|
||||
|
@ -8,8 +8,10 @@ import ModalContainer from 'react-modal-promise'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { AppStreamProvider } from 'AppState'
|
||||
import { ToastUpdate } from 'components/ToastUpdate'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
|
||||
|
||||
markOnce('code/willAuth')
|
||||
// uncomment for xstate inspector
|
||||
// import { DEV } from 'env'
|
||||
// import { inspect } from '@xstate/inspect'
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
|
||||
interface ExecuteArgs {
|
||||
@ -38,6 +39,7 @@ export class KclManager {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
startNodes: [],
|
||||
@ -204,6 +206,7 @@ export class KclManager {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
startNodes: [],
|
||||
@ -255,6 +258,7 @@ export class KclManager {
|
||||
}
|
||||
|
||||
const ast = args.ast || this.ast
|
||||
markOnce('code/startExecuteAst')
|
||||
|
||||
const currentExecutionId = args.executionId || Date.now()
|
||||
this._cancelTokens.set(currentExecutionId, false)
|
||||
@ -329,6 +333,7 @@ export class KclManager {
|
||||
})
|
||||
|
||||
this._cancelTokens.delete(currentExecutionId)
|
||||
markOnce('code/endExecuteAst')
|
||||
}
|
||||
// NOTE: this always updates the code state and editor.
|
||||
// DO NOT CALL THIS from codemirror ever.
|
||||
|
@ -1903,6 +1903,6 @@ describe('parsing errors', () => {
|
||||
const error = result as KCLError
|
||||
expect(error.kind).toBe('syntax')
|
||||
expect(error.msg).toBe('Unexpected token: (')
|
||||
expect(error.sourceRanges).toEqual([[27, 28]])
|
||||
expect(error.sourceRanges).toEqual([[27, 28, 0]])
|
||||
})
|
||||
})
|
||||
|
@ -19,7 +19,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
const sketch001 = execState.memory.get('mySketch001')
|
||||
expect(sketch001).toEqual({
|
||||
type: 'UserVal',
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
value: {
|
||||
type: 'Sketch',
|
||||
on: expect.any(Object),
|
||||
@ -29,7 +29,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [46, 71],
|
||||
sourceRange: [46, 71, 0],
|
||||
},
|
||||
},
|
||||
paths: [
|
||||
@ -39,7 +39,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [77, 102],
|
||||
sourceRange: [77, 102, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
@ -49,13 +49,13 @@ const mySketch001 = startSketchOn('XY')
|
||||
from: [-1.59, -1.54],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
sourceRange: [108, 132],
|
||||
sourceRange: [108, 132, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -80,14 +80,14 @@ const mySketch001 = startSketchOn('XY')
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102],
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132],
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
@ -104,7 +104,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [77, 102],
|
||||
sourceRange: [77, 102, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -114,7 +114,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [108, 132],
|
||||
sourceRange: [108, 132, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -122,7 +122,7 @@ const mySketch001 = startSketchOn('XY')
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
__meta: [{ sourceRange: [46, 71, 0] }],
|
||||
})
|
||||
})
|
||||
test('sketch extrude and sketch on one of the faces', async () => {
|
||||
@ -162,7 +162,7 @@ const sk2 = startSketchOn('XY')
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89],
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
@ -174,14 +174,14 @@ const sk2 = startSketchOn('XY')
|
||||
value: 'p',
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117],
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142],
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
@ -194,7 +194,7 @@ const sk2 = startSketchOn('XY')
|
||||
p: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [114, 116],
|
||||
sourceRange: [114, 116, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
@ -210,7 +210,7 @@ const sk2 = startSketchOn('XY')
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [69, 89],
|
||||
sourceRange: [69, 89, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -225,7 +225,7 @@ const sk2 = startSketchOn('XY')
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [95, 117],
|
||||
sourceRange: [95, 117, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -235,7 +235,7 @@ const sk2 = startSketchOn('XY')
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [123, 142],
|
||||
sourceRange: [123, 142, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -243,7 +243,7 @@ const sk2 = startSketchOn('XY')
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [38, 63] }],
|
||||
__meta: [{ sourceRange: [38, 63, 0] }],
|
||||
},
|
||||
{
|
||||
type: 'Solid',
|
||||
@ -254,7 +254,7 @@ const sk2 = startSketchOn('XY')
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393],
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
@ -266,14 +266,14 @@ const sk2 = startSketchOn('XY')
|
||||
value: 'o',
|
||||
},
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420],
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
{
|
||||
type: 'extrudePlane',
|
||||
faceId: expect.any(String),
|
||||
tag: null,
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445],
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
],
|
||||
sketch: {
|
||||
@ -286,7 +286,7 @@ const sk2 = startSketchOn('XY')
|
||||
o: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [417, 419],
|
||||
sourceRange: [417, 419, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
@ -302,7 +302,7 @@ const sk2 = startSketchOn('XY')
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [373, 393],
|
||||
sourceRange: [373, 393, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -317,7 +317,7 @@ const sk2 = startSketchOn('XY')
|
||||
},
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [399, 420],
|
||||
sourceRange: [399, 420, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -327,7 +327,7 @@ const sk2 = startSketchOn('XY')
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [426, 445],
|
||||
sourceRange: [426, 445, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -335,7 +335,7 @@ const sk2 = startSketchOn('XY')
|
||||
height: 2,
|
||||
startCapId: expect.any(String),
|
||||
endCapId: expect.any(String),
|
||||
__meta: [{ sourceRange: [342, 367] }],
|
||||
__meta: [{ sourceRange: [342, 367, 0] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -9,8 +9,8 @@ describe('test kclErrToDiagnostic', () => {
|
||||
kind: 'semantic',
|
||||
msg: 'Semantic error',
|
||||
sourceRanges: [
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
[0, 1, 0],
|
||||
[2, 3, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -19,8 +19,8 @@ describe('test kclErrToDiagnostic', () => {
|
||||
kind: 'type',
|
||||
msg: 'Type error',
|
||||
sourceRanges: [
|
||||
[4, 5],
|
||||
[6, 7],
|
||||
[4, 5, 0],
|
||||
[6, 7, 0],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
@ -4,15 +4,17 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client'
|
||||
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
|
||||
import { Text } from '@codemirror/state'
|
||||
|
||||
const TOP_LEVEL_MODULE_ID = 0
|
||||
|
||||
type ExtractKind<T> = T extends { kind: infer K } ? K : never
|
||||
export class KCLError extends Error {
|
||||
kind: ExtractKind<RustKclError> | 'name'
|
||||
sourceRanges: [number, number][]
|
||||
sourceRanges: [number, number, number][]
|
||||
msg: string
|
||||
constructor(
|
||||
kind: ExtractKind<RustKclError> | 'name',
|
||||
msg: string,
|
||||
sourceRanges: [number, number][]
|
||||
sourceRanges: [number, number, number][]
|
||||
) {
|
||||
super()
|
||||
this.kind = kind
|
||||
@ -23,63 +25,63 @@ export class KCLError extends Error {
|
||||
}
|
||||
|
||||
export class KCLLexicalError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
constructor(msg: string, sourceRanges: [number, number, number][]) {
|
||||
super('lexical', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLInternalError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
constructor(msg: string, sourceRanges: [number, number, number][]) {
|
||||
super('internal', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLSyntaxError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
constructor(msg: string, sourceRanges: [number, number, number][]) {
|
||||
super('syntax', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLSyntaxError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLSemanticError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
constructor(msg: string, sourceRanges: [number, number, number][]) {
|
||||
super('semantic', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLSemanticError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLTypeError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
constructor(msg: string, sourceRanges: [number, number, number][]) {
|
||||
super('type', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLTypeError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLUnimplementedError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
constructor(msg: string, sourceRanges: [number, number, number][]) {
|
||||
super('unimplemented', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLUnexpectedError extends KCLError {
|
||||
constructor(msg: string, sourceRanges: [number, number][]) {
|
||||
constructor(msg: string, sourceRanges: [number, number, number][]) {
|
||||
super('unexpected', msg, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLValueAlreadyDefined extends KCLError {
|
||||
constructor(key: string, sourceRanges: [number, number][]) {
|
||||
constructor(key: string, sourceRanges: [number, number, number][]) {
|
||||
super('name', `Key ${key} was already defined elsewhere`, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
|
||||
}
|
||||
}
|
||||
|
||||
export class KCLUndefinedValueError extends KCLError {
|
||||
constructor(key: string, sourceRanges: [number, number][]) {
|
||||
constructor(key: string, sourceRanges: [number, number, number][]) {
|
||||
super('name', `Key ${key} has not been defined`, sourceRanges)
|
||||
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
|
||||
}
|
||||
@ -97,13 +99,22 @@ export function lspDiagnosticsToKclErrors(
|
||||
.flatMap(
|
||||
({ range, message }) =>
|
||||
new KCLError('unexpected', message, [
|
||||
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!],
|
||||
[
|
||||
posToOffset(doc, range.start)!,
|
||||
posToOffset(doc, range.end)!,
|
||||
TOP_LEVEL_MODULE_ID,
|
||||
],
|
||||
])
|
||||
)
|
||||
.filter(({ sourceRanges }) => {
|
||||
const [from, to] = sourceRanges[0]
|
||||
const [from, to, moduleId] = sourceRanges[0]
|
||||
return (
|
||||
from !== null && to !== null && from !== undefined && to !== undefined
|
||||
from !== null &&
|
||||
to !== null &&
|
||||
from !== undefined &&
|
||||
to !== undefined &&
|
||||
// Filter out errors that are not from the top-level module.
|
||||
moduleId === TOP_LEVEL_MODULE_ID
|
||||
)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
@ -127,8 +138,16 @@ export function kclErrorsToDiagnostics(
|
||||
errors: KCLError[]
|
||||
): CodeMirrorDiagnostic[] {
|
||||
return errors?.flatMap((err) => {
|
||||
return err.sourceRanges.map(([from, to]) => {
|
||||
return { from, to, message: err.msg, severity: 'error' }
|
||||
})
|
||||
const sourceRanges: CodeMirrorDiagnostic[] = err.sourceRanges
|
||||
// Filter out errors that are not from the top-level module.
|
||||
.filter(([_start, _end, moduleId]) => moduleId === TOP_LEVEL_MODULE_ID)
|
||||
.map(([from, to]) => {
|
||||
return { from, to, message: err.msg, severity: 'error' }
|
||||
})
|
||||
// Make sure we didn't filter out all the source ranges.
|
||||
if (sourceRanges.length === 0) {
|
||||
sourceRanges.push({ from: 0, to: 0, message: err.msg, severity: 'error' })
|
||||
}
|
||||
return sourceRanges
|
||||
})
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ const newVar = myVar + 1`
|
||||
to: [0, 2],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [72, 97],
|
||||
sourceRange: [72, 97, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
tag: {
|
||||
@ -81,7 +81,7 @@ const newVar = myVar + 1`
|
||||
from: [0, 2],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
sourceRange: [103, 119],
|
||||
sourceRange: [103, 119, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
@ -90,7 +90,7 @@ const newVar = myVar + 1`
|
||||
to: [5, -1],
|
||||
from: [2, 3],
|
||||
__geoMeta: {
|
||||
sourceRange: [125, 154],
|
||||
sourceRange: [125, 154, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
tag: {
|
||||
@ -160,14 +160,14 @@ const newVar = myVar + 1`
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
id: expect.any(String),
|
||||
sourceRange: [39, 63],
|
||||
sourceRange: [39, 63, 0],
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
myPath: {
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [109, 116],
|
||||
sourceRange: [109, 116, 0],
|
||||
},
|
||||
],
|
||||
type: 'TagIdentifier',
|
||||
@ -182,7 +182,7 @@ const newVar = myVar + 1`
|
||||
from: [0, 0],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
sourceRange: [69, 85],
|
||||
sourceRange: [69, 85, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
@ -191,7 +191,7 @@ const newVar = myVar + 1`
|
||||
to: [0, 1],
|
||||
from: [1, 1],
|
||||
__geoMeta: {
|
||||
sourceRange: [91, 117],
|
||||
sourceRange: [91, 117, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
tag: {
|
||||
@ -207,15 +207,15 @@ const newVar = myVar + 1`
|
||||
from: [0, 1],
|
||||
tag: null,
|
||||
__geoMeta: {
|
||||
sourceRange: [123, 139],
|
||||
sourceRange: [123, 139, 0],
|
||||
id: expect.any(String),
|
||||
},
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
__meta: [{ sourceRange: [39, 63] }],
|
||||
__meta: [{ sourceRange: [39, 63, 0] }],
|
||||
},
|
||||
__meta: [{ sourceRange: [39, 63] }],
|
||||
__meta: [{ sourceRange: [39, 63, 0] }],
|
||||
})
|
||||
})
|
||||
it('execute array expression', async () => {
|
||||
@ -229,7 +229,7 @@ const newVar = myVar + 1`
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [14, 15],
|
||||
sourceRange: [14, 15, 0],
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -238,7 +238,7 @@ const newVar = myVar + 1`
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [27, 49],
|
||||
sourceRange: [27, 49, 0],
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -257,7 +257,7 @@ const newVar = myVar + 1`
|
||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [27, 83],
|
||||
sourceRange: [27, 83, 0],
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -272,7 +272,7 @@ const newVar = myVar + 1`
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
sourceRange: [41, 50],
|
||||
sourceRange: [41, 50, 0],
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -426,7 +426,7 @@ const theExtrude = startSketchOn('XY')
|
||||
new KCLError(
|
||||
'undefined_value',
|
||||
'memory item key `myVarZ` is not defined',
|
||||
[[129, 135]]
|
||||
[[129, 135, 0]]
|
||||
)
|
||||
)
|
||||
})
|
||||
|
87
src/lang/kclSamples.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { parse, initPromise, programMemoryInit } from './wasm'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { assert } from 'vitest'
|
||||
// These unit tests makes web requests to a public github repository.
|
||||
|
||||
interface KclSampleFile {
|
||||
file: string
|
||||
title: string
|
||||
filename: string
|
||||
description: string
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
})
|
||||
|
||||
// Only used to actually fetch an older version of KCL code that will break in the parser.
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
async function getBrokenSampleCodeForLocalTesting() {
|
||||
const result = await fetch(
|
||||
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/5ccd04a1773ebdbfd02684057917ce5dbe0eaab3/80-20-rail.kcl'
|
||||
)
|
||||
const text = await result.text()
|
||||
return text
|
||||
}
|
||||
|
||||
async function getKclSampleCodeFromGithub(file: string): Promise<string> {
|
||||
const result = await fetch(
|
||||
`https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/${file}/${file}.kcl`
|
||||
)
|
||||
const text = await result.text()
|
||||
return text
|
||||
}
|
||||
|
||||
async function getFileNamesFromManifestJSON(): Promise<KclSampleFile[]> {
|
||||
const result = await fetch(
|
||||
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/manifest.json'
|
||||
)
|
||||
const json = await result.json()
|
||||
json.forEach((file: KclSampleFile) => {
|
||||
const filenameWithoutExtension = file.file.split('.')[0]
|
||||
file.filename = filenameWithoutExtension
|
||||
})
|
||||
return json
|
||||
}
|
||||
|
||||
// Value to use across all tests!
|
||||
let files: KclSampleFile[] = []
|
||||
|
||||
describe('Test KCL Samples from public Github repository', () => {
|
||||
describe('When parsing source code', () => {
|
||||
// THIS RUNS ACROSS OTHER TESTS!
|
||||
it('should fetch files', async () => {
|
||||
files = await getFileNamesFromManifestJSON()
|
||||
})
|
||||
// Run through all of the files in the manifest json. This will allow us to be automatically updated
|
||||
// with the latest changes in github. We won't be hard coding the filenames
|
||||
it(
|
||||
'should run through all the files',
|
||||
async () => {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file: KclSampleFile = files[i]
|
||||
const code = await getKclSampleCodeFromGithub(file.filename)
|
||||
const parsed = parse(code)
|
||||
assert(!(parsed instanceof Error))
|
||||
}
|
||||
},
|
||||
files.length * 1000
|
||||
)
|
||||
})
|
||||
|
||||
describe('when performing enginelessExecutor', () => {
|
||||
it(
|
||||
'should run through all the files',
|
||||
async () => {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file: KclSampleFile = files[i]
|
||||
const code = await getKclSampleCodeFromGithub(file.filename)
|
||||
const parsed = parse(code)
|
||||
assert(!(parsed instanceof Error))
|
||||
await enginelessExecutor(parsed, programMemoryInit())
|
||||
}
|
||||
},
|
||||
files.length * 1000
|
||||
)
|
||||
})
|
||||
})
|
@ -101,15 +101,15 @@ describe('Testing findUniqueName', () => {
|
||||
it('should find a unique name', () => {
|
||||
const result = findUniqueName(
|
||||
JSON.stringify([
|
||||
{ type: 'Identifier', name: 'yo01', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo02', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo03', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo04', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo05', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo06', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo07', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo08', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo09', start: 0, end: 0 },
|
||||
{ type: 'Identifier', name: 'yo01', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo02', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo03', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo04', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo05', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo06', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo07', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo08', start: 0, end: 0, moduleId: 0 },
|
||||
{ type: 'Identifier', name: 'yo09', start: 0, end: 0, moduleId: 0 },
|
||||
] satisfies Node<Identifier>[]),
|
||||
'yo',
|
||||
2
|
||||
@ -124,6 +124,7 @@ describe('Testing addSketchTo', () => {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
nonCodeMeta: { nonCodeNodes: {}, startNodes: [] },
|
||||
},
|
||||
'yz'
|
||||
|
@ -242,6 +242,7 @@ export function mutateObjExpProp(
|
||||
value: updateWith,
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -577,6 +578,7 @@ export function createLiteral(value: string | number): Node<Literal> {
|
||||
type: 'Literal',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
value,
|
||||
raw: `${value}`,
|
||||
}
|
||||
@ -587,6 +589,7 @@ export function createTagDeclarator(value: string): Node<TagDeclarator> {
|
||||
type: 'TagDeclarator',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
value,
|
||||
}
|
||||
@ -597,6 +600,7 @@ export function createIdentifier(name: string): Node<Identifier> {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
name,
|
||||
}
|
||||
@ -607,6 +611,7 @@ export function createPipeSubstitution(): Node<PipeSubstitution> {
|
||||
type: 'PipeSubstitution',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -618,10 +623,12 @@ export function createCallExpressionStdLib(
|
||||
type: 'CallExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
name,
|
||||
},
|
||||
@ -638,10 +645,12 @@ export function createCallExpression(
|
||||
type: 'CallExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
name,
|
||||
},
|
||||
@ -657,6 +666,7 @@ export function createArrayExpression(
|
||||
type: 'ArrayExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
nonCodeMeta: nonCodeMetaEmpty(),
|
||||
elements,
|
||||
@ -670,6 +680,7 @@ export function createPipeExpression(
|
||||
type: 'PipeExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
body,
|
||||
nonCodeMeta: nonCodeMetaEmpty(),
|
||||
@ -686,12 +697,14 @@ export function createVariableDeclaration(
|
||||
type: 'VariableDeclaration',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
id: createIdentifier(varName),
|
||||
init,
|
||||
@ -709,12 +722,14 @@ export function createObjectExpression(properties: {
|
||||
type: 'ObjectExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
nonCodeMeta: nonCodeMetaEmpty(),
|
||||
properties: Object.entries(properties).map(([key, value]) => ({
|
||||
type: 'ObjectProperty',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
key: createIdentifier(key),
|
||||
|
||||
value,
|
||||
@ -730,6 +745,7 @@ export function createUnaryExpression(
|
||||
type: 'UnaryExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
operator,
|
||||
argument,
|
||||
@ -745,6 +761,7 @@ export function createBinaryExpression([left, operator, right]: [
|
||||
type: 'BinaryExpression',
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
|
||||
operator,
|
||||
left,
|
||||
|
@ -13,6 +13,7 @@ Map {
|
||||
"range": [
|
||||
37,
|
||||
64,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"pathIds": [
|
||||
@ -31,6 +32,7 @@ Map {
|
||||
"range": [
|
||||
37,
|
||||
64,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"planeId": "UUID",
|
||||
@ -56,6 +58,7 @@ Map {
|
||||
"range": [
|
||||
70,
|
||||
86,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
@ -77,6 +80,7 @@ Map {
|
||||
"range": [
|
||||
92,
|
||||
119,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeCutId": "UUID",
|
||||
@ -99,6 +103,7 @@ Map {
|
||||
"range": [
|
||||
125,
|
||||
150,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
@ -120,6 +125,7 @@ Map {
|
||||
"range": [
|
||||
156,
|
||||
203,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
@ -141,6 +147,7 @@ Map {
|
||||
"range": [
|
||||
209,
|
||||
217,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
@ -162,6 +169,7 @@ Map {
|
||||
"range": [
|
||||
231,
|
||||
254,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
@ -289,6 +297,7 @@ Map {
|
||||
"range": [
|
||||
260,
|
||||
299,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"consumedEdgeId": "UUID",
|
||||
@ -307,6 +316,7 @@ Map {
|
||||
"range": [
|
||||
350,
|
||||
377,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"planeId": "UUID",
|
||||
@ -331,6 +341,7 @@ Map {
|
||||
"range": [
|
||||
383,
|
||||
398,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
@ -352,6 +363,7 @@ Map {
|
||||
"range": [
|
||||
404,
|
||||
420,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
@ -373,6 +385,7 @@ Map {
|
||||
"range": [
|
||||
426,
|
||||
473,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
@ -394,6 +407,7 @@ Map {
|
||||
"range": [
|
||||
479,
|
||||
487,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
@ -415,6 +429,7 @@ Map {
|
||||
"range": [
|
||||
501,
|
||||
522,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"edgeIds": [
|
||||
|
@ -49,6 +49,26 @@ sketch002 = startSketchOn(extrude001, seg02)
|
||||
extrude002 = extrude(5, sketch002)
|
||||
`
|
||||
|
||||
const exampleCodeNo3D = `sketch003 = startSketchOn('YZ')
|
||||
|> startProfileAt([5.82, 0], %)
|
||||
|> angledLine([180, 11.54], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
8.21
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch004 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 14.36], %)
|
||||
|> line([15.49, 0.05], %)
|
||||
|> tangentialArcTo([0, 0], %)
|
||||
|> tangentialArcTo([-6.8, 8.17], %)
|
||||
`
|
||||
|
||||
const sketchOnFaceOnFaceEtc = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([4, 8], %)
|
||||
@ -83,6 +103,7 @@ extrude004 = extrude(3, sketch004)
|
||||
const codeToWriteCacheFor = {
|
||||
exampleCode1,
|
||||
sketchOnFaceOnFaceEtc,
|
||||
exampleCodeNo3D,
|
||||
} as const
|
||||
|
||||
type CodeKey = keyof typeof codeToWriteCacheFor
|
||||
@ -236,6 +257,69 @@ describe('testing createArtifactGraph', () => {
|
||||
await GraphTheGraph(theMap, 2000, 2000, 'exampleCode1.png')
|
||||
}, 20000)
|
||||
})
|
||||
|
||||
describe(`code with sketches but no extrusions or other 3D elements`, () => {
|
||||
let ast: Program
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it(`setup`, () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
} = getCommands('exampleCodeNo3D')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||
})
|
||||
|
||||
it('there should be two planes, one for each sketch path', () => {
|
||||
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
|
||||
(plane) => expandPlane(plane[1], theMap)
|
||||
)
|
||||
expect(planes).toHaveLength(2)
|
||||
planes.forEach((path) => {
|
||||
expect(path.type).toBe('plane')
|
||||
})
|
||||
})
|
||||
it('there should be two paths, one on each plane', () => {
|
||||
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
||||
(path) => expandPath(path[1], theMap)
|
||||
)
|
||||
expect(paths).toHaveLength(2)
|
||||
paths.forEach((path) => {
|
||||
if (err(path)) throw path
|
||||
expect(path.type).toBe('path')
|
||||
})
|
||||
})
|
||||
|
||||
it(`there should be 1 solid2D, just for the first closed path`, () => {
|
||||
const solid2Ds = [...filterArtifacts({ types: ['solid2D'] }, theMap)]
|
||||
expect(solid2Ds).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('there should be no extrusions', () => {
|
||||
const extrusions = [...filterArtifacts({ types: ['sweep'] }, theMap)].map(
|
||||
(extrusion) => expandSweep(extrusion[1], theMap)
|
||||
)
|
||||
expect(extrusions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('there should be 8 segments, 4 + 1 (close) from the first sketch and 3 from the second', () => {
|
||||
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
|
||||
(segment) => expandSegment(segment[1], theMap)
|
||||
)
|
||||
expect(segments).toHaveLength(8)
|
||||
})
|
||||
|
||||
it('screenshot graph', async () => {
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 2000, 2000, 'exampleCodeNo3D.png')
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('capture graph of sketchOnFaceOnFace...', () => {
|
||||
@ -457,7 +541,10 @@ async function GraphTheGraph(
|
||||
`./src/lang/std/artifactMapGraphs/${imageName}`
|
||||
)
|
||||
// chop the top 30 pixels off the image
|
||||
const originalImg = PNG.sync.read(fs.readFileSync(originalImgPath))
|
||||
const originalImgExists = fs.existsSync(originalImgPath)
|
||||
const originalImg = originalImgExists
|
||||
? PNG.sync.read(fs.readFileSync(originalImgPath))
|
||||
: null
|
||||
// const img1Data = new Uint8Array(img1.data)
|
||||
// const img1DataChopped = img1Data.slice(30 * img1.width * 4)
|
||||
// img1.data = Buffer.from(img1DataChopped)
|
||||
@ -468,10 +555,10 @@ async function GraphTheGraph(
|
||||
const newImageDataChopped = newImageData.slice(30 * newImage.width * 4)
|
||||
newImage.data = Buffer.from(newImageDataChopped)
|
||||
|
||||
const { width, height } = originalImg
|
||||
const { width, height } = originalImg ?? newImage
|
||||
const diff = new PNG({ width, height })
|
||||
|
||||
const imageSizeDifferent = originalImg.data.length !== newImage.data.length
|
||||
const imageSizeDifferent = originalImg?.data.length !== newImage.data.length
|
||||
let numDiffPixels = 0
|
||||
if (!imageSizeDifferent) {
|
||||
numDiffPixels = pixelmatch(
|
||||
@ -523,7 +610,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
sweepId: '',
|
||||
codeRef: {
|
||||
pathToNode: [['body', '']],
|
||||
range: [37, 64],
|
||||
range: [37, 64, 0],
|
||||
},
|
||||
},
|
||||
])
|
||||
@ -535,7 +622,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [231, 254],
|
||||
range: [231, 254, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -545,7 +632,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
planeId: expect.any(String),
|
||||
sweepId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [37, 64],
|
||||
range: [37, 64, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
@ -558,7 +645,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceId: '',
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [70, 86],
|
||||
range: [70, 86, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -568,7 +655,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
planeId: expect.any(String),
|
||||
sweepId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [37, 64],
|
||||
range: [37, 64, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
@ -582,7 +669,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
edgeIds: [],
|
||||
surfaceId: '',
|
||||
codeRef: {
|
||||
range: [260, 299],
|
||||
range: [260, 299, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -592,7 +679,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [92, 119],
|
||||
range: [92, 119, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
@ -612,7 +699,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [156, 203],
|
||||
range: [156, 203, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -623,7 +710,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254],
|
||||
range: [231, 254, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -640,7 +727,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [125, 150],
|
||||
range: [125, 150, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -651,7 +738,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254],
|
||||
range: [231, 254, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -668,7 +755,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [92, 119],
|
||||
range: [92, 119, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
@ -680,7 +767,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254],
|
||||
range: [231, 254, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -697,7 +784,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [70, 86],
|
||||
range: [70, 86, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -708,7 +795,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254],
|
||||
range: [231, 254, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -726,7 +813,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254],
|
||||
range: [231, 254, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
@ -744,7 +831,7 @@ describe('testing getArtifactsToUpdate', () => {
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: expect.any(Array),
|
||||
codeRef: {
|
||||
range: [231, 254],
|
||||
range: [231, 254, 0],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
|
@ -36,9 +36,12 @@ interface solid2D {
|
||||
}
|
||||
export interface PathArtifactRich {
|
||||
type: 'path'
|
||||
/** A path must always lie on a plane */
|
||||
plane: PlaneArtifact | WallArtifact
|
||||
/** A path must always contain 0 or more segments */
|
||||
segments: Array<SegmentArtifact>
|
||||
sweep: SweepArtifact
|
||||
/** A path may not result in a sweep artifact */
|
||||
sweep?: SweepArtifact
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
@ -587,13 +590,15 @@ export function expandPath(
|
||||
{ keys: path.segIds, types: ['segment'] },
|
||||
artifactGraph
|
||||
)
|
||||
const sweep = getArtifactOfTypes(
|
||||
{
|
||||
key: path.sweepId,
|
||||
types: ['sweep'],
|
||||
},
|
||||
artifactGraph
|
||||
)
|
||||
const sweep = path.sweepId
|
||||
? getArtifactOfTypes(
|
||||
{
|
||||
key: path.sweepId,
|
||||
types: ['sweep'],
|
||||
},
|
||||
artifactGraph
|
||||
)
|
||||
: undefined
|
||||
const plane = getArtifactOfTypes(
|
||||
{ key: path.planeId, types: ['plane', 'wall'] },
|
||||
artifactGraph
|
||||
|
BIN
src/lang/std/artifactMapGraphs/exampleCodeNo3D.png
Normal file
After Width: | Height: | Size: 92 KiB |
@ -28,6 +28,7 @@ import {
|
||||
} from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
@ -330,6 +331,7 @@ class EngineConnection extends EventTarget {
|
||||
token?: string
|
||||
callbackOnEngineLiteConnect?: () => void
|
||||
}) {
|
||||
markOnce('code/startInitialEngineConnect')
|
||||
super()
|
||||
|
||||
this.engineCommandManager = engineCommandManager
|
||||
@ -785,6 +787,7 @@ class EngineConnection extends EventTarget {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
|
||||
)
|
||||
markOnce('code/endInitialEngineConnect')
|
||||
}
|
||||
this.unreliableDataChannel?.addEventListener(
|
||||
'open',
|
||||
|
@ -1823,11 +1823,13 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
||||
modifiedAst: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
body: [],
|
||||
|
||||
nonCodeMeta: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
moduleId: 0,
|
||||
startNodes: [],
|
||||
nonCodeNodes: [],
|
||||
},
|
||||
|
@ -120,8 +120,8 @@ const initialise = async () => {
|
||||
|
||||
export const initPromise = initialise()
|
||||
|
||||
export const rangeTypeFix = (ranges: number[][]): [number, number][] =>
|
||||
ranges.map(([start, end]) => [start, end])
|
||||
export const rangeTypeFix = (ranges: number[][]): [number, number, number][] =>
|
||||
ranges.map(([start, end, moduleId]) => [start, end, moduleId])
|
||||
|
||||
export const parse = (code: string | Error): Node<Program> | Error => {
|
||||
if (err(code)) return code
|
||||
|
52
src/lib/commandBarConfigs/routeCommandConfig.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Command } from '../commandTypes'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { NavigateFunction, Location } from 'react-router-dom'
|
||||
export function createRouteCommands(
|
||||
navigate: NavigateFunction,
|
||||
location: Location,
|
||||
filePath: string
|
||||
) {
|
||||
const RouteTelemetryCommand: Command = {
|
||||
name: 'Go to Telemetry',
|
||||
displayName: `Go to Telemetry`,
|
||||
description: 'View the Telemetry metrics',
|
||||
groupId: 'routes',
|
||||
icon: 'settings',
|
||||
needsReview: false,
|
||||
onSubmit: (data) => {
|
||||
const path = location.pathname.includes(PATHS.FILE)
|
||||
? filePath + PATHS.TELEMETRY + '?tab=project'
|
||||
: PATHS.HOME + PATHS.TELEMETRY
|
||||
navigate(path)
|
||||
},
|
||||
}
|
||||
|
||||
const RouteHomeCommand: Command = {
|
||||
name: 'Go to Home',
|
||||
displayName: `Go to Home`,
|
||||
description: 'Go to the home page',
|
||||
groupId: 'routes',
|
||||
icon: 'settings',
|
||||
needsReview: false,
|
||||
onSubmit: (data) => {
|
||||
navigate(PATHS.HOME)
|
||||
},
|
||||
}
|
||||
|
||||
const RouteSettingsCommand: Command = {
|
||||
name: 'Go to Settings',
|
||||
displayName: `Go to Settings`,
|
||||
description: 'Go to the settings page',
|
||||
groupId: 'routes',
|
||||
icon: 'settings',
|
||||
needsReview: false,
|
||||
onSubmit: (data) => {
|
||||
const path = location.pathname.includes(PATHS.FILE)
|
||||
? filePath + PATHS.SETTINGS + '?tab=project'
|
||||
: PATHS.HOME + PATHS.SETTINGS
|
||||
navigate(path)
|
||||
},
|
||||
}
|
||||
|
||||
return { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand }
|
||||
}
|
@ -69,6 +69,8 @@ export const SETTINGS_FILE_NAME = 'settings.toml'
|
||||
export const TOKEN_FILE_NAME = 'token.txt'
|
||||
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||
export const COOKIE_NAME = '__Secure-next-auth.session-token'
|
||||
export const TELEMETRY_FILE_NAME = 'boot.txt'
|
||||
export const TELEMETRY_RAW_FILE_NAME = 'raw-metrics.txt'
|
||||
|
||||
/** localStorage key to determine if we're in Playwright tests */
|
||||
export const PLAYWRIGHT_KEY = 'playwright'
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
PROJECT_FOLDER,
|
||||
PROJECT_SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_NAME,
|
||||
TELEMETRY_FILE_NAME,
|
||||
TELEMETRY_RAW_FILE_NAME,
|
||||
TOKEN_FILE_NAME,
|
||||
} from './constants'
|
||||
import { DeepPartial } from './types'
|
||||
@ -419,6 +421,34 @@ const getTokenFilePath = async () => {
|
||||
return window.electron.path.join(fullPath, TOKEN_FILE_NAME)
|
||||
}
|
||||
|
||||
const getTelemetryFilePath = async () => {
|
||||
const appConfig = await window.electron.getPath('appData')
|
||||
const fullPath = window.electron.path.join(appConfig, getAppFolderName())
|
||||
try {
|
||||
await window.electron.stat(fullPath)
|
||||
} catch (e) {
|
||||
// File/path doesn't exist
|
||||
if (e === 'ENOENT') {
|
||||
await window.electron.mkdir(fullPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(fullPath, TELEMETRY_FILE_NAME)
|
||||
}
|
||||
|
||||
const getRawTelemetryFilePath = async () => {
|
||||
const appConfig = await window.electron.getPath('appData')
|
||||
const fullPath = window.electron.path.join(appConfig, getAppFolderName())
|
||||
try {
|
||||
await window.electron.stat(fullPath)
|
||||
} catch (e) {
|
||||
// File/path doesn't exist
|
||||
if (e === 'ENOENT') {
|
||||
await window.electron.mkdir(fullPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(fullPath, TELEMETRY_RAW_FILE_NAME)
|
||||
}
|
||||
|
||||
const getProjectSettingsFilePath = async (projectPath: string) => {
|
||||
try {
|
||||
await window.electron.stat(projectPath)
|
||||
@ -552,6 +582,18 @@ export const writeTokenFile = async (token: string) => {
|
||||
return window.electron.writeFile(tokenFilePath, token)
|
||||
}
|
||||
|
||||
export const writeTelemetryFile = async (content: string) => {
|
||||
const telemetryFilePath = await getTelemetryFilePath()
|
||||
if (err(content)) return Promise.reject(content)
|
||||
return window.electron.writeFile(telemetryFilePath, content)
|
||||
}
|
||||
|
||||
export const writeRawTelemetryFile = async (content: string) => {
|
||||
const rawTelemetryFilePath = await getRawTelemetryFilePath()
|
||||
if (err(content)) return Promise.reject(content)
|
||||
return window.electron.writeFile(rawTelemetryFilePath, content)
|
||||
}
|
||||
|
||||
let appStateStore: Project | undefined = undefined
|
||||
|
||||
export const getState = async (): Promise<Project | undefined> => {
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
INDEX_IDENTIFIER,
|
||||
MAX_PADDING,
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
PROJECT_ENTRYPOINT,
|
||||
} from 'lib/constants'
|
||||
import { bracket } from './exampleKcl'
|
||||
import { PATHS } from './paths'
|
||||
@ -22,36 +21,20 @@ export const isHidden = (fileOrDir: FileEntry) =>
|
||||
export const isDir = (fileOrDir: FileEntry) =>
|
||||
'children' in fileOrDir && fileOrDir.children !== undefined
|
||||
|
||||
// Deeply sort the files and directories in a project like VS Code does:
|
||||
// The main.kcl file is always first, then files, then directories
|
||||
// Shallow sort the files and directories
|
||||
// Files and directories are sorted alphabetically
|
||||
export function sortProject(project: FileEntry[]): FileEntry[] {
|
||||
const sortedProject = project.sort((a, b) => {
|
||||
if (a.name === PROJECT_ENTRYPOINT) {
|
||||
return -1
|
||||
} else if (b.name === PROJECT_ENTRYPOINT) {
|
||||
export function sortFilesAndDirectories(files: FileEntry[]): FileEntry[] {
|
||||
return files.sort((a, b) => {
|
||||
if (a.children === null && b.children !== null) {
|
||||
return 1
|
||||
} else if (a.children === null && b.children !== null) {
|
||||
return -1
|
||||
} else if (a.children !== null && b.children === null) {
|
||||
return 1
|
||||
return -1
|
||||
} else if (a.name && b.name) {
|
||||
return a.name.localeCompare(b.name)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
return sortedProject.map((fileOrDir: FileEntry) => {
|
||||
if ('children' in fileOrDir && fileOrDir.children !== null) {
|
||||
return {
|
||||
...fileOrDir,
|
||||
children: sortProject(fileOrDir.children || []),
|
||||
}
|
||||
} else {
|
||||
return fileOrDir
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// create a regex to match the project name
|
||||
|
@ -42,6 +42,7 @@ export const PATHS = {
|
||||
SETTINGS_KEYBINDINGS: `${SETTINGS}?tab=keybindings` as const,
|
||||
SIGN_IN: '/signin',
|
||||
ONBOARDING: prependRoutes(onboardingPaths)('/onboarding') as OnboardingPaths,
|
||||
TELEMETRY: '/telemetry',
|
||||
} as const
|
||||
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
||||
|
||||
|
127
src/lib/performance.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
|
||||
function isWeb(): boolean {
|
||||
// Identify browser environment when following property is not present
|
||||
// https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#performancenodetiming
|
||||
return (
|
||||
typeof performance === 'object' &&
|
||||
typeof performance.mark === 'function' &&
|
||||
// @ts-ignore
|
||||
!performance.nodeTiming
|
||||
)
|
||||
}
|
||||
|
||||
function isNode(): boolean {
|
||||
// @ts-ignore
|
||||
return typeof process === 'object' && performance.nodeTiming
|
||||
}
|
||||
|
||||
function getRuntime(): string {
|
||||
if (isDesktop()) {
|
||||
return 'electron'
|
||||
} else if (isNode()) {
|
||||
return 'nodejs'
|
||||
} else if (isWeb()) {
|
||||
return 'web'
|
||||
}
|
||||
return 'runtime unknown, could not detect'
|
||||
}
|
||||
|
||||
export interface PerformanceMarkDetail {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface PerformanceMark {
|
||||
name: string
|
||||
startTime: number
|
||||
entryType: string
|
||||
detail: null | PerformanceMarkDetail
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export interface MarkHelpers {
|
||||
mark(name: string, options?: PerformanceMark): void
|
||||
markOnce(name: string, options?: PerformanceMark): void
|
||||
getMarks(): PerformanceMark[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect performance API environment, either Web or Node.js
|
||||
*/
|
||||
function detectEnvironment(): MarkHelpers {
|
||||
const seenMarks: { [key: string]: boolean } = {}
|
||||
if (isWeb() || isNode() || isDesktop()) {
|
||||
// in a browser context, reuse performance-util
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
|
||||
function _mark(name: string, options?: PerformanceMark) {
|
||||
const _options = {
|
||||
...options,
|
||||
}
|
||||
|
||||
// Automatically append detail data for a canonical form
|
||||
if (!_options.detail) {
|
||||
_options.detail = {}
|
||||
}
|
||||
_options.detail.runtime = getRuntime()
|
||||
|
||||
performance.mark(name, _options)
|
||||
}
|
||||
|
||||
const _helpers: MarkHelpers = {
|
||||
mark(name: string, options?: PerformanceMark) {
|
||||
_mark(name, options)
|
||||
},
|
||||
markOnce(name: string, options?: PerformanceMark) {
|
||||
if (seenMarks[name]) {
|
||||
return
|
||||
}
|
||||
_mark(name, options)
|
||||
seenMarks[name] = true
|
||||
},
|
||||
getMarks() {
|
||||
let timeOrigin = performance.timeOrigin
|
||||
const result: PerformanceMark[] = [
|
||||
{
|
||||
name: 'code/timeOrigin',
|
||||
startTime: Math.round(timeOrigin),
|
||||
detail: { runtime: getRuntime() },
|
||||
entryType: 'mark',
|
||||
},
|
||||
]
|
||||
for (const entry of performance.getEntriesByType('mark')) {
|
||||
result.push({
|
||||
name: entry.name,
|
||||
// Make everything unix time
|
||||
startTime: Math.round(timeOrigin + entry.startTime),
|
||||
// @ts-ignore - we can assume this is just any object with [key:string]: any
|
||||
detail: entry.detail,
|
||||
entryType: entry.entryType,
|
||||
})
|
||||
}
|
||||
return result
|
||||
},
|
||||
}
|
||||
return _helpers
|
||||
} else {
|
||||
// This would be browsers that do not support the performance API.
|
||||
// TODO: Implement a polyfill
|
||||
console.error('No performance API found globally. Going to be a bad time.')
|
||||
return {
|
||||
mark() {
|
||||
/*no op*/
|
||||
},
|
||||
markOnce() {
|
||||
/*no op*/
|
||||
},
|
||||
getMarks() {
|
||||
return []
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const env = detectEnvironment()
|
||||
export const mark = env.mark
|
||||
export const getMarks = env.getMarks
|
||||
export const markOnce = env.markOnce
|
@ -44,6 +44,12 @@ export const settingsLoader: LoaderFunction = async ({
|
||||
return settings
|
||||
}
|
||||
|
||||
export const telemetryLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<null> => {
|
||||
return null
|
||||
}
|
||||
|
||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||
const { settings } = await loadAndValidateSettings()
|
||||
|
109
src/lib/telemetry.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import {
|
||||
columnWidth,
|
||||
MaxWidth,
|
||||
printHeader,
|
||||
printDivider,
|
||||
printRow,
|
||||
} from 'lib/telemetry'
|
||||
|
||||
describe('Telemetry', () => {
|
||||
describe('columnWidth', () => {
|
||||
it('should return 0', () => {
|
||||
const actual = columnWidth([{ '': '' }], '')
|
||||
const expected = 0
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it('should return 10 from column length', () => {
|
||||
const actual = columnWidth([{ thisisten_: 'dog' }], 'thisisten_')
|
||||
const expected = 10
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it('should return 5 from the key length', () => {
|
||||
const actual = columnWidth([{ mph: 'five5' }], 'mph')
|
||||
const expected = 5
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it('should return 6 from multiple values', () => {
|
||||
const actual = columnWidth(
|
||||
[
|
||||
{ mph: '555' },
|
||||
{ mph: '33' },
|
||||
{ mph: '789' },
|
||||
{ mph: '1231' },
|
||||
{ mph: '129532' },
|
||||
],
|
||||
'mph'
|
||||
)
|
||||
const expected = 6
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
})
|
||||
describe('printHeader', () => {
|
||||
it('should print a header based on MaxWidth interface with value lengths', () => {
|
||||
const widths: MaxWidth = {
|
||||
metricA: 7,
|
||||
metricB: 8,
|
||||
metricC: 9,
|
||||
metricD: 10,
|
||||
}
|
||||
const actual = printHeader(widths)
|
||||
const expected = '| metricA | metricB | metricC | metricD |'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it('should print a header based on MaxWidth interface with key lengths', () => {
|
||||
const widths: MaxWidth = {
|
||||
aa: 2,
|
||||
bb: 2,
|
||||
cc: 2,
|
||||
dd: 2,
|
||||
}
|
||||
const actual = printHeader(widths)
|
||||
const expected = '| aa | bb | cc | dd |'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
})
|
||||
describe('printDivider', () => {
|
||||
it('should print a divider based on MaxWidth interface with value lengths', () => {
|
||||
const widths: MaxWidth = {
|
||||
metricA: 7,
|
||||
metricB: 8,
|
||||
metricC: 9,
|
||||
metricD: 10,
|
||||
}
|
||||
const actual = printDivider(widths)
|
||||
const expected = '| ------- | -------- | --------- | ---------- |'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
it('should print a divider based on MaxWidth interface with key lengths', () => {
|
||||
const widths: MaxWidth = {
|
||||
aa: 2,
|
||||
bb: 2,
|
||||
cc: 2,
|
||||
dd: 2,
|
||||
}
|
||||
const actual = printDivider(widths)
|
||||
const expected = '| -- | -- | -- | -- |'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
})
|
||||
describe('printRow', () => {
|
||||
it('should print a row', () => {
|
||||
const widths: MaxWidth = {
|
||||
metricA: 7,
|
||||
metricB: 8,
|
||||
metricC: 9,
|
||||
metricD: 10,
|
||||
}
|
||||
const row = {
|
||||
metricA: 'aa',
|
||||
metricB: 'bb',
|
||||
metricC: 'cc',
|
||||
metricD: 'dd',
|
||||
}
|
||||
const actual = printRow(row, widths)
|
||||
const expected = '| aa | bb | cc | dd |'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
})
|
||||
})
|
170
src/lib/telemetry.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { PerformanceMark, getMarks } from 'lib/performance'
|
||||
import { writeTelemetryFile, writeRawTelemetryFile } from 'lib/desktop'
|
||||
let args: any = null
|
||||
|
||||
// Get the longest width of values or column name
|
||||
export function columnWidth(arr: { [key: string]: any }, key: string): number {
|
||||
let maxLength = key.length
|
||||
|
||||
// for each value of that key, check if the length is longer
|
||||
arr.forEach((value: any) => {
|
||||
const valueAsString = String(value[key])
|
||||
maxLength =
|
||||
valueAsString.length > maxLength ? valueAsString.length : maxLength
|
||||
})
|
||||
return maxLength
|
||||
}
|
||||
|
||||
export function printHeader(columnWidths: MaxWidth): string {
|
||||
const headers = ['|']
|
||||
const padLeft = ' '
|
||||
Object.keys(columnWidths).forEach((key) => {
|
||||
const maxWidth = columnWidths[key]
|
||||
const padLength = maxWidth - key.length
|
||||
const paddingRight = ' '.repeat(padLength + 1)
|
||||
headers.push(padLeft, key, paddingRight, '|')
|
||||
})
|
||||
return headers.join('')
|
||||
}
|
||||
|
||||
export function printDivider(columnWidths: MaxWidth): string {
|
||||
const headers = ['|']
|
||||
const padLeft = ' '
|
||||
Object.keys(columnWidths).forEach((key) => {
|
||||
const keyMaxLength = columnWidths[key]
|
||||
const dashedLines = '-'.repeat(keyMaxLength)
|
||||
headers.push(padLeft, dashedLines, ' ', '|')
|
||||
})
|
||||
return headers.join('')
|
||||
}
|
||||
|
||||
export function printRow(
|
||||
row: { [key: string]: any },
|
||||
columnWidths: MaxWidth
|
||||
): string {
|
||||
const _row = ['|']
|
||||
const padLeft = ' '
|
||||
Object.keys(row).forEach((key) => {
|
||||
const value = String(row[key])
|
||||
const valueLength = value && value.length ? value.length : 0
|
||||
const padLength = columnWidths[key] - valueLength
|
||||
const paddingRight = ' '.repeat(padLength + 1)
|
||||
_row.push(padLeft, value, paddingRight, '|')
|
||||
})
|
||||
return _row.join('')
|
||||
}
|
||||
|
||||
export interface MaxWidth {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export function printMarkDownTable(
|
||||
marks: Array<{ [key: string]: any }>
|
||||
): Array<string> {
|
||||
if (marks.length === 0) {
|
||||
return []
|
||||
}
|
||||
const sample = marks[0]
|
||||
const columnWidths: MaxWidth = {}
|
||||
Object.keys(sample).forEach((key) => {
|
||||
const width = columnWidth(marks, key)
|
||||
columnWidths[key] = width
|
||||
})
|
||||
|
||||
const lines = []
|
||||
lines.push(printHeader(columnWidths))
|
||||
lines.push(printDivider(columnWidths))
|
||||
marks.forEach((row) => {
|
||||
lines.push(printRow(row, columnWidths))
|
||||
})
|
||||
return lines
|
||||
}
|
||||
|
||||
export interface PerformanceDeltaTotal {
|
||||
name: string
|
||||
startTime: number
|
||||
delta: string
|
||||
total: string
|
||||
}
|
||||
|
||||
export function computeDeltaTotal(
|
||||
marks: Array<PerformanceMark>
|
||||
): Array<PerformanceDeltaTotal> {
|
||||
let startTime = -1
|
||||
let total = 0
|
||||
const deltaTotalArray: Array<PerformanceDeltaTotal> = marks.map(
|
||||
(row: PerformanceMark) => {
|
||||
const delta =
|
||||
startTime === -1 ? 0 : Number(row.startTime) - Number(startTime)
|
||||
startTime = row.startTime
|
||||
total += delta
|
||||
const formatted: PerformanceDeltaTotal = {
|
||||
name: row.name,
|
||||
startTime: row.startTime,
|
||||
delta: delta.toFixed(2),
|
||||
total: total.toFixed(2),
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
)
|
||||
return deltaTotalArray
|
||||
}
|
||||
|
||||
export function printDeltaTotal(marks: Array<PerformanceMark>): string[] {
|
||||
const deltaTotalArray = computeDeltaTotal(marks)
|
||||
return printMarkDownTable(deltaTotalArray)
|
||||
}
|
||||
|
||||
export function printRawRow(row: { [key: string]: any }): string {
|
||||
const _row = ['']
|
||||
Object.keys(row).forEach((key) => {
|
||||
const value = String(row[key])
|
||||
_row.push(value, ' ')
|
||||
})
|
||||
return _row.join('')
|
||||
}
|
||||
|
||||
export function printRawMarks(marks: Array<PerformanceMark>): string[] {
|
||||
const headers = ['Name', 'Timestamp', 'Delta', 'Total', 'Detail']
|
||||
const lines = ['```', headers.join(' ')]
|
||||
const deltaTotalArray = computeDeltaTotal(marks)
|
||||
deltaTotalArray.forEach((row) => {
|
||||
lines.push(printRawRow(row))
|
||||
})
|
||||
lines.push('```')
|
||||
return lines
|
||||
}
|
||||
|
||||
export function printInvocationCount(marks: Array<PerformanceMark>): string[] {
|
||||
const counts: { [key: string]: number } = {}
|
||||
marks.forEach((mark: PerformanceMark) => {
|
||||
counts[mark.name] =
|
||||
counts[mark.name] === undefined ? 1 : counts[mark.name] + 1
|
||||
})
|
||||
|
||||
const formattedCounts = Object.entries(counts).map((entry) => {
|
||||
return {
|
||||
name: entry[0],
|
||||
count: entry[1],
|
||||
}
|
||||
})
|
||||
return printMarkDownTable(formattedCounts)
|
||||
}
|
||||
|
||||
export async function maybeWriteToDisk() {
|
||||
if (!args) {
|
||||
args = await window.electron.getArgvParsed()
|
||||
}
|
||||
if (args.telemetry) {
|
||||
setInterval(() => {
|
||||
const marks = getMarks()
|
||||
const deltaTotalTable = printDeltaTotal(marks)
|
||||
writeTelemetryFile(deltaTotalTable.join('\n'))
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
writeRawTelemetryFile(JSON.stringify(marks))
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
}, 5000)
|
||||
}
|
||||
}
|