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:
@ -746,12 +746,12 @@ test.describe('Editor tests', () => {
|
||||
await page.keyboard.press('ArrowRight')
|
||||
|
||||
// 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
|
||||
await page.hover('.cm-lint-marker-info')
|
||||
await expect(
|
||||
page.getByText('Identifiers must be lowerCamelCase')
|
||||
page.getByText('Identifiers must be lowerCamelCase').first()
|
||||
).toBeVisible()
|
||||
|
||||
// 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 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 expect(page.locator('.cm-diagnosticText')).toBeVisible()
|
||||
await expect(page.getByText('Cannot redefine `topAng`')).toBeVisible()
|
||||
await expect(page.locator('.cm-diagnosticText').first()).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 page.keyboard.type('otherAng')
|
||||
|
||||
@ -929,7 +933,9 @@ test.describe('Editor tests', () => {
|
||||
// error in gutter
|
||||
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
|
||||
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
|
||||
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.
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
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()
|
||||
@ -1854,7 +1859,8 @@ test.describe('Copilot ghost text', () => {
|
||||
|
||||
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 }) => {
|
||||
|
@ -47,5 +47,5 @@ const lspAutocompleteKeymap: readonly KeyBinding[] = [
|
||||
]
|
||||
|
||||
export const lspAutocompleteKeymapExt = Prec.highest(
|
||||
keymap.computeN([], () => [lspAutocompleteKeymap])
|
||||
keymap.of(lspAutocompleteKeymap)
|
||||
)
|
||||
|
@ -284,20 +284,17 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
if (!result || !result.length) return null
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const { range, newText } = result[i]
|
||||
this.view.dispatch({
|
||||
changes: {
|
||||
changes: result.map(({ range, newText }) => ({
|
||||
from: posToOffset(this.view.state.doc, range.start)!,
|
||||
to: posToOffset(this.view.state.doc, range.end)!,
|
||||
insert: newText,
|
||||
},
|
||||
annotations: [lspFormatCodeEvent, Transaction.addToHistory.of(true)],
|
||||
})),
|
||||
annotations: lspFormatCodeEvent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async requestCompletion(
|
||||
context: CompletionContext,
|
||||
|
@ -98,11 +98,6 @@ const completionDecoration = StateField.define<CompletionState>({
|
||||
return state
|
||||
}
|
||||
|
||||
// We only care about transactions with effects.
|
||||
if (!transaction.effects) {
|
||||
return state
|
||||
}
|
||||
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(addSuggestion)) {
|
||||
// When adding a suggestion, we set th ghostText
|
||||
@ -232,33 +227,22 @@ export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
|
||||
export class CompletionRequester implements PluginValue {
|
||||
private client: LanguageServerClient
|
||||
private lastPos: number = 0
|
||||
private viewUpdate: ViewUpdate | null = null
|
||||
|
||||
private queuedUids: string[] = []
|
||||
|
||||
private _deffererCodeUpdate = deferExecution(() => {
|
||||
if (this.viewUpdate === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.requestCompletions()
|
||||
}, changesDelay)
|
||||
|
||||
private _deffererUserSelect = deferExecution(() => {
|
||||
if (this.viewUpdate === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.rejectSuggestionCommand()
|
||||
}, changesDelay)
|
||||
|
||||
constructor(client: LanguageServerClient) {
|
||||
constructor(readonly view: EditorView, client: LanguageServerClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
update(viewUpdate: ViewUpdate) {
|
||||
this.viewUpdate = viewUpdate
|
||||
|
||||
const isRelevant = relevantUpdate(viewUpdate)
|
||||
if (!isRelevant.overall) {
|
||||
return
|
||||
@ -275,18 +259,12 @@ export class CompletionRequester implements PluginValue {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastPos = this.viewUpdate.state.selection.main.head
|
||||
this._deffererCodeUpdate(true)
|
||||
this.lastPos = this.view.state.selection.main.head
|
||||
if (viewUpdate.docChanged) this._deffererCodeUpdate(true)
|
||||
}
|
||||
|
||||
ghostText(): GhostText | null {
|
||||
if (!this.viewUpdate) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
this.viewUpdate.view.state.field(completionDecoration)?.ghostText || null
|
||||
)
|
||||
return this.view.state.field(completionDecoration)?.ghostText || null
|
||||
}
|
||||
|
||||
containsGhostText(): boolean {
|
||||
@ -294,33 +272,23 @@ export class CompletionRequester implements PluginValue {
|
||||
}
|
||||
|
||||
autocompleting(): boolean {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
return completionStatus(this.viewUpdate.state) === 'active'
|
||||
return completionStatus(this.view.state) === 'active'
|
||||
}
|
||||
|
||||
notFocused(): boolean {
|
||||
if (!this.viewUpdate) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !this.viewUpdate.view.hasFocus
|
||||
return !this.view.hasFocus
|
||||
}
|
||||
|
||||
async requestCompletions(): Promise<void> {
|
||||
if (
|
||||
this.viewUpdate === null ||
|
||||
this.containsGhostText() ||
|
||||
this.autocompleting() ||
|
||||
this.notFocused() ||
|
||||
!this.viewUpdate.docChanged
|
||||
this.notFocused()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const pos = this.viewUpdate.state.selection.main.head
|
||||
const pos = this.view.state.selection.main.head
|
||||
|
||||
// Check if the position has changed
|
||||
if (pos !== this.lastPos) {
|
||||
@ -328,7 +296,7 @@ export class CompletionRequester implements PluginValue {
|
||||
}
|
||||
|
||||
// Get the current position and source
|
||||
const state = this.viewUpdate.state
|
||||
const state = this.view.state
|
||||
const dUri = state.facet(docPathFacet)
|
||||
|
||||
// Request completion from the server
|
||||
@ -396,14 +364,14 @@ export class CompletionRequester implements PluginValue {
|
||||
|
||||
// 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)
|
||||
const line = this.view.state.doc.lineAt(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)) {
|
||||
displayText = displayText.slice(0, displayText.length - ending.length)
|
||||
} else if (displayText.includes(ending)) {
|
||||
// Remove the ending
|
||||
this.viewUpdate.view.dispatch({
|
||||
this.view.dispatch({
|
||||
changes: {
|
||||
from: pos,
|
||||
to: line.to,
|
||||
@ -416,7 +384,7 @@ export class CompletionRequester implements PluginValue {
|
||||
}
|
||||
}
|
||||
|
||||
this.viewUpdate.view.dispatch({
|
||||
this.view.dispatch({
|
||||
changes: {
|
||||
from: pos,
|
||||
to: pos,
|
||||
@ -442,10 +410,6 @@ export class CompletionRequester implements PluginValue {
|
||||
}
|
||||
|
||||
acceptSuggestionCommand(): boolean {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
const ghostText = this.ghostText()
|
||||
if (!ghostText) {
|
||||
return false
|
||||
@ -463,7 +427,7 @@ export class CompletionRequester implements PluginValue {
|
||||
|
||||
const suggestion = ghostText.text
|
||||
|
||||
this.viewUpdate.view.dispatch({
|
||||
this.view.dispatch({
|
||||
changes: {
|
||||
from: ghostTextStart,
|
||||
to: ghostTextEnd,
|
||||
@ -475,7 +439,7 @@ export class CompletionRequester implements PluginValue {
|
||||
|
||||
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
|
||||
|
||||
this.viewUpdate.view.dispatch({
|
||||
this.view.dispatch({
|
||||
changes: {
|
||||
from: actualTextStart,
|
||||
to: tmpTextEnd,
|
||||
@ -490,10 +454,6 @@ export class CompletionRequester implements PluginValue {
|
||||
}
|
||||
|
||||
rejectSuggestionCommand(): boolean {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
const ghostText = this.ghostText()
|
||||
if (!ghostText) {
|
||||
return false
|
||||
@ -503,7 +463,7 @@ export class CompletionRequester implements PluginValue {
|
||||
const ghostTextStart = ghostText.displayPos
|
||||
const ghostTextEnd = ghostText.endGhostText
|
||||
|
||||
this.viewUpdate.view.dispatch({
|
||||
this.view.dispatch({
|
||||
changes: {
|
||||
from: ghostTextStart,
|
||||
to: ghostTextEnd,
|
||||
@ -521,10 +481,6 @@ export class CompletionRequester implements PluginValue {
|
||||
}
|
||||
|
||||
sameKeyCommand(key: string) {
|
||||
if (!this.viewUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
const ghostText = this.ghostText()
|
||||
if (!ghostText) {
|
||||
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
|
||||
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)) {
|
||||
this.viewUpdate.view.dispatch({
|
||||
this.view.dispatch({
|
||||
selection: { anchor: ghostTextStart + indent.length },
|
||||
effects: typeFirst.of(indent.length),
|
||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||
@ -551,7 +507,7 @@ export class CompletionRequester implements PluginValue {
|
||||
return this.acceptSuggestionCommand()
|
||||
} else {
|
||||
// Use this to delete the first letter of the suggestion
|
||||
this.viewUpdate.view.dispatch({
|
||||
this.view.dispatch({
|
||||
selection: { anchor: ghostTextStart + 1 },
|
||||
effects: typeFirst.of(1),
|
||||
annotations: [copilotPluginEvent, Transaction.addToHistory.of(false)],
|
||||
@ -598,7 +554,7 @@ export class CompletionRequester implements PluginValue {
|
||||
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||
let plugin: CompletionRequester | null = null
|
||||
const completionPlugin = ViewPlugin.define(
|
||||
(view) => (plugin = new CompletionRequester(options.client))
|
||||
(view) => (plugin = new CompletionRequester(view, options.client))
|
||||
)
|
||||
|
||||
const domHandlers = EditorView.domEventHandlers({
|
||||
@ -625,8 +581,6 @@ export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||
})
|
||||
|
||||
const rejectSuggestionCommand = (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
|
||||
|
@ -46,15 +46,7 @@ class KclLanguage extends Language {
|
||||
|
||||
const parser = new KclParser()
|
||||
|
||||
super(
|
||||
data,
|
||||
// For now let's use the javascript parser.
|
||||
// It works really well and has good syntax highlighting.
|
||||
// We can use our lsp for the rest.
|
||||
parser,
|
||||
[plugin],
|
||||
'kcl'
|
||||
)
|
||||
super(data, parser, [plugin], 'kcl')
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user