codemirror lsp highlighter (#2806)

* tokenizer

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

udates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

more cleaniup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

dont react to non relevant events

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

faster

Signed-off-by: Jess Frazelle <github@jessfraz.com>

cleanup code

Signed-off-by: Jess Frazelle <github@jessfraz.com>

defer

Signed-off-by: Jess Frazelle <github@jessfraz.com>

more events

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

user events

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

udpates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates
;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

upfates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

make highlighting code blocks easier

Signed-off-by: Jess Frazelle <github@jessfraz.com>

improvements

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better builds

Signed-off-by: Jess Frazelle <github@jessfraz.com>

remove weird hacks

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better checks

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

make release builds in prod (#2839)

Update package.json

udpates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fix some tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better timing

Signed-off-by: Jess Frazelle <github@jessfraz.com>

tweak delay

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

upfates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

ifxup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

udpates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

udpates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

udpates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

wait for the lsp for all screenshots so consistent

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better playwright

Signed-off-by: Jess Frazelle <github@jessfraz.com>

A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

Call core dump from the bug reporting button(s) (#2783)

*  Add coredump to refresh button - this one indicates that there should be something like a core dump that is triggered.
* Added lower right control bug report button - included custom toasts for bug reporting, supports fallback bug reporting when app cannot generate a core dump

better keymaps

Signed-off-by: Jess Frazelle <github@jessfraz.com>

emptu in comment

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fix logs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fxes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

add a test for tab to autocomplete

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better

Signed-off-by: Jess Frazelle <github@jessfraz.com>

printl

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* upfates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* empty

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Jess Frazelle
2024-06-29 18:10:07 -07:00
committed by GitHub
parent 68fd921a64
commit c7efb4c006
47 changed files with 1569 additions and 1110 deletions

View File

@ -1,7 +0,0 @@
{
"cSpell.words": [
"geos"
],
"editor.tabSize": 2,
"editor.insertSpaces": true,
}

View File

@ -82,7 +82,7 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -236,7 +236,7 @@ test.describe('Testing Camera Movement', () => {
test.skip(process.platform === 'darwin', 'Can moving camera') test.skip(process.platform === 'darwin', 'Can moving camera')
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.closeKclCodePanel() await u.closeKclCodePanel()
@ -404,7 +404,7 @@ test.describe('Testing Camera Movement', () => {
test.skip(process.platform !== 'darwin', 'Zoom should be consistent') test.skip(process.platform !== 'darwin', 'Zoom should be consistent')
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -540,7 +540,6 @@ test('if you click the format button it formats your code', async ({
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -580,18 +579,8 @@ test('hover over functions shows function description', async ({ page }) => {
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) 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 u.waitForAuthSkipAppStart()
await lspStartPromise
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() 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') localStorage.setItem('disableAxis', 'true')
}) })
await page.setViewportSize({ width: 1000, height: 500 }) 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 u.waitForAuthSkipAppStart()
await lspStartPromise
// check no error to begin with // check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() 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 }) => { 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.setViewportSize({ width: 1000, height: 500 })
await page.goto('/') await u.waitForAuthSkipAppStart()
const zooLogo = page.locator('[data-testid="app-logo"]') const zooLogo = page.locator('[data-testid="app-logo"]')
// Make sure it's not a link // 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 }) => { test('if you write kcl with lint errors you get lints', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() 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.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() 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 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('End')
await page.keyboard.press('Backspace') 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 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 }) => { test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -845,19 +822,8 @@ fn squareHole = (l, w) => {
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) 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 u.waitForAuthSkipAppStart()
await lspStartPromise
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -926,7 +892,6 @@ angle: 90
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -959,7 +924,7 @@ test('executes on load', async ({ page }) => {
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// expand variables section // expand variables section
@ -988,7 +953,7 @@ test('re-executes', async ({ page }) => {
localStorage.setItem('persistCode', `const myVar = 5`) localStorage.setItem('persistCode', `const myVar = 5`)
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
const variablesTabButton = page.getByRole('tab', { const variablesTabButton = page.getByRole('tab', {
@ -1020,7 +985,7 @@ const sketchOnPlaneAndBackSideTest = async (
const u = await getUtils(page) const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -1128,18 +1093,8 @@ const example = extrude(5, exampleSketch)
shell({ faces: ['end'], thickness: 0.25 }, 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 u.waitForAuthSkipAppStart()
await lspStartPromise
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() 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') await page.keyboard.press('Enter')
}) })
test('Auto complete works', async ({ page }) => { test.describe('Autocomplete works', () => {
const u = await getUtils(page) test('with enter/click to accept the completion', async ({ page }) => {
// const PUR = 400 / 37.5 //pixeltoUnitRatio const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) // const PUR = 400 / 37.5 //pixeltoUnitRatio
const lspStartPromise = page.waitForEvent('console', async (message) => { await page.setViewportSize({ width: 1200, height: 500 })
// 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
// this test might be brittle as we add and remove functions await u.waitForAuthSkipAppStart()
// 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() // this test might be brittle as we add and remove functions
await page.keyboard.type('const sketch001 = start') // 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 u.codeLocator.click()
await expect(page.locator('.cm-completionLabel')).toHaveCount(6) await page.keyboard.type('const sketch001 = start')
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 page.keyboard.press('Tab') // expect there to be six auto complete options
await page.keyboard.type('12') await expect(page.locator('.cm-completionLabel')).toHaveCount(6)
await page.waitForTimeout(100) // this makes sure we can accept a completion with click
await page.keyboard.press('Tab') await page.getByText('startSketchOn').click()
await page.waitForTimeout(100) await page.keyboard.type("'XZ'")
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.press('Tab') await page.keyboard.press('Enter')
await page.keyboard.press('Enter') await page.keyboard.type(' |> startProfi')
await page.keyboard.type(' |> lin') // 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.keyboard.press('Tab')
await page.waitForTimeout(100) await page.waitForTimeout(100)
// press arrow down twice then enter to accept xLine await page.keyboard.type('12')
await page.keyboard.press('ArrowDown') await page.waitForTimeout(100)
await page.waitForTimeout(100) await page.keyboard.press('Tab')
await page.keyboard.press('ArrowDown') await page.waitForTimeout(100)
await page.waitForTimeout(100) await page.keyboard.press('Tab')
await page.keyboard.press('Enter') await page.waitForTimeout(100)
await page.waitForTimeout(100) await page.keyboard.press('Tab')
// finish line with comment await page.waitForTimeout(100)
await page.keyboard.type('5') await page.keyboard.press('Enter')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.type(' |> lin')
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 expect(page.locator('.cm-content')) await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
.toHaveText(`const sketch001 = startSketchOn('XZ') 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], %) |> startProfileAt([3.14, 12], %)
|> xLine(5, %) // lin`) |> 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 ({ 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.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Check the settings were reset // 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 ({ test('Project settings can be set and override user settings', async ({
page, page,
}) => { }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) await u.waitForAuthSkipAppStart()
await page await page
.getByRole('button', { name: 'Start Sketch' }) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' }) .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 ({ test('Project settings can be opened with keybinding from the editor', async ({
page, page,
}) => { }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) await u.waitForAuthSkipAppStart()
await page await page
.getByRole('button', { name: 'Start Sketch' }) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' }) .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 }) => { test('Project and user settings can be reset', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) await u.waitForAuthSkipAppStart()
await page await page
.getByRole('button', { name: 'Start Sketch' }) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' }) .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 ({ test('Keyboard shortcuts can be viewed through the help menu', async ({
page, page,
}) => { }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await u.waitForAuthSkipAppStart()
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
await page await page
.getByRole('button', { name: 'Start Sketch' }) .getByRole('button', { name: 'Start Sketch' })
@ -1485,7 +1513,7 @@ test.describe('Onboarding tests', () => {
) )
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
@ -1512,7 +1540,7 @@ test.describe('Onboarding tests', () => {
) )
await page.setViewportSize({ width: 1200, height: 1080 }) await page.setViewportSize({ width: 1200, height: 1080 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
@ -1551,7 +1579,7 @@ test.describe('Onboarding tests', () => {
) )
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Test that the redirect happened // Test that the redirect happened
@ -1603,7 +1631,8 @@ test.describe('Onboarding tests', () => {
) )
await page.setViewportSize({ width: 1200, height: 1080 }) await page.setViewportSize({ width: 1200, height: 1080 })
await page.goto('/') await u.waitForAuthSkipAppStart()
await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, { await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
}) })
@ -1647,8 +1676,10 @@ test.describe('Onboarding tests', () => {
} }
) )
await page.setViewportSize({ width: 1200, height: 1080 }) const u = await getUtils(page)
await page.goto('/') await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
// Test that the text in this step is correct // Test that the text in this step is correct
@ -1703,7 +1734,7 @@ test.describe('Testing selections', () => {
const u = await getUtils(page) const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -2163,7 +2194,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
) )
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -2250,7 +2281,7 @@ const extrude001 = extrude(10, sketch001)
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -2329,7 +2360,7 @@ const part001 = startSketchOn('XZ')
) )
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -2379,7 +2410,7 @@ const extrude001 = extrude(50, sketch001)
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -2463,9 +2494,9 @@ const extrude001 = extrude(50, sketch001)
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
test('Command bar works and can change a setting', async ({ page }) => { 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.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) 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 ({ test('Command bar keybinding works from code editor and can change a setting', async ({
page, page,
}) => { }) => {
// Brief boilerplate const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -2591,7 +2622,7 @@ test.describe('Command bar tests', () => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Make sure the stream is up // Make sure the stream is up
@ -2672,7 +2703,7 @@ test('Can add multiple sketches', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize) await page.setViewportSize(viewportSize)
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -2778,7 +2809,7 @@ test('ProgramMemory can be serialised', async ({ page }) => {
) )
}) })
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
const messages: string[] = [] const messages: string[] = []
// Listen for all console events and push the message text to an array // Listen for all console events and push the message text to an array
@ -2855,7 +2886,7 @@ fn yohey = (pos) => {
selectionsSnippets selectionsSnippets
) )
await page.setViewportSize({ width: 1200, height: 1000 }) await page.setViewportSize({ width: 1200, height: 1000 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -2912,7 +2943,7 @@ test('Deselecting line tool should mean nothing happens on click', async ({
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -3033,7 +3064,7 @@ const part002 = startSketchOn('-XZ')
selectionsSnippets selectionsSnippets
) )
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -3069,7 +3100,7 @@ async function doEditSegmentsByDraggingHandle(page: Page, openPanes: string[]) {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) 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.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) 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.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
@ -3427,7 +3458,7 @@ const doSnapAtDifferentScales = async (
) => { ) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -3542,7 +3573,7 @@ test('Sketch on face', async ({ page }) => {
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -3677,7 +3708,7 @@ test('Can code mod a line length', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -3737,7 +3768,7 @@ test('Extrude from command bar selects extrude line after', async ({
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -3781,7 +3812,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %, $seg01)').click() await page.getByText('line([74.36, 130.4], %, $seg01)').click()
@ -3841,7 +3872,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %, $seg01)').click() await page.getByText('line([74.36, 130.4], %, $seg01)').click()
@ -3940,7 +3971,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click() await page.getByText('line([74.36, 130.4], %)').click()
@ -4048,7 +4079,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click() await page.getByText('line([74.36, 130.4], %)').click()
@ -4155,7 +4186,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click() await page.getByText('line([74.36, 130.4], %)').click()
@ -4265,7 +4296,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click() await page.getByText('line([74.36, 130.4], %)').click()
@ -4341,7 +4372,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click() await page.getByText('line([74.36, 130.4], %)').click()
@ -4437,7 +4468,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click() await page.getByText('line([74.36, 130.4], %)').click()
@ -4514,7 +4545,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click() await page.getByText('line([74.36, 130.4], %)').click()
@ -4561,7 +4592,7 @@ const part002 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('line([3.79, 2.68], %, $seg01)').click() await page.getByText('line([3.79, 2.68], %, $seg01)').click()
@ -4817,7 +4848,7 @@ test.describe('Testing segment overlays', () => {
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -4976,7 +5007,7 @@ const part001 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -5056,7 +5087,7 @@ const part001 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -5184,7 +5215,7 @@ const part001 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -5340,7 +5371,7 @@ const part001 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -5453,7 +5484,7 @@ const part001 = startSketchOn('XZ')
}) })
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -5679,7 +5710,7 @@ ${extraLine ? 'const myVar = segLen(seg01, part001)' : ''}`
) )
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.waitForTimeout(300) await page.waitForTimeout(300)
@ -5837,7 +5868,7 @@ ${extraLine ? 'const myVar = segLen(seg01, part001)' : ''}`
) )
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.waitForTimeout(300) 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 // Wait for the app to be ready for use
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') 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 // Wait for the app to be ready for use
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') 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 }) => { test('simulate network down and network little widget', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// This is how we wait until the stream is online // 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) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -6310,7 +6341,7 @@ test.describe('Testing Gizmo', () => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO) localStorage.setItem('persistCode', TEST_CODE_GIZMO)
}, TEST_CODE_GIZMO) }, TEST_CODE_GIZMO)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// wait for execution done // wait for execution done
@ -6400,7 +6431,7 @@ test.describe('Testing Gizmo', () => {
localStorage.setItem('persistCode', TEST_CODE_GIZMO) localStorage.setItem('persistCode', TEST_CODE_GIZMO)
}, TEST_CODE_GIZMO) }, TEST_CODE_GIZMO)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// wait for execution done // wait for execution done
@ -6534,7 +6565,7 @@ const part001 = startSketchOn('-XZ')
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') 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', browserName !== 'firefox',
"This bug is really Firefox-only, which we don't run in CI." "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.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' }) await u.waitForAuthSkipAppStart()
await page await page
.getByRole('button', { name: 'Start Sketch' }) .getByRole('button', { name: 'Start Sketch' })
.waitFor({ state: 'visible' }) .waitFor({ state: 'visible' })

View File

@ -91,8 +91,9 @@ const part001 = startSketchOn('-XZ')
) )
}) })
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude') await u.waitForCmdReceive('extrude')
@ -330,7 +331,7 @@ const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// wait for execution done // wait for execution done
@ -386,8 +387,8 @@ test('Draft segments should look right', async ({ page, context }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await expect( await expect(
@ -443,7 +444,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -490,7 +491,7 @@ test.describe('Client side scene scale should match engine scale', () => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -589,7 +590,7 @@ test.describe('Client side scene scale should match engine scale', () => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -689,7 +690,7 @@ const part002 = startSketchOn(part001, 'seg01')
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -739,7 +740,7 @@ test('Zoom to fit on load - solid 2d', async ({ page, context }) => {
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
@ -776,7 +777,7 @@ test('Zoom to fit on load - solid 3d', async ({ page, context }) => {
}, KCL_DEFAULT_LENGTH) }, KCL_DEFAULT_LENGTH)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -207,6 +207,23 @@ export const getMovementUtils = (opts: any) => {
return { toSU, click00r } 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) { export async function getUtils(page: Page) {
// Chrome devtools protocol session only works in Chromium // Chrome devtools protocol session only works in Chromium
const browserType = page.context().browser()?.browserType().name() 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) browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
return { return {
waitForAuthSkipAppStart: () => waitForPageLoad(page), waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
removeCurrentCode: () => removeCurrentCode(page), removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd), sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => { updateCamPosition: async (xyz: [number, number, number]) => {

View File

@ -1,3 +0,0 @@
// comment
const hi = 5 + 4

View File

@ -3,7 +3,12 @@
"version": "0.22.6", "version": "0.22.6",
"private": true, "private": true,
"dependencies": { "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/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
@ -11,8 +16,6 @@
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.67", "@kittycad/lib": "^0.0.67",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
"@replit/codemirror-interact": "^6.3.1", "@replit/codemirror-interact": "^6.3.1",
"@tauri-apps/api": "2.0.0-beta.12", "@tauri-apps/api": "2.0.0-beta.12",
@ -23,28 +26,17 @@
"@tauri-apps/plugin-process": "^2.0.0-beta.2", "@tauri-apps/plugin-process": "^2.0.0-beta.2",
"@tauri-apps/plugin-shell": "^2.0.0-beta.2", "@tauri-apps/plugin-shell": "^2.0.0-beta.2",
"@tauri-apps/plugin-updater": "^2.0.0-beta.3", "@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", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1", "@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", "@uiw/react-codemirror": "^4.21.25",
"@xstate/inspect": "^0.8.0", "@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2", "@xstate/react": "^3.2.2",
"crypto-js": "^4.2.0", "codemirror": "^6.0.1",
"debounce-promise": "^3.1.2",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"formik": "^2.4.6",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.4.3", "html2canvas-pro": "^1.5.0",
"http-server": "^14.1.1",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"node-fetch": "^3.3.2",
"re-resizable": "^6.9.11", "re-resizable": "^6.9.11",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -55,18 +47,14 @@
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"swr": "^2.2.5",
"three": "^0.164.1", "three": "^0.164.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vitest": "^1.6.0",
"vscode-jsonrpc": "^8.2.1", "vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-protocol": "^3.17.5",
"wasm-pack": "^0.12.1", "vscode-uri": "^3.0.8",
"web-vitals": "^3.5.2", "web-vitals": "^3.5.2",
"ws": "^8.17.0",
"xstate": "^4.38.2", "xstate": "^4.38.2",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
@ -123,11 +111,14 @@
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@tauri-apps/cli": "^2.0.0-beta.13", "@tauri-apps/cli": "^2.0.0-beta.13",
"@types/crypto-js": "^4.2.2", "@testing-library/jest-dom": "^5.14.1",
"@types/debounce-promise": "^3.1.9", "@testing-library/react": "^15.0.2",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^18.19.31",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.2.25",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.163.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
@ -147,8 +138,11 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10", "happy-dom": "^14.3.10",
"http-server": "^14.1.1",
"husky": "^9.0.11", "husky": "^9.0.11",
"node-fetch": "^3.3.2",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
@ -160,8 +154,11 @@
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"vitest-webgl-canvas-mock": "^1.1.0", "vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0", "wait-on": "^7.2.0",
"wasm-pack": "^0.12.1",
"ws": "^8.17.0",
"yarn": "^1.22.22" "yarn": "^1.22.22"
} }
} }

12
src-tauri/Cargo.lock generated
View File

@ -1212,7 +1212,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.18" version = "0.1.19"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"convert_case 0.6.0", "convert_case 0.6.0",
@ -2576,7 +2576,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.67" version = "0.1.68"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -6029,9 +6029,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "ts-rs" name = "ts-rs"
version = "9.0.0" version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357" checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde_json", "serde_json",
@ -6043,9 +6043,9 @@ dependencies = [
[[package]] [[package]]
name = "ts-rs-macros" name = "ts-rs-macros"
version = "9.0.0" version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdee324e50a7402416d9c25270d3df4241ed528af5d36dda18b6f219551c577" checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -568,6 +568,7 @@ export class SceneEntities {
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false }) if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
sceneInfra.resetMouseListeners() sceneInfra.resetMouseListeners()
const { truncatedAst, programMemoryOverride, sketchGroup } = const { truncatedAst, programMemoryOverride, sketchGroup } =
await this.setupSketch({ await this.setupSketch({
sketchPathToNode, sketchPathToNode,

View File

@ -10,7 +10,7 @@ import React, {
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Client from '../editor/plugins/lsp/client' import Client from '../editor/plugins/lsp/client'
import { TEST, VITE_KC_API_BASE_URL } from 'env' 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 { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -31,6 +31,8 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { useNetworkContext } from 'hooks/useNetworkContext' import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus' import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { isTauri } from 'lib/isTauri'
import { codeManager } from 'lib/singletons'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -128,17 +130,31 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const fromServer: FromServer | Error = FromServer.create() const fromServer: FromServer | Error = FromServer.create()
if (err(fromServer)) return { lspClient: null } if (err(fromServer)) return { lspClient: null }
const client = new Client(fromServer, intoServer) const client = new Client(fromServer, intoServer, () => {
setIsLspReady(true)
setIsLspReady(true) })
const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl }) const lspClient = new LanguageServerClient({ client, name: LspWorker.Kcl })
return { lspClient } return { lspClient }
}, [ }, [
// We need a token for authenticating the server. // We need a token for authenticating the server.
token, 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. // 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 // 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 // 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 let plugin = null
if (isKclLspServerReady && !TEST && kclLspClient) { if (isKclLspServerReady && !TEST && kclLspClient) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = kclLanguage({ const lsp = new KclLanguageSupport({
documentUri: `file:///${PROJECT_ENTRYPOINT}`, documentUri: `file:///${PROJECT_ENTRYPOINT}`,
workspaceFolders: getWorkspaceFolders(), workspaceFolders: getWorkspaceFolders(),
client: kclLspClient, client: kclLspClient,
@ -205,9 +221,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const fromServer: FromServer | Error = FromServer.create() const fromServer: FromServer | Error = FromServer.create()
if (err(fromServer)) return { lspClient: null } if (err(fromServer)) return { lspClient: null }
const client = new Client(fromServer, intoServer) const client = new Client(fromServer, intoServer, () => {
setIsCopilotReady(true)
setIsCopilotReady(true) })
const lspClient = new LanguageServerClient({ const lspClient = new LanguageServerClient({
client, client,

View File

@ -71,7 +71,7 @@ import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine' import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast' 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 { CoreDumpManager } from 'lib/coredump'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
@ -80,6 +80,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -281,11 +282,15 @@ export const ModelingMachineProvider = ({
const dispatchSelection = (selection?: EditorSelection) => { const dispatchSelection = (selection?: EditorSelection) => {
if (!selection) return // TODO less of hack for the below please if (!selection) return // TODO less of hack for the below please
if (!editorManager.editorView) return if (!editorManager.editorView) return
editorManager.lastSelectionEvent = Date.now()
setTimeout(() => { setTimeout(() => {
if (editorManager.editorView) { if (!editorManager.editorView) return
editorManager.editorView.dispatch({ selection }) editorManager.editorView.dispatch({
} selection,
annotations: [
modelingMachineEvent,
Transaction.addToHistory.of(false),
],
})
}) })
} }
let selections: Selections = { let selections: Selections = {
@ -328,11 +333,6 @@ export const ModelingMachineProvider = ({
) )
updateSceneObjectColors() 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 { return {
selectionRanges: selections, selectionRanges: selections,
} }

View File

@ -84,6 +84,10 @@ export const KclEditorPane = () => {
const textWrapping = context.textEditor.textWrapping const textWrapping = context.textEditor.textWrapping
const cursorBlinking = context.textEditor.blinkingCursor 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 codeMirrorHotkeys = codeManager.getCodemirrorHotkeys()
const editorExtensions = useMemo(() => { const editorExtensions = useMemo(() => {
@ -134,7 +138,6 @@ export const KclEditorPane = () => {
highlightSelectionMatches(), highlightSelectionMatches(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }), syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
rectangularSelection(), rectangularSelection(),
drawSelection(),
dropCursor(), dropCursor(),
interact({ interact({
rules: [ rules: [
@ -173,13 +176,7 @@ export const KclEditorPane = () => {
} }
return extensions return extensions
}, [ }, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
codeMirrorHotkeys,
])
const initialCode = useRef(codeManager.code) const initialCode = useRef(codeManager.code)
@ -192,9 +189,9 @@ export const KclEditorPane = () => {
value={initialCode.current} value={initialCode.current}
extensions={editorExtensions} extensions={editorExtensions}
theme={theme} theme={theme}
onCreateEditor={(_editorView) => onCreateEditor={(_editorView) => {
editorManager.setEditorView(_editorView) editorManager.setEditorView(_editorView)
} }}
indentWithTab={false} indentWithTab={false}
basicSetup={false} basicSetup={false}
/> />

View File

@ -1,13 +1,25 @@
import { hasNextSnippetField } from '@codemirror/autocomplete'
import { EditorView, ViewUpdate } from '@codemirror/view' import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, SelectionRange } from '@codemirror/state' import { EditorSelection, Annotation, Transaction } from '@codemirror/state'
import { engineCommandManager, sceneInfra } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections' import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands' import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine' import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight } from './highlightextension' import { addLineHighlight } from './highlightextension'
import { forEachDiagnostic, setDiagnostics, Diagnostic } from '@codemirror/lint' import {
forEachDiagnostic,
Diagnostic,
setDiagnosticsEffect,
} from '@codemirror/lint'
const updateOutsideEditorAnnotation = Annotation.define<null>()
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(null)
const modelingMachineAnnotation = Annotation.define<null>()
export const modelingMachineEvent = modelingMachineAnnotation.of(null)
const setDiagnosticsAnnotation = Annotation.define<null>()
export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(null)
function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean { function diagnosticIsEqual(d1: Diagnostic, d2: Diagnostic): boolean {
return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message return d1.from === d2.from && d1.to === d2.to && d1.message === d2.message
@ -22,8 +34,6 @@ export default class EditorManager {
codeBasedSelections: [], codeBasedSelections: [],
} }
private _lastSelectionEvent: number | null = null
lastSelection: string = ''
private _lastEvent: { event: string; time: number } | null = null private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {} private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
@ -57,10 +67,6 @@ export default class EditorManager {
this._selectionRanges = selectionRanges this._selectionRanges = selectionRanges
} }
set lastSelectionEvent(time: number) {
this._lastSelectionEvent = time
}
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) { set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
this._modelingSend = send this._modelingSend = send
} }
@ -83,32 +89,38 @@ export default class EditorManager {
setHighlightRange(selection: Selection['range']): void { setHighlightRange(selection: Selection['range']): void {
this._highlightRange = selection this._highlightRange = selection
const editorView = this.editorView
const safeEnd = Math.min( const safeEnd = Math.min(
selection[1], selection[1],
editorView?.state.doc.length || selection[1] this._editorView?.state.doc.length || selection[1]
) )
if (editorView) { if (this._editorView) {
editorView.dispatch({ this._editorView.dispatch({
effects: addLineHighlight.of([selection[0], safeEnd]), effects: addLineHighlight.of([selection[0], safeEnd]),
annotations: [
updateOutsideEditorEvent,
Transaction.addToHistory.of(false),
],
}) })
} }
} }
clearDiagnostics(): void { clearDiagnostics(): void {
if (!this.editorView) return this.setDiagnostics([])
this.editorView.dispatch(setDiagnostics(this.editorView.state, []))
} }
setDiagnostics(diagnostics: Diagnostic[]): void { setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return if (!this._editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
this._editorView.dispatch({
effects: [setDiagnosticsEffect.of(diagnostics)],
annotations: [setDiagnosticsEvent, Transaction.addToHistory.of(false)],
})
} }
addDiagnostics(diagnostics: Diagnostic[]): void { 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) diagnostics.push(diag)
}) })
@ -122,9 +134,7 @@ export default class EditorManager {
uniqueDiagnostics.add(diagnostic) uniqueDiagnostics.add(diagnostic)
}) })
this.editorView.dispatch( this.setDiagnostics([...uniqueDiagnostics])
setDiagnostics(this.editorView.state, [...uniqueDiagnostics])
)
} }
undo() { undo() {
@ -174,50 +184,33 @@ export default class EditorManager {
].range[1] ].range[1]
) )
) )
if (!this.editorView) {
if (!this._editorView) {
return return
} }
this.editorView.dispatch({
this._editorView.dispatch({
selection: EditorSelection.create(codeBasedSelections, 1), 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 { handleOnViewUpdate(viewUpdate: ViewUpdate): void {
// If we are just fucking around in a snippet, return early and don't if (!this._editorView) {
// 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) {
this.setEditorView(viewUpdate.view) this.setEditorView(viewUpdate.view)
} }
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === this.lastSelection) { const ranges = viewUpdate?.state?.selection?.ranges || []
// onUpdate is noisy and is fired a lot by extensions if (ranges.length === 0) {
// since we're only interested in selections changes we can ignore most of these.
return 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'][] = [ const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool', '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('&')
}

View File

@ -67,8 +67,13 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
#fromServer: FromServer #fromServer: FromServer
private serverCapabilities: LSP.ServerCapabilities<any> = {} private serverCapabilities: LSP.ServerCapabilities<any> = {}
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
private initializedCallback: () => void
constructor(fromServer: FromServer, intoServer: IntoServer) { constructor(
fromServer: FromServer,
intoServer: IntoServer,
initializedCallback: () => void
) {
super( super(
new jsrpc.JSONRPCServer(), new jsrpc.JSONRPCServer(),
new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => { new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => {
@ -82,6 +87,7 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
}) })
) )
this.#fromServer = fromServer this.#fromServer = fromServer
this.initializedCallback = initializedCallback
} }
async start(): Promise<void> { async start(): Promise<void> {
@ -163,6 +169,8 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
// notify "initialized": client --> server // notify "initialized": client --> server
this.notify(LSP.InitializedNotification.type.method, {}) this.notify(LSP.InitializedNotification.type.method, {})
this.initializedCallback()
await Promise.all( await Promise.all(
this.afterInitializedHooks.map((f: () => Promise<void>) => f()) this.afterInitializedHooks.map((f: () => Promise<void>) => f())
) )

View File

@ -4,6 +4,7 @@ import {
Decoration, Decoration,
DecorationSet, DecorationSet,
EditorView, EditorView,
PluginValue,
ViewPlugin, ViewPlugin,
ViewUpdate, ViewUpdate,
} from '@codemirror/view' } from '@codemirror/view'
@ -11,7 +12,6 @@ import {
Annotation, Annotation,
EditorState, EditorState,
Extension, Extension,
Prec,
StateEffect, StateEffect,
StateField, StateField,
Transaction, Transaction,
@ -19,15 +19,30 @@ import {
import { completionStatus } from '@codemirror/autocomplete' import { completionStatus } from '@codemirror/autocomplete'
import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util' import { offsetToPos, posToOffset } from 'editor/plugins/lsp/util'
import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp' import { LanguageServerOptions, LanguageServerClient } from 'editor/plugins/lsp'
import { deferExecution } from 'lib/utils'
import { import {
LanguageServerPlugin, LanguageServerPlugin,
documentUri, TransactionAnnotation,
docPathFacet,
languageId, languageId,
updateInfo,
workspaceFolders, workspaceFolders,
RelevantUpdate,
} from 'editor/plugins/lsp/plugin' } from 'editor/plugins/lsp/plugin'
const copilotPluginAnnotation = Annotation.define<null>()
export const copilotPluginEvent = copilotPluginAnnotation.of(null)
// Effects to tell StateEffect what to do with GhostText
const addSuggestion = StateEffect.define<Suggestion>()
const acceptSuggestion = StateEffect.define<null>()
const clearSuggestion = StateEffect.define<null>()
const typeFirst = StateEffect.define<number>()
const ghostMark = Decoration.mark({ class: 'cm-ghostText' }) const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
const changesDelay = 600
interface Suggestion { interface Suggestion {
text: string text: string
displayText: string displayText: string
@ -38,15 +53,10 @@ interface Suggestion {
uuid: string uuid: string
} }
// Effects to tell StateEffect what to do with GhostText
const addSuggestion = StateEffect.define<Suggestion>()
const acceptSuggestion = StateEffect.define<null>()
const clearSuggestion = StateEffect.define<null>()
const typeFirst = StateEffect.define<number>()
interface CompletionState { interface CompletionState {
ghostText: GhostText | null ghostText: GhostText | null
} }
interface GhostText { interface GhostText {
text: string text: string
displayText: string displayText: string
@ -65,6 +75,16 @@ export const completionDecoration = StateField.define<CompletionState>({
return { ghostText: null } return { ghostText: null }
}, },
update(state: CompletionState, transaction: Transaction) { 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) { for (const effect of transaction.effects) {
if (effect.is(addSuggestion)) { if (effect.is(addSuggestion)) {
// When adding a suggestion, we set th ghostText // When adding a suggestion, we set th ghostText
@ -160,126 +180,376 @@ export const completionDecoration = StateField.define<CompletionState>({
), ),
}) })
const copilotEvent = Annotation.define<null>() export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
const infos = updateInfo(update)
/**************************************************************************** // Make sure we are not in a snippet
************************* COMMANDS ****************************************** if (infos.some((info) => info.inSnippet)) {
*****************************************************************************/ return {
overall: false,
const acceptSuggestionCommand = ( userSelect: false,
copilotClient: LanguageServerClient, time: null,
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
} }
const ghostTextStart = ghostText.displayPos return {
const ghostTextEnd = ghostText.endGhostText overall: infos.some(
(info) =>
const actualTextStart = ghostText.startPos update.focusChanged ||
const actualTextEnd = ghostText.endPos info.annotations.includes(TransactionAnnotation.UserSelect) ||
info.annotations.includes(TransactionAnnotation.UserInput) ||
const replacementEnd = ghostText.endReplacement info.annotations.includes(TransactionAnnotation.UserDelete) ||
info.annotations.includes(TransactionAnnotation.UserUndo) ||
const suggestion = ghostText.text info.annotations.includes(TransactionAnnotation.UserRedo) ||
info.annotations.includes(TransactionAnnotation.UserMove) ||
view.dispatch({ info.annotations.includes(TransactionAnnotation.Copoilot)
changes: { ),
from: ghostTextStart, userSelect: infos.some((info) =>
to: ghostTextEnd, info.annotations.includes(TransactionAnnotation.UserSelect)
insert: '', ),
}, time: infos.length ? infos[0].time : null,
// 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
} }
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 = ( // A view plugin that requests completions from the server after a delay
copilotClient: LanguageServerClient, export class CompletionRequester implements PluginValue {
view: EditorView, private client: LanguageServerClient
key: string private lastPos: number = 0
) => { private viewUpdate: ViewUpdate | null = null
// 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)
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) { private _deffererCodeUpdate = deferExecution(() => {
view.dispatch({ if (this.viewUpdate === null) {
selection: { anchor: ghostTextStart + indent.length }, return
effects: typeFirst.of(indent.length), }
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
}) this.requestCompletions()
return true }, changesDelay)
} else if (key === 'Tab') {
return acceptSuggestionCommand(copilotClient, view) private _deffererUserSelect = deferExecution(() => {
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) { if (this.viewUpdate === null) {
return rejectSuggestionCommand(copilotClient, view) return
} else if (ghostText.displayText.length === 1) { }
return acceptSuggestionCommand(copilotClient, view)
} else { this.rejectSuggestionCommand()
// Use this to delete the first letter of the suggestion }, changesDelay)
view.dispatch({
selection: { anchor: ghostTextStart + 1 }, constructor(client: LanguageServerClient) {
effects: typeFirst.of(1), this.client = client
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], }
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<void> {
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 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) => export const copilotPlugin = (options: LanguageServerOptions): Extension => {
EditorView.domEventHandlers({ const completionPlugin = ViewPlugin.define((view) => {
return new CompletionRequester(options.client)
})
const domHandlers = EditorView.domEventHandlers({
keydown(event, view) { keydown(event, view) {
if ( if (
event.key !== 'Shift' && event.key !== 'Shift' &&
@ -287,204 +557,29 @@ const completionPlugin = (copilotClient: LanguageServerClient) =>
event.key !== 'Alt' && event.key !== 'Alt' &&
event.key !== 'Meta' 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 { } else {
return false 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 [ return [
documentUri.of(options.documentUri), docPathFacet.of(options.documentUri),
languageId.of('kcl'), languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders), workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define( ViewPlugin.define(
(view) => (view) =>
new LanguageServerPlugin(options.client, view, options.allowHTMLContent) new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
), ),
completionPlugin,
domHandlers,
completionDecoration, completionDecoration,
Prec.highest(completionPlugin(options.client)),
Prec.highest(viewCompletionPlugin(options.client)),
completionRequester(options.client),
] ]
} }

View File

@ -1,6 +1,5 @@
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client' import Client from './client'
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens'
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin' import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin'
import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams' import { CopilotLspCompletionParams } from 'wasm-lib/kcl/bindings/CopilotLspCompletionParams'
import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse' import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompletionResponse'
@ -68,7 +67,7 @@ export interface LanguageServerOptions {
export class LanguageServerClient { export class LanguageServerClient {
private client: Client private client: Client
readonly name: string readonly name: LspWorker
public ready: boolean public ready: boolean
@ -76,8 +75,6 @@ export class LanguageServerClient {
public initializePromise: Promise<void> public initializePromise: Promise<void>
private isUpdatingSemanticTokens: boolean = false
private semanticTokens: SemanticToken[] = []
private queuedUids: string[] = [] private queuedUids: string[] = []
constructor(options: LanguageServerClientOptions) { constructor(options: LanguageServerClientOptions) {
@ -111,19 +108,10 @@ export class LanguageServerClient {
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
this.notify('textDocument/didOpen', params) 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) { textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
this.notify('textDocument/didChange', params) this.notify('textDocument/didChange', params)
this.updateSemanticTokens(params.textDocument.uri)
} }
textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) { textDocumentDidClose(params: LSP.DidCloseTextDocumentParams) {
@ -134,18 +122,9 @@ export class LanguageServerClient {
added: LSP.WorkspaceFolder[], added: LSP.WorkspaceFolder[],
removed: 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', { this.notify('workspace/didChangeWorkspaceFolders', {
event: { added, removed }, 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) { workspaceDidCreateFiles(params: LSP.CreateFilesParams) {
@ -160,33 +139,13 @@ export class LanguageServerClient {
this.notify('workspace/didDeleteFiles', params) this.notify('workspace/didDeleteFiles', params)
} }
async updateSemanticTokens(uri: string) { async textDocumentSemanticTokensFull(params: LSP.SemanticTokensParams) {
const serverCapabilities = this.getServerCapabilities() const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) { if (!serverCapabilities.semanticTokensProvider) {
return return
} }
// Make sure we can only run, if we aren't already running. return this.request('textDocument/semanticTokens/full', params)
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
} }
async textDocumentHover(params: LSP.HoverParams) { async textDocumentHover(params: LSP.HoverParams) {

View File

@ -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 { Extension, EditorState, Prec } from '@codemirror/state'
import { import {
ViewPlugin, ViewPlugin,
@ -7,6 +17,8 @@ import {
keymap, keymap,
KeyBinding, KeyBinding,
tooltips, tooltips,
PluginValue,
ViewUpdate,
} from '@codemirror/view' } from '@codemirror/view'
import { CompletionTriggerKind } from 'vscode-languageserver-protocol' import { CompletionTriggerKind } from 'vscode-languageserver-protocol'
import { offsetToPos } from 'editor/plugins/lsp/util' 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 { syntaxTree, indentService, foldService } from '@codemirror/language'
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint' import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
import { import {
docPathFacet,
LanguageServerPlugin, LanguageServerPlugin,
documentUri,
languageId, languageId,
workspaceFolders, workspaceFolders,
updateInfo,
RelevantUpdate,
TransactionAnnotation,
} from 'editor/plugins/lsp/plugin' } from 'editor/plugins/lsp/plugin'
import { deferExecution } from 'lib/utils'
import { codeManager, editorManager, kclManager } from 'lib/singletons'
const changesDelay = 600
export const kclIndentService = () => { export const kclIndentService = () => {
// Match the indentation of the previous line (if present). // 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 { export function kclPlugin(options: LanguageServerOptions): Extension {
let plugin: LanguageServerPlugin | null = null let plugin: LanguageServerPlugin | null = null
const viewPlugin = ViewPlugin.define( const viewPlugin = ViewPlugin.define(
@ -58,8 +152,8 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
// Get the current plugin from the map. // Get the current plugin from the map.
const p = view.plugin(viewPlugin) const p = view.plugin(viewPlugin)
if (p === null) return false if (p === null) return false
p.requestFormatting() p.requestFormatting()
return true return true
}, },
@ -68,6 +162,39 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
// Create an extension for the key mappings. // Create an extension for the key mappings.
const kclKeymapExt = Prec.highest(keymap.computeN([], () => [kclKeymap])) 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( const folding = foldService.of(
(state: EditorState, lineStart: number, lineEnd: number) => { (state: EditorState, lineStart: number, lineEnd: number) => {
if (plugin == null) return null if (plugin == null) return null
@ -79,10 +206,11 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
) )
return [ return [
documentUri.of(options.documentUri), docPathFacet.of(options.documentUri),
languageId.of('kcl'), languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders), workspaceFolders.of(options.workspaceFolders),
viewPlugin, viewPlugin,
ViewPlugin.define((view) => new KclPlugin()),
kclKeymapExt, kclKeymapExt,
kclIndentService(), kclIndentService(),
hoverTooltip( hoverTooltip(
@ -104,8 +232,9 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
return diagnostics return diagnostics
}), }),
folding, folding,
autocompleteKeymapExt,
autocompletion({ autocompletion({
defaultKeymap: true, defaultKeymap: false,
override: [ override: [
async (context) => { async (context) => {
if (plugin == null) return null if (plugin == null) return null

View File

@ -8,10 +8,19 @@ import {
import { LanguageServerClient } from 'editor/plugins/lsp' import { LanguageServerClient } from 'editor/plugins/lsp'
import { kclPlugin } from '.' import { kclPlugin } from '.'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { parser as jsParser } from '@lezer/javascript' import KclParser from './parser'
import { EditorState } from '@uiw/react-codemirror' 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 { export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[] workspaceFolders: LSP.WorkspaceFolder[]
@ -28,34 +37,24 @@ class KclLanguage extends Language {
client: options.client, client: options.client,
}) })
const parser = new KclParser()
super( super(
data, data,
// For now let's use the javascript parser. // For now let's use the javascript parser.
// It works really well and has good syntax highlighting. // It works really well and has good syntax highlighting.
// We can use our lsp for the rest. // We can use our lsp for the rest.
jsParser, parser,
[ [plugin],
plugin,
EditorState.languageData.of(() => [
{
// https://codemirror.net/docs/ref/#commands.CommentTokens
commentTokens: {
line: '//',
block: {
open: '/*',
close: '*/',
},
},
},
]),
],
'kcl' 'kcl'
) )
} }
} }
export default function kclLanguage(options: LanguageOptions): LanguageSupport { export default class KclLanguageSupport extends LanguageSupport {
const lang = new KclLanguage(options) constructor(options: LanguageOptions) {
const lang = new KclLanguage(options)
return new LanguageSupport(lang) super(lang, [semanticTokenField])
}
} }

View File

@ -1,4 +1,6 @@
// Extends the codemirror Parser for kcl. // Extends the codemirror Parser for kcl.
// This is really just a no-op parser since we use semantic tokens from the LSP
// server.
import { import {
Parser, Parser,
@ -7,91 +9,27 @@ import {
PartialParse, PartialParse,
Tree, Tree,
NodeType, NodeType,
NodeSet,
} from '@lezer/common' } 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 { DocInput } from '@codemirror/language'
import { tags, styleTags } from '@lezer/highlight'
export default class KclParser extends Parser { export default class KclParser extends Parser {
private client: LanguageServerClient
constructor(client: LanguageServerClient) {
super()
this.client = client
}
createParse( createParse(
input: Input, input: Input,
fragments: readonly TreeFragment[], fragments: readonly TreeFragment[],
ranges: readonly { from: number; to: number }[] ranges: readonly { from: number; to: number }[]
): PartialParse { ): PartialParse {
let parse: PartialParse = new Context(this, input, fragments, ranges) let parse: PartialParse = new Context(input)
return parse return parse
} }
getTokenTypes(): string[] {
return this.client.getServerCapabilities().semanticTokensProvider!.legend
.tokenTypes
}
getSemanticTokens(): SemanticToken[] {
return this.client.getSemanticTokens()
}
} }
class Context implements PartialParse { class Context implements PartialParse {
private parser: KclParser
private input: DocInput private input: DocInput
private fragments: readonly TreeFragment[]
private ranges: readonly { from: number; to: number }[]
private nodeTypes: { [key: string]: NodeType }
stoppedAt: number = 0 stoppedAt: number = 0
private semanticTokens: SemanticToken[] = [] constructor(input: Input) {
private currentLine: number = 0
private currentColumn: number = 0
private nodeSet: NodeSet
constructor(
/// The parser configuration used.
parser: KclParser,
input: Input,
fragments: readonly TreeFragment[],
ranges: readonly { from: number; to: number }[]
) {
this.parser = parser
this.input = input as DocInput this.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 { get parsedPos(): number {
@ -99,67 +37,8 @@ class Context implements PartialParse {
} }
advance(): Tree | null { 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 this.stoppedAt = this.input.doc.length
return tree return new Tree(NodeType.none, [], [], this.input.doc.length)
}
createTree(token: SemanticToken, index: number): Tree {
const changedLine = token.delta_line !== 0
this.currentLine += token.delta_line
if (changedLine) {
this.currentColumn = 0
}
this.currentColumn += token.delta_start
// Let's get our position relative to the start of the file.
let currentPosition = posToOffset(this.input.doc, {
line: this.currentLine,
character: this.currentColumn,
})
const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id]
if (currentPosition === undefined) {
// This is bad and weird.
return new Tree(nodeType, [], [], token.length)
}
if (index >= this.semanticTokens.length - 1) {
// We have no children.
return new Tree(nodeType, [], [], token.length)
}
const nextIndex = index + 1
const nextToken = this.semanticTokens[nextIndex]
const changedLineNext = nextToken.delta_line !== 0
const nextLine = this.currentLine + nextToken.delta_line
const nextColumn = changedLineNext
? nextToken.delta_start
: this.currentColumn + nextToken.delta_start
const nextPosition = posToOffset(this.input.doc, {
line: nextLine,
character: nextColumn,
})
if (nextPosition === undefined) {
// This is bad and weird.
return new Tree(nodeType, [], [], token.length)
}
// Let's get the
return new Tree(
nodeType,
[this.createTree(nextToken, nextIndex)],
// The positions (offsets relative to the start of this tree) of the children.
[nextPosition - currentPosition],
token.length
)
} }
stopAt(pos: number) { stopAt(pos: number) {

View File

@ -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<SemanticToken[]> {
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
}

View File

@ -1,7 +1,24 @@
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete' import {
import { setDiagnostics } from '@codemirror/lint' completeFromList,
import { Facet } from '@codemirror/state' hasNextSnippetField,
import { EditorView, Tooltip } from '@codemirror/view' 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 { import {
DiagnosticSeverity, DiagnosticSeverity,
CompletionItemKind, CompletionItemKind,
@ -21,49 +38,247 @@ import { LanguageServerClient } from 'editor/plugins/lsp'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { posToOffset } from 'editor/plugins/lsp/util' import { posToOffset } from 'editor/plugins/lsp/util'
import { Program, ProgramMemory } from 'lang/wasm' 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 type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' 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, '') const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const documentUri = Facet.define<string, string>({ combine: useLast }) export const docPathFacet = Facet.define<string, string>({
combine: useLast,
})
export const languageId = Facet.define<string, string>({ combine: useLast }) export const languageId = Facet.define<string, string>({ combine: useLast })
export const workspaceFolders = Facet.define< export const workspaceFolders = Facet.define<
LSP.WorkspaceFolder[], LSP.WorkspaceFolder[],
LSP.WorkspaceFolder[] LSP.WorkspaceFolder[]
>({ combine: useLast }) >({ combine: useLast })
enum LspAnnotation {
SemanticTokens = 'semantic-tokens',
}
const lspEvent = Annotation.define<LspAnnotation>()
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
const CompletionItemKindMap = Object.fromEntries( const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
) as Record<CompletionItemKind, string> ) as Record<CompletionItemKind, string>
const changesDelay = 600 const changesDelay = 600
let debounceTimer: ReturnType<typeof setTimeout> | null = null
const updateDelay = 100 const addToken = StateEffect.define<SemanticToken>({
map: (token: SemanticToken, change) => ({
...token,
from: change.mapPos(token.from),
to: change.mapPos(token.to),
}),
})
export const semanticTokenField = StateField.define<DecorationSet>({
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 { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
public documentUri: string
public languageId: string
public workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
private foldingRanges: LSP.FoldingRange[] | null = null private foldingRanges: LSP.FoldingRange[] | null = null
private viewUpdate: ViewUpdate | null = null
private previousSemanticTokens: SemanticToken[] = []
private _defferer = deferExecution((code: string) => { private _defferer = deferExecution((code: string) => {
try { try {
// Update the state (not the editor) with the new code. // Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({ this.client.textDocumentDidChange({
textDocument: { textDocument: {
uri: this.documentUri, uri: this.getDocUri(),
version: this.documentVersion++, version: this.documentVersion++,
}, },
contentChanges: [{ text: code }], contentChanges: [{ text: code }],
}) })
if (this.viewUpdate) { this.requestSemanticTokens(this.view)
editorManager.handleOnViewUpdate(this.viewUpdate)
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -75,41 +290,43 @@ export class LanguageServerPlugin implements PluginValue {
private allowHTMLContent: boolean private allowHTMLContent: boolean
) { ) {
this.client = client 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.documentVersion = 0
this.client.attachPlugin(this) this.client.attachPlugin(this)
this.initialize({ this.initialize({
documentText: this.view.state.doc.toString(), documentText: this.getDocText(),
}) })
} }
update(viewUpdate: ViewUpdate) { private getDocPath(view = this.view) {
this.viewUpdate = viewUpdate return view.state.facet(docPathFacet)
if (!viewUpdate.docChanged) { }
// debounce the view update. private getDocText(view = this.view) {
// otherwise it is laggy for typing. return view.state.doc.toString()
if (debounceTimer) { }
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => { private getDocUri(view = this.view) {
editorManager.handleOnViewUpdate(viewUpdate) return URI.file(this.getDocPath(view)).toString()
}, updateDelay) }
private getLanguageId(view = this.view) {
return view.state.facet(languageId)
}
update(viewUpdate: ViewUpdate) {
const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) {
return return
} }
const newCode = this.view.state.doc.toString() // If the doc didn't change we can return early.
if (!viewUpdate.docChanged) {
codeManager.code = newCode return
codeManager.writeToFile() }
kclManager.executeCode()
this.sendChange({ this.sendChange({
documentText: newCode, documentText: viewUpdate.state.doc.toString(),
}) })
} }
@ -121,14 +338,17 @@ export class LanguageServerPlugin implements PluginValue {
if (this.client.initializePromise) { if (this.client.initializePromise) {
await this.client.initializePromise await this.client.initializePromise
} }
this.client.textDocumentDidOpen({ this.client.textDocumentDidOpen({
textDocument: { textDocument: {
uri: this.documentUri, uri: this.getDocUri(),
languageId: this.languageId, languageId: this.getLanguageId(),
text: documentText, text: documentText,
version: this.documentVersion, version: this.documentVersion,
}, },
}) })
this.requestSemanticTokens(this.view)
} }
async sendChange({ documentText }: { documentText: string }) { async sendChange({ documentText }: { documentText: string }) {
@ -138,7 +358,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
requestDiagnostics(view: EditorView) { requestDiagnostics(view: EditorView) {
this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: this.getDocText() })
} }
async requestHoverTooltip( async requestHoverTooltip(
@ -151,9 +371,9 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: this.getDocText() })
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.getDocUri() },
position: { line, character }, position: { line, character },
}) })
if (!result) return null if (!result) return null
@ -181,7 +401,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
const result = await this.client.textDocumentFoldingRange({ const result = await this.client.textDocumentFoldingRange({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.getDocUri() },
}) })
return result || null return result || null
@ -228,9 +448,9 @@ export class LanguageServerPlugin implements PluginValue {
return await this.client.updateUnits({ return await this.client.updateUnits({
textDocument: { textDocument: {
uri: this.documentUri, uri: this.getDocUri(),
}, },
text: this.view.state.doc.toString(), text: this.getDocText(),
units, units,
}) })
} }
@ -254,7 +474,6 @@ export class LanguageServerPlugin implements PluginValue {
}) })
} }
} }
console.log('[lsp] kcl: updated canExecute', canExecute, response)
return response return response
} }
@ -267,14 +486,14 @@ export class LanguageServerPlugin implements PluginValue {
this.client.textDocumentDidChange({ this.client.textDocumentDidChange({
textDocument: { textDocument: {
uri: this.documentUri, uri: this.getDocUri(),
version: this.documentVersion++, version: this.documentVersion++,
}, },
contentChanges: [{ text: this.view.state.doc.toString() }], contentChanges: [{ text: this.getDocText() }],
}) })
const result = await this.client.textDocumentFormatting({ const result = await this.client.textDocumentFormatting({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.getDocUri() },
options: { options: {
tabSize: 2, tabSize: 2,
insertSpaces: true, insertSpaces: true,
@ -285,16 +504,8 @@ export class LanguageServerPlugin implements PluginValue {
if (!result) return null if (!result) return null
for (let i = 0; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
const { range, newText } = result[i] const { newText } = result[i]
this.view.dispatch({ codeManager.updateCodeStateEditor(newText)
changes: [
{
from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!,
insert: newText,
},
],
})
} }
} }
@ -320,7 +531,7 @@ export class LanguageServerPlugin implements PluginValue {
}) })
const result = await this.client.textDocumentCompletion({ const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.getDocUri() },
position: { line, character }, position: { line, character },
context: { context: {
triggerKind, triggerKind,
@ -379,16 +590,107 @@ export class LanguageServerPlugin implements PluginValue {
return completeFromList(options)(context) 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<SemanticToken | Extension>[] =
this.previousSemanticTokens.map((tokenRange: any) =>
addToken.of(tokenRange)
)
view.dispatch({
effects,
annotations: [lspSemanticTokensEvent, Transaction.addToHistory.of(false)],
})
}
async processNotification(notification: LSP.NotificationMessage) { async processNotification(notification: LSP.NotificationMessage) {
try { try {
switch (notification.method) { switch (notification.method) {
case 'textDocument/publishDiagnostics': 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( console.log(
'[lsp] [window/publishDiagnostics]', '[lsp] [window/publishDiagnostics]',
this.client.getName(), this.client.getName(),
notification.params params
) )
const params = notification.params as PublishDiagnosticsParams
// this is sometimes slower than our actual typing. // this is sometimes slower than our actual typing.
this.processDiagnostics(params) this.processDiagnostics(params)
break break
@ -420,7 +722,6 @@ export class LanguageServerPlugin implements PluginValue {
// The server has updated the memory, we should update elsewhere. // The server has updated the memory, we should update elsewhere.
let updatedMemory = notification.params as ProgramMemory let updatedMemory = notification.params as ProgramMemory
console.log('[lsp]: Updated Memory', updatedMemory) console.log('[lsp]: Updated Memory', updatedMemory)
kclManager.programMemory = updatedMemory
break break
} }
} catch (error) { } catch (error) {
@ -429,7 +730,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
processDiagnostics(params: PublishDiagnosticsParams) { processDiagnostics(params: PublishDiagnosticsParams) {
if (params.uri !== this.documentUri) return if (params.uri !== this.getDocUri()) return
const diagnostics = params.diagnostics const diagnostics = params.diagnostics
.map(({ range, message, severity }) => ({ .map(({ range, message, severity }) => ({
@ -459,7 +760,7 @@ export class LanguageServerPlugin implements PluginValue {
return 0 return 0
}) })
this.view.dispatch(setDiagnostics(this.view.state, diagnostics)) editorManager.addDiagnostics(diagnostics)
} }
} }

View File

@ -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
}

View File

@ -6,10 +6,13 @@ import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs' import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons' 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 PERSIST_CODE_TOKEN = 'persistCode'
const codeManagerUpdateAnnotation = Annotation.define<null>()
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(null)
export default class CodeManager { export default class CodeManager {
private _code: string = bracket private _code: string = bracket
#updateState: (arg: string) => void = () => {} #updateState: (arg: string) => void = () => {}
@ -90,6 +93,10 @@ export default class CodeManager {
to: editorManager.editorView.state.doc.length, to: editorManager.editorView.state.doc.length,
insert: code, insert: code,
}, },
annotations: [
codeManagerUpdateEvent,
Transaction.addToHistory.of(true),
],
}) })
} }
} }

View File

@ -19,7 +19,6 @@ import {
createPipeSubstitution, createPipeSubstitution,
} from './modifyAst' } from './modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { warn } from 'node:console'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise

View File

@ -282,8 +282,10 @@ function moreNodePathFromSourceRange(
} }
return path return path
} }
if (_node.type === 'PipeSubstitution' && isInRange) return path if (_node.type === 'PipeSubstitution' && isInRange) return path
console.error('not implemented: ' + node.type) console.error('not implemented: ' + node.type)
return path return path
} }

View File

@ -712,7 +712,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.19" version = "0.1.20"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -1385,7 +1385,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.68" version = "0.1.69"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -1453,7 +1453,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper", "hyper",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "derive-docs" name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros" description = "A tool for generating documentation from Rust derive macros"
version = "0.1.19" version = "0.1.20"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-test-server" name = "kcl-test-server"
description = "A test server for KCL" description = "A test server for KCL"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.1.68" version = "0.1.69"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" 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 } clap = { version = "4.5.7", default-features = false, optional = true }
dashmap = "6.0.1" dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] } 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" form_urlencoded = "1.2.1"
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
git_rev = "0.1.0" git_rev = "0.1.0"

View File

@ -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()) self.insert_code_map(params.uri.to_string(), params.text.as_bytes().to_vec())
.await; .await;
println!("on_change after insert: {:?}", params);
self.inner_on_change(params, false).await; self.inner_on_change(params, false).await;
} }

View File

@ -39,11 +39,10 @@ use tower_lsp::{
Client, LanguageServer, Client, LanguageServer,
}; };
#[cfg(not(target_arch = "wasm32"))]
use crate::lint::checks;
use crate::{ use crate::{
ast::types::{Value, VariableKind}, ast::types::{Value, VariableKind},
executor::SourceRange, executor::SourceRange,
lint::checks,
lsp::{backend::Backend as _, util::IntoDiagnostic}, lsp::{backend::Backend as _, util::IntoDiagnostic},
parser::PIPE_OPERATOR, parser::PIPE_OPERATOR,
token::TokenType, token::TokenType,
@ -269,15 +268,12 @@ impl crate::lsp::backend::Backend for Backend {
// Update our semantic tokens. // Update our semantic tokens.
self.update_semantic_tokens(&tokens, &params).await; self.update_semantic_tokens(&tokens, &params).await;
#[cfg(not(target_arch = "wasm32"))] let discovered_findings = ast
{ .lint(checks::lint_variables)
let discovered_findings = ast .into_iter()
.lint(checks::lint_variables) .flatten()
.into_iter() .collect::<Vec<_>>();
.flatten() self.add_to_diagnostics(&params, &discovered_findings, false).await;
.collect::<Vec<_>>();
self.add_to_diagnostics(&params, &discovered_findings, false).await;
}
} }
// Send the notification to the client that the ast was updated. // Send the notification to the client that the ast was updated.
@ -533,9 +529,9 @@ impl Backend {
diagnostics: &[DiagT], diagnostics: &[DiagT],
clear_all_before_add: bool, clear_all_before_add: bool,
) { ) {
self.client if diagnostics.is_empty() {
.log_message(MessageType::INFO, format!("adding {:?} to diag", diagnostics)) return;
.await; }
if clear_all_before_add { if clear_all_before_add {
self.clear_diagnostics_map(&params.uri, None).await; self.clear_diagnostics_map(&params.uri, None).await;
@ -645,20 +641,6 @@ impl Backend {
modifier modifier
} }
async fn completions_get_variables_from_ast(&self, file_name: &str) -> Vec<CompletionItem> {
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<Vec<u8>> { pub async fn create_zip(&self) -> Result<Vec<u8>> {
// Collect all the file data we know. // Collect all the file data we know.
let mut buf = vec![]; let mut buf = vec![];
@ -1055,9 +1037,34 @@ impl LanguageServer for Backend {
completions.extend(self.stdlib_completions.values().cloned()); completions.extend(self.stdlib_completions.values().cloned());
let variables = self // Add more to the completions if we have more.
.completions_get_variables_from_ast(params.text_document_position.text_document.uri.as_ref()) let Some(ast) = self
.await; .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(&current_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. // Get our variables from our AST to include in our completions.
completions.extend(variables); completions.extend(variables);

View File

@ -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")] #[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_completions_tags() { async fn test_kcl_lsp_completions_tags() {
let server = kcl_lsp_server(false).await.unwrap(); let server = kcl_lsp_server(false).await.unwrap();

View File

@ -93,6 +93,8 @@ impl TryFrom<TokenType> for SemanticTokenType {
impl TokenType { impl TokenType {
// This is for the lsp server. // 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<Vec<SemanticTokenType>> { pub fn all_semantic_token_types() -> Result<Vec<SemanticTokenType>> {
let mut settings = schemars::gen::SchemaSettings::openapi3(); let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true; settings.inline_subschemas = true;

View File

@ -9,7 +9,13 @@ const config = defineConfig({
open: true, open: true,
port: 3000, port: 3000,
watch: { watch: {
ignored: ['**/target/**'], ignored: [
'**/target/**',
'**/dist/**',
'**/build/**',
'**/test-results/**',
'**/playwright-report/**',
],
}, },
}, },
test: { test: {

199
yarn.lock
View File

@ -1165,7 +1165,7 @@
"@babel/helper-validator-identifier" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0" 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" version "6.16.2"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz#ac4e191cd599503e45f35e97366b432d30b8f37a" resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz#ac4e191cd599503e45f35e97366b432d30b8f37a"
integrity sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw== integrity sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==
@ -1175,7 +1175,17 @@
"@codemirror/view" "^6.17.0" "@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.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" version "6.6.0"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.6.0.tgz#d308f143fe1b8896ca25fdb855f66acdaf019dd4" resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.6.0.tgz#d308f143fe1b8896ca25fdb855f66acdaf019dd4"
integrity sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg== integrity sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==
@ -1185,7 +1195,7 @@
"@codemirror/view" "^6.27.0" "@codemirror/view" "^6.27.0"
"@lezer/common" "^1.1.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" version "6.10.2"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61" resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.2.tgz#4056dc219619627ffe995832eeb09cea6060be61"
integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA== integrity sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==
@ -1206,7 +1216,16 @@
"@codemirror/view" "^6.0.0" "@codemirror/view" "^6.0.0"
crelt "^1.0.5" 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" version "6.5.6"
resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.6.tgz#8f858b9e678d675869112e475f082d1e8488db93" resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.6.tgz#8f858b9e678d675869112e475f082d1e8488db93"
integrity sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q== integrity sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==
@ -1215,7 +1234,7 @@
"@codemirror/view" "^6.0.0" "@codemirror/view" "^6.0.0"
crelt "^1.0.5" 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" version "6.4.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b" resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b"
integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A== integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==
@ -1568,28 +1587,19 @@
ts-node "^10.9.1" ts-node "^10.9.1"
tslib "~2.4" 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" version "1.2.1"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049" resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049"
integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ== 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" version "1.2.0"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780" resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780"
integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA== integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==
dependencies: dependencies:
"@lezer/common" "^1.0.0" "@lezer/common" "^1.0.0"
"@lezer/javascript@^1.4.9": "@lezer/lr@^1.0.0":
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":
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.1.tgz#fe25f051880a754e820b28148d90aa2a96b8bdd2" resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.1.tgz#fe25f051880a754e820b28148d90aa2a96b8bdd2"
integrity sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw== integrity sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==
@ -1631,16 +1641,6 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" 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": "@pkgjs/parseargs@^0.11.0":
version "0.11.0" version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
@ -2021,11 +2021,6 @@
"@testing-library/dom" "^10.0.0" "@testing-library/dom" "^10.0.0"
"@types/react-dom" "^18.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": "@tootallnate/quickjs-emscripten@^0.23.0":
version "0.23.0" version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
@ -2101,16 +2096,6 @@
dependencies: dependencies:
"@babel/types" "^7.20.7" "@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": "@types/eslint@^8.4.5":
version "8.56.10" version "8.56.10"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" 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" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== 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": "@types/http-cache-semantics@^4.0.2":
version "4.0.4" version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" 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" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
codemirror@^6.0.0: codemirror@^6.0.0, codemirror@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== 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" shebang-command "^2.0.0"
which "^2.0.1" 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: css-line-break@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" 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" es-errors "^1.3.0"
is-data-view "^1.0.1" 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: 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" version "4.3.5"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" 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" resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a"
integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw== 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: defaults@^1.0.3:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
@ -4752,20 +4714,6 @@ formdata-polyfill@^4.0.10:
dependencies: dependencies:
fetch-blob "^3.1.2" 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: fraction.js@^4.3.7:
version "4.3.7" version "4.3.7"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" 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" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 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: hosted-git-info@^7.0.0:
version "7.0.2" version "7.0.2"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" 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: dependencies:
whatwg-encoding "^2.0.0" whatwg-encoding "^2.0.0"
html2canvas-pro@^1.4.3: html2canvas-pro@^1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.5.0.tgz#18925def43505bad352a394b95fffb45d6d46a8f" resolved "https://registry.yarnpkg.com/html2canvas-pro/-/html2canvas-pro-1.5.0.tgz#18925def43505bad352a394b95fffb45d6d46a8f"
integrity sha512-izxSphcINRwfEVV6eamsPVdhsxSYqX8n/hxzK+niVWdB+onM+aYRoVO+xgS9iMmZoUleZTWg1tJwryikib2hXg== integrity sha512-izxSphcINRwfEVV6eamsPVdhsxSYqX8n/hxzK+niVWdB+onM+aYRoVO+xgS9iMmZoUleZTWg1tJwryikib2hXg==
@ -5581,11 +5522,6 @@ isomorphic-fetch@^3.0.0:
node-fetch "^2.6.1" node-fetch "^2.6.1"
whatwg-fetch "^3.4.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: iterator.prototype@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0"
@ -5884,11 +5820,6 @@ locate-path@^7.1.0:
dependencies: dependencies:
p-locate "^6.0.0" 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: lodash.clonedeep@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" 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" loose-envify "^1.1.0"
scheduler "^0.23.2" 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: react-hot-toast@^2.4.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994" 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" resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz#807b389b15256daf6a813a1ec09e6698064fe97f"
integrity sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug== integrity sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==
react-is@^16.13.1, react-is@^16.7.0: react-is@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -7583,17 +7509,21 @@ streamx@^2.15.0, streamx@^2.18.0:
optionalDependencies: optionalDependencies:
bare-events "^2.2.0" 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: string-natural-compare@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== 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" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -7671,7 +7601,14 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" 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" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 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" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 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: tailwindcss@^3.4.1:
version "3.4.4" version "3.4.4"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.4.tgz#351d932273e6abfa75ce7d226b5bf3a6cb257c05" 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" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== 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: tinybench@^2.5.1:
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.8.0.tgz#30e19ae3a27508ee18273ffed9ac7018949acd7b" 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" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== 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" version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== 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" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 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" version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== 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" 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== 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" version "1.2.2"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== 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" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== 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: w3c-keyname@^2.2.4:
version "2.2.8" version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" 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" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== 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" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -8570,6 +8499,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.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: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" 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" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== 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: ws@^8.17.0, ws@^8.8.0:
version "8.17.1" version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"