Shrimp was broken
This commit is contained in:
parent
4a27a8b474
commit
1458da58cc
1
examples/find.shrimp
Normal file
1
examples/find.shrimp
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
echo
|
||||||
345
vscode-extension/docs/plans/2025-01-24-autocomplete-design.md
Normal file
345
vscode-extension/docs/plans/2025-01-24-autocomplete-design.md
Normal file
|
|
@ -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)
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
"shrimp"
|
"shrimp"
|
||||||
],
|
],
|
||||||
"extensions": [
|
"extensions": [
|
||||||
".sh"
|
".shrimp"
|
||||||
],
|
],
|
||||||
"configuration": "./language-configuration.json"
|
"configuration": "./language-configuration.json"
|
||||||
}
|
}
|
||||||
|
|
@ -80,11 +80,12 @@
|
||||||
"publisher": "shrimp-lang",
|
"publisher": "shrimp-lang",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vscode:prepublish": "bun run package",
|
"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: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",
|
"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",
|
"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",
|
"check-types": "tsc --noEmit",
|
||||||
"build-and-install": "bun run package && bunx @vscode/vsce package --allow-missing-repository && code --install-extension shrimp-*.vsix"
|
"build-and-install": "bun run package && bunx @vscode/vsce package --allow-missing-repository && code --install-extension shrimp-*.vsix"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
117
vscode-extension/scripts/generate-prelude-metadata.ts
Normal file
117
vscode-extension/scripts/generate-prelude-metadata.ts
Normal file
|
|
@ -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<string, any>) => {
|
||||||
|
const metadata: Record<string, { params: string[] }> = {}
|
||||||
|
|
||||||
|
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<string, any> = {}
|
||||||
|
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<string, { params: string[] }> = {}
|
||||||
|
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`
|
||||||
|
)
|
||||||
52
vscode-extension/server/src/completion/completionProvider.ts
Normal file
52
vscode-extension/server/src/completion/completionProvider.ts
Normal file
|
|
@ -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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
66
vscode-extension/server/src/completion/contextAnalyzer.ts
Normal file
66
vscode-extension/server/src/completion/contextAnalyzer.ts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { SyntaxNode } from '@lezer/common'
|
import { SyntaxNode } from '@lezer/common'
|
||||||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
import * as Terms from '../../../src/parser/shrimp.terms'
|
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.
|
* Tracks variables in scope at a given position in the parse tree.
|
||||||
|
|
@ -13,8 +13,7 @@ export class EditorScopeAnalyzer {
|
||||||
|
|
||||||
constructor(document: TextDocument) {
|
constructor(document: TextDocument) {
|
||||||
this.document = document
|
this.document = document
|
||||||
const preludeKeys = Object.keys(globals)
|
this.scopeCache.set(0, new Set(PRELUDE_NAMES))
|
||||||
this.scopeCache.set(0, new Set(preludeKeys))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
732
vscode-extension/server/src/metadata/prelude-completions.ts
Normal file
732
vscode-extension/server/src/metadata/prelude-completions.ts
Normal file
|
|
@ -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
|
||||||
40
vscode-extension/server/src/metadata/prelude-names.ts
Normal file
40
vscode-extension/server/src/metadata/prelude-names.ts
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
import { buildDiagnostics } from './diagnostics'
|
import { buildDiagnostics } from './diagnostics'
|
||||||
import { buildSemanticTokens, TOKEN_MODIFIERS, TOKEN_TYPES } from './semanticTokens'
|
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 { parser } from '../../../src/parser/shrimp'
|
||||||
|
import { setGlobals } from '../../../src/parser/tokenizer'
|
||||||
import { Compiler } from '../../../src/compiler/compiler'
|
import { Compiler } from '../../../src/compiler/compiler'
|
||||||
import {
|
import {
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
|
|
@ -12,6 +15,9 @@ import {
|
||||||
CompletionItemKind,
|
CompletionItemKind,
|
||||||
} from 'vscode-languageserver/node'
|
} 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 connection = createConnection(ProposedFeatures.all)
|
||||||
const documents = new TextDocuments(TextDocument)
|
const documents = new TextDocuments(TextDocument)
|
||||||
documents.listen(connection)
|
documents.listen(connection)
|
||||||
|
|
@ -71,12 +77,40 @@ function handleDocumentChange(change: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCompletion(params: 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,
|
label: keyword,
|
||||||
kind: CompletionItemKind.Keyword,
|
kind: CompletionItemKind.Keyword,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const preludeCompletions = PRELUDE_NAMES.map((name) => ({
|
||||||
|
label: name,
|
||||||
|
kind: CompletionItemKind.Function,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [...keywordCompletions, ...preludeCompletions]
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleParseTree(params: { uri: string }) {
|
function handleParseTree(params: { uri: string }) {
|
||||||
|
|
|
||||||
41
vscode-extension/tmp/test-dotget-parse.ts
Normal file
41
vscode-extension/tmp/test-dotget-parse.ts
Normal file
|
|
@ -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})`)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user