From 1458da58cca6472dd57942e6cf4d648a5d287276 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Mon, 24 Nov 2025 12:19:58 -0800 Subject: [PATCH] Shrimp was broken --- examples/find.shrimp | 1 + .../plans/2025-01-24-autocomplete-design.md | 345 +++++++++ .../{example.sh => example.shrimp} | 0 vscode-extension/package.json | 7 +- .../scripts/generate-prelude-metadata.ts | 117 +++ .../src/completion/completionProvider.ts | 52 ++ .../server/src/completion/contextAnalyzer.ts | 66 ++ .../server/src/editorScopeAnalyzer.ts | 5 +- .../src/metadata/prelude-completions.ts | 732 ++++++++++++++++++ .../server/src/metadata/prelude-names.ts | 40 + vscode-extension/server/src/server.ts | 38 +- vscode-extension/tmp/test-dotget-parse.ts | 41 + 12 files changed, 1436 insertions(+), 8 deletions(-) create mode 100644 examples/find.shrimp create mode 100644 vscode-extension/docs/plans/2025-01-24-autocomplete-design.md rename vscode-extension/{example.sh => example.shrimp} (100%) create mode 100644 vscode-extension/scripts/generate-prelude-metadata.ts create mode 100644 vscode-extension/server/src/completion/completionProvider.ts create mode 100644 vscode-extension/server/src/completion/contextAnalyzer.ts create mode 100644 vscode-extension/server/src/metadata/prelude-completions.ts create mode 100644 vscode-extension/server/src/metadata/prelude-names.ts create mode 100644 vscode-extension/tmp/test-dotget-parse.ts 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/docs/plans/2025-01-24-autocomplete-design.md b/vscode-extension/docs/plans/2025-01-24-autocomplete-design.md new file mode 100644 index 0000000..ea3b05e --- /dev/null +++ b/vscode-extension/docs/plans/2025-01-24-autocomplete-design.md @@ -0,0 +1,345 @@ +# Autocomplete Design for Shrimp VSCode Extension + +**Date:** 2025-01-24 +**Status:** Approved, ready for implementation + +## Goal + +Add intelligent autocomplete for Shrimp prelude functions and dollar properties in the VSCode extension. + +## Scope + +### Phase 1 (This Implementation) +- **Module completions:** `dict.*`, `list.*`, `str.*` member functions +- **Dollar completions:** `$.*` runtime properties +- **Trigger behavior:** + - Auto-trigger on `.` for module/dollar members + - Manual trigger (Ctrl+Space) for keywords (existing behavior) + +### Future Phases (Deferred) +- Top-level prelude globals (echo, type, inspect, etc.) +- Completion descriptions and documentation +- Chained property access (e.g., `dict.keys.`) + +## Requirements + +1. Show function signatures with parameter names (e.g., `get(dict, key)`) +2. Use proper VSCode completion kinds (Method for functions, Property for dollar) +3. Parser-based context detection (no regex hacks) +4. TypeScript metadata for easy maintenance and editor support +5. Minimal, readable code + +## Architecture + +### File Structure + +``` +vscode-extension/ +├── server/src/ +│ ├── metadata/ +│ │ └── prelude-completions.ts # TypeScript metadata +│ ├── completion/ +│ │ ├── completionProvider.ts # Main completion logic +│ │ └── contextAnalyzer.ts # Parser-based context detection +│ └── server.ts # Wire up completion handler +``` + +### Data Flow + +1. User types `dict.` → VSCode sends `onCompletion` request with cursor position +2. `contextAnalyzer` uses Shrimp parser to determine context: + - Is cursor in a DotGet node? + - What's on the left side of the dot? +3. `completionProvider` returns appropriate completions based on context: + - Module functions if `dict.`, `list.`, or `str.` + - Dollar properties if `$.` + - Empty list otherwise (fall back to keywords) + +## Parser Behavior Analysis + +### Test Results + +We tested how incomplete and partial DotGet expressions parse: + +| Input | Parse Result | Node at cursor | +|------------|-----------------------------------------|----------------| +| `dict.` | `DotGet(IdentifierBeforeDot, ⚠)` | `DotGet` | +| `dict.g` | `DotGet(IdentifierBeforeDot, Identifier)` | `Identifier` | +| `dict.get` | `DotGet(IdentifierBeforeDot, Identifier)` | `Identifier` | +| `$.` | `DotGet(Dollar, ⚠)` | `DotGet` | +| `$.e` | `DotGet(Dollar, Identifier)` | `Identifier` | +| `$.env` | `DotGet(Dollar, Identifier)` | `Identifier` | + +**Key Insights:** +- Incomplete DotGet (`dict.`, `$.`) → `resolveInner` returns `DotGet` node directly +- Partial identifier (`dict.g`, `$.e`) → `resolveInner` returns `Identifier` node (parent is `DotGet`) +- Error node (⚠) marks incomplete input, perfect for triggering autocomplete + +### Context Detection Strategy + +Handle both cases: +1. **Node type is `DotGet`** → cursor right after dot +2. **Node type is `Identifier` with `DotGet` parent** → typing property name + +Extract left side of dot to determine if it's a known module or `$`. + +## Implementation Details + +### 1. Metadata Structure + +**File:** `server/src/metadata/prelude-completions.ts` + +```typescript +export type CompletionMetadata = { + params: string[] + description?: string // Optional for now, add later +} + +export const completions = { + modules: { + dict: { + 'get': { params: ['dict', 'key'] }, + 'set': { params: ['dict', 'key', 'value'] }, + 'keys': { params: ['dict'] }, + 'values': { params: ['dict'] }, + 'has?': { params: ['dict', 'key'] }, + }, + list: { + 'map': { params: ['list', 'fn'] }, + 'filter': { params: ['list', 'fn'] }, + 'reduce': { params: ['list', 'fn', 'initial'] }, + 'reverse': { params: ['list'] }, + 'length': { params: ['list'] }, + }, + str: { + 'split': { params: ['str', 'separator'] }, + 'join': { params: ['list', 'separator'] }, + 'trim': { params: ['str'] }, + 'uppercase': { params: ['str'] }, + 'lowercase': { params: ['str'] }, + }, + }, + dollar: { + 'env': { params: [] }, + 'pid': { params: [] }, + 'args': { params: [] }, + 'argv': { params: [] }, + 'cwd': { params: [] }, + 'script': { params: [] }, + }, +} as const +``` + +**Design decisions:** +- TypeScript (not JSON) for editor autocomplete while editing metadata +- `as const` for type safety +- Single export with nested structure (`completions.modules`, `completions.dollar`) +- Start with ~20 most common functions per module + +### 2. Context Analyzer + +**File:** `server/src/completion/contextAnalyzer.ts` + +```typescript +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' } + +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 + let node = tree.resolveInner(offset, -1) + + // Case 1: Incomplete DotGet (dict. or $.) + // resolveInner returns DotGet node directly + if (node.type.id === Terms.DotGet) { + const leftSide = extractLeftSide(node, text) + if (leftSide === '$') return { type: 'dollar' } + if (['dict', 'list', 'str'].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) + if (leftSide === '$') return { type: 'dollar' } + if (['dict', 'list', 'str'].includes(leftSide)) { + return { type: 'module', moduleName: leftSide } + } + } + + return { type: 'none' } +} + +const extractLeftSide = (dotGetNode: SyntaxNode, text: string): string => { + const firstChild = dotGetNode.firstChild + if (!firstChild) return '' + return text.slice(firstChild.from, firstChild.to) +} +``` + +**Design decisions:** +- Use Shrimp parser (not regex) for accurate token detection +- Return typed context for type-safe consumption +- Handle both incomplete and partial completion cases +- Simple left-side extraction from first child of DotGet + +### 3. Completion Provider + +**File:** `server/src/completion/completionProvider.ts` + +```typescript +import { CompletionItem, CompletionItemKind } from 'vscode-languageserver/node' +import { TextDocument } from 'vscode-languageserver-textdocument' +import { completions } from '../metadata/prelude-completions' +import { analyzeCompletionContext } from './contextAnalyzer' + +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 +} + +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, + })) +} + +const buildDollarCompletions = (): CompletionItem[] => { + return Object.entries(completions.dollar).map(([name, meta]) => ({ + label: name, + kind: CompletionItemKind.Property, + insertText: name, + })) +} +``` + +**Design decisions:** +- `CompletionItemKind.Method` for module functions (shows function icon) +- `CompletionItemKind.Property` for dollar properties (shows property icon) +- `detail` field shows parameter signature +- Simple and focused - easy to extend later with descriptions + +### 4. Server Integration + +**File:** `server/src/server.ts` (updates) + +**Update initialization to declare trigger character:** + +```typescript +function handleInitialize(): InitializeResult { + connection.console.log('🦐 Server initialized with capabilities') + return { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Full, + completionProvider: { + triggerCharacters: ['.'], // Auto-trigger on dot + }, + semanticTokensProvider: { /* ... existing ... */ }, + }, + } +} +``` + +**Update completion handler:** + +```typescript +import { provideCompletions } from './completion/completionProvider' + +function handleCompletion(params: any) { + const document = documents.get(params.textDocument.uri) + if (!document) return [] + + const position = params.position + + // First try context-aware completions (module/dollar) + const contextCompletions = provideCompletions(document, position) + if (contextCompletions.length > 0) { + return contextCompletions + } + + // Fall back to keywords (for Ctrl+Space in general context) + const keywords = ['if', 'else', 'do', 'end', 'and', 'or', 'true', 'false', 'null'] + return keywords.map((keyword) => ({ + label: keyword, + kind: CompletionItemKind.Keyword, + })) +} +``` + +**Design decisions:** +- Try context completions first (module/dollar) +- Fall back to keywords if no context match +- Preserves existing keyword completion behavior +- Simple, linear flow + +## Testing Strategy + +### Manual Testing +1. Type `dict.` → should show dict completions +2. Type `list.m` → should filter to `map` +3. Type `$.` → should show dollar properties +4. Type random identifier + `.` → should show nothing (fall back to keywords) +5. Verify signatures appear in completion detail + +### Future Automated Testing +- Unit tests for context analyzer with various parse trees +- Unit tests for completion provider with mock contexts +- Integration tests for end-to-end completion flow + +## Future Enhancements + +### Phase 2: Documentation +- Add `description` field to metadata +- Show in completion item documentation +- Consider pulling from actual prelude JSDoc comments + +### Phase 3: Top-Level Completions +- Show prelude globals on Ctrl+Space +- Requires different trigger logic (not just dot) +- May need position-aware filtering (don't show in strings/comments) + +### Phase 4: Advanced Features +- Chained completions (`dict.keys.`) +- Type-aware completions (know what `dict.keys` returns) +- Snippet completions with parameter placeholders +- Fuzzy matching for better search + +## References + +- [VSCode Language Server Guide](https://code.visualstudio.com/api/language-extensions/language-server-extension-guide) +- [CompletionItemKind Documentation](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind) +- Shrimp Grammar: `src/parser/shrimp.grammar` (lines 245-248 for DotGet definition) 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..9177abb 100644 --- a/vscode-extension/server/src/server.ts +++ b/vscode-extension/server/src/server.ts @@ -1,7 +1,10 @@ import { TextDocument } from 'vscode-languageserver-textdocument' import { buildDiagnostics } from './diagnostics' import { buildSemanticTokens, TOKEN_MODIFIERS, TOKEN_TYPES } from './semanticTokens' +import { provideCompletions } from './completion/completionProvider' +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 +15,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) @@ -71,12 +77,40 @@ 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 handleParseTree(params: { uri: string }) { 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})`) +}