Compare commits

..

43 Commits

Author SHA1 Message Date
757a50e23e fix ./bin/shrimp parse 2025-12-02 17:11:39 -08:00
cb7cdaea62 trim keys in inspect 2025-12-02 17:11:39 -08:00
688181654e enable [ a = true ] 2025-12-02 17:11:39 -08:00
728c5df9eb bun check 2025-12-02 17:11:39 -08:00
04e14cd83e wrong return type 2025-12-02 17:11:39 -08:00
b2d298ec6f fix search/replace 2025-12-02 17:11:39 -08:00
Chris Wanstrath
5ad6125527 you too 2025-12-02 17:11:39 -08:00
Chris Wanstrath
f160093c4d match lezer API 2025-12-02 17:11:39 -08:00
Chris Wanstrath
1ea130f8e0 pipes end expressions 2025-12-02 17:11:39 -08:00
Chris Wanstrath
ae9896c8a2 switch bin/shrimp to new parser 2025-12-02 17:11:39 -08:00
Chris Wanstrath
0d3f9867e6 we get globals for free now 2025-12-02 17:11:39 -08:00
Chris Wanstrath
cbc75f5ed7 use new parser in curlys 2025-12-02 17:11:39 -08:00
Chris Wanstrath
a836591854 keywords are magical 2025-12-02 17:11:39 -08:00
Chris Wanstrath
d0005d9ccd fix | 2025-12-02 17:11:39 -08:00
Chris Wanstrath
cc604bea49 fix dot.get + thing 2025-12-02 17:11:39 -08:00
Chris Wanstrath
2c2b277b29 throw takes an expression 2025-12-02 17:11:39 -08:00
Chris Wanstrath
1682a7ccb7 fix curly strings 2025-12-02 17:11:39 -08:00
Chris Wanstrath
0e92525b54 regex flags, bad regexs become Words 2025-12-02 17:11:39 -08:00
Chris Wanstrath
6a6675d30f fix bitwise precedence 2025-12-02 17:11:39 -08:00
Chris Wanstrath
d003d65a15 disable errors... for now! 2025-12-02 17:11:39 -08:00
Chris Wanstrath
579d755205 make more compiler tests pass 2025-12-02 17:11:39 -08:00
Chris Wanstrath
566beb87ef do allowed in arg/dict values 2025-12-02 17:11:39 -08:00
Chris Wanstrath
9e4471ad38 try to match lezer API more closely 2025-12-02 17:11:39 -08:00
Chris Wanstrath
3eac0a27a5 hwhitespace 2025-12-02 17:11:39 -08:00
Chris Wanstrath
e38e8d4f1e minor 2025-12-02 17:11:39 -08:00
abd78108c8 new parser(-ish) 2025-12-02 17:11:39 -08:00
ae46988219 sorry lezer... 2025-12-02 17:11:39 -08:00
e4bdddc762 Merge pull request 'Cache the parsing' (#28) from less-parsing into main
Reviewed-on: #28
2025-12-01 21:51:33 +00:00
7feb3cd7b0 Merge remote-tracking branch 'origin/main' into less-parsing 2025-12-01 13:51:24 -08:00
1fec471da9 Merge pull request 'broken-shrimp' (#50) from broken-shrimp into main
Reviewed-on: #50
2025-12-01 21:49:04 +00:00
09d2420508 add some arg help 2025-11-24 16:04:03 -08:00
028ccf2bf9 Delete 2025-01-24-autocomplete-design.md 2025-11-24 12:20:54 -08:00
1458da58cc Shrimp was broken 2025-11-24 12:19:58 -08:00
4a27a8b474 Delete shrimp-0.0.1.vsix 2025-11-24 09:42:24 -08:00
13adbe4c0e Merge branch 'less-parsing' of 54.219.130.253:probablycorey/shrimp into less-parsing 2025-11-07 07:30:21 -08:00
b3ec6995db Update server.ts 2025-11-07 07:30:10 -08:00
854ed02625 Merge branch 'mini-fix' into less-parsing 2025-11-07 07:28:56 -08:00
c325bca611 Merge branch 'main' into less-parsing 2025-11-07 15:28:12 +00:00
1082cc1281 Forgot to set globals in server 2025-11-07 07:27:20 -08:00
5b363c833a Merge remote-tracking branch 'origin/main' into less-parsing 2025-11-06 16:59:53 -08:00
d6aea4b0f9 cache it 2025-11-06 13:36:22 -08:00
44b30d2339 light cleanup 2025-11-06 12:45:02 -08:00
3aa75843ac get rid of this 2025-11-06 12:44:31 -08:00
18 changed files with 1258 additions and 32 deletions

4
.gitignore vendored
View File

@ -34,4 +34,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.DS_Store .DS_Store
/tmp /tmp
/docs /docs
*.vsix

1
examples/find.shrimp Normal file
View File

@ -0,0 +1 @@
echo

View File

View File

@ -1,4 +0,0 @@
node_modules
client/dist
server/dist
*.vsix

View File

@ -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"
}, },

