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,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++) {
 | 
			
		||||
      const { range, newText } = result[i]
 | 
			
		||||
      this.view.dispatch({
 | 
			
		||||
        changes: {
 | 
			
		||||
          from: posToOffset(this.view.state.doc, range.start)!,
 | 
			
		||||
          to: posToOffset(this.view.state.doc, range.end)!,
 | 
			
		||||
          insert: newText,
 | 
			
		||||
        },
 | 
			
		||||
        annotations: [lspFormatCodeEvent, Transaction.addToHistory.of(true)],
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    this.view.dispatch({
 | 
			
		||||
      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,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async requestCompletion(
 | 
			
		||||
 | 
			
		||||
@ -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