Small codemirror changes (#2898)

* Drop unneeded compute indirection in lspAutocompleteKeymapExt

* Dispatch only a single transaction in requestFormatting

Remove addToHistory.of(true), since that is the default.

* Remove old comment and some useless tests

* Just store the view, not the previous viewUpdate, in CompletionRequester

* small codemirror changes from  marijnh

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

* fix some flaky tests

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

* fix

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Marijn Haverbeke <marijn@haverbeke.berlin>
This commit is contained in:
Jess Frazelle
2024-07-03 19:28:46 -07:00
committed by GitHub
parent c0f04d5f86
commit 7cfc927d5c
5 changed files with 46 additions and 97 deletions

View File

@ -746,12 +746,12 @@ test.describe('Editor tests', () => {
await page.keyboard.press('ArrowRight') await page.keyboard.press('ArrowRight')
// error in guter // error in guter
await expect(page.locator('.cm-lint-marker-info')).toBeVisible() await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-info') await page.hover('.cm-lint-marker-info')
await expect( await expect(
page.getByText('Identifiers must be lowerCamelCase') page.getByText('Identifiers must be lowerCamelCase').first()
).toBeVisible() ).toBeVisible()
// select the line that's causing the error and delete it // select the line that's causing the error and delete it
@ -859,13 +859,17 @@ test.describe('Editor tests', () => {
await page.keyboard.press('ArrowRight') await page.keyboard.press('ArrowRight')
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
await expect(page.locator('.cm-lintRange.cm-lintRange-error')).toBeVisible() await expect(
page.locator('.cm-lintRange.cm-lintRange-error').first()
).toBeVisible()
await page.locator('.cm-lintRange.cm-lintRange-error').hover() await page.locator('.cm-lintRange.cm-lintRange-error').hover()
await expect(page.locator('.cm-diagnosticText')).toBeVisible() await expect(page.locator('.cm-diagnosticText').first()).toBeVisible()
await expect(page.getByText('Cannot redefine `topAng`')).toBeVisible() await expect(
page.getByText('Cannot redefine `topAng`').first()
).toBeVisible()
const secondTopAng = await page.getByText('topAng').first() const secondTopAng = page.getByText('topAng').first()
await secondTopAng?.dblclick() await secondTopAng?.dblclick()
await page.keyboard.type('otherAng') await page.keyboard.type('otherAng')
@ -929,7 +933,9 @@ test.describe('Editor tests', () => {
// error in gutter // error in gutter
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
await page.hover('.cm-lint-marker-error:first-child') await page.hover('.cm-lint-marker-error:first-child')
await expect(page.getByText('Expected 2 arguments, got 3')).toBeVisible() await expect(
page.getByText('Expected 2 arguments, got 3').first()
).toBeVisible()
// Make sure there are two diagnostics // Make sure there are two diagnostics
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2)
@ -1831,7 +1837,6 @@ test.describe('Copilot ghost text', () => {
// We wanna make sure the code saves. // We wanna make sure the code saves.
await page.waitForTimeout(800) await page.waitForTimeout(800)
await expect(page.locator('.cm-ghostText')).not.toBeVisible()
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await expect(page.locator('.cm-ghostText').first()).toBeVisible() await expect(page.locator('.cm-ghostText').first()).toBeVisible()
@ -1854,7 +1859,8 @@ test.describe('Copilot ghost text', () => {
await expect(page.locator('.cm-ghostText').first()).not.toBeVisible() await expect(page.locator('.cm-ghostText').first()).not.toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(``) // TODO when we make codemirror a widget, we can test this.
//await expect(page.locator('.cm-content')).toHaveText(``)
}) })
test('delete in code rejects the suggestion', async ({ page }) => { test('delete in code rejects the suggestion', async ({ page }) => {

View File

@ -47,5 +47,5 @@ const lspAutocompleteKeymap: readonly KeyBinding[] = [
] ]
export const lspAutocompleteKeymapExt = Prec.highest( export const lspAutocompleteKeymapExt = Prec.highest(
keymap.computeN([], () => [lspAutocompleteKeymap]) keymap.of(lspAutocompleteKeymap)
) )

View File

@ -284,19 +284,16 @@ export class LanguageServerPlugin implements PluginValue {
}, },
}) })
if (!result) return null if (!result || !result.length) return null
for (let i = 0; i < result.length; i++) { this.view.dispatch({
const { range, newText } = result[i] changes: result.map(({ range, newText }) => ({
this.view.dispatch({ from: posToOffset(this.view.state.doc, range.start)!,
changes: { to: posToOffset(this.view.state.doc, range.end)!,
from: posToOffset(this.view.state.doc, range.start)!, insert: newText,
to: posToOffset(this.view.state.doc, range.end)!, })),
insert: newText, annotations: lspFormatCodeEvent,
}, })
annotations: [lspFormatCodeEvent, Transaction.addToHistory.of(true)],
})
}
} }
async requestCompletion( async requestCompletion(

View File

@ -98,11 +98,6 @@ const completionDecoration = StateField.define<CompletionState>({
return state 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
@ -232,33 +227,22 @@ export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
export class CompletionRequester implements PluginValue { export class CompletionRequester implements PluginValue {
private client: LanguageServerClient private client: LanguageServerClient
private lastPos: number = 0 private lastPos: number = 0
private viewUpdate: ViewUpdate | null = null
private queuedUids: string[] = [] private queuedUids: string[] = []
private _deffererCodeUpdate = deferExecution(() => { private _deffererCodeUpdate = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.requestCompletions() this.requestCompletions()
}, changesDelay) }, changesDelay)
private _deffererUserSelect = deferExecution(() => { private _deffererUserSelect = deferExecution(() => {
if (this.viewUpdate === null) {
return
}
this.rejectSuggestionCommand() this.rejectSuggestionCommand()
}, changesDelay) }, changesDelay)
constructor(client: LanguageServerClient) { constructor(readonly view: EditorView, client: LanguageServerClient) {
this.client = client this.client = client
} }
update(viewUpdate: ViewUpdate) { update(viewUpdate: ViewUpdate) {
this.viewUpdate = viewUpdate
const isRelevant = relevantUpdate(viewUpdate) const isRelevant = relevantUpdate(viewUpdate)
if (!isRelevant.overall) { if (!isRelevant.overall) {
return return
@ -275,18 +259,12 @@ export class CompletionRequester implements PluginValue {
return return
} }
this.lastPos = this.viewUpdate.state.selection.main.head this.lastPos = this.view.state.selection.main.head
this._deffererCodeUpdate(true) if (viewUpdate.docChanged) this._deffererCodeUpdate(true)
} }
ghostText(): GhostText | null { ghostText(): GhostText | null {
if (!this.viewUpdate) { return this.view.state.field(completionDecoration)?.ghostText || null
return null
}
return (
this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null
)
} }
containsGhostText(): boolean { containsGhostText(): boolean {
@ -294,33 +272,23 @@ export class CompletionRequester implements PluginValue {
} }
autocompleting(): boolean { autocompleting(): boolean {
if (!this.viewUpdate) { return completionStatus(this.view.state) === 'active'
return false
}
return completionStatus(this.viewUpdate.state) === 'active'
} }
notFocused(): boolean { notFocused(): boolean {
if (!this.viewUpdate) { return !this.view.hasFocus
return true
}
return !this.viewUpdate.view.hasFocus
} }
async requestCompletions(): Promise<void> { async requestCompletions(): Promise<void> {
if ( if (
this.viewUpdate === null ||
this.containsGhostText() || this.containsGhostText() ||
this.autocompleting() || this.autocompleting() ||
this.notFocused() || this.notFocused()
!this.viewUpdate.docChanged
) { ) {
return return
} }
const pos = this.viewUpdate.state.selection.main.head const pos = this.view.state.selection.main.head
// Check if the position has changed // Check if the position has changed
if (pos !== this.lastPos) { if (pos !== this.lastPos) {
@ -328,7 +296,7 @@ export class CompletionRequester implements PluginValue {
} }
// Get the current position and source // Get the current position and source
const state = this.viewUpdate.state const state = this.view.state
const dUri = state.facet(docPathFacet) const dUri = state.facet(docPathFacet)
// Request completion from the server // Request completion from the server
@ -396,14 +364,14 @@ export class CompletionRequester implements PluginValue {
// Dispatch an effect to add the suggestion // 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. // 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) const line = this.view.state.doc.lineAt(pos)
if (line.to !== pos) { if (line.to !== pos) {
const ending = this.viewUpdate.view.state.doc.sliceString(pos, line.to) const ending = this.view.state.doc.sliceString(pos, line.to)
if (displayText.endsWith(ending)) { if (displayText.endsWith(ending)) {
displayText = displayText.slice(0, displayText.length - ending.length) displayText = displayText.slice(0, displayText.length - ending.length)
} else if (displayText.includes(ending)) { } else if (displayText.includes(ending)) {
// Remove the ending // Remove the ending
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: pos, from: pos,
to: line.to, to: line.to,
@ -416,7 +384,7 @@ export class CompletionRequester implements PluginValue {
} }
} }
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: pos, from: pos,
to: pos, to: pos,
@ -442,10 +410,6 @@ export class CompletionRequester implements PluginValue {
} }
acceptSuggestionCommand(): boolean { acceptSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText() const ghostText = this.ghostText()
if (!ghostText) { if (!ghostText) {
return false return false
@ -463,7 +427,7 @@ export class CompletionRequester implements PluginValue {
const suggestion = ghostText.text const suggestion = ghostText.text
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: ghostTextStart, from: ghostTextStart,
to: ghostTextEnd, to: ghostTextEnd,
@ -475,7 +439,7 @@ export class CompletionRequester implements PluginValue {
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart) const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: actualTextStart, from: actualTextStart,
to: tmpTextEnd, to: tmpTextEnd,
@ -490,10 +454,6 @@ export class CompletionRequester implements PluginValue {
} }
rejectSuggestionCommand(): boolean { rejectSuggestionCommand(): boolean {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText() const ghostText = this.ghostText()
if (!ghostText) { if (!ghostText) {
return false return false
@ -503,7 +463,7 @@ export class CompletionRequester implements PluginValue {
const ghostTextStart = ghostText.displayPos const ghostTextStart = ghostText.displayPos
const ghostTextEnd = ghostText.endGhostText const ghostTextEnd = ghostText.endGhostText
this.viewUpdate.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: ghostTextStart, from: ghostTextStart,
to: ghostTextEnd, to: ghostTextEnd,
@ -521,10 +481,6 @@ export class CompletionRequester implements PluginValue {
} }
sameKeyCommand(key: string) { sameKeyCommand(key: string) {
if (!this.viewUpdate) {
return false
}
const ghostText = this.ghostText() const ghostText = this.ghostText()
if (!ghostText) { if (!ghostText) {
return false return false
@ -534,10 +490,10 @@ export class CompletionRequester implements PluginValue {
// 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 // 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 ghostTextStart = ghostText.displayPos
const indent = this.viewUpdate.view.state.facet(indentUnit) const indent = this.view.state.facet(indentUnit)
if (key === tabKey && ghostText.displayText.startsWith(indent)) { if (key === tabKey && ghostText.displayText.startsWith(indent)) {
this.viewUpdate.view.dispatch({ this.view.dispatch({
selection: { anchor: ghostTextStart + indent.length }, selection: { anchor: ghostTextStart + indent.length },
effects: typeFirst.of(indent.length), effects: typeFirst.of(indent.length),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
@ -551,7 +507,7 @@ export class CompletionRequester implements PluginValue {
return this.acceptSuggestionCommand() return this.acceptSuggestionCommand()
} else { } else {
// Use this to delete the first letter of the suggestion // Use this to delete the first letter of the suggestion
this.viewUpdate.view.dispatch({ this.view.dispatch({
selection: { anchor: ghostTextStart + 1 }, selection: { anchor: ghostTextStart + 1 },
effects: typeFirst.of(1), effects: typeFirst.of(1),
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)], annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
@ -598,7 +554,7 @@ export class CompletionRequester implements PluginValue {
export const copilotPlugin = (options: LanguageServerOptions): Extension => { export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: CompletionRequester | null = null let plugin: CompletionRequester | null = null
const completionPlugin = ViewPlugin.define( const completionPlugin = ViewPlugin.define(
(view) => (plugin = new CompletionRequester(options.client)) (view) => (plugin = new CompletionRequester(view, options.client))
) )
const domHandlers = EditorView.domEventHandlers({ const domHandlers = EditorView.domEventHandlers({
@ -625,8 +581,6 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
}) })
const rejectSuggestionCommand = (view: EditorView): boolean => { const rejectSuggestionCommand = (view: EditorView): boolean => {
if (view.plugin === null) return false
// Get the current plugin from the map. // Get the current plugin from the map.
const p = view.plugin(completionPlugin) const p = view.plugin(completionPlugin)
if (p === null) return false if (p === null) return false

View File

@ -46,15 +46,7 @@ class KclLanguage extends Language {
const parser = new KclParser() const parser = new KclParser()
super( super(data, parser, [plugin], 'kcl')
data,
// For now let's use the javascript parser.
// It works really well and has good syntax highlighting.
// We can use our lsp for the rest.
parser,
[plugin],
'kcl'
)
} }
} }