From 09d24205084a1ee63aaef63c3823dd68a7d513a5 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Mon, 24 Nov 2025 16:04:03 -0800 Subject: [PATCH] add some arg help --- vscode-extension/server/src/server.ts | 11 ++ vscode-extension/server/src/signatureHelp.ts | 105 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 vscode-extension/server/src/signatureHelp.ts diff --git a/vscode-extension/server/src/server.ts b/vscode-extension/server/src/server.ts index 9177abb..5feacd9 100644 --- a/vscode-extension/server/src/server.ts +++ b/vscode-extension/server/src/server.ts @@ -2,6 +2,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument' import { buildDiagnostics } from './diagnostics' import { buildSemanticTokens, TOKEN_MODIFIERS, TOKEN_TYPES } from './semanticTokens' import { provideCompletions } from './completion/completionProvider' +import { provideSignatureHelp } from './signatureHelp' import { PRELUDE_NAMES } from './metadata/prelude-names' import { parser } from '../../../src/parser/shrimp' import { setGlobals } from '../../../src/parser/tokenizer' @@ -29,6 +30,7 @@ connection.onInitialize(handleInitialize) connection.languages.semanticTokens.on(handleSemanticTokens) documents.onDidChangeContent(handleDocumentChange) connection.onCompletion(handleCompletion) +connection.onSignatureHelp(handleSignatureHelp) // Debug commands connection.onRequest('shrimp/parseTree', handleParseTree) @@ -49,6 +51,9 @@ function handleInitialize(): InitializeResult { completionProvider: { triggerCharacters: ['.'], }, + signatureHelpProvider: { + triggerCharacters: [' '], + }, semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, @@ -113,6 +118,12 @@ function handleCompletion(params: any) { return [...keywordCompletions, ...preludeCompletions] } +function handleSignatureHelp(params: any) { + const document = documents.get(params.textDocument.uri) + if (!document) return + return provideSignatureHelp(document, params.position) +} + function handleParseTree(params: { uri: string }) { connection.console.log(`🦐 Parse tree requested for: ${params.uri}`) const document = documents.get(params.uri) diff --git a/vscode-extension/server/src/signatureHelp.ts b/vscode-extension/server/src/signatureHelp.ts new file mode 100644 index 0000000..b356397 --- /dev/null +++ b/vscode-extension/server/src/signatureHelp.ts @@ -0,0 +1,105 @@ +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 } +}