diff --git a/examples/find.shrimp b/examples/find.shrimp new file mode 100644 index 0000000..1a84c32 --- /dev/null +++ b/examples/find.shrimp @@ -0,0 +1 @@ +echo \ No newline at end of file diff --git a/vscode-extension/example.sh b/vscode-extension/example.shrimp similarity index 100% rename from vscode-extension/example.sh rename to vscode-extension/example.shrimp diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 0b5d6bb..1422480 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -19,7 +19,7 @@ "shrimp" ], "extensions": [ - ".sh" + ".shrimp" ], "configuration": "./language-configuration.json" } @@ -80,11 +80,12 @@ "publisher": "shrimp-lang", "scripts": { "vscode:prepublish": "bun run package", - "compile": "bun run compile:client && bun run compile:server", + "generate-prelude-metadata": "bun scripts/generate-prelude-metadata.ts", + "compile": "bun run generate-prelude-metadata && bun run compile:client && bun run compile:server", "compile:client": "bun build client/src/extension.ts --outdir client/dist --target node --format cjs --external vscode", "compile:server": "bun build server/src/server.ts --outdir server/dist --target node --format cjs", "watch": "bun run compile:client --watch & bun run compile:server --watch", - "package": "bun run compile:client --minify && bun run compile:server --minify", + "package": "bun run generate-prelude-metadata && bun run compile:client --minify && bun run compile:server --minify", "check-types": "tsc --noEmit", "build-and-install": "bun run package && bunx @vscode/vsce package --allow-missing-repository && code --install-extension shrimp-*.vsix" }, diff --git a/vscode-extension/scripts/generate-prelude-metadata.ts b/vscode-extension/scripts/generate-prelude-metadata.ts new file mode 100644 index 0000000..594a725 --- /dev/null +++ b/vscode-extension/scripts/generate-prelude-metadata.ts @@ -0,0 +1,117 @@ +#!/usr/bin/env bun +/** + * Generates prelude metadata for the VSCode extension. + * - Prelude names (for parser scope tracking) + * - Function signatures (for autocomplete) + */ + +import { writeFileSync } from 'fs' +import { join } from 'path' +import { globals } from '../../src/prelude' + +// Extract parameter names from a function +const extractParams = (fn: Function): string[] => { + const fnStr = fn.toString() + const match = fnStr.match(/\(([^)]*)\)/) + if (!match) return [] + + const paramsStr = match[1]!.trim() + if (!paramsStr) return [] + + // Split by comma, but be careful of default values with commas + const params: string[] = [] + let current = '' + let inString = false + let stringChar = '' + + for (let i = 0; i < paramsStr.length; i++) { + const char = paramsStr[i] + if ((char === '"' || char === "'") && (i === 0 || paramsStr[i - 1] !== '\\')) { + if (!inString) { + inString = true + stringChar = char + } else if (char === stringChar) { + inString = false + } + } + + if (char === ',' && !inString) { + params.push(current.trim()) + current = '' + } else { + current += char + } + } + if (current.trim()) params.push(current.trim()) + + return params + .map((p) => p.split(/[=:]/)[0]!.trim()) // Handle defaults and types + .filter((p) => p && p !== 'this') +} + +// Generate metadata for a module +const generateModuleMetadata = (module: Record) => { + const metadata: Record = {} + + for (const [name, value] of Object.entries(module)) { + if (typeof value === 'function') { + metadata[name] = { params: extractParams(value) } + } + } + + return metadata +} + +// Generate names list +const names = Object.keys(globals).sort() + +// Generate module metadata +const moduleMetadata: Record = {} +for (const [name, value] of Object.entries(globals)) { + if (typeof value === 'object' && value !== null && name !== '$') { + moduleMetadata[name] = generateModuleMetadata(value) + } +} + +// Generate dollar metadata +const dollarMetadata: Record = {} +if (globals.$ && typeof globals.$ === 'object') { + for (const key of Object.keys(globals.$)) { + dollarMetadata[key] = { params: [] } + } +} + +// Write prelude-names.ts +const namesOutput = `// Auto-generated by scripts/generate-prelude-metadata.ts +// Do not edit manually - run 'bun run generate-prelude-metadata' to regenerate + +export const PRELUDE_NAMES = ${JSON.stringify(names, null, 2)} as const +` + +const namesPath = join(import.meta.dir, '../server/src/metadata/prelude-names.ts') +writeFileSync(namesPath, namesOutput) + +// Write prelude-completions.ts +const completionsOutput = `// Auto-generated by scripts/generate-prelude-metadata.ts +// Do not edit manually - run 'bun run generate-prelude-metadata' to regenerate + +export type CompletionMetadata = { + params: string[] + description?: string +} + +export const completions = { + modules: ${JSON.stringify(moduleMetadata, null, 2)}, + dollar: ${JSON.stringify(dollarMetadata, null, 2)}, +} as const +` + +const completionsPath = join(import.meta.dir, '../server/src/metadata/prelude-completions.ts') +writeFileSync(completionsPath, completionsOutput) + +console.log(`✓ Generated ${names.length} prelude names to server/src/metadata/prelude-names.ts`) +console.log( + `✓ Generated completions for ${ + Object.keys(moduleMetadata).length + } modules to server/src/metadata/prelude-completions.ts` +) diff --git a/vscode-extension/server/src/completion/completionProvider.ts b/vscode-extension/server/src/completion/completionProvider.ts new file mode 100644 index 0000000..11d11cd --- /dev/null +++ b/vscode-extension/server/src/completion/completionProvider.ts @@ -0,0 +1,52 @@ +import { CompletionItem, CompletionItemKind } from 'vscode-languageserver/node' +import { TextDocument } from 'vscode-languageserver-textdocument' +import { completions } from '../metadata/prelude-completions' +import { analyzeCompletionContext } from './contextAnalyzer' + +/** + * Provides context-aware completions for Shrimp code. + * Returns module function completions (dict.*, list.*, str.*) or dollar property + * completions ($.*) based on the cursor position. + */ +export const provideCompletions = ( + document: TextDocument, + position: { line: number; character: number } +): CompletionItem[] => { + const context = analyzeCompletionContext(document, position) + + if (context.type === 'module') { + return buildModuleCompletions(context.moduleName) + } + + if (context.type === 'dollar') { + return buildDollarCompletions() + } + + return [] // No completions for other contexts yet +} + +/** + * Builds completion items for module functions (dict.*, list.*, str.*). + */ +const buildModuleCompletions = (moduleName: string): CompletionItem[] => { + const functions = completions.modules[moduleName as keyof typeof completions.modules] + if (!functions) return [] + + return Object.entries(functions).map(([name, meta]) => ({ + label: name, + kind: CompletionItemKind.Method, + detail: `(${meta.params.join(', ')})`, + insertText: name, + })) +} + +/** + * Builds completion items for dollar properties ($.*). + */ +const buildDollarCompletions = (): CompletionItem[] => { + return Object.entries(completions.dollar).map(([name, meta]) => ({ + label: name, + kind: CompletionItemKind.Property, + insertText: name, + })) +} diff --git a/vscode-extension/server/src/completion/contextAnalyzer.ts b/vscode-extension/server/src/completion/contextAnalyzer.ts new file mode 100644 index 0000000..07d2aff --- /dev/null +++ b/vscode-extension/server/src/completion/contextAnalyzer.ts @@ -0,0 +1,66 @@ +import { TextDocument } from 'vscode-languageserver-textdocument' +import { SyntaxNode } from '@lezer/common' +import { parser } from '../../../../src/parser/shrimp' +import * as Terms from '../../../../src/parser/shrimp.terms' + +export type CompletionContext = + | { type: 'module'; moduleName: string } + | { type: 'dollar' } + | { type: 'none' } + +/** + * Analyzes the document at the given position to determine what kind of + * completion context we're in (module member access, dollar property, or none). + */ +export const analyzeCompletionContext = ( + document: TextDocument, + position: { line: number; character: number } +): CompletionContext => { + const offset = document.offsetAt(position) + const text = document.getText() + const tree = parser.parse(text) + + // Find node at cursor - could be DotGet or Identifier inside DotGet + const node = tree.resolveInner(offset, -1) + + console.log(`🔍 Node at cursor: ${node.name} (type: ${node.type.id})`) + console.log(`🔍 Parent: ${node.parent?.name} (type: ${node.parent?.type.id})`) + console.log(`🔍 Node text: "${text.slice(node.from, node.to)}"`) + + const SUPPORTED_MODULES = ['dict', 'list', 'str', 'math', 'fs', 'json', 'load'] + + // Case 1: Incomplete DotGet (dict. or $.) + // resolveInner returns DotGet node directly + if (node.type.id === Terms.DotGet) { + const leftSide = extractLeftSide(node, text) + console.log(`✅ Case 1: DotGet found, left side: "${leftSide}"`) + if (leftSide === '$') return { type: 'dollar' } + if (SUPPORTED_MODULES.includes(leftSide)) { + return { type: 'module', moduleName: leftSide } + } + } + + // Case 2: Partial identifier (dict.g or $.e) + // resolveInner returns Identifier, parent is DotGet + if (node.type.id === Terms.Identifier && node.parent?.type.id === Terms.DotGet) { + const dotGetNode = node.parent + const leftSide = extractLeftSide(dotGetNode, text) + console.log(`✅ Case 2: Identifier in DotGet found, left side: "${leftSide}"`) + if (leftSide === '$') return { type: 'dollar' } + if (SUPPORTED_MODULES.includes(leftSide)) { + return { type: 'module', moduleName: leftSide } + } + } + + console.log(`❌ No matching context found`) + return { type: 'none' } +} + +/** + * Extracts the text of the left side of a DotGet node (the part before the dot). + */ +const extractLeftSide = (dotGetNode: SyntaxNode, text: string): string => { + const firstChild = dotGetNode.firstChild + if (!firstChild) return '' + return text.slice(firstChild.from, firstChild.to) +} diff --git a/vscode-extension/server/src/editorScopeAnalyzer.ts b/vscode-extension/server/src/editorScopeAnalyzer.ts index 449c3df..a911970 100644 --- a/vscode-extension/server/src/editorScopeAnalyzer.ts +++ b/vscode-extension/server/src/editorScopeAnalyzer.ts @@ -1,7 +1,7 @@ import { SyntaxNode } from '@lezer/common' import { TextDocument } from 'vscode-languageserver-textdocument' import * as Terms from '../../../src/parser/shrimp.terms' -import { globals } from '../../../src/prelude' +import { PRELUDE_NAMES } from './metadata/prelude-names' /** * Tracks variables in scope at a given position in the parse tree. @@ -13,8 +13,7 @@ export class EditorScopeAnalyzer { constructor(document: TextDocument) { this.document = document - const preludeKeys = Object.keys(globals) - this.scopeCache.set(0, new Set(preludeKeys)) + this.scopeCache.set(0, new Set(PRELUDE_NAMES)) } /** diff --git a/vscode-extension/server/src/metadata/prelude-completions.ts b/vscode-extension/server/src/metadata/prelude-completions.ts new file mode 100644 index 0000000..1b9992a --- /dev/null +++ b/vscode-extension/server/src/metadata/prelude-completions.ts @@ -0,0 +1,732 @@ +// Auto-generated by scripts/generate-prelude-metadata.ts +// Do not edit manually - run 'bun run generate-prelude-metadata' to regenerate + +export type CompletionMetadata = { + params: string[] + description?: string +} + +export const completions = { + modules: { + "dict": { + "keys": { + "params": [ + "dict" + ] + }, + "values": { + "params": [ + "dict" + ] + }, + "entries": { + "params": [ + "dict" + ] + }, + "has?": { + "params": [ + "dict", + "key" + ] + }, + "get": { + "params": [ + "dict", + "key", + "defaultValue" + ] + }, + "set": { + "params": [ + "dict", + "key", + "value" + ] + }, + "merge": { + "params": [ + "...dicts" + ] + }, + "empty?": { + "params": [ + "dict" + ] + }, + "map": { + "params": [ + "dict", + "cb" + ] + }, + "filter": { + "params": [ + "dict", + "cb" + ] + }, + "from-entries": { + "params": [ + "entries" + ] + } + }, + "fs": { + "ls": { + "params": [ + "path" + ] + }, + "mkdir": { + "params": [ + "path" + ] + }, + "rmdir": { + "params": [ + "path" + ] + }, + "pwd": { + "params": [] + }, + "cd": { + "params": [ + "path" + ] + }, + "read": { + "params": [ + "path" + ] + }, + "cat": { + "params": [ + "path" + ] + }, + "read-bytes": { + "params": [ + "path" + ] + }, + "write": { + "params": [ + "path", + "content" + ] + }, + "append": { + "params": [ + "path", + "content" + ] + }, + "delete": { + "params": [ + "path" + ] + }, + "rm": { + "params": [ + "path" + ] + }, + "copy": { + "params": [ + "from", + "to" + ] + }, + "move": { + "params": [ + "from", + "to" + ] + }, + "mv": { + "params": [ + "from", + "to" + ] + }, + "basename": { + "params": [ + "path" + ] + }, + "dirname": { + "params": [ + "path" + ] + }, + "extname": { + "params": [ + "path" + ] + }, + "join": { + "params": [ + "...paths" + ] + }, + "resolve": { + "params": [ + "...paths" + ] + }, + "stat": { + "params": [ + "path" + ] + }, + "exists?": { + "params": [ + "path" + ] + }, + "file?": { + "params": [ + "path" + ] + }, + "dir?": { + "params": [ + "path" + ] + }, + "symlink?": { + "params": [ + "path" + ] + }, + "exec?": { + "params": [ + "path" + ] + }, + "size": { + "params": [ + "path" + ] + }, + "chmod": { + "params": [ + "path", + "mode" + ] + }, + "symlink": { + "params": [ + "target", + "path" + ] + }, + "readlink": { + "params": [ + "path" + ] + }, + "glob": { + "params": [ + "pattern" + ] + }, + "watch": { + "params": [ + "path", + "callback" + ] + }, + "cp": { + "params": [ + "from", + "to" + ] + } + }, + "json": { + "encode": { + "params": [ + "s" + ] + }, + "decode": { + "params": [ + "s" + ] + }, + "parse": { + "params": [ + "s" + ] + }, + "stringify": { + "params": [ + "s" + ] + } + }, + "list": { + "slice": { + "params": [ + "list", + "start", + "end" + ] + }, + "map": { + "params": [ + "list", + "cb" + ] + }, + "filter": { + "params": [ + "list", + "cb" + ] + }, + "reject": { + "params": [ + "list", + "cb" + ] + }, + "reduce": { + "params": [ + "list", + "cb", + "initial" + ] + }, + "find": { + "params": [ + "list", + "cb" + ] + }, + "empty?": { + "params": [ + "list" + ] + }, + "contains?": { + "params": [ + "list", + "item" + ] + }, + "includes?": { + "params": [ + "list", + "item" + ] + }, + "has?": { + "params": [ + "list", + "item" + ] + }, + "any?": { + "params": [ + "list", + "cb" + ] + }, + "all?": { + "params": [ + "list", + "cb" + ] + }, + "push": { + "params": [ + "list", + "item" + ] + }, + "pop": { + "params": [ + "list" + ] + }, + "shift": { + "params": [ + "list" + ] + }, + "unshift": { + "params": [ + "list", + "item" + ] + }, + "splice": { + "params": [ + "list", + "start", + "deleteCount", + "...items" + ] + }, + "insert": { + "params": [ + "list", + "index", + "item" + ] + }, + "reverse": { + "params": [ + "list" + ] + }, + "sort": { + "params": [ + "list", + "cb" + ] + }, + "concat": { + "params": [ + "...lists" + ] + }, + "flatten": { + "params": [ + "list", + "depth" + ] + }, + "unique": { + "params": [ + "list" + ] + }, + "zip": { + "params": [ + "list1", + "list2" + ] + }, + "first": { + "params": [ + "list" + ] + }, + "last": { + "params": [ + "list" + ] + }, + "rest": { + "params": [ + "list" + ] + }, + "take": { + "params": [ + "list", + "n" + ] + }, + "drop": { + "params": [ + "list", + "n" + ] + }, + "append": { + "params": [ + "list", + "item" + ] + }, + "prepend": { + "params": [ + "list", + "item" + ] + }, + "index-of": { + "params": [ + "list", + "item" + ] + }, + "sum": { + "params": [ + "list" + ] + }, + "count": { + "params": [ + "list", + "cb" + ] + }, + "partition": { + "params": [ + "list", + "cb" + ] + }, + "compact": { + "params": [ + "list" + ] + }, + "group-by": { + "params": [ + "list", + "cb" + ] + } + }, + "math": { + "abs": { + "params": [ + "n" + ] + }, + "floor": { + "params": [ + "n" + ] + }, + "ceil": { + "params": [ + "n" + ] + }, + "round": { + "params": [ + "n" + ] + }, + "min": { + "params": [ + "...nums" + ] + }, + "max": { + "params": [ + "...nums" + ] + }, + "pow": { + "params": [ + "base", + "exp" + ] + }, + "sqrt": { + "params": [ + "n" + ] + }, + "random": { + "params": [] + }, + "clamp": { + "params": [ + "n", + "min", + "max" + ] + }, + "sign": { + "params": [ + "n" + ] + }, + "trunc": { + "params": [ + "n" + ] + }, + "even?": { + "params": [ + "n" + ] + }, + "odd?": { + "params": [ + "n" + ] + }, + "positive?": { + "params": [ + "n" + ] + }, + "negative?": { + "params": [ + "n" + ] + }, + "zero?": { + "params": [ + "n" + ] + } + }, + "str": { + "join": { + "params": [ + "arr", + "sep" + ] + }, + "split": { + "params": [ + "str", + "sep" + ] + }, + "to-upper": { + "params": [ + "str" + ] + }, + "to-lower": { + "params": [ + "str" + ] + }, + "trim": { + "params": [ + "str" + ] + }, + "starts-with?": { + "params": [ + "str", + "prefix" + ] + }, + "ends-with?": { + "params": [ + "str", + "suffix" + ] + }, + "contains?": { + "params": [ + "str", + "substr" + ] + }, + "empty?": { + "params": [ + "str" + ] + }, + "index-of": { + "params": [ + "str", + "search" + ] + }, + "last-index-of": { + "params": [ + "str", + "search" + ] + }, + "replace": { + "params": [ + "str", + "search", + "replacement" + ] + }, + "replace-all": { + "params": [ + "str", + "search", + "replacement" + ] + }, + "slice": { + "params": [ + "str", + "start", + "end" + ] + }, + "substring": { + "params": [ + "str", + "start", + "end" + ] + }, + "repeat": { + "params": [ + "str", + "count" + ] + }, + "pad-start": { + "params": [ + "str", + "length", + "pad" + ] + }, + "pad-end": { + "params": [ + "str", + "length", + "pad" + ] + }, + "lines": { + "params": [ + "str" + ] + }, + "chars": { + "params": [ + "str" + ] + }, + "match": { + "params": [ + "str", + "regex" + ] + }, + "test?": { + "params": [ + "str", + "regex" + ] + } + } +}, + dollar: { + "args": { + "params": [] + }, + "argv": { + "params": [] + }, + "env": { + "params": [] + }, + "pid": { + "params": [] + }, + "cwd": { + "params": [] + }, + "script": { + "params": [] + } +}, +} as const diff --git a/vscode-extension/server/src/metadata/prelude-names.ts b/vscode-extension/server/src/metadata/prelude-names.ts new file mode 100644 index 0000000..9e268b8 --- /dev/null +++ b/vscode-extension/server/src/metadata/prelude-names.ts @@ -0,0 +1,40 @@ +// Auto-generated by scripts/generate-prelude-metadata.ts +// Do not edit manually - run 'bun run generate-prelude-metadata' to regenerate + +export const PRELUDE_NAMES = [ + "$", + "array?", + "at", + "bnot", + "boolean?", + "dec", + "describe", + "dict", + "dict?", + "each", + "echo", + "empty?", + "exit", + "fs", + "function?", + "identity", + "import", + "inc", + "inspect", + "json", + "length", + "list", + "load", + "math", + "not", + "null?", + "number?", + "range", + "ref", + "some?", + "str", + "string?", + "type", + "var", + "var?" +] as const diff --git a/vscode-extension/server/src/server.ts b/vscode-extension/server/src/server.ts index 194b35e..5feacd9 100644 --- a/vscode-extension/server/src/server.ts +++ b/vscode-extension/server/src/server.ts @@ -1,7 +1,11 @@ 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' import { Compiler } from '../../../src/compiler/compiler' import { InitializeResult, @@ -12,6 +16,9 @@ import { CompletionItemKind, } from 'vscode-languageserver/node' +// Initialize parser with prelude globals so it knows dict/list/str are in scope +setGlobals(PRELUDE_NAMES) + const connection = createConnection(ProposedFeatures.all) const documents = new TextDocuments(TextDocument) documents.listen(connection) @@ -23,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) @@ -43,6 +51,9 @@ function handleInitialize(): InitializeResult { completionProvider: { triggerCharacters: ['.'], }, + signatureHelpProvider: { + triggerCharacters: [' '], + }, semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, @@ -71,12 +82,46 @@ function handleDocumentChange(change: any) { } function handleCompletion(params: any) { - const keywords = ['if', 'else', 'do', 'end', 'and', 'or', 'true', 'false', 'null'] + console.log(`🌭 YOU ARE COMPLETING at position`, params.position) + const document = documents.get(params.textDocument.uri) + if (!document) { + console.log('❌ No document found') + return [] + } - return keywords.map((keyword) => ({ + const position = params.position + const text = document.getText() + const offset = document.offsetAt(position) + console.log(`📍 Text around cursor: "${text.slice(Math.max(0, offset - 10), offset + 10)}"`) + + // First try context-aware completions (module/dollar) + const contextCompletions = provideCompletions(document, position) + console.log(`🎯 Context completions count: ${contextCompletions.length}`) + if (contextCompletions.length > 0) { + console.log(`✅ Returning ${contextCompletions.length} completions:`, contextCompletions.map(c => c.label).join(', ')) + return contextCompletions + } + + // Fall back to keywords + prelude globals (for Ctrl+Space in general context) + console.log(`⌨️ Falling back to keywords + prelude globals`) + const keywords = ['if', 'else', 'do', 'end', 'and', 'or', 'true', 'false', 'null'] + const keywordCompletions = keywords.map((keyword) => ({ label: keyword, kind: CompletionItemKind.Keyword, })) + + const preludeCompletions = PRELUDE_NAMES.map((name) => ({ + label: name, + kind: CompletionItemKind.Function, + })) + + 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 }) { 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 } +} diff --git a/vscode-extension/tmp/test-dotget-parse.ts b/vscode-extension/tmp/test-dotget-parse.ts new file mode 100644 index 0000000..7a654fb --- /dev/null +++ b/vscode-extension/tmp/test-dotget-parse.ts @@ -0,0 +1,41 @@ +import { parser } from '../../src/parser/shrimp' +import { setGlobals } from '../../src/parser/tokenizer' +import { PRELUDE_NAMES } from '../server/src/prelude-names' + +// Set globals for DotGet detection +setGlobals(PRELUDE_NAMES as unknown as string[]) + +// Test cases - does incomplete DotGet parse correctly? +const testCases = [ + 'dict.', + 'dict.g', + 'dict.get', + '$.', + '$.e', + '$.env', +] + +for (const code of testCases) { + console.log(`\nTesting: "${code}"`) + const tree = parser.parse(code) + const cursor = tree.cursor() + + // Print the parse tree + const printTree = (depth = 0) => { + const indent = ' '.repeat(depth) + console.log(`${indent}${cursor.name} [${cursor.from}-${cursor.to}]`) + + if (cursor.firstChild()) { + do { + printTree(depth + 1) + } while (cursor.nextSibling()) + cursor.parent() + } + } + + printTree() + + // Check at cursor position (end of string) + const node = tree.resolveInner(code.length, -1) + console.log(`Node at end: ${node.name} (type: ${node.type.id})`) +}