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 } }