diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3702c0bb8..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "cSpell.words": [ - "geos" - ], - "editor.tabSize": 2, - "editor.insertSpaces": true, -} \ No newline at end of file diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 864d47d1a..34941eff0 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -82,7 +82,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -236,7 +236,7 @@ test.describe('Testing Camera Movement', () => { test.skip(process.platform === 'darwin', 'Can moving camera') const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openAndClearDebugPanel() await u.closeKclCodePanel() @@ -404,7 +404,7 @@ test.describe('Testing Camera Movement', () => { test.skip(process.platform !== 'darwin', 'Zoom should be consistent') const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -540,7 +540,6 @@ test('if you click the format button it formats your code', async ({ }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') await u.waitForAuthSkipAppStart() @@ -580,18 +579,8 @@ test('hover over functions shows function description', async ({ page }) => { ) }) await page.setViewportSize({ width: 1000, height: 500 }) - const lspStartPromise = page.waitForEvent('console', async (message) => { - // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') - // but that doesn't seem to make it to the console for macos/safari :( - if (message.text().includes('start kcl lsp')) { - await new Promise((resolve) => setTimeout(resolve, 200)) - return true - } - return false - }) - await page.goto('/') + await u.waitForAuthSkipAppStart() - await lspStartPromise // check no error to begin with await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() @@ -633,18 +622,8 @@ test('if you use the format keyboard binding it formats your code', async ({ localStorage.setItem('disableAxis', 'true') }) await page.setViewportSize({ width: 1000, height: 500 }) - const lspStartPromise = page.waitForEvent('console', async (message) => { - // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') - // but that doesn't seem to make it to the console for macos/safari :( - if (message.text().includes('start kcl lsp')) { - await new Promise((resolve) => setTimeout(resolve, 200)) - return true - } - return false - }) - await page.goto('/') + await u.waitForAuthSkipAppStart() - await lspStartPromise // check no error to begin with await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() @@ -669,8 +648,9 @@ test('if you use the format keyboard binding it formats your code', async ({ }) test('ensure the Zoo logo is not a link in browser app', async ({ page }) => { + const u = await getUtils(page) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() const zooLogo = page.locator('[data-testid="app-logo"]') // Make sure it's not a link @@ -680,7 +660,6 @@ test('ensure the Zoo logo is not a link in browser app', async ({ page }) => { test('if you write kcl with lint errors you get lints', async ({ page }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') await u.waitForAuthSkipAppStart() @@ -733,7 +712,6 @@ test('if you fixup kcl errors you clear lints', async ({ page }) => { }) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') await u.waitForAuthSkipAppStart() @@ -744,19 +722,18 @@ test('if you fixup kcl errors you clear lints', async ({ page }) => { await page.getByText(' |> line([2.48, 2.44], %)').click() - await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() + await expect(page.locator('.cm-lint-marker-error').first()).not.toBeVisible() await page.keyboard.press('End') await page.keyboard.press('Backspace') - await expect(page.locator('.cm-lint-marker-error')).toBeVisible() + await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() await page.keyboard.type(')') - await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() + await expect(page.locator('.cm-lint-marker-error').first()).not.toBeVisible() }) test('if you write invalid kcl you get inlined errors', async ({ page }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') await u.waitForAuthSkipAppStart() @@ -845,19 +822,8 @@ fn squareHole = (l, w) => { ) }) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') - const lspStartPromise = page.waitForEvent('console', async (message) => { - // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') - // but that doesn't seem to make it to the console for macos/safari :( - if (message.text().includes('start kcl lsp')) { - await new Promise((resolve) => setTimeout(resolve, 200)) - return true - } - return false - }) - await page.goto('/') + await u.waitForAuthSkipAppStart() - await lspStartPromise await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') @@ -926,7 +892,6 @@ angle: 90 }) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') await u.waitForAuthSkipAppStart() @@ -959,7 +924,7 @@ test('executes on load', async ({ page }) => { ) }) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // expand variables section @@ -988,7 +953,7 @@ test('re-executes', async ({ page }) => { localStorage.setItem('persistCode', `const myVar = 5`) }) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() const variablesTabButton = page.getByRole('tab', { @@ -1020,7 +985,7 @@ const sketchOnPlaneAndBackSideTest = async ( const u = await getUtils(page) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -1128,18 +1093,8 @@ const example = extrude(5, exampleSketch) shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` ) }) - const lspStartPromise = page.waitForEvent('console', async (message) => { - // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') - // but that doesn't seem to make it to the console for macos/safari :( - if (message.text().includes('start kcl lsp')) { - await new Promise((resolve) => setTimeout(resolve, 200)) - return true - } - return false - }) - await page.goto('/') + await u.waitForAuthSkipAppStart() - await lspStartPromise // error in guter await expect(page.locator('.cm-lint-marker-error')).toBeVisible() @@ -1163,78 +1118,146 @@ shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)` await page.keyboard.press('Enter') }) -test('Auto complete works', async ({ page }) => { - const u = await getUtils(page) - // const PUR = 400 / 37.5 //pixeltoUnitRatio - await page.setViewportSize({ width: 1200, height: 500 }) - const lspStartPromise = page.waitForEvent('console', async (message) => { - // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') - // but that doesn't seem to make it to the console for macos/safari :( - if (message.text().includes('start kcl lsp')) { - await new Promise((resolve) => setTimeout(resolve, 200)) - return true - } - return false - }) - await page.goto('/') - await u.waitForAuthSkipAppStart() - await lspStartPromise +test.describe('Autocomplete works', () => { + test('with enter/click to accept the completion', async ({ page }) => { + const u = await getUtils(page) + // const PUR = 400 / 37.5 //pixeltoUnitRatio + await page.setViewportSize({ width: 1200, height: 500 }) - // 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.waitForAuthSkipAppStart() - await u.codeLocator.click() - await page.keyboard.type('const sketch001 = start') + // 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 - // expect there to be six auto complete options - await expect(page.locator('.cm-completionLabel')).toHaveCount(6) - await page.getByText('startSketchOn').click() - await page.keyboard.type("'XZ'") - await page.keyboard.press('Tab') - await page.keyboard.press('Enter') - await page.keyboard.type(' |> startProfi') - // expect there be a single auto complete option that we can just hit enter on - await expect(page.locator('.cm-completionLabel')).toBeVisible() - await page.waitForTimeout(100) - await page.keyboard.press('Enter') // accepting the auto complete, not a new line + await u.codeLocator.click() + await page.keyboard.type('const sketch001 = start') - await page.keyboard.press('Tab') - await page.keyboard.type('12') - await page.waitForTimeout(100) - await page.keyboard.press('Tab') - await page.waitForTimeout(100) - await page.keyboard.press('Tab') - await page.keyboard.press('Tab') - await page.keyboard.press('Enter') - await page.keyboard.type(' |> lin') + // expect there to be six auto complete options + await expect(page.locator('.cm-completionLabel')).toHaveCount(6) + // this makes sure we can accept a completion with click + await page.getByText('startSketchOn').click() + await page.keyboard.type("'XZ'") + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await page.keyboard.type(' |> startProfi') + // expect there be a single auto complete option that we can just hit enter on + await expect(page.locator('.cm-completionLabel')).toBeVisible() + await page.waitForTimeout(100) + await page.keyboard.press('Enter') // accepting the auto complete, not a new line - await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible() - await page.waitForTimeout(100) - // press arrow down twice then enter to accept xLine - await page.keyboard.press('ArrowDown') - await page.waitForTimeout(100) - await page.keyboard.press('ArrowDown') - await page.waitForTimeout(100) - await page.keyboard.press('Enter') - await page.waitForTimeout(100) - // finish line with comment - await page.keyboard.type('5') - await page.waitForTimeout(100) - await page.keyboard.press('Tab') - await page.waitForTimeout(100) - await page.keyboard.press('Tab') - await page.waitForTimeout(100) - await page.keyboard.type(' // lin') - await page.waitForTimeout(100) - // there shouldn't be any auto complete options for 'lin' in the comment - await expect(page.locator('.cm-completionLabel')).not.toBeVisible() + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.type('12') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Enter') + await page.waitForTimeout(100) + await page.keyboard.type(' |> lin') - await expect(page.locator('.cm-content')) - .toHaveText(`const sketch001 = startSketchOn('XZ') + await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible() + await page.waitForTimeout(100) + // press arrow down twice then enter to accept xLine + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Enter') + // finish line with comment + await page.keyboard.type('5') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + + await page.keyboard.type(' // ') + // Since we need to parse the ast to know we are in a comment we gotta hang tight. + await page.waitForTimeout(700) + await page.keyboard.type('lin ') + await page.waitForTimeout(200) + // there shouldn't be any auto complete options for 'lin' in the comment + await expect(page.locator('.cm-completionLabel')).not.toBeVisible() + + await expect(page.locator('.cm-content')) + .toHaveText(`const sketch001 = startSketchOn('XZ') |> startProfileAt([3.14, 12], %) |> xLine(5, %) // lin`) + }) + + test('with tab to accept the completion', async ({ page }) => { + const u = await getUtils(page) + // const PUR = 400 / 37.5 //pixeltoUnitRatio + await page.setViewportSize({ width: 1200, height: 500 }) + + 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('const sketch001 = startSketchO') + await page.waitForTimeout(100) + + // Make sure just hitting tab will take the only one left + await expect(page.locator('.cm-completionLabel')).toHaveCount(1) + await page.waitForTimeout(500) + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Tab') + await page.waitForTimeout(500) + await page.keyboard.type("'XZ'") + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await page.keyboard.type(' |> startProfi') + // expect there be a single auto complete option that we can just hit enter on + await expect(page.locator('.cm-completionLabel')).toBeVisible() + await page.waitForTimeout(100) + await page.keyboard.press('Tab') // accepting the auto complete, not a new line + + await page.keyboard.press('Tab') + await page.keyboard.type('12') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Enter') + await page.waitForTimeout(100) + await page.keyboard.type(' |> lin') + + await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible() + await page.waitForTimeout(100) + // press arrow down twice then tab to accept xLine + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Tab') + // finish line with comment + await page.keyboard.type('5') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + await page.waitForTimeout(100) + await page.keyboard.press('Tab') + + await page.keyboard.type(' // ') + // Since we need to parse the ast to know we are in a comment we gotta hang tight. + await page.waitForTimeout(700) + await page.keyboard.type('lin ') + await page.waitForTimeout(200) + // there shouldn't be any auto complete options for 'lin' in the comment + await expect(page.locator('.cm-completionLabel')).not.toBeVisible() + + await expect(page.locator('.cm-content')) + .toHaveText(`const sketch001 = startSketchOn('XZ') + |> startProfileAt([3.14, 12], %) + |> xLine(5, %) // lin`) + }) }) test('Stored settings are validated and fall back to defaults', async ({ @@ -1255,7 +1278,7 @@ test('Stored settings are validated and fall back to defaults', async ({ ) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // Check the settings were reset @@ -1278,8 +1301,9 @@ test('Stored settings are validated and fall back to defaults', async ({ test('Project settings can be set and override user settings', async ({ page, }) => { + const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/', { waitUntil: 'domcontentloaded' }) + await u.waitForAuthSkipAppStart() await page .getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' }) @@ -1326,8 +1350,9 @@ test('Project settings can be set and override user settings', async ({ test('Project settings can be opened with keybinding from the editor', async ({ page, }) => { + const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/', { waitUntil: 'domcontentloaded' }) + await u.waitForAuthSkipAppStart() await page .getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' }) @@ -1375,8 +1400,9 @@ test('Project settings can be opened with keybinding from the editor', async ({ }) test('Project and user settings can be reset', async ({ page }) => { + const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/', { waitUntil: 'domcontentloaded' }) + await u.waitForAuthSkipAppStart() await page .getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' }) @@ -1450,8 +1476,10 @@ test('Project and user settings can be reset', async ({ page }) => { test('Keyboard shortcuts can be viewed through the help menu', async ({ page, }) => { + const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() + await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) await page .getByRole('button', { name: 'Start Sketch' }) @@ -1485,7 +1513,7 @@ test.describe('Onboarding tests', () => { ) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // Test that the onboarding pane loaded @@ -1512,7 +1540,7 @@ test.describe('Onboarding tests', () => { ) await page.setViewportSize({ width: 1200, height: 1080 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // Test that the onboarding pane loaded @@ -1551,7 +1579,7 @@ test.describe('Onboarding tests', () => { ) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // Test that the redirect happened @@ -1603,7 +1631,8 @@ test.describe('Onboarding tests', () => { ) await page.setViewportSize({ width: 1200, height: 1080 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() + await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, { waitUntil: 'domcontentloaded', }) @@ -1647,8 +1676,10 @@ test.describe('Onboarding tests', () => { } ) - await page.setViewportSize({ width: 1200, height: 1080 }) - await page.goto('/') + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + await u.waitForAuthSkipAppStart() + await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) // Test that the text in this step is correct @@ -1703,7 +1734,7 @@ test.describe('Testing selections', () => { const u = await getUtils(page) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -2163,7 +2194,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02) ) }, KCL_DEFAULT_LENGTH) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -2250,7 +2281,7 @@ const extrude001 = extrude(10, sketch001) ) }) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -2329,7 +2360,7 @@ const part001 = startSketchOn('XZ') ) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openAndClearDebugPanel() @@ -2379,7 +2410,7 @@ const extrude001 = extrude(50, sketch001) }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openAndClearDebugPanel() @@ -2463,9 +2494,9 @@ const extrude001 = extrude(50, sketch001) test.describe('Command bar tests', () => { test('Command bar works and can change a setting', async ({ page }) => { - // Brief boilerplate + const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/', { waitUntil: 'domcontentloaded' }) + await u.waitForAuthSkipAppStart() await expect( page.getByRole('button', { name: 'Start Sketch' }) @@ -2524,9 +2555,9 @@ test.describe('Command bar tests', () => { test('Command bar keybinding works from code editor and can change a setting', async ({ page, }) => { - // Brief boilerplate + const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/', { waitUntil: 'domcontentloaded' }) + await u.waitForAuthSkipAppStart() await expect( page.getByRole('button', { name: 'Start Sketch' }) @@ -2591,7 +2622,7 @@ test.describe('Command bar tests', () => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // Make sure the stream is up @@ -2672,7 +2703,7 @@ test('Can add multiple sketches', async ({ page }) => { const u = await getUtils(page) const viewportSize = { width: 1200, height: 500 } await page.setViewportSize(viewportSize) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -2778,7 +2809,7 @@ test('ProgramMemory can be serialised', async ({ page }) => { ) }) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + const messages: string[] = [] // Listen for all console events and push the message text to an array @@ -2855,7 +2886,7 @@ fn yohey = (pos) => { selectionsSnippets ) await page.setViewportSize({ width: 1200, height: 1000 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -2912,7 +2943,7 @@ test('Deselecting line tool should mean nothing happens on click', async ({ }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -3033,7 +3064,7 @@ const part002 = startSketchOn('-XZ') selectionsSnippets ) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -3069,7 +3100,7 @@ async function doEditSegmentsByDraggingHandle(page: Page, openPanes: string[]) { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await expect( page.getByRole('button', { name: 'Start Sketch' }) @@ -3235,7 +3266,7 @@ test('Can edit a sketch that has been extruded in the same pipe', async ({ }) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await expect( page.getByRole('button', { name: 'Start Sketch' }) @@ -3336,7 +3367,7 @@ test('Can edit a sketch that has been revolved in the same pipe', async ({ }) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await expect( page.getByRole('button', { name: 'Start Sketch' }) @@ -3427,7 +3458,7 @@ const doSnapAtDifferentScales = async ( ) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -3542,7 +3573,7 @@ test('Sketch on face', async ({ page }) => { }) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -3677,7 +3708,7 @@ test('Can code mod a line length', async ({ page }) => { const u = await getUtils(page) const PUR = 400 / 37.5 //pixeltoUnitRatio await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -3737,7 +3768,7 @@ test('Extrude from command bar selects extrude line after', async ({ const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -3781,7 +3812,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %, $seg01)').click() @@ -3841,7 +3872,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %, $seg01)').click() @@ -3940,7 +3971,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %)').click() @@ -4048,7 +4079,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %)').click() @@ -4155,7 +4186,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %)').click() @@ -4265,7 +4296,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %)').click() @@ -4341,7 +4372,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %)').click() @@ -4437,7 +4468,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %)').click() @@ -4514,7 +4545,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([74.36, 130.4], %)').click() @@ -4561,7 +4592,7 @@ const part002 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.getByText('line([3.79, 2.68], %, $seg01)').click() @@ -4817,7 +4848,7 @@ test.describe('Testing segment overlays', () => { }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -4976,7 +5007,7 @@ const part001 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -5056,7 +5087,7 @@ const part001 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -5184,7 +5215,7 @@ const part001 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -5340,7 +5371,7 @@ const part001 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -5453,7 +5484,7 @@ const part001 = startSketchOn('XZ') }) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -5679,7 +5710,7 @@ ${extraLine ? 'const myVar = segLen(seg01, part001)' : ''}` ) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.waitForTimeout(300) @@ -5837,7 +5868,7 @@ ${extraLine ? 'const myVar = segLen(seg01, part001)' : ''}` ) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.waitForTimeout(300) @@ -5892,7 +5923,7 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn // Wait for the app to be ready for use const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') @@ -5973,7 +6004,7 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => { // Wait for the app to be ready for use const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') @@ -6062,7 +6093,7 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => { test('simulate network down and network little widget', async ({ page }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // This is how we wait until the stream is online @@ -6133,7 +6164,7 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -6310,7 +6341,7 @@ test.describe('Testing Gizmo', () => { localStorage.setItem('persistCode', TEST_CODE_GIZMO) }, TEST_CODE_GIZMO) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.waitForTimeout(100) // wait for execution done @@ -6400,7 +6431,7 @@ test.describe('Testing Gizmo', () => { localStorage.setItem('persistCode', TEST_CODE_GIZMO) }, TEST_CODE_GIZMO) await page.setViewportSize({ width: 1000, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await page.waitForTimeout(100) // wait for execution done @@ -6534,7 +6565,7 @@ const part001 = startSketchOn('-XZ') ) }) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') @@ -6566,8 +6597,9 @@ test('Paste should not work unless an input is focused', async ({ browserName !== 'firefox', "This bug is really Firefox-only, which we don't run in CI." ) + const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/', { waitUntil: 'domcontentloaded' }) + await u.waitForAuthSkipAppStart() await page .getByRole('button', { name: 'Start Sketch' }) .waitFor({ state: 'visible' }) diff --git a/e2e/playwright/snapshot-tests.spec.ts b/e2e/playwright/snapshot-tests.spec.ts index 5ef2ef4e7..f056dc68e 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -91,8 +91,9 @@ const part001 = startSketchOn('-XZ') ) }) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() + await u.openDebugPanel() await u.expectCmdLog('[data-message-type="execution-done"]') await u.waitForCmdReceive('extrude') @@ -330,7 +331,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() // wait for execution done @@ -386,8 +387,8 @@ test('Draft segments should look right', async ({ page, context }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio - await page.goto('/') await u.waitForAuthSkipAppStart() + await u.openDebugPanel() await expect( @@ -443,7 +444,7 @@ test('Draft rectangles should look right', async ({ page, context }) => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -490,7 +491,7 @@ test.describe('Client side scene scale should match engine scale', () => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -589,7 +590,7 @@ test.describe('Client side scene scale should match engine scale', () => { const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) const PUR = 400 / 37.5 //pixeltoUnitRatio - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -689,7 +690,7 @@ const part002 = startSketchOn(part001, 'seg01') }, KCL_DEFAULT_LENGTH) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -739,7 +740,7 @@ test('Zoom to fit on load - solid 2d', async ({ page, context }) => { }, KCL_DEFAULT_LENGTH) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() @@ -776,7 +777,7 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => { }, KCL_DEFAULT_LENGTH) await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/') + await u.waitForAuthSkipAppStart() await u.openDebugPanel() diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png index 1aa071d18..f92f26be9 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png index b5c2ca7cc..2e8e23f77 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png index cec5c00c3..082761b9d 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png index 360d58bb4..4f814e977 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png index e6f4d6f8c..067c352e8 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png index a29bbb1fc..9dbdea0f3 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png index 39f8c6c43..e846138f3 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png index 5297f525f..72ab43755 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png index be0796636..f896102b1 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png index c9068226d..f72ed7668 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png index a4124f77b..15fa899af 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png index 56bf354c4..2f7352a16 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png index 8bd41189d..70ba597e9 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index c72585288..8ec590396 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -207,6 +207,23 @@ export const getMovementUtils = (opts: any) => { return { toSU, click00r } } +async function waitForAuthAndLsp(page: Page) { + const waitForLspPromise = page.waitForEvent('console', async (message) => { + // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') + // but that doesn't seem to make it to the console for macos/safari :( + if (message.text().includes('start kcl lsp')) { + await new Promise((resolve) => setTimeout(resolve, 200)) + return true + } + return false + }) + + await page.goto('/') + await waitForPageLoad(page) + + return waitForLspPromise +} + export async function getUtils(page: Page) { // Chrome devtools protocol session only works in Chromium const browserType = page.context().browser()?.browserType().name() @@ -214,7 +231,7 @@ export async function getUtils(page: Page) { browserType !== 'chromium' ? null : await page.context().newCDPSession(page) return { - waitForAuthSkipAppStart: () => waitForPageLoad(page), + waitForAuthSkipAppStart: () => waitForAuthAndLsp(page), removeCurrentCode: () => removeCurrentCode(page), sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd), updateCamPosition: async (xyz: [number, number, number]) => { diff --git a/examples/addition.cado b/examples/addition.cado deleted file mode 100644 index 3a390cd68..000000000 --- a/examples/addition.cado +++ /dev/null @@ -1,3 +0,0 @@ -// comment - -const hi = 5 + 4 diff --git a/package.json b/package.json index 69ff7e6ff..71f80073b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,12 @@ "version": "0.22.6", "private": true, "dependencies": { - "@codemirror/autocomplete": "^6.16.0", + "@codemirror/autocomplete": "^6.16.3", + "@codemirror/commands": "^6.6.0", + "@codemirror/language": "^6.10.2", + "@codemirror/lint": "^6.8.1", + "@codemirror/search": "^6.5.6", + "@codemirror/state": "^6.4.1", "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", @@ -11,8 +16,6 @@ "@headlessui/react": "^1.7.19", "@headlessui/tailwindcss": "^0.2.0", "@kittycad/lib": "^0.0.67", - "@lezer/javascript": "^1.4.9", - "@open-rpc/client-js": "^1.8.1", "@react-hook/resize-observer": "^2.0.1", "@replit/codemirror-interact": "^6.3.1", "@tauri-apps/api": "2.0.0-beta.12", @@ -23,28 +26,17 @@ "@tauri-apps/plugin-process": "^2.0.0-beta.2", "@tauri-apps/plugin-shell": "^2.0.0-beta.2", "@tauri-apps/plugin-updater": "^2.0.0-beta.3", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^15.0.2", - "@testing-library/user-event": "^14.5.2", "@ts-stack/markdown": "^1.5.0", "@tweenjs/tween.js": "^23.1.1", - "@types/node": "^18.19.31", - "@types/react": "^18.3.2", - "@types/react-dom": "^18.2.25", "@uiw/react-codemirror": "^4.21.25", "@xstate/inspect": "^0.8.0", "@xstate/react": "^3.2.2", - "crypto-js": "^4.2.0", - "debounce-promise": "^3.1.2", + "codemirror": "^6.0.1", "decamelize": "^6.0.0", - "eslint-plugin-suggest-no-throw": "^1.0.0", - "formik": "^2.4.6", "fuse.js": "^7.0.0", - "html2canvas-pro": "^1.4.3", - "http-server": "^14.1.1", + "html2canvas-pro": "^1.5.0", "json-rpc-2.0": "^1.6.0", "jszip": "^3.10.1", - "node-fetch": "^3.3.2", "re-resizable": "^6.9.11", "react": "^18.3.1", "react-dom": "^18.2.0", @@ -55,18 +47,14 @@ "react-modal-promise": "^1.0.2", "react-router-dom": "^6.23.1", "sketch-helpers": "^0.0.4", - "swr": "^2.2.5", "three": "^0.164.1", - "ts-node": "^10.9.2", "typescript": "^5.4.5", "ua-parser-js": "^1.0.37", "uuid": "^9.0.1", - "vitest": "^1.6.0", "vscode-jsonrpc": "^8.2.1", "vscode-languageserver-protocol": "^3.17.5", - "wasm-pack": "^0.12.1", + "vscode-uri": "^3.0.8", "web-vitals": "^3.5.2", - "ws": "^8.17.0", "xstate": "^4.38.2", "zustand": "^4.5.2" }, @@ -123,11 +111,14 @@ "@iarna/toml": "^2.2.5", "@playwright/test": "^1.44.1", "@tauri-apps/cli": "^2.0.0-beta.13", - "@types/crypto-js": "^4.2.2", - "@types/debounce-promise": "^3.1.9", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^15.0.2", "@types/mocha": "^10.0.6", + "@types/node": "^18.19.31", "@types/pixelmatch": "^5.2.6", "@types/pngjs": "^6.0.4", + "@types/react": "^18.3.2", + "@types/react-dom": "^18.2.25", "@types/react-modal": "^3.16.3", "@types/three": "^0.163.0", "@types/ua-parser-js": "^0.7.39", @@ -147,8 +138,11 @@ "eslint": "^8.57.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-css-modules": "^2.12.0", + "eslint-plugin-suggest-no-throw": "^1.0.0", "happy-dom": "^14.3.10", + "http-server": "^14.1.1", "husky": "^9.0.11", + "node-fetch": "^3.3.2", "pixelmatch": "^5.3.0", "pngjs": "^7.0.0", "postcss": "^8.4.31", @@ -160,8 +154,11 @@ "vite-plugin-eslint": "^1.8.1", "vite-plugin-package-version": "^1.1.0", "vite-tsconfig-paths": "^4.3.2", + "vitest": "^1.6.0", "vitest-webgl-canvas-mock": "^1.1.0", "wait-on": "^7.2.0", + "wasm-pack": "^0.12.1", + "ws": "^8.17.0", "yarn": "^1.22.22" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1193c6908..1e8cd2efc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1212,7 +1212,7 @@ dependencies = [ [[package]] name = "derive-docs" -version = "0.1.18" +version = "0.1.19" dependencies = [ "Inflector", "convert_case 0.6.0", @@ -2576,7 +2576,7 @@ dependencies = [ [[package]] name = "kcl-lib" -version = "0.1.67" +version = "0.1.68" dependencies = [ "anyhow", "approx", @@ -6029,9 +6029,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" -version = "9.0.0" +version = "9.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357" +checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c" dependencies = [ "chrono", "serde_json", @@ -6043,9 +6043,9 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "9.0.0" +version = "9.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577" +checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130" dependencies = [ "proc-macro2", "quote", diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index 89aaa329d..ff7533f49 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -568,6 +568,7 @@ export class SceneEntities { if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) sceneInfra.resetMouseListeners() + const { truncatedAst, programMemoryOverride, sketchGroup } = await this.setupSketch({ sketchPathToNode, diff --git a/src/components/LspProvider.tsx b/src/components/LspProvider.tsx index f7d5bce53..f3135c40e 100644 --- a/src/components/LspProvider.tsx +++ b/src/components/LspProvider.tsx @@ -10,7 +10,7 @@ import React, { import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' import Client from '../editor/plugins/lsp/client' import { TEST, VITE_KC_API_BASE_URL } from 'env' -import kclLanguage from 'editor/plugins/lsp/kcl/language' +import KclLanguageSupport from 'editor/plugins/lsp/kcl/language' import { copilotPlugin } from 'editor/plugins/lsp/copilot' import { useStore } from 'useStore' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' @@ -31,6 +31,8 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants' import { useNetworkContext } from 'hooks/useNetworkContext' import { NetworkHealthState } from 'hooks/useNetworkStatus' import { err } from 'lib/trap' +import { isTauri } from 'lib/isTauri' +import { codeManager } from 'lib/singletons' function getWorkspaceFolders(): LSP.WorkspaceFolder[] { return [] @@ -128,17 +130,31 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { const fromServer: FromServer | Error = FromServer.create() if (err(fromServer)) return { lspClient: null } - const client = new Client(fromServer, intoServer) - - setIsLspReady(true) + const client = new Client(fromServer, intoServer, () => { + setIsLspReady(true) + }) const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl }) + return { lspClient } }, [ // We need a token for authenticating the server. token, ]) + useMemo(() => { + if (!isTauri() && isKclLspServerReady && kclLspClient && codeManager.code) { + kclLspClient.textDocumentDidOpen({ + textDocument: { + uri: `file:///${PROJECT_ENTRYPOINT}`, + languageId: 'kcl', + version: 1, + text: codeManager.code, + }, + }) + } + }, [kclLspClient, isKclLspServerReady]) + // Here we initialize the plugin which will start the client. // Now that we have multi-file support the name of the file is a dep of // this use memo, as well as the directory structure, which I think is @@ -148,7 +164,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { let plugin = null if (isKclLspServerReady && !TEST && kclLspClient) { // Set up the lsp plugin. - const lsp = kclLanguage({ + const lsp = new KclLanguageSupport({ documentUri: `file:///${PROJECT_ENTRYPOINT}`, workspaceFolders: getWorkspaceFolders(), client: kclLspClient, @@ -205,9 +221,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => { const fromServer: FromServer | Error = FromServer.create() if (err(fromServer)) return { lspClient: null } - const client = new Client(fromServer, intoServer) - - setIsCopilotReady(true) + const client = new Client(fromServer, intoServer, () => { + setIsCopilotReady(true) + }) const lspClient = new LanguageServerClient({ client, diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 637f0fd70..740c102cc 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -71,7 +71,7 @@ import { TEST } from 'env' import { exportFromEngine } from 'lib/exportFromEngine' import { Models } from '@kittycad/lib/dist/types/src' import toast from 'react-hot-toast' -import { EditorSelection } from '@uiw/react-codemirror' +import { EditorSelection, Transaction } from '@uiw/react-codemirror' import { CoreDumpManager } from 'lib/coredump' import { useSearchParams } from 'react-router-dom' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' @@ -80,6 +80,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper' import { uuidv4 } from 'lib/utils' import { err, trap } from 'lib/trap' import { useCommandsContext } from 'hooks/useCommandsContext' +import { modelingMachineEvent } from 'editor/manager' type MachineContext = { state: StateFrom @@ -281,11 +282,15 @@ export const ModelingMachineProvider = ({ const dispatchSelection = (selection?: EditorSelection) => { if (!selection) return // TODO less of hack for the below please if (!editorManager.editorView) return - editorManager.lastSelectionEvent = Date.now() setTimeout(() => { - if (editorManager.editorView) { - editorManager.editorView.dispatch({ selection }) - } + if (!editorManager.editorView) return + editorManager.editorView.dispatch({ + selection, + annotations: [ + modelingMachineEvent, + Transaction.addToHistory.of(false), + ], + }) }) } let selections: Selections = { @@ -328,11 +333,6 @@ export const ModelingMachineProvider = ({ ) updateSceneObjectColors() - // side effect to stop code mirror from updating the same selections again - editorManager.lastSelection = selections.codeBasedSelections - .map(({ range }) => `${range[1]}->${range[1]}`) - .join('&') - return { selectionRanges: selections, } diff --git a/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx b/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx index 14438b1d6..b5ab2399d 100644 --- a/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx +++ b/src/components/ModelingSidebar/ModelingPanes/KclEditorPane.tsx @@ -84,6 +84,10 @@ export const KclEditorPane = () => { const textWrapping = context.textEditor.textWrapping const cursorBlinking = context.textEditor.blinkingCursor + // DO NOT ADD THE CODEMIRROR HOTKEYS HERE TO THE DEPENDENCY ARRAY + // It reloads the editor every time we do _anything_ in the editor + // I have no idea why. + // Instead, hot load hotkeys via code mirror native. const codeMirrorHotkeys = codeManager.getCodemirrorHotkeys() const editorExtensions = useMemo(() => { @@ -134,7 +138,6 @@ export const KclEditorPane = () => { highlightSelectionMatches(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), rectangularSelection(), - drawSelection(), dropCursor(), interact({ rules: [ @@ -173,13 +176,7 @@ export const KclEditorPane = () => { } return extensions - }, [ - kclLSP, - copilotLSP, - textWrapping.current, - cursorBlinking.current, - codeMirrorHotkeys, - ]) + }, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current]) const initialCode = useRef(codeManager.code) @@ -192,9 +189,9 @@ export const KclEditorPane = () => { value={initialCode.current} extensions={editorExtensions} theme={theme} - onCreateEditor={(_editorView) => + onCreateEditor={(_editorView) => { editorManager.setEditorView(_editorView) - } + }} indentWithTab={false} basicSetup={false} /> diff --git a/src/editor/manager.ts b/src/editor/manager.ts index a858ffac7..400cba309 100644 --- a/src/editor/manager.ts +++ b/src/editor/manager.ts @@ -1,13 +1,25 @@ -import { hasNextSnippetField } from '@codemirror/autocomplete' import { EditorView, ViewUpdate } from '@codemirror/view' -import { EditorSelection, SelectionRange } from '@codemirror/state' -import { engineCommandManager, sceneInfra } from 'lib/singletons' +import { EditorSelection, Annotation, Transaction } from '@codemirror/state' +import { engineCommandManager } from 'lib/singletons' import { ModelingMachineEvent } from 'machines/modelingMachine' import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections' import { undo, redo } from '@codemirror/commands' import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { addLineHighlight } from './highlightextension' -import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint' +import { + forEachDiagnostic, + Diagnostic, + setDiagnosticsEffect, +} from '@codemirror/lint' + +const updateOutsideEditorAnnotation = Annotation.define() +export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(null) + +const modelingMachineAnnotation = Annotation.define() +export const modelingMachineEvent = modelingMachineAnnotation.of(null) + +const setDiagnosticsAnnotation = Annotation.define() +export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(null) function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean { return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message @@ -22,8 +34,6 @@ export default class EditorManager { codeBasedSelections: [], } - private _lastSelectionEvent: number | null = null - lastSelection: string = '' private _lastEvent: { event: string; time: number } | null = null private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {} @@ -57,10 +67,6 @@ export default class EditorManager { this._selectionRanges = selectionRanges } - set lastSelectionEvent(time: number) { - this._lastSelectionEvent = time - } - set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) { this._modelingSend = send } @@ -83,32 +89,38 @@ export default class EditorManager { setHighlightRange(selection: Selection['range']): void { this._highlightRange = selection - const editorView = this.editorView const safeEnd = Math.min( selection[1], - editorView?.state.doc.length || selection[1] + this._editorView?.state.doc.length || selection[1] ) - if (editorView) { - editorView.dispatch({ + if (this._editorView) { + this._editorView.dispatch({ effects: addLineHighlight.of([selection[0], safeEnd]), + annotations: [ + updateOutsideEditorEvent, + Transaction.addToHistory.of(false), + ], }) } } clearDiagnostics(): void { - if (!this.editorView) return - this.editorView.dispatch(setDiagnostics(this.editorView.state, [])) + this.setDiagnostics([]) } setDiagnostics(diagnostics: Diagnostic[]): void { - if (!this.editorView) return - this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics)) + if (!this._editorView) return + + this._editorView.dispatch({ + effects: [setDiagnosticsEffect.of(diagnostics)], + annotations: [setDiagnosticsEvent, Transaction.addToHistory.of(false)], + }) } addDiagnostics(diagnostics: Diagnostic[]): void { - if (!this.editorView) return + if (!this._editorView) return - forEachDiagnostic(this.editorView.state, function (diag) { + forEachDiagnostic(this._editorView.state, function (diag) { diagnostics.push(diag) }) @@ -122,9 +134,7 @@ export default class EditorManager { uniqueDiagnostics.add(diagnostic) }) - this.editorView.dispatch( - setDiagnostics(this.editorView.state, [...uniqueDiagnostics]) - ) + this.setDiagnostics([...uniqueDiagnostics]) } undo() { @@ -174,50 +184,33 @@ export default class EditorManager { ].range[1] ) ) - if (!this.editorView) { + + if (!this._editorView) { return } - this.editorView.dispatch({ + + this._editorView.dispatch({ selection: EditorSelection.create(codeBasedSelections, 1), + annotations: [ + updateOutsideEditorEvent, + Transaction.addToHistory.of(false), + ], }) } + // We will ONLY get here if the user called a select event. + // This is handled by the code mirror kcl plugin. + // If you call this function from somewhere else, you best know wtf you are + // doing. (jess) handleOnViewUpdate(viewUpdate: ViewUpdate): void { - // If we are just fucking around in a snippet, return early and don't - // trigger stuff below that might cause the component to re-render. - // Otherwise we will not be able to tab thru the snippet portions. - // We explicitly dont check HasPrevSnippetField because we always add - // a ${} to the end of the function so that's fine. - if (hasNextSnippetField(viewUpdate.view.state)) { - return - } - - if (this.editorView === null) { + if (!this._editorView) { this.setEditorView(viewUpdate.view) } - const selString = stringifyRanges( - viewUpdate?.state?.selection?.ranges || [] - ) - if (selString === this.lastSelection) { - // onUpdate is noisy and is fired a lot by extensions - // since we're only interested in selections changes we can ignore most of these. + const ranges = viewUpdate?.state?.selection?.ranges || [] + if (ranges.length === 0) { return } - // note this is also set from the "Set selection" action to stop code mirror from updating selections right after - // selections are made from the scene - this.lastSelection = selString - - if ( - this._lastSelectionEvent && - Date.now() - this._lastSelectionEvent < 150 - ) { - return // update triggered by scene selection - } - - if (sceneInfra.selected) { - return // mid drag - } const ignoreEvents: ModelingMachineEvent['type'][] = [ 'Equip Line tool', @@ -266,7 +259,3 @@ export default class EditorManager { ) } } - -function stringifyRanges(ranges: readonly SelectionRange[]): string { - return ranges.map(({ to, from }) => `${to}->${from}`).join('&') -} diff --git a/src/editor/plugins/lsp/client.ts b/src/editor/plugins/lsp/client.ts index 54b4bbaf1..f11342c97 100644 --- a/src/editor/plugins/lsp/client.ts +++ b/src/editor/plugins/lsp/client.ts @@ -67,8 +67,13 @@ export default class Client extends jsrpc.JSONRPCServerAndClient { #fromServer: FromServer private serverCapabilities: LSP.ServerCapabilities = {} private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null + private initializedCallback: () => void - constructor(fromServer: FromServer, intoServer: IntoServer) { + constructor( + fromServer: FromServer, + intoServer: IntoServer, + initializedCallback: () => void + ) { super( new jsrpc.JSONRPCServer(), new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => { @@ -82,6 +87,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient { }) ) this.#fromServer = fromServer + this.initializedCallback = initializedCallback } async start(): Promise { @@ -163,6 +169,8 @@ export default class Client extends jsrpc.JSONRPCServerAndClient { // notify "initialized": client --> server this.notify(LSP.InitializedNotification.type.method, {}) + this.initializedCallback() + await Promise.all( this.afterInitializedHooks.map((f: () => Promise) => f()) ) diff --git a/src/editor/plugins/lsp/copilot/index.ts b/src/editor/plugins/lsp/copilot/index.ts index 79b258317..cea90bda1 100644 --- a/src/editor/plugins/lsp/copilot/index.ts +++ b/src/editor/plugins/lsp/copilot/index.ts @@ -4,6 +4,7 @@ import { Decoration, DecorationSet, EditorView, + PluginValue, ViewPlugin, ViewUpdate, } from '@codemirror/view' @@ -11,7 +12,6 @@ import { Annotation, EditorState, Extension, - Prec, StateEffect, StateField, Transaction, @@ -19,15 +19,30 @@ import { import { completionStatus } from '@codemirror/autocomplete' import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util' import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp' +import { deferExecution } from 'lib/utils' import { LanguageServerPlugin, - documentUri, + TransactionAnnotation, + docPathFacet, languageId, + updateInfo, workspaceFolders, + RelevantUpdate, } from 'editor/plugins/lsp/plugin' +const copilotPluginAnnotation = Annotation.define() +export const copilotPluginEvent = copilotPluginAnnotation.of(null) + +// Effects to tell StateEffect what to do with GhostText +const addSuggestion = StateEffect.define() +const acceptSuggestion = StateEffect.define() +const clearSuggestion = StateEffect.define() +const typeFirst = StateEffect.define() + const ghostMark = Decoration.mark({ class: 'cm-ghostText' }) +const changesDelay = 600 + interface Suggestion { text: string displayText: string @@ -38,15 +53,10 @@ interface Suggestion { uuid: string } -// Effects to tell StateEffect what to do with GhostText -const addSuggestion = StateEffect.define() -const acceptSuggestion = StateEffect.define() -const clearSuggestion = StateEffect.define() -const typeFirst = StateEffect.define() - interface CompletionState { ghostText: GhostText | null } + interface GhostText { text: string displayText: string @@ -65,6 +75,16 @@ export const completionDecoration = StateField.define({ return { ghostText: null } }, update(state: CompletionState, transaction: Transaction) { + // We only care about events from this plugin. + if (transaction.annotation(copilotPluginEvent.type) === undefined) { + return state + } + + // We only care about transactions with effects. + if (!transaction.effects) { + return state + } + for (const effect of transaction.effects) { if (effect.is(addSuggestion)) { // When adding a suggestion, we set th ghostText @@ -160,126 +180,376 @@ export const completionDecoration = StateField.define({ ), }) -const copilotEvent = Annotation.define() +export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => { + const infos = updateInfo(update) -/**************************************************************************** - ************************* COMMANDS ****************************************** - *****************************************************************************/ - -const acceptSuggestionCommand = ( - copilotClient: LanguageServerClient, - view: EditorView -) => { - // We delete the ghost text and insert the suggestion. - // We also set the cursor to the end of the suggestion. - const ghostText = view.state.field(completionDecoration)!.ghostText - if (!ghostText) { - return false + // Make sure we are not in a snippet + if (infos.some((info) => info.inSnippet)) { + return { + overall: false, + userSelect: false, + time: null, + } } - const ghostTextStart = ghostText.displayPos - const ghostTextEnd = ghostText.endGhostText - - const actualTextStart = ghostText.startPos - const actualTextEnd = ghostText.endPos - - const replacementEnd = ghostText.endReplacement - - const suggestion = ghostText.text - - view.dispatch({ - changes: { - from: ghostTextStart, - to: ghostTextEnd, - insert: '', - }, - // selection: {anchor: actualTextEnd}, - effects: acceptSuggestion.of(null), - annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], - }) - - const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart) - - view.dispatch({ - changes: { - from: actualTextStart, - to: tmpTextEnd, - insert: suggestion, - }, - selection: { anchor: actualTextEnd }, - annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)], - }) - - copilotClient.accept(ghostText.uuid) - return true -} -export const rejectSuggestionCommand = ( - copilotClient: LanguageServerClient, - view: EditorView -) => { - // We delete the suggestion, then carry through with the original keypress - const ghostText = view.state.field(completionDecoration)!.ghostText - if (!ghostText) { - return false + return { + overall: infos.some( + (info) => + update.focusChanged || + info.annotations.includes(TransactionAnnotation.UserSelect) || + info.annotations.includes(TransactionAnnotation.UserInput) || + info.annotations.includes(TransactionAnnotation.UserDelete) || + info.annotations.includes(TransactionAnnotation.UserUndo) || + info.annotations.includes(TransactionAnnotation.UserRedo) || + info.annotations.includes(TransactionAnnotation.UserMove) || + info.annotations.includes(TransactionAnnotation.Copoilot) + ), + userSelect: infos.some((info) => + info.annotations.includes(TransactionAnnotation.UserSelect) + ), + time: infos.length ? infos[0].time : null, } - - const ghostTextStart = ghostText.displayPos - const ghostTextEnd = ghostText.endGhostText - - view.dispatch({ - changes: { - from: ghostTextStart, - to: ghostTextEnd, - insert: '', - }, - effects: clearSuggestion.of(null), - annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], - }) - - copilotClient.reject() - return false } -const sameKeyCommand = ( - copilotClient: LanguageServerClient, - view: EditorView, - key: string -) => { - // When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress - const ghostText = view.state.field(completionDecoration)!.ghostText - if (!ghostText) { - return false - } - const ghostTextStart = ghostText.displayPos - const indent = view.state.facet(indentUnit) +// A view plugin that requests completions from the server after a delay +export class CompletionRequester implements PluginValue { + private client: LanguageServerClient + private lastPos: number = 0 + private viewUpdate: ViewUpdate | null = null - if (key === 'Tab' && ghostText.displayText.startsWith(indent)) { - view.dispatch({ - selection: { anchor: ghostTextStart + indent.length }, - effects: typeFirst.of(indent.length), - annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], - }) - return true - } else if (key === 'Tab') { - return acceptSuggestionCommand(copilotClient, view) - } else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) { - return rejectSuggestionCommand(copilotClient, view) - } else if (ghostText.displayText.length === 1) { - return acceptSuggestionCommand(copilotClient, view) - } else { - // Use this to delete the first letter of the suggestion - view.dispatch({ - selection: { anchor: ghostTextStart + 1 }, - effects: typeFirst.of(1), - annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], + private _deffererCodeUpdate = deferExecution(() => { + if (this.viewUpdate === null) { + return + } + + this.requestCompletions() + }, changesDelay) + + private _deffererUserSelect = deferExecution(() => { + if (this.viewUpdate === null) { + return + } + + this.rejectSuggestionCommand() + }, changesDelay) + + constructor(client: LanguageServerClient) { + this.client = client + } + + update(viewUpdate: ViewUpdate) { + this.viewUpdate = viewUpdate + + const isRelevant = relevantUpdate(viewUpdate) + if (!isRelevant.overall) { + return + } + + // If we have a user select event, we want to clear the ghost text. + if (isRelevant.userSelect) { + this._deffererUserSelect(true) + return + } + + if (viewUpdate.focusChanged) { + this.rejectSuggestionCommand() + return + } + + if (!viewUpdate.docChanged) { + return + } + + this.lastPos = this.viewUpdate.state.selection.main.head + this._deffererCodeUpdate(true) + } + + ghostText(): GhostText | null { + if (!this.viewUpdate) { + return null + } + + return ( + this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null + ) + } + + containsGhostText(): boolean { + return this.ghostText() !== null + } + + autocompleting(): boolean { + if (!this.viewUpdate) { + return false + } + + return completionStatus(this.viewUpdate.state) === 'active' + } + + notFocused(): boolean { + if (!this.viewUpdate) { + return true + } + + return !this.viewUpdate.view.hasFocus + } + + async requestCompletions(): Promise { + if ( + this.viewUpdate === null || + this.containsGhostText() || + this.autocompleting() || + this.notFocused() || + !this.viewUpdate.docChanged + ) { + return + } + + const pos = this.viewUpdate.state.selection.main.head + + // Check if the position has changed + if (pos !== this.lastPos) { + return + } + + // Get the current position and source + const state = this.viewUpdate.state + const dUri = state.facet(docPathFacet) + + // Request completion from the server + const completionResult = await this.client.getCompletion({ + doc: { + source: state.doc.toString(), + tabSize: state.facet(EditorState.tabSize), + indentSize: 1, + insertSpaces: true, + path: dUri.split('/').pop()!, + uri: dUri, + relativePath: dUri.replace('file://', ''), + languageId: state.facet(languageId), + position: offsetToPos(state.doc, pos), + }, }) + if (completionResult.completions.length === 0) { + return + } + + let { + text, + displayText, + range: { start }, + position, + uuid, + } = completionResult.completions[0] + + if (text.length === 0 || displayText.length === 0) { + return + } + + const startPos = posToOffset(state.doc, { + line: start.line, + character: start.character, + }) + + if (startPos === undefined) { + return + } + + const endGhostOffset = posToOffset(state.doc, { + line: position.line, + character: position.character, + }) + if (endGhostOffset === undefined) { + return + } + const endGhostPos = endGhostOffset + displayText.length + // EndPos is the position that marks the complete end + // of what is to be replaced when we accept a completion + // result + const endPos = startPos + text.length + + // Check if they changed position. + if (pos !== this.lastPos) { + return + } + + // Make sure we are not currently completing. + if (this.autocompleting() || this.notFocused()) { + return + } + + // Dispatch an effect to add the suggestion + // If the completion starts before the end of the line, check the end of the line with the end of the completion. + const line = this.viewUpdate.view.state.doc.lineAt(pos) + if (line.to !== pos) { + const ending = this.viewUpdate.view.state.doc.sliceString(pos, line.to) + if (displayText.endsWith(ending)) { + displayText = displayText.slice(0, displayText.length - ending.length) + } else if (displayText.includes(ending)) { + // Remove the ending + this.viewUpdate.view.dispatch({ + changes: { + from: pos, + to: line.to, + insert: '', + }, + selection: { anchor: pos }, + effects: typeFirst.of(ending.length), + annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], + }) + } + } + + this.viewUpdate.view.dispatch({ + changes: { + from: pos, + to: pos, + insert: displayText, + }, + effects: [ + addSuggestion.of({ + displayText, + endReplacement: endGhostPos, + text, + cursorPos: pos, + startPos, + endPos, + uuid, + }), + ], + annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], + }) + + this.lastPos = pos + + return + } + + acceptSuggestionCommand(): boolean { + if (!this.viewUpdate) { + return false + } + + const ghostText = this.ghostText() + if (!ghostText) { + return false + } + + // We delete the ghost text and insert the suggestion. + // We also set the cursor to the end of the suggestion. + const ghostTextStart = ghostText.displayPos + const ghostTextEnd = ghostText.endGhostText + + const actualTextStart = ghostText.startPos + const actualTextEnd = ghostText.endPos + + const replacementEnd = ghostText.endReplacement + + const suggestion = ghostText.text + + this.viewUpdate.view.dispatch({ + changes: { + from: ghostTextStart, + to: ghostTextEnd, + insert: '', + }, + effects: acceptSuggestion.of(null), + annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], + }) + + const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart) + + this.viewUpdate.view.dispatch({ + changes: { + from: actualTextStart, + to: tmpTextEnd, + insert: suggestion, + }, + selection: { anchor: actualTextEnd }, + annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)], + }) + + this.client.accept(ghostText.uuid) return true } + + rejectSuggestionCommand(): boolean { + if (!this.viewUpdate) { + return false + } + + const ghostText = this.ghostText() + if (!ghostText) { + return false + } + + // We delete the suggestion, then carry through with the original keypress + const ghostTextStart = ghostText.displayPos + const ghostTextEnd = ghostText.endGhostText + + this.viewUpdate.view.dispatch({ + changes: { + from: ghostTextStart, + to: ghostTextEnd, + insert: '', + }, + effects: clearSuggestion.of(null), + annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], + }) + + this.client.reject() + return false + } + + sameKeyCommand(key: string) { + if (!this.viewUpdate) { + return false + } + + const ghostText = this.ghostText() + if (!ghostText) { + return false + } + + const tabKey = 'Tab' + + // When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress + const ghostTextStart = ghostText.displayPos + const indent = this.viewUpdate.view.state.facet(indentUnit) + + if (key === tabKey && ghostText.displayText.startsWith(indent)) { + this.viewUpdate.view.dispatch({ + selection: { anchor: ghostTextStart + indent.length }, + effects: typeFirst.of(indent.length), + annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], + }) + return true + } else if (key === tabKey) { + return this.acceptSuggestionCommand() + } else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) { + return this.rejectSuggestionCommand() + } else if (ghostText.displayText.length === 1) { + return this.acceptSuggestionCommand() + } else { + // Use this to delete the first letter of the suggestion + this.viewUpdate.view.dispatch({ + selection: { anchor: ghostTextStart + 1 }, + effects: typeFirst.of(1), + annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], + }) + + return true + } + } } -const completionPlugin = (copilotClient: LanguageServerClient) => - EditorView.domEventHandlers({ +export const copilotPlugin = (options: LanguageServerOptions): Extension => { + const completionPlugin = ViewPlugin.define((view) => { + return new CompletionRequester(options.client) + }) + + const domHandlers = EditorView.domEventHandlers({ keydown(event, view) { if ( event.key !== 'Shift' && @@ -287,204 +557,29 @@ const completionPlugin = (copilotClient: LanguageServerClient) => event.key !== 'Alt' && event.key !== 'Meta' ) { - return sameKeyCommand(copilotClient, view, event.key) + if (view.plugin === null) return false + + // Get the current plugin from the map. + const p = view.plugin(completionPlugin) + if (p === null) return false + + return p.sameKeyCommand(event.key) } else { return false } }, - mousedown(event, view) { - return rejectSuggestionCommand(copilotClient, view) - }, }) -const viewCompletionPlugin = (copilotClient: LanguageServerClient) => - EditorView.updateListener.of((update) => { - if (update.focusChanged) { - rejectSuggestionCommand(copilotClient, update.view) - } - }) -// A view plugin that requests completions from the server after a delay -const completionRequester = (client: LanguageServerClient) => { - let timeout: any = null - let lastPos = 0 - - const badUpdate = (update: ViewUpdate) => { - for (const tr of update.transactions) { - if (tr.annotation(copilotEvent) !== undefined) { - return true - } - } - return false - } - const containsGhostText = (update: ViewUpdate) => { - return update.state.field(completionDecoration).ghostText != null - } - const autocompleting = (update: ViewUpdate) => { - return completionStatus(update.state) === 'active' - } - const notFocused = (update: ViewUpdate) => { - return !update.view.hasFocus - } - - return EditorView.updateListener.of((update: ViewUpdate) => { - if ( - update.docChanged && - !update.transactions.some((tr) => - tr.effects.some((e) => e.is(acceptSuggestion) || e.is(clearSuggestion)) - ) - ) { - // Cancel the previous timeout - if (timeout) { - clearTimeout(timeout) - } - if ( - badUpdate(update) || - containsGhostText(update) || - autocompleting(update) || - notFocused(update) - ) { - return - } - - // Get the current position and source - const state = update.state - const pos = state.selection.main.head - const source = state.doc.toString() - - const dUri = state.facet(documentUri) - const path = dUri.split('/').pop()! - const relativePath = dUri.replace('file://', '') - - // Set a new timeout to request completion - timeout = setTimeout(async () => { - // Check if the position has changed - if (pos === lastPos) { - // Request completion from the server - try { - const completionResult = await client.getCompletion({ - doc: { - source, - tabSize: state.facet(EditorState.tabSize), - indentSize: 1, - insertSpaces: true, - path, - uri: dUri, - relativePath, - languageId: state.facet(languageId), - position: offsetToPos(state.doc, pos), - }, - }) - - if (completionResult.completions.length === 0) { - return - } - - let { - text, - displayText, - range: { start }, - position, - uuid, - } = completionResult.completions[0] - - const startPos = posToOffset(state.doc, { - line: start.line, - character: start.character, - })! - - const endGhostPos = - posToOffset(state.doc, { - line: position.line, - character: position.character, - })! + displayText.length - // EndPos is the position that marks the complete end - // of what is to be replaced when we accept a completion - // result - const endPos = startPos + text.length - - // Check if the position is still the same - if ( - pos === lastPos && - completionStatus(update.view.state) !== 'active' && - update.view.hasFocus - ) { - // Dispatch an effect to add the suggestion - // If the completion starts before the end of the line, check the end of the line with the end of the completion - const line = update.view.state.doc.lineAt(pos) - if (line.to !== pos) { - const ending = update.view.state.doc.sliceString(pos, line.to) - if (displayText.endsWith(ending)) { - displayText = displayText.slice( - 0, - displayText.length - ending.length - ) - } else if (displayText.includes(ending)) { - // Remove the ending - update.view.dispatch({ - changes: { - from: pos, - to: line.to, - insert: '', - }, - selection: { anchor: pos }, - effects: typeFirst.of(ending.length), - annotations: [ - copilotEvent.of(null), - Transaction.addToHistory.of(false), - ], - }) - } - } - update.view.dispatch({ - changes: { - from: pos, - to: pos, - insert: displayText, - }, - effects: [ - addSuggestion.of({ - displayText, - endReplacement: endGhostPos, - text, - cursorPos: pos, - startPos, - endPos, - uuid, - }), - ], - annotations: [ - copilotEvent.of(null), - Transaction.addToHistory.of(false), - ], - }) - } - } catch (error) { - console.warn('copilot completion failed', error) - // Javascript wait for 500ms for some reason is necessary here. - // TODO - FIGURE OUT WHY THIS RESOLVES THE BUG - - await new Promise((resolve) => setTimeout(resolve, 300)) - } - } - }, 150) - // Update the last position - lastPos = pos - } - }) -} - -export const copilotPlugin = (options: LanguageServerOptions): Extension => { return [ - documentUri.of(options.documentUri), + docPathFacet.of(options.documentUri), languageId.of('kcl'), workspaceFolders.of(options.workspaceFolders), ViewPlugin.define( (view) => new LanguageServerPlugin(options.client, view, options.allowHTMLContent) ), + completionPlugin, + domHandlers, completionDecoration, - Prec.highest(completionPlugin(options.client)), - Prec.highest(viewCompletionPlugin(options.client)), - completionRequester(options.client), ] } diff --git a/src/editor/plugins/lsp/index.ts b/src/editor/plugins/lsp/index.ts index 5d3ea372c..2fcb292a8 100644 --- a/src/editor/plugins/lsp/index.ts +++ b/src/editor/plugins/lsp/index.ts @@ -1,6 +1,5 @@ import type * as LSP from 'vscode-languageserver-protocol' import Client from './client' -import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens' import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin' import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams' import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse' @@ -68,7 +67,7 @@ export interface LanguageServerOptions { export class LanguageServerClient { private client: Client - readonly name: string + readonly name: LspWorker public ready: boolean @@ -76,8 +75,6 @@ export class LanguageServerClient { public initializePromise: Promise - private isUpdatingSemanticTokens: boolean = false - private semanticTokens: SemanticToken[] = [] private queuedUids: string[] = [] constructor(options: LanguageServerClientOptions) { @@ -111,19 +108,10 @@ export class LanguageServerClient { textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { this.notify('textDocument/didOpen', params) - - // Update the facet of the plugins to the correct value. - for (const plugin of this.plugins) { - plugin.documentUri = params.textDocument.uri - plugin.languageId = params.textDocument.languageId - } - - this.updateSemanticTokens(params.textDocument.uri) } textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) { this.notify('textDocument/didChange', params) - this.updateSemanticTokens(params.textDocument.uri) } textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) { @@ -134,18 +122,9 @@ export class LanguageServerClient { added: LSP.WorkspaceFolder[], removed: LSP.WorkspaceFolder[] ) { - // Add all the current workspace folders in the plugin to removed. - for (const plugin of this.plugins) { - removed.push(...plugin.workspaceFolders) - } this.notify('workspace/didChangeWorkspaceFolders', { event: { added, removed }, }) - - // Add all the new workspace folders to the plugins. - for (const plugin of this.plugins) { - plugin.workspaceFolders = added - } } workspaceDidCreateFiles(params: LSP.CreateFilesParams) { @@ -160,33 +139,13 @@ export class LanguageServerClient { this.notify('workspace/didDeleteFiles', params) } - async updateSemanticTokens(uri: string) { + async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) { const serverCapabilities = this.getServerCapabilities() if (!serverCapabilities.semanticTokensProvider) { return } - // Make sure we can only run, if we aren't already running. - if (!this.isUpdatingSemanticTokens) { - this.isUpdatingSemanticTokens = true - - const result = await this.request('textDocument/semanticTokens/full', { - textDocument: { - uri, - }, - }) - - this.semanticTokens = await deserializeTokens( - result.data, - this.getServerCapabilities().semanticTokensProvider - ) - - this.isUpdatingSemanticTokens = false - } - } - - getSemanticTokens(): SemanticToken[] { - return this.semanticTokens + return this.request('textDocument/semanticTokens/full', params) } async textDocumentHover(params: LSP.HoverParams) { diff --git a/src/editor/plugins/lsp/kcl/index.ts b/src/editor/plugins/lsp/kcl/index.ts index 439f22660..e1a7aff83 100644 --- a/src/editor/plugins/lsp/kcl/index.ts +++ b/src/editor/plugins/lsp/kcl/index.ts @@ -1,4 +1,14 @@ -import { autocompletion } from '@codemirror/autocomplete' +import { + acceptCompletion, + autocompletion, + clearSnippet, + closeCompletion, + hasNextSnippetField, + moveCompletionSelection, + nextSnippetField, + prevSnippetField, + startCompletion, +} from '@codemirror/autocomplete' import { Extension, EditorState, Prec } from '@codemirror/state' import { ViewPlugin, @@ -7,6 +17,8 @@ import { keymap, KeyBinding, tooltips, + PluginValue, + ViewUpdate, } from '@codemirror/view' import { CompletionTriggerKind } from 'vscode-languageserver-protocol' import { offsetToPos } from 'editor/plugins/lsp/util' @@ -14,11 +26,18 @@ import { LanguageServerOptions } from 'editor/plugins/lsp' import { syntaxTree, indentService, foldService } from '@codemirror/language' import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint' import { + docPathFacet, LanguageServerPlugin, - documentUri, languageId, workspaceFolders, + updateInfo, + RelevantUpdate, + TransactionAnnotation, } from 'editor/plugins/lsp/plugin' +import { deferExecution } from 'lib/utils' +import { codeManager, editorManager, kclManager } from 'lib/singletons' + +const changesDelay = 600 export const kclIndentService = () => { // Match the indentation of the previous line (if present). @@ -39,6 +58,81 @@ export const kclIndentService = () => { }) } +export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => { + const infos = updateInfo(update) + // Make sure we are not in a snippet + if (infos.some((info) => info.inSnippet)) { + return { + overall: false, + userSelect: false, + time: null, + } + } + return { + overall: infos.some( + (info) => + info.annotations.includes(TransactionAnnotation.UserSelect) || + info.annotations.includes(TransactionAnnotation.UserInput) || + info.annotations.includes(TransactionAnnotation.UserDelete) || + info.annotations.includes(TransactionAnnotation.UserUndo) || + info.annotations.includes(TransactionAnnotation.UserRedo) || + info.annotations.includes(TransactionAnnotation.UserMove) + ), + userSelect: infos.some((info) => + info.annotations.includes(TransactionAnnotation.UserSelect) + ), + time: infos.length ? infos[0].time : null, + } +} + +// A view plugin that requests completions from the server after a delay +export class KclPlugin implements PluginValue { + private viewUpdate: ViewUpdate | null = null + + private _deffererCodeUpdate = deferExecution(() => { + if (this.viewUpdate === null) { + return + } + + kclManager.executeCode() + }, changesDelay) + + private _deffererUserSelect = deferExecution(() => { + if (this.viewUpdate === null) { + return + } + + editorManager.handleOnViewUpdate(this.viewUpdate) + }, 50) + + update(viewUpdate: ViewUpdate) { + this.viewUpdate = viewUpdate + editorManager.setEditorView(viewUpdate.view) + + const isRelevant = relevantUpdate(viewUpdate) + if (!isRelevant.overall) { + return + } + + // If we have a user select event, we want to update what parts are + // highlighted. + if (isRelevant.userSelect) { + this._deffererUserSelect(true) + return + } + + if (!viewUpdate.docChanged) { + return + } + + const newCode = viewUpdate.state.doc.toString() + codeManager.code = newCode + codeManager.writeToFile() + + this._deffererCodeUpdate(true) + } +} + export function kclPlugin(options: LanguageServerOptions): Extension { let plugin: LanguageServerPlugin | null = null const viewPlugin = ViewPlugin.define( @@ -58,8 +152,8 @@ export function kclPlugin(options: LanguageServerOptions): Extension { // Get the current plugin from the map. const p = view.plugin(viewPlugin) - if (p === null) return false + p.requestFormatting() return true }, @@ -68,6 +162,39 @@ export function kclPlugin(options: LanguageServerOptions): Extension { // Create an extension for the key mappings. const kclKeymapExt = Prec.highest(keymap.computeN([], () => [kclKeymap])) + const autocompleteKeymap: readonly KeyBinding[] = [ + { key: 'Ctrl-Space', run: startCompletion }, + { + key: 'Escape', + run: (view: EditorView): boolean => { + if (clearSnippet(view)) return true + + return closeCompletion(view) + }, + }, + { key: 'ArrowDown', run: moveCompletionSelection(true) }, + { key: 'ArrowUp', run: moveCompletionSelection(false) }, + { key: 'PageDown', run: moveCompletionSelection(true, 'page') }, + { key: 'PageUp', run: moveCompletionSelection(false, 'page') }, + { key: 'Enter', run: acceptCompletion }, + { + key: 'Tab', + run: (view: EditorView): boolean => { + if (hasNextSnippetField(view.state)) { + const result = nextSnippetField(view) + return result + } + + return acceptCompletion(view) + }, + shift: prevSnippetField, + }, + ] + + const autocompleteKeymapExt = Prec.highest( + keymap.computeN([], () => [autocompleteKeymap]) + ) + const folding = foldService.of( (state: EditorState, lineStart: number, lineEnd: number) => { if (plugin == null) return null @@ -79,10 +206,11 @@ export function kclPlugin(options: LanguageServerOptions): Extension { ) return [ - documentUri.of(options.documentUri), + docPathFacet.of(options.documentUri), languageId.of('kcl'), workspaceFolders.of(options.workspaceFolders), viewPlugin, + ViewPlugin.define((view) => new KclPlugin()), kclKeymapExt, kclIndentService(), hoverTooltip( @@ -104,8 +232,9 @@ export function kclPlugin(options: LanguageServerOptions): Extension { return diagnostics }), folding, + autocompleteKeymapExt, autocompletion({ - defaultKeymap: true, + defaultKeymap: false, override: [ async (context) => { if (plugin == null) return null diff --git a/src/editor/plugins/lsp/kcl/language.ts b/src/editor/plugins/lsp/kcl/language.ts index b3fad3294..494b003a2 100644 --- a/src/editor/plugins/lsp/kcl/language.ts +++ b/src/editor/plugins/lsp/kcl/language.ts @@ -8,10 +8,19 @@ import { import { LanguageServerClient } from 'editor/plugins/lsp' import { kclPlugin } from '.' import type * as LSP from 'vscode-languageserver-protocol' -import { parser as jsParser } from '@lezer/javascript' -import { EditorState } from '@uiw/react-codemirror' +import KclParser from './parser' +import { semanticTokenField } from '../plugin' -const data = defineLanguageFacet({}) +const data = defineLanguageFacet({ + // https://codemirror.net/docs/ref/#commands.CommentTokens + commentTokens: { + line: '//', + block: { + open: '/*', + close: '*/', + }, + }, +}) export interface LanguageOptions { workspaceFolders: LSP.WorkspaceFolder[] @@ -28,34 +37,24 @@ class KclLanguage extends Language { client: options.client, }) + const parser = new KclParser() + super( data, // For now let's use the javascript parser. // It works really well and has good syntax highlighting. // We can use our lsp for the rest. - jsParser, - [ - plugin, - EditorState.languageData.of(() => [ - { - // https://codemirror.net/docs/ref/#commands.CommentTokens - commentTokens: { - line: '//', - block: { - open: '/*', - close: '*/', - }, - }, - }, - ]), - ], + parser, + [plugin], 'kcl' ) } } -export default function kclLanguage(options: LanguageOptions): LanguageSupport { - const lang = new KclLanguage(options) +export default class KclLanguageSupport extends LanguageSupport { + constructor(options: LanguageOptions) { + const lang = new KclLanguage(options) - return new LanguageSupport(lang) + super(lang, [semanticTokenField]) + } } diff --git a/src/editor/plugins/lsp/kcl/parser.ts b/src/editor/plugins/lsp/kcl/parser.ts index 50a5daf4a..51d249a36 100644 --- a/src/editor/plugins/lsp/kcl/parser.ts +++ b/src/editor/plugins/lsp/kcl/parser.ts @@ -1,4 +1,6 @@ // Extends the codemirror Parser for kcl. +// This is really just a no-op parser since we use semantic tokens from the LSP +// server. import { Parser, @@ -7,91 +9,27 @@ import { PartialParse, Tree, NodeType, - NodeSet, } from '@lezer/common' -import { LanguageServerClient } from 'editor/plugins/lsp' -import { posToOffset } from 'editor/plugins/lsp/util' -import { SemanticToken } from './semantic_tokens' import { DocInput } from '@codemirror/language' -import { tags, styleTags } from '@lezer/highlight' export default class KclParser extends Parser { - private client: LanguageServerClient - - constructor(client: LanguageServerClient) { - super() - this.client = client - } - createParse( input: Input, fragments: readonly TreeFragment[], ranges: readonly { from: number; to: number }[] ): PartialParse { - let parse: PartialParse = new Context(this, input, fragments, ranges) + let parse: PartialParse = new Context(input) return parse } - - getTokenTypes(): string[] { - return this.client.getServerCapabilities().semanticTokensProvider!.legend - .tokenTypes - } - - getSemanticTokens(): SemanticToken[] { - return this.client.getSemanticTokens() - } } class Context implements PartialParse { - private parser: KclParser private input: DocInput - private fragments: readonly TreeFragment[] - private ranges: readonly { from: number; to: number }[] - private nodeTypes: { [key: string]: NodeType } stoppedAt: number = 0 - private semanticTokens: SemanticToken[] = [] - private currentLine: number = 0 - private currentColumn: number = 0 - private nodeSet: NodeSet - - constructor( - /// The parser configuration used. - parser: KclParser, - input: Input, - fragments: readonly TreeFragment[], - ranges: readonly { from: number; to: number }[] - ) { - this.parser = parser + constructor(input: Input) { this.input = input as DocInput - this.fragments = fragments - this.ranges = ranges - - // Iterate over the semantic token types and create a node type for each. - this.nodeTypes = {} - let nodeArray: NodeType[] = [] - this.parser.getTokenTypes().forEach((tokenType, index) => { - const nodeType = NodeType.define({ - id: index, - name: tokenType, - // props: [this.styleTags], - }) - this.nodeTypes[tokenType] = nodeType - nodeArray.push(nodeType) - }) - - this.semanticTokens = this.parser.getSemanticTokens() - const styles = styleTags({ - number: tags.number, - variable: tags.variableName, - operator: tags.operator, - keyword: tags.keyword, - string: tags.string, - comment: tags.comment, - function: tags.function(tags.variableName), - }) - this.nodeSet = new NodeSet(nodeArray).extend(styles) } get parsedPos(): number { @@ -99,67 +37,8 @@ class Context implements PartialParse { } advance(): Tree | null { - if (this.semanticTokens.length === 0) { - return new Tree(NodeType.none, [], [], 0) - } - const tree = this.createTree(this.semanticTokens[0], 0) this.stoppedAt = this.input.doc.length - return tree - } - - createTree(token: SemanticToken, index: number): Tree { - const changedLine = token.delta_line !== 0 - this.currentLine += token.delta_line - if (changedLine) { - this.currentColumn = 0 - } - this.currentColumn += token.delta_start - - // Let's get our position relative to the start of the file. - let currentPosition = posToOffset(this.input.doc, { - line: this.currentLine, - character: this.currentColumn, - }) - - const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id] - - if (currentPosition === undefined) { - // This is bad and weird. - return new Tree(nodeType, [], [], token.length) - } - - if (index >= this.semanticTokens.length - 1) { - // We have no children. - return new Tree(nodeType, [], [], token.length) - } - - const nextIndex = index + 1 - const nextToken = this.semanticTokens[nextIndex] - const changedLineNext = nextToken.delta_line !== 0 - const nextLine = this.currentLine + nextToken.delta_line - const nextColumn = changedLineNext - ? nextToken.delta_start - : this.currentColumn + nextToken.delta_start - const nextPosition = posToOffset(this.input.doc, { - line: nextLine, - character: nextColumn, - }) - - if (nextPosition === undefined) { - // This is bad and weird. - return new Tree(nodeType, [], [], token.length) - } - - // Let's get the - - return new Tree( - nodeType, - [this.createTree(nextToken, nextIndex)], - - // The positions (offsets relative to the start of this tree) of the children. - [nextPosition - currentPosition], - token.length - ) + return new Tree(NodeType.none, [], [], this.input.doc.length) } stopAt(pos: number) { diff --git a/src/editor/plugins/lsp/kcl/semantic_tokens.ts b/src/editor/plugins/lsp/kcl/semantic_tokens.ts deleted file mode 100644 index 4fd0a1148..000000000 --- a/src/editor/plugins/lsp/kcl/semantic_tokens.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type * as LSP from 'vscode-languageserver-protocol' - -export class SemanticToken { - delta_line: number - delta_start: number - length: number - token_type: string - token_modifiers_bitset: string - - constructor( - delta_line = 0, - delta_start = 0, - length = 0, - token_type = '', - token_modifiers_bitset = '' - ) { - this.delta_line = delta_line - this.delta_start = delta_start - this.length = length - this.token_type = token_type - this.token_modifiers_bitset = token_modifiers_bitset - } -} - -export async function deserializeTokens( - data: number[], - semanticTokensProvider?: LSP.SemanticTokensOptions -): Promise { - if (!semanticTokensProvider) { - return [] - } - // Check if data length is divisible by 5 - if (data.length % 5 !== 0) { - return Promise.reject(new Error('Length is not divisible by 5')) - } - - const tokens = [] - for (let i = 0; i < data.length; i += 5) { - tokens.push( - new SemanticToken( - data[i], - data[i + 1], - data[i + 2], - semanticTokensProvider.legend.tokenTypes[data[i + 3]], - semanticTokensProvider.legend.tokenModifiers[data[i + 4]] - ) - ) - } - - return tokens -} diff --git a/src/editor/plugins/lsp/plugin.ts b/src/editor/plugins/lsp/plugin.ts index 81f92c8dd..774770f9c 100644 --- a/src/editor/plugins/lsp/plugin.ts +++ b/src/editor/plugins/lsp/plugin.ts @@ -1,7 +1,24 @@ -import { completeFromList, snippetCompletion } from '@codemirror/autocomplete' -import { setDiagnostics } from '@codemirror/lint' -import { Facet } from '@codemirror/state' -import { EditorView, Tooltip } from '@codemirror/view' +import { + completeFromList, + hasNextSnippetField, + pickedCompletion, + snippetCompletion, +} from '@codemirror/autocomplete' +import { + Facet, + StateEffect, + StateField, + Extension, + Annotation, + Transaction, +} from '@codemirror/state' +import { + EditorView, + Tooltip, + Decoration, + DecorationSet, +} from '@codemirror/view' +import { URI } from 'vscode-uri' import { DiagnosticSeverity, CompletionItemKind, @@ -21,49 +38,247 @@ import { LanguageServerClient } from 'editor/plugins/lsp' import { Marked } from '@ts-stack/markdown' import { posToOffset } from 'editor/plugins/lsp/util' import { Program, ProgramMemory } from 'lang/wasm' -import { codeManager, editorManager, kclManager } from 'lib/singletons' +import { codeManager, editorManager } from 'lib/singletons' import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' +import { copilotPluginEvent } from './copilot' +import { codeManagerUpdateEvent } from 'lang/codeManager' +import { + modelingMachineEvent, + updateOutsideEditorEvent, + setDiagnosticsEvent, +} from 'editor/manager' +import { SemanticToken, getTag } from 'editor/plugins/lsp/semantic_token' +import { highlightingFor } from '@codemirror/language' const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '') -export const documentUri = Facet.define({ combine: useLast }) +export const docPathFacet = Facet.define({ + combine: useLast, +}) export const languageId = Facet.define({ combine: useLast }) export const workspaceFolders = Facet.define< LSP.WorkspaceFolder[], LSP.WorkspaceFolder[] >({ combine: useLast }) +enum LspAnnotation { + SemanticTokens = 'semantic-tokens', +} + +const lspEvent = Annotation.define() +export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens) + const CompletionItemKindMap = Object.fromEntries( Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) ) as Record const changesDelay = 600 -let debounceTimer: ReturnType | null = null -const updateDelay = 100 + +const addToken = StateEffect.define({ + map: (token: SemanticToken, change) => ({ + ...token, + from: change.mapPos(token.from), + to: change.mapPos(token.to), + }), +}) + +export const semanticTokenField = StateField.define({ + create() { + return Decoration.none + }, + update(highlights, tr) { + // Nothing can come before this line, this is very important! + // It makes sure the highlights are updated correctly for the changes. + highlights = highlights.map(tr.changes) + + const isSemanticTokensEvent = tr.annotation(lspSemanticTokensEvent.type) + if (!isSemanticTokensEvent) { + return highlights + } + + // Check if any of the changes are addToken + const hasAddToken = tr.effects.some((e) => e.is(addToken)) + if (hasAddToken) { + highlights = highlights.update({ + filter: (from, to) => false, + }) + } + + for (const e of tr.effects) + if (e.is(addToken)) { + const tag = getTag(e.value) + const className = tag + ? highlightingFor(tr.startState, [tag]) + : undefined + + if (e.value.from < e.value.to && tag) { + if (className) { + highlights = highlights.update({ + add: [ + Decoration.mark({ class: className }).range( + e.value.from, + e.value.to + ), + ], + }) + } + } + } + return highlights + }, + provide: (f) => EditorView.decorations.from(f), +}) + +export enum TransactionAnnotation { + Diagnostics = 'diagnostics', + Remote = 'remote', + UserSelect = 'user.select', + UserInput = 'user.input', + UserMove = 'user.move', + UserDelete = 'user.delete', + UserUndo = 'user.undo', + UserRedo = 'user.redo', + + Copoilot = 'copilot', + OutsideEditor = 'outsideEditor', + CodeManager = 'codeManager', + ModelingMachine = 'modelingMachineEvent', + LspSemanticTokens = 'lspSemanticTokensEvent', + + PickedCompletion = 'pickedCompletion', +} + +export interface TransactionInfo { + annotations: TransactionAnnotation[] + time: number | null + docChanged: boolean + addToHistory: boolean + inSnippet: boolean +} + +export const updateInfo = (update: ViewUpdate): TransactionInfo[] => { + let transactionInfos: TransactionInfo[] = [] + + for (const tr of update.transactions) { + let annotations: TransactionAnnotation[] = [] + + if (tr.isUserEvent('select')) { + annotations.push(TransactionAnnotation.UserSelect) + } + + if (tr.isUserEvent('input')) { + annotations.push(TransactionAnnotation.UserInput) + } + if (tr.isUserEvent('delete')) { + annotations.push(TransactionAnnotation.UserDelete) + } + if (tr.isUserEvent('undo')) { + annotations.push(TransactionAnnotation.UserUndo) + } + if (tr.isUserEvent('redo')) { + annotations.push(TransactionAnnotation.UserRedo) + } + if (tr.isUserEvent('move')) { + annotations.push(TransactionAnnotation.UserMove) + } + + if (tr.annotation(pickedCompletion) !== undefined) { + annotations.push(TransactionAnnotation.PickedCompletion) + } + + if (tr.annotation(copilotPluginEvent.type) !== undefined) { + annotations.push(TransactionAnnotation.Copoilot) + } + + if (tr.annotation(updateOutsideEditorEvent.type) !== undefined) { + annotations.push(TransactionAnnotation.OutsideEditor) + } + + if (tr.annotation(codeManagerUpdateEvent.type) !== undefined) { + annotations.push(TransactionAnnotation.CodeManager) + } + + if (tr.annotation(modelingMachineEvent.type) !== undefined) { + annotations.push(TransactionAnnotation.ModelingMachine) + } + + if (tr.annotation(lspSemanticTokensEvent.type) !== undefined) { + annotations.push(TransactionAnnotation.LspSemanticTokens) + } + + if (tr.annotation(setDiagnosticsEvent.type) !== undefined) { + annotations.push(TransactionAnnotation.Diagnostics) + } + + if (tr.annotation(Transaction.remote) !== undefined) { + annotations.push(TransactionAnnotation.Remote) + } + + transactionInfos.push({ + annotations, + time: tr.annotation(Transaction.time) || null, + docChanged: tr.docChanged, + addToHistory: tr.annotation(Transaction.addToHistory) || false, + inSnippet: hasNextSnippetField(update.state), + }) + } + + return transactionInfos +} + +export interface RelevantUpdate { + overall: boolean + userSelect: boolean + time: number | null +} + +export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => { + const infos = updateInfo(update) + // Make sure we are not in a snippet + if (infos.some((info) => info.inSnippet)) { + return { + overall: false, + userSelect: false, + time: null, + } + } + return { + overall: infos.some( + (info) => + info.docChanged || + info.annotations.includes(TransactionAnnotation.UserInput) || + info.annotations.includes(TransactionAnnotation.UserDelete) || + info.annotations.includes(TransactionAnnotation.UserUndo) || + info.annotations.includes(TransactionAnnotation.UserRedo) || + info.annotations.includes(TransactionAnnotation.UserMove) + ), + userSelect: infos.some((info) => + info.annotations.includes(TransactionAnnotation.UserSelect) + ), + time: infos.length ? infos[0].time : null, + } +} export class LanguageServerPlugin implements PluginValue { public client: LanguageServerClient - public documentUri: string - public languageId: string - public workspaceFolders: LSP.WorkspaceFolder[] private documentVersion: number private foldingRanges: LSP.FoldingRange[] | null = null - private viewUpdate: ViewUpdate | null = null + + private previousSemanticTokens: SemanticToken[] = [] + private _defferer = deferExecution((code: string) => { try { // Update the state (not the editor) with the new code. this.client.textDocumentDidChange({ textDocument: { - uri: this.documentUri, + uri: this.getDocUri(), version: this.documentVersion++, }, contentChanges: [{ text: code }], }) - if (this.viewUpdate) { - editorManager.handleOnViewUpdate(this.viewUpdate) - } + this.requestSemanticTokens(this.view) } catch (e) { console.error(e) } @@ -75,41 +290,43 @@ export class LanguageServerPlugin implements PluginValue { private allowHTMLContent: boolean ) { this.client = client - this.documentUri = this.view.state.facet(documentUri) - this.languageId = this.view.state.facet(languageId) - this.workspaceFolders = this.view.state.facet(workspaceFolders) this.documentVersion = 0 this.client.attachPlugin(this) this.initialize({ - documentText: this.view.state.doc.toString(), + documentText: this.getDocText(), }) } - update(viewUpdate: ViewUpdate) { - this.viewUpdate = viewUpdate - if (!viewUpdate.docChanged) { - // debounce the view update. - // otherwise it is laggy for typing. - if (debounceTimer) { - clearTimeout(debounceTimer) - } + private getDocPath(view = this.view) { + return view.state.facet(docPathFacet) + } + private getDocText(view = this.view) { + return view.state.doc.toString() + } - debounceTimer = setTimeout(() => { - editorManager.handleOnViewUpdate(viewUpdate) - }, updateDelay) + private getDocUri(view = this.view) { + return URI.file(this.getDocPath(view)).toString() + } + + private getLanguageId(view = this.view) { + return view.state.facet(languageId) + } + + update(viewUpdate: ViewUpdate) { + const isRelevant = relevantUpdate(viewUpdate) + if (!isRelevant.overall) { return } - const newCode = this.view.state.doc.toString() - - codeManager.code = newCode - codeManager.writeToFile() - kclManager.executeCode() + // If the doc didn't change we can return early. + if (!viewUpdate.docChanged) { + return + } this.sendChange({ - documentText: newCode, + documentText: viewUpdate.state.doc.toString(), }) } @@ -121,14 +338,17 @@ export class LanguageServerPlugin implements PluginValue { if (this.client.initializePromise) { await this.client.initializePromise } + this.client.textDocumentDidOpen({ textDocument: { - uri: this.documentUri, - languageId: this.languageId, + uri: this.getDocUri(), + languageId: this.getLanguageId(), text: documentText, version: this.documentVersion, }, }) + + this.requestSemanticTokens(this.view) } async sendChange({ documentText }: { documentText: string }) { @@ -138,7 +358,7 @@ export class LanguageServerPlugin implements PluginValue { } requestDiagnostics(view: EditorView) { - this.sendChange({ documentText: view.state.doc.toString() }) + this.sendChange({ documentText: this.getDocText() }) } async requestHoverTooltip( @@ -151,9 +371,9 @@ export class LanguageServerPlugin implements PluginValue { ) return null - this.sendChange({ documentText: view.state.doc.toString() }) + this.sendChange({ documentText: this.getDocText() }) const result = await this.client.textDocumentHover({ - textDocument: { uri: this.documentUri }, + textDocument: { uri: this.getDocUri() }, position: { line, character }, }) if (!result) return null @@ -181,7 +401,7 @@ export class LanguageServerPlugin implements PluginValue { ) return null const result = await this.client.textDocumentFoldingRange({ - textDocument: { uri: this.documentUri }, + textDocument: { uri: this.getDocUri() }, }) return result || null @@ -228,9 +448,9 @@ export class LanguageServerPlugin implements PluginValue { return await this.client.updateUnits({ textDocument: { - uri: this.documentUri, + uri: this.getDocUri(), }, - text: this.view.state.doc.toString(), + text: this.getDocText(), units, }) } @@ -254,7 +474,6 @@ export class LanguageServerPlugin implements PluginValue { }) } } - console.log('[lsp] kcl: updated canExecute', canExecute, response) return response } @@ -267,14 +486,14 @@ export class LanguageServerPlugin implements PluginValue { this.client.textDocumentDidChange({ textDocument: { - uri: this.documentUri, + uri: this.getDocUri(), version: this.documentVersion++, }, - contentChanges: [{ text: this.view.state.doc.toString() }], + contentChanges: [{ text: this.getDocText() }], }) const result = await this.client.textDocumentFormatting({ - textDocument: { uri: this.documentUri }, + textDocument: { uri: this.getDocUri() }, options: { tabSize: 2, insertSpaces: true, @@ -285,16 +504,8 @@ export class LanguageServerPlugin implements PluginValue { if (!result) return null for (let i = 0; i < result.length; i++) { - const { range, newText } = result[i] - this.view.dispatch({ - changes: [ - { - from: posToOffset(this.view.state.doc, range.start)!, - to: posToOffset(this.view.state.doc, range.end)!, - insert: newText, - }, - ], - }) + const { newText } = result[i] + codeManager.updateCodeStateEditor(newText) } } @@ -320,7 +531,7 @@ export class LanguageServerPlugin implements PluginValue { }) const result = await this.client.textDocumentCompletion({ - textDocument: { uri: this.documentUri }, + textDocument: { uri: this.getDocUri() }, position: { line, character }, context: { triggerKind, @@ -379,16 +590,107 @@ export class LanguageServerPlugin implements PluginValue { return completeFromList(options)(context) } + parseSemanticTokens(view: EditorView, data: number[]) { + // decode the lsp semantic token types + const tokens = [] + for (let i = 0; i < data.length; i += 5) { + tokens.push({ + deltaLine: data[i], + startChar: data[i + 1], + length: data[i + 2], + tokenType: data[i + 3], + modifiers: data[i + 4], + }) + } + + // convert the tokens into an array of {to, from, type} objects + const tokenTypes = + this.client.getServerCapabilities().semanticTokensProvider!.legend + .tokenTypes + const tokenModifiers = + this.client.getServerCapabilities().semanticTokensProvider!.legend + .tokenModifiers + const tokenRanges: any = [] + let curLine = 0 + let prevStart = 0 + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + const tokenType = tokenTypes[token.tokenType] + // get a list of modifiers + const tokenModifier = [] + for (let j = 0; j < tokenModifiers.length; j++) { + if (token.modifiers & (1 << j)) { + tokenModifier.push(tokenModifiers[j]) + } + } + + if (token.deltaLine !== 0) prevStart = 0 + + const tokenRange = { + from: posToOffset(view.state.doc, { + line: curLine + token.deltaLine, + character: prevStart + token.startChar, + })!, + to: posToOffset(view.state.doc, { + line: curLine + token.deltaLine, + character: prevStart + token.startChar + token.length, + })!, + type: tokenType, + modifiers: tokenModifier, + } + tokenRanges.push(tokenRange) + + curLine += token.deltaLine + prevStart += token.startChar + } + + // sort by from + tokenRanges.sort((a: any, b: any) => a.from - b.from) + return tokenRanges + } + + async requestSemanticTokens(view: EditorView) { + if ( + !this.client.ready || + !this.client.getServerCapabilities().semanticTokensProvider + ) { + return null + } + + const result = await this.client.textDocumentSemanticTokensFull({ + textDocument: { uri: this.getDocUri() }, + }) + if (!result) return null + + const { data } = result + this.previousSemanticTokens = this.parseSemanticTokens(view, data) + + const effects: StateEffect[] = + this.previousSemanticTokens.map((tokenRange: any) => + addToken.of(tokenRange) + ) + + view.dispatch({ + effects, + + annotations: [lspSemanticTokensEvent, Transaction.addToHistory.of(false)], + }) + } + async processNotification(notification: LSP.NotificationMessage) { try { switch (notification.method) { case 'textDocument/publishDiagnostics': + if (notification === undefined) break + if (notification.params === undefined) break + if (!notification.params) break + const params = notification.params as PublishDiagnosticsParams + if (!params) break console.log( '[lsp] [window/publishDiagnostics]', this.client.getName(), - notification.params + params ) - const params = notification.params as PublishDiagnosticsParams // this is sometimes slower than our actual typing. this.processDiagnostics(params) break @@ -420,7 +722,6 @@ export class LanguageServerPlugin implements PluginValue { // The server has updated the memory, we should update elsewhere. let updatedMemory = notification.params as ProgramMemory console.log('[lsp]: Updated Memory', updatedMemory) - kclManager.programMemory = updatedMemory break } } catch (error) { @@ -429,7 +730,7 @@ export class LanguageServerPlugin implements PluginValue { } processDiagnostics(params: PublishDiagnosticsParams) { - if (params.uri !== this.documentUri) return + if (params.uri !== this.getDocUri()) return const diagnostics = params.diagnostics .map(({ range, message, severity }) => ({ @@ -459,7 +760,7 @@ export class LanguageServerPlugin implements PluginValue { return 0 }) - this.view.dispatch(setDiagnostics(this.view.state, diagnostics)) + editorManager.addDiagnostics(diagnostics) } } diff --git a/src/editor/plugins/lsp/semantic_token.ts b/src/editor/plugins/lsp/semantic_token.ts new file mode 100644 index 000000000..4c517eb99 --- /dev/null +++ b/src/editor/plugins/lsp/semantic_token.ts @@ -0,0 +1,112 @@ +import { Tag, tags } from '@lezer/highlight' + +export interface SemanticToken { + from: number + to: number + type: string + modifiers: string[] +} + +export function getTag(semanticToken: SemanticToken): Tag | null { + let tokenType = convertSemanticTokenTypeToCodeMirrorTag(semanticToken.type) + + if ( + semanticToken.modifiers === undefined || + semanticToken.modifiers === null || + semanticToken.modifiers.length === 0 + ) { + return tokenType + } + + for (let modifier of semanticToken.modifiers) { + tokenType = convertSemanticTokenToCodeMirrorTag( + '', + modifier, + tokenType || undefined + ) + } + + return tokenType +} + +export function getTagName(semanticToken: SemanticToken): string { + let tokenType = semanticToken.type + + if ( + semanticToken.modifiers === undefined || + semanticToken.modifiers === null || + semanticToken.modifiers.length === 0 + ) { + return tokenType + } + + for (let modifier of semanticToken.modifiers) { + tokenType = `${tokenType}.${modifier}` + } + + return tokenType +} + +function convertSemanticTokenTypeToCodeMirrorTag( + tokenType: string +): Tag | null { + switch (tokenType) { + case 'keyword': + return tags.keyword + case 'variable': + return tags.variableName + case 'string': + return tags.string + case 'number': + return tags.number + case 'comment': + return tags.comment + case 'operator': + return tags.operator + case 'function': + return tags.function(tags.name) + case 'type': + return tags.typeName + case 'property': + return tags.propertyName + case 'parameter': + return tags.local(tags.name) + default: + console.error('Unknown token type:', tokenType) + return null + } +} + +function convertSemanticTokenToCodeMirrorTag( + tokenType: string, + tokenModifier: string, + givenTag?: Tag +): Tag | null { + let tag = givenTag + ? givenTag + : convertSemanticTokenTypeToCodeMirrorTag(tokenType) + + if (!tag) { + return null + } + + if (tokenModifier) { + switch (tokenModifier) { + case 'definition': + return tags.definition(tag) + case 'declaration': + return tags.definition(tag) + case 'readonly': + return tags.constant(tag) + case 'static': + return tags.constant(tag) + case 'defaultLibrary': + return tags.standard(tag) + default: + console.error('Unknown token modifier:', tokenModifier) + return tag + } + } + + return tag +} diff --git a/src/lang/codeManager.ts b/src/lang/codeManager.ts index 1f80b69fa..140cb7777 100644 --- a/src/lang/codeManager.ts +++ b/src/lang/codeManager.ts @@ -6,10 +6,13 @@ import { isTauri } from 'lib/isTauri' import { writeTextFile } from '@tauri-apps/plugin-fs' import toast from 'react-hot-toast' import { editorManager } from 'lib/singletons' -import { KeyBinding } from '@uiw/react-codemirror' +import { Annotation, KeyBinding, Transaction } from '@uiw/react-codemirror' const PERSIST_CODE_TOKEN = 'persistCode' +const codeManagerUpdateAnnotation = Annotation.define() +export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(null) + export default class CodeManager { private _code: string = bracket #updateState: (arg: string) => void = () => {} @@ -90,6 +93,10 @@ export default class CodeManager { to: editorManager.editorView.state.doc.length, insert: code, }, + annotations: [ + codeManagerUpdateEvent, + Transaction.addToHistory.of(true), + ], }) } } diff --git a/src/lang/queryAst.test.ts b/src/lang/queryAst.test.ts index 150f1604e..e7ae5d8f3 100644 --- a/src/lang/queryAst.test.ts +++ b/src/lang/queryAst.test.ts @@ -19,7 +19,6 @@ import { createPipeSubstitution, } from './modifyAst' import { err } from 'lib/trap' -import { warn } from 'node:console' beforeAll(async () => { await initPromise diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index d2ee48554..6523b44ca 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -282,8 +282,10 @@ function moreNodePathFromSourceRange( } return path } + if (_node.type === 'PipeSubstitution' && isInRange) return path console.error('not implemented: ' + node.type) + return path } diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index a1fd2b58c..81d869529 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -712,7 +712,7 @@ dependencies = [ [[package]] name = "derive-docs" -version = "0.1.19" +version = "0.1.20" dependencies = [ "Inflector", "anyhow", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "kcl-lib" -version = "0.1.68" +version = "0.1.69" dependencies = [ "anyhow", "approx", @@ -1453,7 +1453,7 @@ dependencies = [ [[package]] name = "kcl-test-server" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "hyper", diff --git a/src/wasm-lib/derive-docs/Cargo.toml b/src/wasm-lib/derive-docs/Cargo.toml index ae72a0895..43f430b2b 100644 --- a/src/wasm-lib/derive-docs/Cargo.toml +++ b/src/wasm-lib/derive-docs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "derive-docs" description = "A tool for generating documentation from Rust derive macros" -version = "0.1.19" +version = "0.1.20" edition = "2021" license = "MIT" repository = "https://github.com/KittyCAD/modeling-app" diff --git a/src/wasm-lib/kcl-test-server/Cargo.toml b/src/wasm-lib/kcl-test-server/Cargo.toml index 18b0997bd..1fc39234f 100644 --- a/src/wasm-lib/kcl-test-server/Cargo.toml +++ b/src/wasm-lib/kcl-test-server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kcl-test-server" description = "A test server for KCL" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "MIT" diff --git a/src/wasm-lib/kcl/Cargo.toml b/src/wasm-lib/kcl/Cargo.toml index f3d24bb77..69452295f 100644 --- a/src/wasm-lib/kcl/Cargo.toml +++ b/src/wasm-lib/kcl/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kcl-lib" description = "KittyCAD Language implementation and tools" -version = "0.1.68" +version = "0.1.69" edition = "2021" license = "MIT" repository = "https://github.com/KittyCAD/modeling-app" @@ -19,7 +19,7 @@ chrono = "0.4.38" clap = { version = "4.5.7", default-features = false, optional = true } dashmap = "6.0.1" databake = { version = "0.1.8", features = ["derive"] } -derive-docs = { version = "0.1.19", path = "../derive-docs" } +derive-docs = { version = "0.1.20", path = "../derive-docs" } form_urlencoded = "1.2.1" futures = { version = "0.3.30" } git_rev = "0.1.0" diff --git a/src/wasm-lib/kcl/src/lsp/backend.rs b/src/wasm-lib/kcl/src/lsp/backend.rs index 0a2a10338..d4e2d538a 100644 --- a/src/wasm-lib/kcl/src/lsp/backend.rs +++ b/src/wasm-lib/kcl/src/lsp/backend.rs @@ -70,11 +70,8 @@ where } } - println!("on_change after check: {:?}", params); - self.insert_code_map(params.uri.to_string(), params.text.as_bytes().to_vec()) .await; - println!("on_change after insert: {:?}", params); self.inner_on_change(params, false).await; } diff --git a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs index f5a0d7f73..1bb2622f6 100644 --- a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs @@ -39,11 +39,10 @@ use tower_lsp::{ Client, LanguageServer, }; -#[cfg(not(target_arch = "wasm32"))] -use crate::lint::checks; use crate::{ ast::types::{Value, VariableKind}, executor::SourceRange, + lint::checks, lsp::{backend::Backend as _, util::IntoDiagnostic}, parser::PIPE_OPERATOR, token::TokenType, @@ -269,15 +268,12 @@ impl crate::lsp::backend::Backend for Backend { // Update our semantic tokens. self.update_semantic_tokens(&tokens, ¶ms).await; - #[cfg(not(target_arch = "wasm32"))] - { - let discovered_findings = ast - .lint(checks::lint_variables) - .into_iter() - .flatten() - .collect::>(); - self.add_to_diagnostics(¶ms, &discovered_findings, false).await; - } + let discovered_findings = ast + .lint(checks::lint_variables) + .into_iter() + .flatten() + .collect::>(); + self.add_to_diagnostics(¶ms, &discovered_findings, false).await; } // Send the notification to the client that the ast was updated. @@ -533,9 +529,9 @@ impl Backend { diagnostics: &[DiagT], clear_all_before_add: bool, ) { - self.client - .log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostics)) - .await; + if diagnostics.is_empty() { + return; + } if clear_all_before_add { self.clear_diagnostics_map(¶ms.uri, None).await; @@ -645,20 +641,6 @@ impl Backend { modifier } - async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec { - let ast = match self.ast_map.get(file_name) { - Some(ast) => ast, - None => return vec![], - }; - - // Get the completion items. - match ast.completion_items() { - Ok(items) => items, - // TODO: don't ignore an error here. - Err(_err) => vec![], - } - } - pub async fn create_zip(&self) -> Result> { // Collect all the file data we know. let mut buf = vec![]; @@ -1055,9 +1037,34 @@ impl LanguageServer for Backend { completions.extend(self.stdlib_completions.values().cloned()); - let variables = self - .completions_get_variables_from_ast(params.text_document_position.text_document.uri.as_ref()) - .await; + // Add more to the completions if we have more. + let Some(ast) = self + .ast_map + .get(params.text_document_position.text_document.uri.as_ref()) + else { + return Ok(Some(CompletionResponse::Array(completions))); + }; + + let Some(current_code) = self + .code_map + .get(params.text_document_position.text_document.uri.as_ref()) + else { + return Ok(Some(CompletionResponse::Array(completions))); + }; + let Ok(current_code) = std::str::from_utf8(¤t_code) else { + return Ok(Some(CompletionResponse::Array(completions))); + }; + + let position = position_to_char_index(params.text_document_position.position, current_code); + if ast.get_non_code_meta_for_position(position).is_some() { + // If we are in a code comment we don't want to show completions. + return Ok(None); + } + + // Get the completion items forem the ast. + let Ok(variables) = ast.completion_items() else { + return Ok(Some(CompletionResponse::Array(completions))); + }; // Get our variables from our AST to include in our completions. completions.extend(variables); diff --git a/src/wasm-lib/kcl/src/lsp/tests.rs b/src/wasm-lib/kcl/src/lsp/tests.rs index 88f00e54e..354a1e51c 100644 --- a/src/wasm-lib/kcl/src/lsp/tests.rs +++ b/src/wasm-lib/kcl/src/lsp/tests.rs @@ -660,6 +660,41 @@ st"# } } +#[tokio::test(flavor = "multi_thread")] +async fn test_kcl_lsp_completions_empty_in_comment() { + let server = kcl_lsp_server(false).await.unwrap(); + + // Send open file. + server + .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { + text_document: tower_lsp::lsp_types::TextDocumentItem { + uri: "file:///test.kcl".try_into().unwrap(), + language_id: "kcl".to_string(), + version: 1, + text: r#"const thing= 1 // st"#.to_string(), + }, + }) + .await; + + // Send completion request. + let completions = server + .completion(tower_lsp::lsp_types::CompletionParams { + text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams { + text_document: tower_lsp::lsp_types::TextDocumentIdentifier { + uri: "file:///test.kcl".try_into().unwrap(), + }, + position: tower_lsp::lsp_types::Position { line: 0, character: 19 }, + }, + context: None, + partial_result_params: Default::default(), + work_done_progress_params: Default::default(), + }) + .await + .unwrap(); + + assert!(completions.is_none()); +} + #[tokio::test(flavor = "multi_thread")] async fn test_kcl_lsp_completions_tags() { let server = kcl_lsp_server(false).await.unwrap(); diff --git a/src/wasm-lib/kcl/src/token.rs b/src/wasm-lib/kcl/src/token.rs index 8fab3b212..b4c930118 100644 --- a/src/wasm-lib/kcl/src/token.rs +++ b/src/wasm-lib/kcl/src/token.rs @@ -93,6 +93,8 @@ impl TryFrom for SemanticTokenType { impl TokenType { // This is for the lsp server. + // Don't call this function directly in the code use a lazy_static instead + // like we do in the lsp server. pub fn all_semantic_token_types() -> Result> { let mut settings = schemars::gen::SchemaSettings::openapi3(); settings.inline_subschemas = true; diff --git a/vite.config.ts b/vite.config.ts index 05b22b562..abc6cf767 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,13 @@ const config = defineConfig({ open: true, port: 3000, watch: { - ignored: ['**/target/**'], + ignored: [ + '**/target/**', + '**/dist/**', + '**/build/**', + '**/test-results/**', + '**/playwright-report/**', + ], }, }, test: { diff --git a/yarn.lock b/yarn.lock index e2213167a..6c34a40ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,7 +1165,7 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" -"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.16.0": +"@codemirror/autocomplete@^6.0.0": version "6.16.2" resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz#ac4e191cd599503e45f35e97366b432d30b8f37a" integrity sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw== @@ -1175,7 +1175,17 @@ "@codemirror/view" "^6.17.0" "@lezer/common" "^1.0.0" -"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0": +"@codemirror/autocomplete@^6.16.3": + version "6.16.3" + resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz#04d5a4e4e44ccae1ba525d47db53a5479bf46338" + integrity sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.17.0" + "@lezer/common" "^1.0.0" + +"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0", "@codemirror/commands@^6.6.0": version "6.6.0" resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.6.0.tgz#d308f143fe1b8896ca25fdb855f66acdaf019dd4" integrity sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg== @@ -1185,7 +1195,7 @@ "@codemirror/view" "^6.27.0" "@lezer/common" "^1.1.0" -"@codemirror/language@^6.0.0": +"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.2": version "6.10.2" resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61" integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA== @@ -1206,7 +1216,16 @@ "@codemirror/view" "^6.0.0" crelt "^1.0.5" -"@codemirror/search@^6.0.0": +"@codemirror/lint@^6.8.1": + version "6.8.1" + resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.1.tgz#6427848815baaf68c08e98c7673b804d3d8c0e7f" + integrity sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/search@^6.0.0", "@codemirror/search@^6.5.6": version "6.5.6" resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.6.tgz#8f858b9e678d675869112e475f082d1e8488db93" integrity sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q== @@ -1215,7 +1234,7 @@ "@codemirror/view" "^6.0.0" crelt "^1.0.5" -"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.2.1", "@codemirror/state@^6.4.0": +"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.2.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1": version "6.4.1" resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b" integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A== @@ -1568,28 +1587,19 @@ ts-node "^10.9.1" tslib "~2.4" -"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0": +"@lezer/common@^1.0.0", "@lezer/common@^1.1.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049" integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ== -"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3": +"@lezer/highlight@^1.0.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780" integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA== dependencies: "@lezer/common" "^1.0.0" -"@lezer/javascript@^1.4.9": - version "1.4.17" - resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.17.tgz#8456e369f960c328b9e823342d0c72d704238c31" - integrity sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA== - dependencies: - "@lezer/common" "^1.2.0" - "@lezer/highlight" "^1.1.3" - "@lezer/lr" "^1.3.0" - -"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0": +"@lezer/lr@^1.0.0": version "1.4.1" resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.1.tgz#fe25f051880a754e820b28148d90aa2a96b8bdd2" integrity sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw== @@ -1631,16 +1641,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@open-rpc/client-js@^1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@open-rpc/client-js/-/client-js-1.8.1.tgz#73b5a5bf237f24b14c3c89205b1fca3aea213213" - integrity sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g== - dependencies: - isomorphic-fetch "^3.0.0" - isomorphic-ws "^5.0.0" - strict-event-emitter-types "^2.0.0" - ws "^7.0.0" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2021,11 +2021,6 @@ "@testing-library/dom" "^10.0.0" "@types/react-dom" "^18.0.0" -"@testing-library/user-event@^14.5.2": - version "14.5.2" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" - integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== - "@tootallnate/quickjs-emscripten@^0.23.0": version "0.23.0" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" @@ -2101,16 +2096,6 @@ dependencies: "@babel/types" "^7.20.7" -"@types/crypto-js@^4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" - integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== - -"@types/debounce-promise@^3.1.9": - version "3.1.9" - resolved "https://registry.yarnpkg.com/@types/debounce-promise/-/debounce-promise-3.1.9.tgz#b59346fe5c24636ebe0fb88f2f7e41b888b1cd7c" - integrity sha512-awNxydYSU+E2vL7EiOAMtiSOfL5gUM5X4YSE2A92qpxDJQ/rXz6oMPYBFDcDywlUmvIDI6zsqgq17cGm5CITQw== - "@types/eslint@^8.4.5": version "8.56.10" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" @@ -2124,14 +2109,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/hoist-non-react-statics@^3.3.1": - version "3.3.5" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" - integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/http-cache-semantics@^4.0.2": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" @@ -3502,7 +3479,7 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -codemirror@^6.0.0: +codemirror@^6.0.0, codemirror@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== @@ -3671,11 +3648,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - css-line-break@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" @@ -3755,11 +3727,6 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -debounce-promise@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/debounce-promise/-/debounce-promise-3.1.2.tgz#320fb8c7d15a344455cd33cee5ab63530b6dc7c5" - integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg== - debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" @@ -3815,11 +3782,6 @@ deepmerge-ts@^5.0.0, deepmerge-ts@^5.1.0: resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a" integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw== -deepmerge@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" - integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== - defaults@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" @@ -4752,20 +4714,6 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" -formik@^2.4.6: - version "2.4.6" - resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.6.tgz#4da75ca80f1a827ab35b08fd98d5a76e928c9686" - integrity sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g== - dependencies: - "@types/hoist-non-react-statics" "^3.3.1" - deepmerge "^2.1.1" - hoist-non-react-statics "^3.3.0" - lodash "^4.17.21" - lodash-es "^4.17.21" - react-fast-compare "^2.0.1" - tiny-warning "^1.0.2" - tslib "^2.0.0" - fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -5132,13 +5080,6 @@ he@1.2.0, he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -hoist-non-react-statics@^3.3.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - hosted-git-info@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" @@ -5153,7 +5094,7 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" -html2canvas-pro@^1.4.3: +html2canvas-pro@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.5.0.tgz#18925def43505bad352a394b95fffb45d6d46a8f" integrity sha512-izxSphcINRwfEVV6eamsPVdhsxSYqX8n/hxzK+niVWdB+onM+aYRoVO+xgS9iMmZoUleZTWg1tJwryikib2hXg== @@ -5581,11 +5522,6 @@ isomorphic-fetch@^3.0.0: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" -isomorphic-ws@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" - integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== - iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -5884,11 +5820,6 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -6937,11 +6868,6 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-fast-compare@^2.0.1: - version "2.0.4" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" - integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== - react-hot-toast@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994" @@ -6954,7 +6880,7 @@ react-hotkeys-hook@^4.5.0: resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz#807b389b15256daf6a813a1ec09e6698064fe97f" integrity sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug== -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7583,17 +7509,21 @@ streamx@^2.15.0, streamx@^2.18.0: optionalDependencies: bare-events "^2.2.0" -strict-event-emitter-types@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" - integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== - string-natural-compare@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7671,7 +7601,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7758,14 +7695,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -swr@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b" - integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg== - dependencies: - client-only "^0.0.1" - use-sync-external-store "^1.2.0" - tailwindcss@^3.4.1: version "3.4.4" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.4.tgz#351d932273e6abfa75ce7d226b5bf3a6cb257c05" @@ -7883,11 +7812,6 @@ tiny-invariant@^1.3.3: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== -tiny-warning@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - tinybench@^2.5.1: version "2.8.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.8.0.tgz#30e19ae3a27508ee18273ffed9ac7018949acd7b" @@ -7932,7 +7856,7 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -ts-node@^10.9.1, ts-node@^10.9.2: +ts-node@^10.9.1: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -7971,7 +7895,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0: +tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== @@ -8189,7 +8113,7 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== -use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: +use-sync-external-store@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== @@ -8331,6 +8255,11 @@ vscode-languageserver-types@3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== +vscode-uri@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + w3c-keyname@^2.2.4: version "2.2.8" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" @@ -8552,7 +8481,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8570,6 +8499,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -8589,11 +8527,6 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== -ws@^7.0.0: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - ws@^8.17.0, ws@^8.8.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"