106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
import { SignatureHelp, SignatureInformation, ParameterInformation } from 'vscode-languageserver/node'
|
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
|
import { Tree, SyntaxNode } from '@lezer/common'
|
|
import { parser } from '../../../src/parser/shrimp'
|
|
import { completions } from './metadata/prelude-completions'
|
|
|
|
export const provideSignatureHelp = (
|
|
document: TextDocument,
|
|
position: { line: number; character: number }
|
|
): SignatureHelp | undefined => {
|
|
const text = document.getText()
|
|
const tree = parser.parse(text)
|
|
const cursorPos = document.offsetAt(position)
|
|
|
|
const context = findCallContext(tree, cursorPos, text)
|
|
if (!context) return
|
|
|
|
const params = lookupFunctionParams(context.funcName)
|
|
if (!params) return
|
|
|
|
return {
|
|
signatures: [buildSignature(context.funcName, params)],
|
|
activeParameter: Math.min(context.argCount, params.length - 1),
|
|
}
|
|
}
|
|
|
|
const findCallContext = (tree: Tree, cursorPos: number, text: string) => {
|
|
const findBestCall = (node: SyntaxNode): SyntaxNode | undefined => {
|
|
let result: SyntaxNode | undefined
|
|
|
|
const isCall = node.name === 'FunctionCall' || node.name === 'FunctionCallOrIdentifier'
|
|
|
|
// Call ends just before cursor (within 5 chars)
|
|
if (isCall && node.to <= cursorPos && cursorPos <= node.to + 5) {
|
|
result = node
|
|
}
|
|
|
|
// Cursor is inside the call's span
|
|
if (isCall && node.from < cursorPos && cursorPos < node.to) {
|
|
result = node
|
|
}
|
|
|
|
// Recurse - prefer smaller spans (more specific)
|
|
let child = node.firstChild
|
|
while (child) {
|
|
const found = findBestCall(child)
|
|
if (found) {
|
|
const foundSpan = found.to - found.from
|
|
const resultSpan = result ? result.to - result.from : Infinity
|
|
if (foundSpan < resultSpan) {
|
|
result = found
|
|
}
|
|
}
|
|
child = child.nextSibling
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
const call = findBestCall(tree.topNode)
|
|
if (!call) return
|
|
|
|
// Count args before cursor
|
|
let argCount = 0
|
|
let child = call.firstChild
|
|
while (child) {
|
|
if ((child.name === 'PositionalArg' || child.name === 'NamedArg') && child.to <= cursorPos) {
|
|
argCount++
|
|
}
|
|
child = child.nextSibling
|
|
}
|
|
|
|
// Extract function name
|
|
const firstChild = call.firstChild
|
|
if (!firstChild) return
|
|
|
|
let funcName: string | undefined
|
|
if (firstChild.name === 'DotGet') {
|
|
funcName = text.slice(firstChild.from, firstChild.to)
|
|
} else if (firstChild.name === 'Identifier') {
|
|
funcName = text.slice(firstChild.from, firstChild.to)
|
|
}
|
|
|
|
if (!funcName) return
|
|
|
|
return { funcName, argCount }
|
|
}
|
|
|
|
const lookupFunctionParams = (funcName: string): string[] | undefined => {
|
|
// Handle module functions: "list.map" → modules.list.map
|
|
if (funcName.includes('.')) {
|
|
const [moduleName, methodName] = funcName.split('.')
|
|
const module = completions.modules[moduleName as keyof typeof completions.modules]
|
|
const method = module?.[methodName as keyof typeof module]
|
|
return method?.params as string[] | undefined
|
|
}
|
|
|
|
// TODO: Handle top-level prelude functions (print, range, etc.)
|
|
}
|
|
|
|
const buildSignature = (funcName: string, params: string[]): SignatureInformation => {
|
|
const label = `${funcName}(${params.join(', ')})`
|
|
const parameters: ParameterInformation[] = params.map(p => ({ label: p }))
|
|
return { label, parameters }
|
|
}
|