Use a CodeMirror tooltip for the signature help in lsp-client

This commit is contained in:
Marijn Haverbeke
2025-05-10 08:09:55 +02:00
parent 9265af9115
commit 24a241c05a
2 changed files with 81 additions and 50 deletions

View File

@ -47,7 +47,7 @@ import {
import { isArray } from '../lib/utils'
import lspGoToDefinitionExt from './go-to-definition'
import lspRenameExt from './rename'
import lspSignatureHelpExt from './signature-help'
import { lspSignatureHelpExt, setSignatureTooltip } from './signature-help'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const docPathFacet = Facet.define<string, string>({
@ -695,6 +695,8 @@ export class LanguageServerPlugin implements PluginValue {
// Create the tooltip container
const dom = this.createTooltipContainer()
dom.className =
'documentation hover-tooltip cm-tooltip cm-signature-tooltip'
// Get active signature
const activeSignatureIndex = result.activeSignature ?? 0
@ -769,42 +771,7 @@ export class LanguageServerPlugin implements PluginValue {
)
if (tooltip) {
// Create and show the tooltip manually
const { pos: tooltipPos, create } = tooltip
const tooltipView = create(view)
const tooltipElement = document.createElement('div')
tooltipElement.className =
'documentation hover-tooltip cm-tooltip cm-signature-tooltip'
tooltipElement.style.position = 'absolute'
tooltipElement.style.zIndex = '99999999'
tooltipElement.appendChild(tooltipView.dom)
// Position the tooltip
const coords = view.coordsAtPos(tooltipPos)
if (coords) {
tooltipElement.style.left = `${coords.left}px`
tooltipElement.style.top = `${coords.bottom + 5}px`
// Add to DOM
document.body.appendChild(tooltipElement)
// Remove after a delay or on editor changes
setTimeout(() => {
removeTooltip() // Use the function that also cleans up event listeners
}, 10000) // Show for 10 seconds
// Also remove on any user input
const removeTooltip = () => {
tooltipElement.remove()
view.dom.removeEventListener('keydown', removeTooltip)
view.dom.removeEventListener('mousedown', removeTooltip)
}
view.dom.addEventListener('keydown', removeTooltip)
view.dom.addEventListener('mousedown', removeTooltip)
}
view.dispatch({ effects: setSignatureTooltip.of(tooltip) })
}
}

View File

@ -1,12 +1,68 @@
import type { Extension } from '@codemirror/state'
import { Prec } from '@codemirror/state'
import type { ViewPlugin } from '@codemirror/view'
import { EditorView } from '@codemirror/view'
import { keymap } from '@codemirror/view'
import {
type EditorState,
type Extension,
Prec,
StateEffect,
StateField,
} from '@codemirror/state'
import type { Tooltip } from '@codemirror/view'
import { type ViewPlugin, showTooltip } from '@codemirror/view'
import { EditorView, keymap } from '@codemirror/view'
import { syntaxTree } from '@codemirror/language'
import { type SyntaxNode } from '@lezer/common'
import type { LanguageServerPlugin } from './lsp'
export default function lspSignatureHelpExt(
export const setSignatureTooltip = StateEffect.define<Tooltip | null>()
function findParenthesized(
state: EditorState,
pos: number,
side: 1 | 0 | -1 = 0
) {
let context: SyntaxNode | null = syntaxTree(state).resolveInner(pos, side)
while (context) {
const open = context.firstChild
if (
open &&
open.from == context.from &&
open.to == context.from + 1 &&
state.doc.sliceString(open.from, open.to) == '('
)
break
context = context.parent
}
return context
}
const signatureTooltip = StateField.define<Tooltip | null>({
create: () => null,
update(value, tr) {
for (let effect of tr.effects) {
if (effect.is(setSignatureTooltip)) return effect.value
}
if (!value) return null
if (tr.selection) {
let parens = findParenthesized(tr.state, tr.selection.main.head)
if (!parens || parens.from != value.pos) return null
}
return tr.docChanged
? { ...value, pos: tr.changes.mapPos(value.pos) }
: value
},
provide: (f) => [
showTooltip.from(f),
EditorView.domEventHandlers({
blur: (_, view) => {
if (view.state.field(f)) {
view.dispatch({ effects: setSignatureTooltip.of(null) })
}
},
}),
],
})
export function lspSignatureHelpExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
@ -18,9 +74,13 @@ export default function lspSignatureHelpExt(
const value = view.plugin(plugin)
if (!value) return false
const pos = view.state.selection.main.head
const parens = findParenthesized(
view.state,
view.state.selection.main.head
)
if (!parens) return false
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.showSignatureHelpTooltip(view, pos)
value.showSignatureHelpTooltip(view, parens.from)
return true
},
},
@ -66,12 +126,16 @@ export default function lspSignatureHelpExt(
})
if (triggerPos >= 0) {
await value.showSignatureHelpTooltip(
update.view,
triggerPos,
triggerCharacter
)
const parens = findParenthesized(update.view.state, triggerPos, -1)
if (parens) {
await value.showSignatureHelpTooltip(
update.view,
parens.from,
triggerCharacter
)
}
}
}),
signatureTooltip,
]
}