fixes up some playwright tests and adds a test for the ghost text plugin only in dev mode (#2849)
* things Signed-off-by: Jess Frazelle <github@jessfraz.com> * things Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix up most tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixups 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> * fix lints Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * typo Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
@ -482,19 +482,19 @@ test.describe('Testing Camera Movement', () => {
|
||||
await page.waitForTimeout(100)
|
||||
// hover over horizontal line
|
||||
await page.mouse.move(858, y, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(x, 325)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
// hover over vertical line
|
||||
await page.mouse.move(857, y)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
// now click it
|
||||
await page.mouse.click(857, y)
|
||||
|
||||
@ -511,7 +511,7 @@ test.describe('Testing Camera Movement', () => {
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(x, 419, { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
@ -526,7 +526,7 @@ test.describe('Testing Camera Movement', () => {
|
||||
await hoverOverNothing()
|
||||
|
||||
await page.mouse.move(x, 419)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
|
||||
await hoverOverNothing()
|
||||
|
||||
@ -834,7 +834,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token')).toBeVisible()
|
||||
await expect(page.getByText('Unexpected token').first()).toBeVisible()
|
||||
|
||||
// select the line that's causing the error and delete it
|
||||
await page.getByText('$ error').click()
|
||||
@ -1177,16 +1177,194 @@ shell({ faces: ['end'], thickness: 0.25 }, exampleSketch)`
|
||||
// Okay execution finished, let's start editing text below the error.
|
||||
await u.codeLocator.click()
|
||||
// Go to the end of the editor
|
||||
// This bug happens when there is a diagnostic in the editor and you try to
|
||||
// edit text below it.
|
||||
// Or delete a huge chunk of text and then try to edit below it.
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.up('Shift')
|
||||
await page.keyboard.press('Backspace')
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// Get to the area where we want to type.
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
}
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('thing: "blah"', { delay: 100 })
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const exampleSketch = startSketchOn("XZ")
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine({ angle: 50, length: 45 }, %)
|
||||
|> yLineTo(0, %)
|
||||
|> close(%)
|
||||
|
||||
thing: "blah"`)
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Copilot ghost text', () => {
|
||||
test('completes code in empty file', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
|
||||
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
|
||||
)
|
||||
await expect(page.locator('.cm-ghostText').first()).toHaveText(
|
||||
`fn cube = (pos, scale) => {`
|
||||
)
|
||||
|
||||
// We should be able to hit Tab to accept the completion.
|
||||
await page.keyboard.press('Tab')
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
|
||||
)
|
||||
|
||||
// Hit enter a few times.
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %) `
|
||||
)
|
||||
|
||||
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('going elsewhere in code rejects the suggestion', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
|
||||
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
|
||||
)
|
||||
await expect(page.locator('.cm-ghostText').first()).toHaveText(
|
||||
`fn cube = (pos, scale) => {`
|
||||
)
|
||||
|
||||
// Going elsewhere in the code should hide the ghost text.
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
})
|
||||
|
||||
test('delete in code rejects the suggestion', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
|
||||
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
|
||||
)
|
||||
await expect(page.locator('.cm-ghostText').first()).toHaveText(
|
||||
`fn cube = (pos, scale) => {`
|
||||
)
|
||||
|
||||
// Going elsewhere in the code should hide the ghost text.
|
||||
await page.keyboard.press('Delete')
|
||||
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
})
|
||||
|
||||
test('backspace in code rejects the suggestion', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
|
||||
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
|
||||
)
|
||||
await expect(page.locator('.cm-ghostText').first()).toHaveText(
|
||||
`fn cube = (pos, scale) => {`
|
||||
)
|
||||
|
||||
// Going elsewhere in the code should hide the ghost text.
|
||||
await page.keyboard.press('Backspace')
|
||||
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
})
|
||||
|
||||
test('focus outside code pane rejects the suggestion', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
|
||||
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.locator('.cm-ghostText').first()).toBeVisible()
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`fn cube = (pos, scale) => { const sg = startSketchOn('XY') |> startProfileAt(pos, %) |> line([0, scale], %) |> line([scale, 0], %) |> line([0, -scale], %) return sg}const part001 = cube([0,0], 20) |> close(%) |> extrude(20, %)`
|
||||
)
|
||||
await expect(page.locator('.cm-ghostText').first()).toHaveText(
|
||||
`fn cube = (pos, scale) => {`
|
||||
)
|
||||
|
||||
// Going outside the editor should hide the ghost text.
|
||||
await page.mouse.move(0, 0)
|
||||
await page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.waitFor({ state: 'visible' })
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(``)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Autocomplete works', () => {
|
||||
@ -1869,10 +2047,10 @@ test.describe('Testing selections', () => {
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||
// and will be an easy fix if it breaks because we change the colour
|
||||
await expect(page.locator('.bg-yellow-200')).toBeVisible()
|
||||
await expect(page.locator('.bg-yellow-200').first()).toBeVisible()
|
||||
|
||||
// check mousing off, than mousing onto another line
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||
@ -2293,6 +2471,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|
||||
},
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const extrusionTop: Coords2d = [800, 240]
|
||||
const flatExtrusionFace: Coords2d = [960, 160]
|
||||
@ -2307,25 +2486,25 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await page.mouse.move(extrusionTop[0], extrusionTop[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(arc[0], arc[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(close[0], close[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(19) // multiple lines
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
})
|
||||
test("Extrude button should be disabled if there's no extrudable geometry when nothing is selected", async ({
|
||||
page,
|
||||
@ -2454,7 +2633,7 @@ const part001 = startSketchOn('XZ')
|
||||
await page.mouse.move(pos[0], pos[1], { steps: 5 })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toHaveText(
|
||||
expectedCode
|
||||
removeAfterFirstParenthesis(expectedCode)
|
||||
)
|
||||
// hover over segment, click it and check the cursor has move to the right place
|
||||
await page.mouse.click(pos[0], pos[1])
|
||||
@ -2520,8 +2699,10 @@ const extrude001 = extrude(50, sketch001)
|
||||
await page.mouse.move(nothing.x, nothing.y)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(extrudeWall.x, extrudeWall.y)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight')).toContainText(extrudeText)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
||||
removeAfterFirstParenthesis(extrudeText)
|
||||
)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(
|
||||
await u.getGreatestPixDiff(extrudeWall, hoverColor)
|
||||
@ -2549,8 +2730,10 @@ const extrude001 = extrude(50, sketch001)
|
||||
|
||||
await expect(await u.getGreatestPixDiff(cap, noHoverColor)).toBeLessThan(5)
|
||||
await page.mouse.move(cap.x, cap.y)
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight')).toContainText(capText)
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
await expect(page.getByTestId('hover-highlight').first()).toContainText(
|
||||
removeAfterFirstParenthesis(capText)
|
||||
)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(5)
|
||||
await page.mouse.click(cap.x, cap.y)
|
||||
@ -6707,3 +6890,11 @@ test('Paste should not work unless an input is focused', async ({
|
||||
)
|
||||
).toContain(pasteContent)
|
||||
})
|
||||
|
||||
function removeAfterFirstParenthesis(inputString: string) {
|
||||
const index = inputString.indexOf('(')
|
||||
if (index !== -1) {
|
||||
return inputString.substring(0, index)
|
||||
}
|
||||
return inputString // return the original string if '(' is not found
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { Extension } from '@codemirror/state'
|
||||
import { linter, forEachDiagnostic, Diagnostic } from '@codemirror/lint'
|
||||
|
||||
import { LanguageServerPlugin } from './lsp'
|
||||
|
||||
export default function lspLintExt(): Extension {
|
||||
return linter((view) => {
|
||||
let diagnostics: Diagnostic[] = []
|
||||
|
||||
@ -12,7 +12,6 @@ import type {
|
||||
ViewPlugin,
|
||||
} from '@codemirror/view'
|
||||
import { EditorView, Tooltip } from '@codemirror/view'
|
||||
import { setDiagnosticsEffect } from '@codemirror/lint'
|
||||
|
||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
@ -26,7 +25,6 @@ import { LanguageServerClient } from '../client'
|
||||
import {
|
||||
lspSemanticTokensEvent,
|
||||
lspFormatCodeEvent,
|
||||
lspDiagnosticsEvent,
|
||||
relevantUpdate,
|
||||
} from './annotations'
|
||||
import { CompletionItemKindMap } from './autocomplete'
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
import { StateField, StateEffect } from '@codemirror/state'
|
||||
import { StateField, StateEffect, Annotation } from '@codemirror/state'
|
||||
import { EditorView, Decoration } from '@codemirror/view'
|
||||
|
||||
export { EditorView }
|
||||
|
||||
export const addLineHighlight = StateEffect.define<[number, number]>()
|
||||
|
||||
const addLineHighlightAnnotation = Annotation.define<null>()
|
||||
export const addLineHighlightEvent = addLineHighlightAnnotation.of(null)
|
||||
|
||||
export const lineHighlightField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none
|
||||
},
|
||||
update(lines, tr) {
|
||||
lines = lines.map(tr.changes)
|
||||
|
||||
const isLineHighlightEvent = tr.annotation(addLineHighlightEvent.type)
|
||||
if (isLineHighlightEvent === undefined) {
|
||||
return lines
|
||||
}
|
||||
|
||||
const deco = []
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(addLineHighlight)) {
|
||||
|
||||
@ -5,7 +5,7 @@ import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { addLineHighlight } from './highlightextension'
|
||||
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
||||
import {
|
||||
forEachDiagnostic,
|
||||
Diagnostic,
|
||||
@ -98,6 +98,7 @@ export default class EditorManager {
|
||||
effects: addLineHighlight.of([selection[0], safeEnd]),
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
addLineHighlightEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
/// Thanks to the Cursor folks for their heavy lifting here.
|
||||
/// This has been heavily modified from their original implementation but we are
|
||||
/// still grateful.
|
||||
import { indentUnit } from '@codemirror/language'
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
KeyBinding,
|
||||
PluginValue,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
keymap,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
Annotation,
|
||||
EditorState,
|
||||
Extension,
|
||||
Prec,
|
||||
StateEffect,
|
||||
StateField,
|
||||
Transaction,
|
||||
@ -39,6 +44,9 @@ import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotReje
|
||||
const copilotPluginAnnotation = Annotation.define<null>()
|
||||
export const copilotPluginEvent = copilotPluginAnnotation.of(null)
|
||||
|
||||
const rejectSuggestionAnnotation = Annotation.define<null>()
|
||||
export const rejectSuggestionCommand = rejectSuggestionAnnotation.of(null)
|
||||
|
||||
// Effects to tell StateEffect what to do with GhostText
|
||||
const addSuggestion = StateEffect.define<Suggestion>()
|
||||
const acceptSuggestion = StateEffect.define<null>()
|
||||
@ -76,13 +84,16 @@ interface GhostText {
|
||||
uuid: string
|
||||
}
|
||||
|
||||
export const completionDecoration = StateField.define<CompletionState>({
|
||||
const completionDecoration = StateField.define<CompletionState>({
|
||||
create(_state: EditorState) {
|
||||
return { ghostText: null }
|
||||
},
|
||||
update(state: CompletionState, transaction: Transaction) {
|
||||
// We only care about events from this plugin.
|
||||
if (transaction.annotation(copilotPluginEvent.type) === undefined) {
|
||||
if (
|
||||
transaction.annotation(copilotPluginEvent.type) === undefined &&
|
||||
transaction.annotation(rejectSuggestionCommand.type) === undefined
|
||||
) {
|
||||
return state
|
||||
}
|
||||
|
||||
@ -201,7 +212,6 @@ export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
|
||||
return {
|
||||
overall: infos.some(
|
||||
(info: TransactionInfo) =>
|
||||
update.focusChanged ||
|
||||
info.transaction.annotation(copilotPluginEvent.type) !== undefined ||
|
||||
info.annotations.includes(TransactionAnnotation.UserSelect) ||
|
||||
info.annotations.includes(TransactionAnnotation.UserInput) ||
|
||||
@ -259,15 +269,6 @@ export class CompletionRequester implements PluginValue {
|
||||
return
|
||||
}
|
||||
|
||||
if (viewUpdate.focusChanged) {
|
||||
this.rejectSuggestionCommand()
|
||||
return
|
||||
}
|
||||
|
||||
if (!viewUpdate.docChanged) {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastPos = this.viewUpdate.state.selection.main.head
|
||||
this._deffererCodeUpdate(true)
|
||||
}
|
||||
@ -503,7 +504,10 @@ export class CompletionRequester implements PluginValue {
|
||||
insert: '',
|
||||
},
|
||||
effects: clearSuggestion.of(null),
|
||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||
annotations: [
|
||||
rejectSuggestionCommand,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
|
||||
this.reject()
|
||||
@ -586,9 +590,10 @@ export class CompletionRequester implements PluginValue {
|
||||
}
|
||||
|
||||
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||
const completionPlugin = ViewPlugin.define((view) => {
|
||||
return new CompletionRequester(options.client)
|
||||
})
|
||||
let plugin: CompletionRequester | null = null
|
||||
const completionPlugin = ViewPlugin.define(
|
||||
(view) => (plugin = new CompletionRequester(options.client))
|
||||
)
|
||||
|
||||
const domHandlers = EditorView.domEventHandlers({
|
||||
keydown(event, view) {
|
||||
@ -596,6 +601,8 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||
event.key !== 'Shift' &&
|
||||
event.key !== 'Control' &&
|
||||
event.key !== 'Alt' &&
|
||||
event.key !== 'Backspace' &&
|
||||
event.key !== 'Delete' &&
|
||||
event.key !== 'Meta'
|
||||
) {
|
||||
if (view.plugin === null) return false
|
||||
@ -611,10 +618,61 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||
},
|
||||
})
|
||||
|
||||
const copilotAutocompleteKeymap: readonly KeyBinding[] = [
|
||||
{
|
||||
key: 'Tab',
|
||||
run: (view: EditorView): boolean => {
|
||||
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('Tab')
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Backspace',
|
||||
run: (view: EditorView): boolean => {
|
||||
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.rejectSuggestionCommand()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Delete',
|
||||
run: (view: EditorView): boolean => {
|
||||
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.rejectSuggestionCommand()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const copilotAutocompleteKeymapExt = Prec.highest(
|
||||
keymap.computeN([], () => [copilotAutocompleteKeymap])
|
||||
)
|
||||
|
||||
return [
|
||||
lspPlugin(options),
|
||||
completionPlugin,
|
||||
copilotAutocompleteKeymapExt,
|
||||
domHandlers,
|
||||
completionDecoration,
|
||||
EditorView.focusChangeEffect.of((_, focusing) => {
|
||||
if (plugin === null) return null
|
||||
|
||||
plugin.rejectSuggestionCommand()
|
||||
|
||||
return null
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@ -64,6 +64,9 @@ pub struct Backend {
|
||||
pub diagnostics_map: DashMap<String, Vec<Diagnostic>>,
|
||||
|
||||
pub is_initialized: Arc<tokio::sync::RwLock<bool>>,
|
||||
|
||||
/// Are we running in dev mode.
|
||||
pub dev_mode: bool,
|
||||
}
|
||||
|
||||
// Implement the shared backend trait for the language server.
|
||||
@ -209,7 +212,7 @@ impl Backend {
|
||||
// Let's not call it yet since it's not our model.
|
||||
// We will need to wrap in spawn_local like we do in kcl/mod.rs for wasm only.
|
||||
#[cfg(test)]
|
||||
let completion_list = self
|
||||
let mut completion_list = self
|
||||
.get_completions(doc_params.language, doc_params.prefix, doc_params.suffix)
|
||||
.await
|
||||
.map_err(|err| Error {
|
||||
@ -218,7 +221,25 @@ impl Backend {
|
||||
message: Cow::from(format!("Failed to get completions: {}", err)),
|
||||
})?;
|
||||
#[cfg(not(test))]
|
||||
let completion_list = vec![];
|
||||
let mut completion_list = vec![];
|
||||
|
||||
if self.dev_mode {
|
||||
completion_list.push(
|
||||
r#"fn cube = (pos, scale) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|
||||
return sg
|
||||
}
|
||||
const part001 = cube([0,0], 20)
|
||||
|> close(%)
|
||||
|> extrude(20, %)"#
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let response = CopilotCompletionResponse::from_str_vec(completion_list, line_before, doc_params.pos);
|
||||
// Set the telemetry data for each completion.
|
||||
|
||||
@ -99,6 +99,7 @@ pub async fn copilot_lsp_server() -> Result<crate::lsp::copilot::Backend> {
|
||||
telemetry: Default::default(),
|
||||
is_initialized: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
dev_mode: Default::default(),
|
||||
});
|
||||
let server = service.inner();
|
||||
|
||||
|
||||
@ -356,6 +356,11 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: Strin
|
||||
|
||||
is_initialized: Default::default(),
|
||||
diagnostics_map: Default::default(),
|
||||
dev_mode: if baseurl == "https://api.dev.zoo.dev" {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
},
|
||||
})
|
||||
.custom_method("copilot/setEditorInfo", kcl_lib::lsp::copilot::Backend::set_editor_info)
|
||||
.custom_method(
|
||||
|
||||
Reference in New Issue
Block a user