View 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`
)

View 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,
}))
}

View 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)
}

View File

@ -1,12 +1,12 @@
import { TextDocument, Position } from 'vscode-languageserver-textdocument' import { TextDocument, Position } from 'vscode-languageserver-textdocument'
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver/node' import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver/node'
import { parser } from '../../../src/parser/shrimp' import { Tree } from '@lezer/common'
import { Compiler } from '../../../src/compiler/compiler' import { Compiler } from '../../../src/compiler/compiler'
import { CompilerError } from '../../../src/compiler/compilerError' import { CompilerError } from '../../../src/compiler/compilerError'
export const buildDiagnostics = (textDocument: TextDocument): Diagnostic[] => { export const buildDiagnostics = (textDocument: TextDocument, tree: Tree): Diagnostic[] => {
const text = textDocument.getText() const text = textDocument.getText()
const diagnostics = getParseErrors(textDocument) const diagnostics = getParseErrors(textDocument, tree)
if (diagnostics.length > 0) { if (diagnostics.length > 0) {
return diagnostics return diagnostics
@ -59,9 +59,7 @@ const unknownDiagnostic = (message: string): Diagnostic => {
return diagnostic return diagnostic
} }
const getParseErrors = (textDocument: TextDocument): Diagnostic[] => { const getParseErrors = (textDocument: TextDocument, tree: Tree): Diagnostic[] => {
const tree = parser.parse(textDocument.getText())
const ranges: { start: Position; end: Position }[] = [] const ranges: { start: Position; end: Position }[] = []
tree.iterate({ tree.iterate({
enter(n) { enter(n) {

View File

@ -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))
} }
/** /**

View 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

View 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

View File

@ -1,6 +1,6 @@
import { parser } from '../../../src/parser/shrimp' import { parser } from '../../../src/parser/shrimp'
import * as Terms from '../../../src/parser/shrimp.terms' import * as Terms from '../../../src/parser/shrimp.terms'
import { SyntaxNode } from '@lezer/common' import { SyntaxNode, Tree } from '@lezer/common'
import { TextDocument } from 'vscode-languageserver-textdocument' import { TextDocument } from 'vscode-languageserver-textdocument'
import { import {
SemanticTokensBuilder, SemanticTokensBuilder,
@ -28,9 +28,7 @@ export const TOKEN_MODIFIERS = [
SemanticTokenModifiers.readonly, SemanticTokenModifiers.readonly,
] ]
export function buildSemanticTokens(document: TextDocument): number[] { export function buildSemanticTokens(document: TextDocument, tree: Tree): number[] {
const text = document.getText()
const tree = parser.parse(text)
const builder = new SemanticTokensBuilder() const builder = new SemanticTokensBuilder()
const scopeTracker = new EditorScopeAnalyzer(document) const scopeTracker = new EditorScopeAnalyzer(document)

View File

@ -1,8 +1,13 @@
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 { provideSignatureHelp } from './signatureHelp'
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 { Tree } from '@lezer/common'
import { import {
InitializeResult, InitializeResult,
TextDocuments, TextDocuments,
@ -10,19 +15,30 @@ import {
createConnection, createConnection,
ProposedFeatures, ProposedFeatures,
CompletionItemKind, CompletionItemKind,
TextDocumentChangeEvent,
} from 'vscode-languageserver/node' } from 'vscode-languageserver/node'
import { setGlobals } from '../../../src/parser/tokenizer'
import { globals } from '../../../src/prelude'
// 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)
const documentTrees = new Map<string, Tree>()
// Server capabilities // Server capabilities
connection.onInitialize(handleInitialize) connection.onInitialize(handleInitialize)
// Language features // Language features
connection.languages.semanticTokens.on(handleSemanticTokens) connection.languages.semanticTokens.on(handleSemanticTokens)
documents.onDidOpen(handleDocumentOpen)
documents.onDidChangeContent(handleDocumentChange) documents.onDidChangeContent(handleDocumentChange)
documents.onDidClose(handleDocumentClose)
connection.onCompletion(handleCompletion) connection.onCompletion(handleCompletion)
connection.onSignatureHelp(handleSignatureHelp)
// Debug commands // Debug commands
connection.onRequest('shrimp/parseTree', handleParseTree) connection.onRequest('shrimp/parseTree', handleParseTree)
@ -31,10 +47,7 @@ connection.onRequest('shrimp/bytecode', handleBytecode)
// Start listening // Start listening
connection.listen() connection.listen()
// ============================================================================
// Handler implementations // Handler implementations
// ============================================================================
function handleInitialize(): InitializeResult { function handleInitialize(): InitializeResult {
connection.console.log('🦐 Server initialized with capabilities') connection.console.log('🦐 Server initialized with capabilities')
const result: InitializeResult = { const result: InitializeResult = {
@ -43,6 +56,9 @@ function handleInitialize(): InitializeResult {
completionProvider: { completionProvider: {
triggerCharacters: ['.'], triggerCharacters: ['.'],
}, },
signatureHelpProvider: {
triggerCharacters: [' '],
},
semanticTokensProvider: { semanticTokensProvider: {
legend: { legend: {
tokenTypes: TOKEN_TYPES, tokenTypes: TOKEN_TYPES,
@ -56,27 +72,84 @@ function handleInitialize(): InitializeResult {
return result return result
} }
function handleDocumentOpen(event: TextDocumentChangeEvent<TextDocument>) {
const document = event.document
setGlobals(Object.keys(globals))
const tree = parser.parse(document.getText())
documentTrees.set(document.uri, tree)
}
function handleSemanticTokens(params: any) { function handleSemanticTokens(params: any) {
const document = documents.get(params.textDocument.uri) const document = documents.get(params.textDocument.uri)
if (!document) return { data: [] } if (!document) return { data: [] }
const data = buildSemanticTokens(document) const tree = documentTrees.get(params.textDocument.uri)
if (!tree) return { data: [] }
const data = buildSemanticTokens(document, tree)
return { data } return { data }
} }
function handleDocumentChange(change: any) { function handleDocumentChange(change: TextDocumentChangeEvent<TextDocument>) {
const textDocument = change.document const document = change.document
const diagnostics = buildDiagnostics(textDocument)
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) // Parse and cache
setGlobals(Object.keys(globals))
const tree = parser.parse(document.getText())
documentTrees.set(document.uri, tree)
// Build diagnostics using cached tree
const diagnostics = buildDiagnostics(document, tree)
connection.sendDiagnostics({ uri: document.uri, diagnostics })
}
function handleDocumentClose(event: TextDocumentChangeEvent<TextDocument>) {
documentTrees.delete(event.document.uri)
} }
function handleCompletion(params: any) { function handleCompletion(params: any) {
const keywords = ['if', 'else', 'do', 'end', 'and', 'or', 'true', 'false', 'null'] 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 handleSignatureHelp(params: any) {
const document = documents.get(params.textDocument.uri)
if (!document) return
return provideSignatureHelp(document, params.position)
} }
function handleParseTree(params: { uri: string }) { function handleParseTree(params: { uri: string }) {
@ -84,8 +157,13 @@ function handleParseTree(params: { uri: string }) {
const document = documents.get(params.uri) const document = documents.get(params.uri)
if (!document) return 'Document not found' if (!document) return 'Document not found'
const tree = documentTrees.get(params.uri)
if (!tree) {
connection.console.error(`🦐 No cached tree for ${params.uri}`)
return 'No cached parse tree available'
}
const text = document.getText() const text = document.getText()
const tree = parser.parse(text)
const cursor = tree.cursor() const cursor = tree.cursor()
let formatted = '' let formatted = ''

View File

@ -0,0 +1,105 @@
import { SignatureHelp, SignatureInformation, ParameterInformation } from 'vscode-languageserver/node'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { Tree, SyntaxNode } from '@lezer/common'
import { parser } from '../../../src/parser/shrimp'
import { completions } from './metadata/prelude-completions'
export const provideSignatureHelp = (
document: TextDocument,
position: { line: number; character: number }
): SignatureHelp | undefined => {
const text = document.getText()
const tree = parser.parse(text)
const cursorPos = document.offsetAt(position)
const context = findCallContext(tree, cursorPos, text)
if (!context) return
const params = lookupFunctionParams(context.funcName)
if (!params) return
return {
signatures: [buildSignature(context.funcName, params)],
activeParameter: Math.min(context.argCount, params.length - 1),
}
}
const findCallContext = (tree: Tree, cursorPos: number, text: string) => {
const findBestCall = (node: SyntaxNode): SyntaxNode | undefined => {
let result: SyntaxNode | undefined
const isCall = node.name === 'FunctionCall' || node.name === 'FunctionCallOrIdentifier'
// Call ends just before cursor (within 5 chars)
if (isCall && node.to <= cursorPos && cursorPos <= node.to + 5) {
result = node
}
// Cursor is inside the call's span
if (isCall && node.from < cursorPos && cursorPos < node.to) {
result = node
}
// Recurse - prefer smaller spans (more specific)
let child = node.firstChild
while (child) {
const found = findBestCall(child)
if (found) {
const foundSpan = found.to - found.from
const resultSpan = result ? result.to - result.from : Infinity
if (foundSpan < resultSpan) {
result = found
}
}
child = child.nextSibling
}
return result
}
const call = findBestCall(tree.topNode)
if (!call) return
// Count args before cursor
let argCount = 0
let child = call.firstChild
while (child) {
if ((child.name === 'PositionalArg' || child.name === 'NamedArg') && child.to <= cursorPos) {
argCount++
}
child = child.nextSibling
}
// Extract function name
const firstChild = call.firstChild
if (!firstChild) return
let funcName: string | undefined
if (firstChild.name === 'DotGet') {
funcName = text.slice(firstChild.from, firstChild.to)
} else if (firstChild.name === 'Identifier') {
funcName = text.slice(firstChild.from, firstChild.to)
}
if (!funcName) return
return { funcName, argCount }
}
const lookupFunctionParams = (funcName: string): string[] | undefined => {
// Handle module functions: "list.map" → modules.list.map
if (funcName.includes('.')) {
const [moduleName, methodName] = funcName.split('.')
const module = completions.modules[moduleName as keyof typeof completions.modules]
const method = module?.[methodName as keyof typeof module]
return method?.params as string[] | undefined
}
// TODO: Handle top-level prelude functions (print, range, etc.)
}
const buildSignature = (funcName: string, params: string[]): SignatureInformation => {
const label = `${funcName}(${params.join(', ')})`
const parameters: ParameterInformation[] = params.map(p => ({ label: p }))
return { label, parameters }
}

Binary file not shown.

View 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})`)
}