Delete 2025-01-24-autocomplete-design.md
This commit is contained in:
parent
1458da58cc
commit
028ccf2bf9
|
|
@ -1,345 +0,0 @@
|
|||
# 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)
|
||||
Loading…
Reference in New Issue
Block a user