Compare commits
3 Commits
main
...
autocomple
| Author | SHA1 | Date | |
|---|---|---|---|
| d195c5321c | |||
| 9b57304b87 | |||
| a1693078f9 |
|
|
@ -94,7 +94,6 @@ export class Compiler {
|
||||||
|
|
||||||
#compileNode(node: SyntaxNode, input: string): ProgramItem[] {
|
#compileNode(node: SyntaxNode, input: string): ProgramItem[] {
|
||||||
const value = input.slice(node.from, node.to)
|
const value = input.slice(node.from, node.to)
|
||||||
|
|
||||||
if (DEBUG) console.log(`🫦 ${node.name}: ${value}`)
|
if (DEBUG) console.log(`🫦 ${node.name}: ${value}`)
|
||||||
|
|
||||||
switch (node.type.id) {
|
switch (node.type.id) {
|
||||||
|
|
@ -190,10 +189,15 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.DotGet: {
|
case terms.DotGet: {
|
||||||
const { objectName, propertyName } = getDotGetParts(node, input)
|
const { objectName, property } = getDotGetParts(node, input)
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
instructions.push(['TRY_LOAD', objectName])
|
instructions.push(['TRY_LOAD', objectName])
|
||||||
instructions.push(['PUSH', propertyName])
|
if (property.type.id === terms.ParenExpr) {
|
||||||
|
instructions.push(...this.#compileNode(property, input))
|
||||||
|
} else {
|
||||||
|
const propertyValue = input.slice(property.from, property.to)
|
||||||
|
instructions.push(['PUSH', propertyValue])
|
||||||
|
}
|
||||||
instructions.push(['DOT_GET'])
|
instructions.push(['DOT_GET'])
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
@ -265,6 +269,10 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
case terms.FunctionCallOrIdentifier: {
|
case terms.FunctionCallOrIdentifier: {
|
||||||
|
if (node.firstChild?.type.id === terms.DotGet) {
|
||||||
|
return this.#compileNode(node.firstChild, input)
|
||||||
|
}
|
||||||
|
|
||||||
return [['TRY_CALL', value]]
|
return [['TRY_CALL', value]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@ describe('compiler', () => {
|
||||||
end
|
end
|
||||||
|
|
||||||
abc
|
abc
|
||||||
`)
|
`).toEvaluateTo(true)
|
||||||
.toEvaluateTo(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('simple conditionals', () => {
|
test('simple conditionals', () => {
|
||||||
|
|
@ -238,3 +237,20 @@ describe('native functions', () => {
|
||||||
expect(`add 5 9`).toEvaluateTo(14, { add })
|
expect(`add 5 9`).toEvaluateTo(14, { add })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('dot get', () => {
|
||||||
|
const array = (...items: any) => items
|
||||||
|
const dict = (atNamed: any) => atNamed
|
||||||
|
|
||||||
|
test('access array element', () => {
|
||||||
|
expect(`arr = array 'a' 'b' 'c'; arr.1`).toEvaluateTo('b', { array })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('access dict element', () => {
|
||||||
|
expect(`dict = dict a=1 b=2; dict.a`).toEvaluateTo(1, { dict })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('use parens expr with dot-get', () => {
|
||||||
|
expect(`a = 1; arr = array 'a' 'b' 'c'; arr.(1 + a)`).toEvaluateTo('c', { array })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
const children = getAllChildren(node)
|
const children = getAllChildren(node)
|
||||||
const [object, property] = children
|
const [object, property] = children
|
||||||
|
|
||||||
if (children.length !== 2) {
|
if (!object || !property) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`DotGet expected 2 identifier children, got ${children.length}`,
|
`DotGet expected 2 identifier children, got ${children.length}`,
|
||||||
node.from,
|
node.from,
|
||||||
|
|
@ -219,7 +219,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property.type.id !== terms.Identifier && property.type.id !== terms.Number) {
|
if (![terms.Identifier, terms.Number, terms.ParenExpr].includes(property.type.id)) {
|
||||||
throw new CompilerError(
|
throw new CompilerError(
|
||||||
`DotGet property must be an Identifier or Number, got ${property.type.name}`,
|
`DotGet property must be an Identifier or Number, got ${property.type.name}`,
|
||||||
property.from,
|
property.from,
|
||||||
|
|
@ -228,7 +228,6 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectName = input.slice(object.from, object.to)
|
const objectName = input.slice(object.from, object.to)
|
||||||
const propertyName = input.slice(property.from, property.to)
|
|
||||||
|
|
||||||
return { objectName, propertyName }
|
return { objectName, property }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
203
src/editor/autocomplete.test.ts
Normal file
203
src/editor/autocomplete.test.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
import { expect, describe, test } from 'bun:test'
|
||||||
|
import { EditorState } from '@codemirror/state'
|
||||||
|
import { autocomplete, type CompletionItem } from './autocomplete'
|
||||||
|
import { shrimpLanguage } from './plugins/shrimpLanguage'
|
||||||
|
|
||||||
|
describe('autocomplete function names', () => {
|
||||||
|
test('collects top-level assignments before cursor, excludes after', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
x = 5
|
||||||
|
<cursor>
|
||||||
|
y = 10
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['x'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('collects matches for identifier', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
alpha = 5
|
||||||
|
bravo = 10
|
||||||
|
a<cursor>
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['alpha'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('function parameters and local assignments are visible inside function', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
do x y:
|
||||||
|
z = 10
|
||||||
|
<cursor>
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['x', 'y', 'z'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('function parameters and locals not visible outside function', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
do x:
|
||||||
|
y = 1
|
||||||
|
end
|
||||||
|
<cursor>
|
||||||
|
`)
|
||||||
|
expect(names).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('inner functions see outer scope variables', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
x = 1
|
||||||
|
do:
|
||||||
|
y = 2
|
||||||
|
do:
|
||||||
|
<cursor>
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['y', 'x'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('if expressions do not create scope boundaries', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
if true:
|
||||||
|
x = 5
|
||||||
|
end
|
||||||
|
<cursor>
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['x'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assigned function names are visible in scope', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
add = do x: x + 1 end
|
||||||
|
<cursor>
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['add'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('identifiers are visible in conditional scope', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
add = if true:
|
||||||
|
x = 1
|
||||||
|
<cursor>
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['add', 'x'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('multiple assignments', () => {
|
||||||
|
const names = getNamesInScope(`
|
||||||
|
alpha = alamo = 4
|
||||||
|
<cursor>
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
expect(names).toEqual(['alpha', 'alamo'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('autocomplete positional arguments', () => {
|
||||||
|
test('shows args for shrimp function', () => {
|
||||||
|
const args = getArgsInScope(`
|
||||||
|
add = do x y: x + y end
|
||||||
|
add <cursor>
|
||||||
|
`)
|
||||||
|
expect(args).toEqual(['x', 'y'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not include args already used', () => {
|
||||||
|
const args = getArgsInScope(`
|
||||||
|
add = do x y: x + y end
|
||||||
|
add 5 <cursor>
|
||||||
|
`)
|
||||||
|
expect(args).toEqual(['y'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('no args when cursor in function name', () => {
|
||||||
|
const items = getVarsInScope(`
|
||||||
|
add = do x y: x + y end
|
||||||
|
ad<cursor>d
|
||||||
|
`)
|
||||||
|
expect(items.every((i) => i.kind !== 'arg')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('no args when function not found', () => {
|
||||||
|
const args = getArgsInScope(`
|
||||||
|
unknown <cursor>
|
||||||
|
`)
|
||||||
|
expect(args).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('autocomplete named arguments', () => {
|
||||||
|
test('shows remaining args after positional arg used', () => {
|
||||||
|
const args = getArgsInScope(`
|
||||||
|
add = do alpha bravo charlie: alpha + bravo + charlie end
|
||||||
|
add 5 <cursor>
|
||||||
|
`)
|
||||||
|
// alpha is used positionally
|
||||||
|
expect(args).toEqual(['bravo', 'charlie'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('filters args by prefix when typing', () => {
|
||||||
|
const args = getArgsInScope(`
|
||||||
|
add = do alpha bravo charlie: alpha + bravo + charlie end
|
||||||
|
add 5 b<cursor>
|
||||||
|
`)
|
||||||
|
// alpha is used, typing 'b' filters to bravo
|
||||||
|
expect(args).toEqual(['bravo'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('excludes named arg already used', () => {
|
||||||
|
const args = getArgsInScope(`
|
||||||
|
add = do alpha bravo charlie: alpha + bravo + charlie end
|
||||||
|
add bravo=10 <cursor>
|
||||||
|
`)
|
||||||
|
// bravo is used as named arg
|
||||||
|
expect(args).toEqual(['alpha', 'charlie'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('positional fills first slot, skips named args', () => {
|
||||||
|
const args = getArgsInScope(`
|
||||||
|
add = do alpha bravo charlie: alpha + bravo + charlie end
|
||||||
|
add bravo=5 <cursor>
|
||||||
|
`)
|
||||||
|
// bravo is named, 10 fills alpha (first positional slot)
|
||||||
|
expect(args).toEqual(['alpha', 'charlie'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
const getVarsInScope = (codeWithCursor: string): CompletionItem[] => {
|
||||||
|
const cursorPos = codeWithCursor.indexOf('<cursor>')
|
||||||
|
if (cursorPos === -1) {
|
||||||
|
throw new Error('Test code must contain <cursor> marker')
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = codeWithCursor.replace('<cursor>', '')
|
||||||
|
const view = createMockView(code, cursorPos)
|
||||||
|
|
||||||
|
const items = autocomplete(view)
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockView = (code: string, cursorPos: number) => {
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: code,
|
||||||
|
selection: { anchor: cursorPos },
|
||||||
|
extensions: [shrimpLanguage],
|
||||||
|
})
|
||||||
|
|
||||||
|
return { state, dispatch: () => {} } as any
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNamesInScope = (codeWithCursor: string): string[] => {
|
||||||
|
return getVarsInScope(codeWithCursor).map((v) => v.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getArgsInScope = (codeWithCursor: string): string[] => {
|
||||||
|
const items = getVarsInScope(codeWithCursor)
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.kind !== 'arg') {
|
||||||
|
throw new Error(`Expected only arg items, but got kind=${item.kind}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((v) => v.name)
|
||||||
|
}
|
||||||
334
src/editor/autocomplete.ts
Normal file
334
src/editor/autocomplete.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
import type { EditorView } from '@codemirror/view'
|
||||||
|
import { syntaxTree } from '@codemirror/language'
|
||||||
|
import type { SyntaxNode, Tree } from '@lezer/common'
|
||||||
|
import * as Terms from '../parser/shrimp.terms'
|
||||||
|
import { noseSignals, type CommandDef } from '#editor/noseClient'
|
||||||
|
|
||||||
|
export const autocomplete = (view: EditorView): CompletionItem[] => {
|
||||||
|
const fnCallContext = findFunctionCallContext(view)
|
||||||
|
|
||||||
|
if (fnCallContext) {
|
||||||
|
return autocompleteArg(view, fnCallContext)
|
||||||
|
} else {
|
||||||
|
return autocompleteIdentifier(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const autocompleteIdentifier = (view: EditorView): CompletionItem[] => {
|
||||||
|
const cursor = view.state.selection.main.head
|
||||||
|
const tree = syntaxTree(view.state)
|
||||||
|
const node = tree.resolveInner(cursor, -1)
|
||||||
|
|
||||||
|
const textBeforeCursor = view.state.doc.sliceString(node.from, cursor)
|
||||||
|
const wordToComplete = /\W$/.test(textBeforeCursor) ? '' : textBeforeCursor
|
||||||
|
if (wordToComplete !== '' && node.type.id !== Terms.Identifier) return []
|
||||||
|
|
||||||
|
const scope = [...completionItemsInScope(view, tree, cursor), ...noseCompletionItems]
|
||||||
|
scope.push(...noseCompletionItems)
|
||||||
|
|
||||||
|
const matchedItems = scope.filter((scopeItem) => scopeItem.name.startsWith(wordToComplete))
|
||||||
|
return matchedItems
|
||||||
|
}
|
||||||
|
|
||||||
|
const autocompleteArg = (view: EditorView, context: FunctionCallContext): CompletionItem[] => {
|
||||||
|
const fnNameText = view.state.doc.sliceString(context.fnName.from, context.fnName.to)
|
||||||
|
|
||||||
|
const cursor = view.state.selection.main.head
|
||||||
|
const tree = syntaxTree(view.state)
|
||||||
|
const scope = [...completionItemsInScope(view, tree, cursor), ...noseCompletionItems]
|
||||||
|
const fn = scope.find((item) => item.name === fnNameText)
|
||||||
|
|
||||||
|
if (!fn) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect used args
|
||||||
|
const usedPositionalArgs = countPositionalArgs(context.fnCallNode, cursor)
|
||||||
|
const usedNamedArgs = collectUsedNamedArgs(context.fnCallNode, cursor, view)
|
||||||
|
|
||||||
|
if (fn.kind === 'shrimp-fn') {
|
||||||
|
// Filter out named args, then skip positional slots
|
||||||
|
const availableParams = fn.params.filter((p) => !usedNamedArgs.has(p))
|
||||||
|
const remainingParams = availableParams.slice(usedPositionalArgs)
|
||||||
|
return remainingParams.map((paramName) => ({ kind: 'arg', name: paramName }))
|
||||||
|
} else if (fn.kind === 'nose-command') {
|
||||||
|
// Filter out named args, then skip positional slots
|
||||||
|
const availableParams = fn.def.signature.params.filter((p) => !usedNamedArgs.has(p.name))
|
||||||
|
const remainingParams = availableParams.slice(usedPositionalArgs)
|
||||||
|
return remainingParams.map((param) => ({
|
||||||
|
kind: 'arg',
|
||||||
|
name: param.name,
|
||||||
|
type: param.type,
|
||||||
|
optional: param.optional,
|
||||||
|
default: param.default,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionItemsInScope = (view: EditorView, tree: Tree, cursor: number): CompletionItem[] => {
|
||||||
|
const items: CompletionItem[] = []
|
||||||
|
|
||||||
|
const containingFunctions = [...findContainingFunctions(tree, cursor), tree.topNode]
|
||||||
|
|
||||||
|
for (const fn of containingFunctions) {
|
||||||
|
const params = collectParams(view, fn)
|
||||||
|
items.push(...params)
|
||||||
|
|
||||||
|
const assignments = collectAssignments(view, fn, cursor)
|
||||||
|
items.push(...assignments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates by name, keeping first occurrence
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return items.filter((item) => {
|
||||||
|
if (seen.has(item.name)) return false
|
||||||
|
seen.add(item.name)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const findContainingFunctions = (tree: Tree, cursor: number): SyntaxNode[] => {
|
||||||
|
const functions: SyntaxNode[] = []
|
||||||
|
|
||||||
|
tree.topNode.cursor().iterate((node) => {
|
||||||
|
if (node.type.id === Terms.FunctionDef) {
|
||||||
|
const fn = node.node
|
||||||
|
if (isCursorInNode(cursor, fn)) {
|
||||||
|
functions.push(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return functions
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectParams = (view: EditorView, fnNode: SyntaxNode): CompletionItem[] => {
|
||||||
|
const params: CompletionItem[] = []
|
||||||
|
|
||||||
|
// Find Params child
|
||||||
|
let child = fnNode.firstChild
|
||||||
|
while (child) {
|
||||||
|
if (child.type.id === Terms.Params) {
|
||||||
|
let param = child.firstChild
|
||||||
|
while (param) {
|
||||||
|
if (param.type.id === Terms.Identifier) {
|
||||||
|
const text = view.state.doc.sliceString(param.from, param.to)
|
||||||
|
params.push({ kind: 'var', name: text })
|
||||||
|
}
|
||||||
|
param = param.nextSibling
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
child = child.nextSibling
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectAssignments = (
|
||||||
|
view: EditorView,
|
||||||
|
scopeNode: SyntaxNode,
|
||||||
|
cursor: number
|
||||||
|
): CompletionItem[] => {
|
||||||
|
const assignments: CompletionItem[] = []
|
||||||
|
|
||||||
|
scopeNode.cursor().iterate((node) => {
|
||||||
|
if (node.from >= cursor) return false
|
||||||
|
|
||||||
|
if (node.type.id === Terms.Assign) {
|
||||||
|
const assignNode = node.node
|
||||||
|
let child = assignNode.firstChild
|
||||||
|
if (child?.type.id === Terms.AssignableIdentifier) {
|
||||||
|
const name = view.state.doc.sliceString(child.from, child.to)
|
||||||
|
|
||||||
|
// Check if RHS is a FunctionDef
|
||||||
|
const rhs = child.nextSibling?.nextSibling // Skip the '=' token
|
||||||
|
if (rhs?.type.id === Terms.FunctionDef) {
|
||||||
|
const params = extractParamsFromFunctionDef(view, rhs)
|
||||||
|
assignments.push({ kind: 'shrimp-fn', name, params })
|
||||||
|
} else {
|
||||||
|
assignments.push({ kind: 'var', name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't go into nested functions unless it's the current scope
|
||||||
|
if (node.type.id === Terms.FunctionDef && node.node !== scopeNode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return assignments
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractParamsFromFunctionDef = (view: EditorView, fnNode: SyntaxNode): string[] => {
|
||||||
|
const params: string[] = []
|
||||||
|
let child = fnNode.firstChild
|
||||||
|
while (child) {
|
||||||
|
if (child.type.id === Terms.Params) {
|
||||||
|
let param = child.firstChild
|
||||||
|
while (param) {
|
||||||
|
if (param.type.id === Terms.Identifier) {
|
||||||
|
const text = view.state.doc.sliceString(param.from, param.to)
|
||||||
|
params.push(text)
|
||||||
|
}
|
||||||
|
param = param.nextSibling
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
child = child.nextSibling
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCursorInNode = (cursor: number, node: SyntaxNode): boolean => {
|
||||||
|
return cursor >= node.from && cursor <= node.to
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Find first ancestor matching a condition
|
||||||
|
const findAncestor = (
|
||||||
|
node: SyntaxNode | null,
|
||||||
|
predicate: (node: SyntaxNode) => boolean
|
||||||
|
): SyntaxNode | null => {
|
||||||
|
let current = node
|
||||||
|
while (current) {
|
||||||
|
if (predicate(current)) return current
|
||||||
|
current = current.parent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const findFunctionCallContext = (view: EditorView) => {
|
||||||
|
const cursor = view.state.selection.main.head
|
||||||
|
const tree = syntaxTree(view.state)
|
||||||
|
const node = tree.resolveInner(cursor, -1)
|
||||||
|
|
||||||
|
// Don't provide arg completion if we're typing an identifier
|
||||||
|
if (node.type.id === Terms.Identifier) return null
|
||||||
|
|
||||||
|
// Only show arg completion after whitespace
|
||||||
|
if (cursor > 0) {
|
||||||
|
const charBefore = view.state.doc.sliceString(cursor - 1, cursor)
|
||||||
|
if (!/\s/.test(charBefore)) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're inside an existing arg (editing it)
|
||||||
|
const inArg = findAncestor(
|
||||||
|
node,
|
||||||
|
(n) => n.type.id === Terms.PositionalArg || n.type.id === Terms.NamedArg
|
||||||
|
)
|
||||||
|
if (inArg) return null
|
||||||
|
|
||||||
|
// Scan back until we find a different node
|
||||||
|
const startNode = node
|
||||||
|
const maxScanBack = 10
|
||||||
|
|
||||||
|
for (let pos = cursor - 1; pos >= Math.max(0, cursor - maxScanBack); pos--) {
|
||||||
|
const nodeAtPos = tree.resolveInner(pos, -1)
|
||||||
|
|
||||||
|
// Found a different node - now walk UP from it
|
||||||
|
if (nodeAtPos !== startNode) {
|
||||||
|
const fnCall = findAncestor(
|
||||||
|
nodeAtPos,
|
||||||
|
(n) => n.type.id === Terms.FunctionCall || n.type.id === Terms.FunctionCallOrIdentifier
|
||||||
|
)
|
||||||
|
|
||||||
|
if (fnCall) {
|
||||||
|
const fnName = fnCall.firstChild
|
||||||
|
if (fnName?.type.id === Terms.Identifier) {
|
||||||
|
return { fnCallNode: fnCall, fnName, currentArgNode: node }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break // Stop after checking first different node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunctionCallContext = {
|
||||||
|
fnCallNode: SyntaxNode
|
||||||
|
fnName: SyntaxNode
|
||||||
|
currentArgNode: SyntaxNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const countPositionalArgs = (fnCallNode: SyntaxNode, cursor: number): number => {
|
||||||
|
let count = 0
|
||||||
|
let child = fnCallNode.firstChild
|
||||||
|
|
||||||
|
while (child) {
|
||||||
|
// Only count PositionalArg nodes that end before cursor
|
||||||
|
if (child.type.id === Terms.PositionalArg && child.to <= cursor) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
child = child.nextSibling
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectUsedNamedArgs = (
|
||||||
|
fnCallNode: SyntaxNode,
|
||||||
|
cursor: number,
|
||||||
|
view: EditorView
|
||||||
|
): Set<string> => {
|
||||||
|
const usedNames = new Set<string>()
|
||||||
|
let child = fnCallNode.firstChild
|
||||||
|
|
||||||
|
while (child) {
|
||||||
|
// Only collect NamedArg nodes that end before cursor
|
||||||
|
if (child.type.id === Terms.NamedArg && child.to <= cursor) {
|
||||||
|
// Find the NamedArgPrefix child which contains "paramname="
|
||||||
|
const prefixNode = child.firstChild
|
||||||
|
if (prefixNode?.type.id === Terms.NamedArgPrefix) {
|
||||||
|
const prefixText = view.state.doc.sliceString(prefixNode.from, prefixNode.to)
|
||||||
|
// Remove the trailing '=' to get the param name
|
||||||
|
const paramName = prefixText.slice(0, -1)
|
||||||
|
usedNames.add(paramName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child = child.nextSibling
|
||||||
|
}
|
||||||
|
|
||||||
|
return usedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShrimpFn = {
|
||||||
|
kind: 'shrimp-fn'
|
||||||
|
name: string
|
||||||
|
params: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoseCommand = {
|
||||||
|
kind: 'nose-command'
|
||||||
|
name: string
|
||||||
|
def: CommandDef
|
||||||
|
}
|
||||||
|
|
||||||
|
type Var = {
|
||||||
|
kind: 'var'
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArgCompletion = {
|
||||||
|
kind: 'arg'
|
||||||
|
name: string
|
||||||
|
type?: string // For NoseCommand
|
||||||
|
optional?: boolean // For NoseCommand
|
||||||
|
default?: any // For NoseCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompletionItem = ShrimpFn | NoseCommand | Var | ArgCompletion
|
||||||
|
|
||||||
|
// Nose Command Signal
|
||||||
|
let noseCompletionItems: CompletionItem[] = []
|
||||||
|
noseSignals.connect((data) => {
|
||||||
|
if (data.type !== 'command-defs') return
|
||||||
|
noseCompletionItems = data.data.map((def) => ({
|
||||||
|
kind: 'nose-command',
|
||||||
|
name: def.name,
|
||||||
|
def,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
@ -32,7 +32,6 @@ export const Editor = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
multilineModeSignal.connect((isMultiline) => {
|
multilineModeSignal.connect((isMultiline) => {
|
||||||
console.log(`🌭 hey babe`, isMultiline)
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []),
|
effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,19 @@ type IncomingMessage =
|
||||||
}
|
}
|
||||||
| { type: 'reef-output'; data: Value }
|
| { type: 'reef-output'; data: Value }
|
||||||
| { type: 'error'; data: string }
|
| { type: 'error'; data: string }
|
||||||
|
| {
|
||||||
|
type: 'command-defs'
|
||||||
|
data: CommandDef[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandDef = {
|
||||||
|
name: string
|
||||||
|
signature: {
|
||||||
|
params: { default: any; name: string; optional: boolean; rest: boolean; type: string }[]
|
||||||
|
returnType: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const noseSignals = new Signal<IncomingMessage>()
|
export const noseSignals = new Signal<IncomingMessage>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,29 @@ export const debugTags = ViewPlugin.fromClass(
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatusBar(view: EditorView) {
|
updateStatusBar(view: EditorView) {
|
||||||
const pos = view.state.selection.main.head + 1
|
const pos = view.state.selection.main.head
|
||||||
const tree = syntaxTree(view.state)
|
const tree = syntaxTree(view.state)
|
||||||
|
|
||||||
let tags: string[] = []
|
let tags: string[] = []
|
||||||
let node = tree.resolveInner(pos, -1)
|
let node = tree.resolveInner(pos, -1)
|
||||||
|
|
||||||
|
let nodes = []
|
||||||
|
let lastNode = null
|
||||||
while (node) {
|
while (node) {
|
||||||
|
nodes.push(node)
|
||||||
tags.push(node.type.name)
|
tags.push(node.type.name)
|
||||||
node = node.parent!
|
node = node.parent!
|
||||||
if (!node) break
|
if (!node) break
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugText = tags.length ? tags.reverse().slice(1).join(' > ') : 'No nodes'
|
;(window as any).l = nodes
|
||||||
|
|
||||||
|
tags = tags.length ? tags.reverse().slice(1) : ['∅'] // remove Program and reverse
|
||||||
|
if (tags.length > 5) {
|
||||||
|
tags = ['...', ...tags.slice(-5)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugText = tags.join(' > ')
|
||||||
statusBarSignal.emit({
|
statusBarSignal.emit({
|
||||||
side: 'right',
|
side: 'right',
|
||||||
message: debugText,
|
message: debugText,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { autocomplete } from '#editor/autocomplete'
|
||||||
import { multilineModeSignal, outputSignal } from '#editor/editor'
|
import { multilineModeSignal, outputSignal } from '#editor/editor'
|
||||||
import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode'
|
import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode'
|
||||||
import { EditorState } from '@codemirror/state'
|
import { EditorState } from '@codemirror/state'
|
||||||
|
|
@ -49,10 +50,26 @@ const customKeymap = keymap.of([
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
run: (view) => {
|
run: (view) => {
|
||||||
|
// if only whitespace is before the cursor on the line, insert two spaces
|
||||||
|
const line = view.state.doc.lineAt(view.state.selection.main.from)
|
||||||
|
const textBeforeCursor = line.text.slice(0, view.state.selection.main.from - line.from)
|
||||||
|
if (textBeforeCursor.trim() === '') {
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: { from: view.state.selection.main.from, insert: ' ' },
|
changes: { from: view.state.selection.main.from, insert: ' ' },
|
||||||
selection: { anchor: view.state.selection.main.from + 2 },
|
selection: { anchor: view.state.selection.main.from + 2 },
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
const matchedItems = autocomplete(view)
|
||||||
|
const colors = {
|
||||||
|
'shrimp-fn': 'black',
|
||||||
|
'nose-command': 'green',
|
||||||
|
var: 'gray',
|
||||||
|
}
|
||||||
|
for (const item of matchedItems) {
|
||||||
|
// print out item with different colors using console.log
|
||||||
|
console.log(`%c${item.name}`, `color: ${colors[item.kind]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { shrimpLanguage } from './shrimpLanguage'
|
||||||
import { shrimpErrors } from './errors'
|
import { shrimpErrors } from './errors'
|
||||||
import { persistencePlugin } from './persistence'
|
import { persistencePlugin } from './persistence'
|
||||||
import { catchErrors } from './catchErrors'
|
import { catchErrors } from './catchErrors'
|
||||||
|
import { debugTags } from '#editor/plugins/debugTags'
|
||||||
|
|
||||||
export const shrimpSetup = (lineNumbersCompartment: Compartment) => {
|
export const shrimpSetup = (lineNumbersCompartment: Compartment) => {
|
||||||
return [
|
return [
|
||||||
|
|
@ -31,5 +32,6 @@ export const shrimpSetup = (lineNumbersCompartment: Compartment) => {
|
||||||
shrimpHighlighting,
|
shrimpHighlighting,
|
||||||
shrimpErrors,
|
shrimpErrors,
|
||||||
persistencePlugin,
|
persistencePlugin,
|
||||||
|
debugTags,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ expression {
|
||||||
|
|
||||||
@skip {} {
|
@skip {} {
|
||||||
DotGet {
|
DotGet {
|
||||||
IdentifierBeforeDot dot (Number | Identifier)
|
IdentifierBeforeDot dot (Number | Identifier | ParenExpr)
|
||||||
}
|
}
|
||||||
|
|
||||||
String { "'" stringContent* "'" }
|
String { "'" stringContent* "'" }
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import {highlighting} from "./highlight"
|
||||||
const spec_Identifier = {__proto__:null,null:64, end:74, if:88, elseif:96, else:100}
|
const spec_Identifier = {__proto__:null,null:64, end:74, if:88, elseif:96, else:100}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "/SQYQbOOO!TOpO'#CqO#aQcO'#CtO$ZOSO'#CvO%aQcO'#DsOOQa'#Ds'#DsO&gQcO'#DrO'OQRO'#CuO'^QcO'#DnO'uQbO'#D{OOQ`'#DO'#DOO'}QbO'#CsOOQ`'#Do'#DoO(oQbO'#DnO(}QbO'#EROOQ`'#DX'#DXO)lQRO'#DaOOQ`'#Dn'#DnO)qQQO'#DmOOQ`'#Dm'#DmOOQ`'#Db'#DbQYQbOOO)yObO,59]OOQa'#Dr'#DrOOQ`'#DS'#DSO*RQbO'#DUOOQ`'#EQ'#EQOOQ`'#Df'#DfO*]QbO,59[O*pQbO'#CxO*xQWO'#CyOOOO'#Du'#DuOOOO'#Dc'#DcO+^OSO,59bOOQa,59b,59bO(}QbO,59aO(}QbO,59aOOQ`'#Dd'#DdO+lQbO'#DPO+tQQO,5:gO+yQRO,59_O-`QRO'#CuO-pQRO,59_O-|QQO,59_O.RQQO,59_O.ZQbO'#DgO.fQbO,59ZO.wQRO,5:mO/OQQO,5:mO/TQbO,59{OOQ`,5:X,5:XOOQ`-E7`-E7`OOQa1G.w1G.wOOQ`,59p,59pOOQ`-E7d-E7dOOOO,59d,59dOOOO,59e,59eOOOO-E7a-E7aOOQa1G.|1G.|OOQa1G.{1G.{O/_QcO1G.{OOQ`-E7b-E7bO/yQbO1G0ROOQa1G.y1G.yO(}QbO,59iO(}QbO,59iO!YQbO'#CtO$iQbO'#CpOOQ`,5:R,5:ROOQ`-E7e-E7eO0WQbO1G0XOOQ`1G/g1G/gO0eQbO7+%mO0jQbO7+%nOOQO1G/T1G/TO0zQRO1G/TOOQ`'#DZ'#DZO1UQbO7+%sO1ZQbO7+%tOOQ`<<IX<<IXOOQ`'#De'#DeO1qQQO'#DeO1vQbO'#EOO2^QbO<<IYOOQ`<<I_<<I_OOQ`'#D['#D[O2cQbO<<I`OOQ`,5:P,5:POOQ`-E7c-E7cOOQ`AN>tAN>tO(}QbO'#D]OOQ`'#Dh'#DhO2nQbOAN>zO2yQQO'#D_OOQ`AN>zAN>zO3OQbOAN>zO3TQRO,59wO3[QQO,59wOOQ`-E7f-E7fOOQ`G24fG24fO3aQbOG24fO3fQQO,59yO3kQQO1G/cOOQ`LD*QLD*QO0jQbO1G/eO1ZQbO7+$}OOQ`7+%P7+%POOQ`<<Hi<<Hi",
|
states: "/SQYQbOOO#[QcO'#CtO$UOSO'#CvO%[QcO'#DsOOQa'#Ds'#DsO&bQcO'#DrO&yQRO'#CuO'XQcO'#DnO'pQbO'#D{OOQ`'#DO'#DOO'xQbO'#CsO(jOpO'#CqOOQ`'#Do'#DoO(oQbO'#DnO(}QbO'#EROOQ`'#DX'#DXO)lQRO'#DaOOQ`'#Dn'#DnO)qQQO'#DmOOQ`'#Dm'#DmOOQ`'#Db'#DbQYQbOOOOQa'#Dr'#DrOOQ`'#DS'#DSO)yQbO'#DUOOQ`'#EQ'#EQOOQ`'#Df'#DfO*TQbO,59[O*hQbO'#CxO*pQWO'#CyOOOO'#Du'#DuOOOO'#Dc'#DcO+UOSO,59bOOQa,59b,59bO(}QbO,59aO(}QbO,59aOOQ`'#Dd'#DdO+dQbO'#DPO+lQQO,5:gO+qQRO,59_O-WQRO'#CuO-hQRO,59_O-tQQO,59_O-yQQO,59_O.RObO,59]O.^QbO'#DgO.iQbO,59ZO.zQRO,5:mO/RQQO,5:mO/WQbO,59{OOQ`,5:X,5:XOOQ`-E7`-E7`OOQ`,59p,59pOOQ`-E7d-E7dOOOO,59d,59dOOOO,59e,59eOOOO-E7a-E7aOOQa1G.|1G.|OOQa1G.{1G.{O/bQcO1G.{OOQ`-E7b-E7bO/|QbO1G0ROOQa1G.y1G.yO(}QbO,59iO(}QbO,59iOOQa1G.w1G.wO!TQbO'#CtO$dQbO'#CpOOQ`,5:R,5:ROOQ`-E7e-E7eO0ZQbO1G0XOOQ`1G/g1G/gO0hQbO7+%mO0mQbO7+%nOOQO1G/T1G/TO0}QRO1G/TOOQ`'#DZ'#DZO1XQbO7+%sO1^QbO7+%tOOQ`<<IX<<IXOOQ`'#De'#DeO1tQQO'#DeO1yQbO'#EOO2aQbO<<IYOOQ`<<I_<<I_OOQ`'#D['#D[O2fQbO<<I`OOQ`,5:P,5:POOQ`-E7c-E7cOOQ`AN>tAN>tO(}QbO'#D]OOQ`'#Dh'#DhO2qQbOAN>zO2|QQO'#D_OOQ`AN>zAN>zO3RQbOAN>zO3WQRO,59wO3_QQO,59wOOQ`-E7f-E7fOOQ`G24fG24fO3dQbOG24fO3iQQO,59yO3nQQO1G/cOOQ`LD*QLD*QO0mQbO1G/eO1^QbO7+$}OOQ`7+%P7+%POOQ`<<Hi<<Hi",
|
||||||
stateData: "3s~O!_OS!`OS~O]QO^`O_TO`POaXOfTOnTOoTOpTO|^O!eZO!hRO!qcO~O!dfO~O]gO_TO`POaXOfTOnTOoTOpTOwhOyiO!eZO!hROzhX!qhX!whX!shXuhX~OP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~P!YOkoO!hrO!jmO!knO~O]gO_TO`POaXOfTOnTOoTOpTOwhOyiO!eZO!hRO~OP!gXQ!gXR!gXS!gX!q!gX!w!gXT!gXU!gXV!gXW!gXX!gXY!gXZ!gX[!gX!s!gXu!gX~P$iOP!fXQ!fXR!fXS!fX!q!bX!w!bXu!bX~OPsOQsORtOStO~OPsOQsORtOStO!q!bX!w!bXu!bX~O]uOtsP~O]QO_TO`POaXOfTOnTOoTOpTO!eZO!hRO~Oz}O!q!bX!w!bXu!bX~O]gO_TO`POfTOnTOoTOpTO!eZO!hRO~OV!RO~O!q!SO!w!SO~O]!UOf!UO~OaXOw!VO~P(}Ozda!qda!wda!sdauda~P$iO]!XO!eZO~O!h!YO!j!YO!k!YO!l!YO!m!YO!n!YO~OkoO!h![O!jmO!knO~O]uOtsX~Ot!`O~O!s!aOP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~OT!cOU!cOV!bOW!bOX!bOY!bOZ!bO[!bO~OPsOQsORtOStO~P,tOPsOQsORtOStO!s!aO~Oz}O!s!aO~O]!dO`PO!eZO~Oz}O!qca!wca!scauca~Ot!hO~P,tOt!hO~O^`O|^O~P'}OPsOQsORiiSii!qii!wii!siiuii~O^`O|^O!q!kO~P'}O^`O|^O!q!pO~P'}Ou!qO~O^`O|^O!q!rOu!rP~P'}O!sqitqi~P,tOu!vO~O^`O|^O!q!rOu!rP!Q!rP!S!rP~P'}O!q!yO~O^`O|^O!q!rOu!rX!Q!rX!S!rX~P'}Ou!{O~Ou#QO!Q!|O!S#PO~Ou#VO!Q!|O!S#PO~Ot#XO~Ou#VO~Ot#YO~P,tOt#YO~Ou#ZO~O!q#[O~O!q#]O~Ofo~",
|
stateData: "3v~O!_OS!`OS~O]PO^`O_SO`ZOaWOfSOnSOoSOpSO|^O!eYO!hQO!qcO~O]fO_SO`ZOaWOfSOnSOoSOpSOwgOyhO!eYO!hQOzhX!qhX!whX!shXuhX~OP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~P!TOknO!hqO!jlO!kmO~O]fO_SO`ZOaWOfSOnSOoSOpSOwgOyhO!eYO!hQO~OP!gXQ!gXR!gXS!gX!q!gX!w!gXT!gXU!gXV!gXW!gXX!gXY!gXZ!gX[!gX!s!gXu!gX~P$dOP!fXQ!fXR!fXS!fX!q!bX!w!bXu!bX~OPrOQrORsOSsO~OPrOQrORsOSsO!q!bX!w!bXu!bX~O]tOtsP~O]PO_SO`ZOaWOfSOnSOoSOpSO!eYO!hQO~O!d|O~Oz}O!q!bX!w!bXu!bX~O]fO_SO`ZOfSOnSOoSOpSO!eYO!hQO~OV!RO~O!q!SO!w!SO~OaWOw!UO~P(}Ozda!qda!wda!sdauda~P$dO]!WO!eYO~O!h!XO!j!XO!k!XO!l!XO!m!XO!n!XO~OknO!h!ZO!jlO!kmO~O]tOtsX~Ot!_O~O!s!`OP!fXQ!fXR!fXS!fXT!fXU!fXV!fXW!fXX!fXY!fXZ!fX[!fX~OT!bOU!bOV!aOW!aOX!aOY!aOZ!aO[!aO~OPrOQrORsOSsO~P,lOPrOQrORsOSsO!s!`O~Oz}O!s!`O~O]!cOf!cO!eYO~O]!dO`ZO!eYO~Oz}O!qca!wca!scauca~Ot!hO~P,lOt!hO~O^`O|^O~P'xOPrOQrORiiSii!qii!wii!siiuii~O^`O|^O!q!kO~P'xO^`O|^O!q!pO~P'xOu!qO~O^`O|^O!q!rOu!rP~P'xO!sqitqi~P,lOu!vO~O^`O|^O!q!rOu!rP!Q!rP!S!rP~P'xO!q!yO~O^`O|^O!q!rOu!rX!Q!rX!S!rX~P'xOu!{O~Ou#QO!Q!|O!S#PO~Ou#VO!Q!|O!S#PO~Ot#XO~Ou#VO~Ot#YO~P,lOt#YO~Ou#ZO~O!q#[O~O!q#]O~Ofo~",
|
||||||
goto: ",`!wPPPPPPPPPPPPPPPPPPP!x#X#gP$V#X$x%_P%x%xPPP%|&Y&sPP&vP&vPP&}P'Z'^'gP'kP&}'q'w'}(T(^(g(nPPPP(t(x)^PP)p*mP+[PPPPP+`+`P+sP+{,S,SdaOe!R!`!h!k!p!t#[#]R{Zi[OZe}!R!`!h!k!p!t#[#]fQOZe!R!`!h!k!p!t#[#]hgQS^ilst!b!c!d!e!|R!d}fSOZe!R!`!h!k!p!t#[#]hTQS^ilst!b!c!d!e!|Q!XmR!e}dWOe!R!`!h!k!p!t#[#]QzZQ!]sR!^t!PTOQSZ^eilst!R!`!b!c!d!e!h!k!p!t!|#[#]ToRqQ{ZQ!Q^Q!l!cR#T!|daOe!R!`!h!k!p!t#[#]YhQSl!d!eQ{ZR!ViRwXZjQSl!d!eeaOe!R!`!h!k!p!t#[#]R!o!hQ!x!pQ#^#[R#_#]T!}!x#OQ#R!xR#W#OQeOR!TeQqRR!ZqQvXR!_vW!t!k!p#[#]R!z!tWlQS!d!eR!WlS!O]|R!g!OQ#O!xR#U#OTdOeSbOeQ!i!RQ!j!`Q!n!hZ!s!k!p!t#[#]d]Oe!R!`!h!k!p!t#[#]Q|ZR!f}dVOe!R!`!h!k!p!t#[#]YhQSl!d!eQyZQ!P^Q!ViQ!]sQ!^tQ!l!bQ!m!cR#S!|dUOe!R!`!h!k!p!t#[#]hgQS^ilst!b!c!d!e!|RxZTpRqsYOQSZeil!R!`!d!e!h!k!p!t#[#]Q!u!kV!w!p#[#]ZkQSl!d!ee_Oe!R!`!h!k!p!t#[#]",
|
goto: ",c!wPPPPPPPPPPPPPPPPPPP!x#X#gP$V#X${%bP%{%{PPP&P&]&vPP&yP&yPP'QP'^'a'jP'nP'Q't'z(Q(W(a(j(qPPPP(w({)aPP)s*pP+_PPPPP+c+cP+vP,O,V,VdaOe!R!_!h!k!p!t#[#]RzYi[OYe}!R!_!h!k!p!t#[#]fPOYe!R!_!h!k!p!t#[#]hfPR^hkrs!a!b!d!e!|R!d}fROYe!R!_!h!k!p!t#[#]hSPR^hkrs!a!b!d!e!|Q!WlQ!c|R!e}dVOe!R!_!h!k!p!t#[#]QyYQ![rR!]s!PSOPRY^ehkrs!R!_!a!b!d!e!h!k!p!t!|#[#]TnQpQzYQ!Q^Q!l!bR#T!|daOe!R!_!h!k!p!t#[#]YgPRk!d!eQzYR!UhRvWZiPRk!d!eeaOe!R!_!h!k!p!t#[#]R!o!hQ!x!pQ#^#[R#_#]T!}!x#OQ#R!xR#W#OQeOR!TeQpQR!YpQuWR!^uW!t!k!p#[#]R!z!tWkPR!d!eR!VkS!O]{R!g!OQ#O!xR#U#OTdOeSbOeQ!i!RQ!j!_Q!n!hZ!s!k!p!t#[#]d]Oe!R!_!h!k!p!t#[#]Q{YR!f}dUOe!R!_!h!k!p!t#[#]YgPRk!d!eQxYQ!P^Q!UhQ![rQ!]sQ!l!aQ!m!bR#S!|dTOe!R!_!h!k!p!t#[#]hfPR^hkrs!a!b!d!e!|RwYToQpsXOPRYehk!R!_!d!e!h!k!p!t#[#]Q!u!kV!w!p#[#]ZjPRk!d!ee_Oe!R!_!h!k!p!t#[#]",
|
||||||
nodeNames: "⚠ Star Slash Plus Minus And Or Eq Neq Lt Lte Gt Gte Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Null ConditionalOp FunctionDef Params colon keyword PositionalArg Underscore NamedArg NamedArgPrefix operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
|
nodeNames: "⚠ Star Slash Plus Minus And Or Eq Neq Lt Lte Gt Gte Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Null ConditionalOp FunctionDef Params colon keyword PositionalArg Underscore NamedArg NamedArgPrefix operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
|
||||||
maxTerm: 85,
|
maxTerm: 85,
|
||||||
context: trackScope,
|
context: trackScope,
|
||||||
|
|
@ -23,5 +23,5 @@ export const parser = LRParser.deserialize({
|
||||||
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!d~~", 11)],
|
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!d~~", 11)],
|
||||||
topRules: {"Program":[0,18]},
|
topRules: {"Program":[0,18]},
|
||||||
specialized: [{term: 13, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 13, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
specialized: [{term: 13, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 13, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
||||||
tokenPrec: 860
|
tokenPrec: 863
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -274,4 +274,28 @@ end`).toMatchTree(`
|
||||||
Identifier heya
|
Identifier heya
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('can use the result of a parens expression as the property of dot get', () => {
|
||||||
|
expect('obj = list 1 2 3; obj.(1 + 2)').toMatchTree(`
|
||||||
|
Assign
|
||||||
|
AssignableIdentifier obj
|
||||||
|
Eq =
|
||||||
|
FunctionCall
|
||||||
|
Identifier list
|
||||||
|
PositionalArg
|
||||||
|
Number 1
|
||||||
|
PositionalArg
|
||||||
|
Number 2
|
||||||
|
PositionalArg
|
||||||
|
Number 3
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
DotGet
|
||||||
|
IdentifierBeforeDot obj
|
||||||
|
ParenExpr
|
||||||
|
BinOp
|
||||||
|
Number 1
|
||||||
|
Plus +
|
||||||
|
Number 2
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp'
|
||||||
import { $ } from 'bun'
|
import { $ } from 'bun'
|
||||||
import { assert, errorMessage } from '#utils/utils'
|
import { assert, errorMessage } from '#utils/utils'
|
||||||
import { Compiler } from '#compiler/compiler'
|
import { Compiler } from '#compiler/compiler'
|
||||||
import { run, VM } from 'reefvm'
|
import { run, VM, type TypeScriptFunction } from 'reefvm'
|
||||||
import { treeToString, VMResultToValue } from '#utils/tree'
|
import { treeToString, VMResultToValue } from '#utils/tree'
|
||||||
|
|
||||||
const regenerateParser = async () => {
|
const regenerateParser = async () => {
|
||||||
|
|
@ -33,13 +33,16 @@ declare module 'bun:test' {
|
||||||
toMatchTree(expected: string): T
|
toMatchTree(expected: string): T
|
||||||
toMatchExpression(expected: string): T
|
toMatchExpression(expected: string): T
|
||||||
toFailParse(): T
|
toFailParse(): T
|
||||||
toEvaluateTo(expected: unknown, nativeFunctions?: Record<string, Function>): Promise<T>
|
toEvaluateTo(
|
||||||
|
expected: unknown,
|
||||||
|
nativeFunctions?: Record<string, TypeScriptFunction>
|
||||||
|
): Promise<T>
|
||||||
toFailEvaluation(): Promise<T>
|
toFailEvaluation(): Promise<T>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
toMatchTree(received: unknown, expected: string) {
|
toMatchTree(received, expected) {
|
||||||
assert(typeof received === 'string', 'toMatchTree can only be used with string values')
|
assert(typeof received === 'string', 'toMatchTree can only be used with string values')
|
||||||
|
|
||||||
const tree = parser.parse(received)
|
const tree = parser.parse(received)
|
||||||
|
|
@ -58,7 +61,7 @@ expect.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toFailParse(received: unknown) {
|
toFailParse(received) {
|
||||||
assert(typeof received === 'string', 'toFailParse can only be used with string values')
|
assert(typeof received === 'string', 'toFailParse can only be used with string values')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -93,11 +96,7 @@ expect.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async toEvaluateTo(
|
async toEvaluateTo(received, expected, nativeFunctions = {}) {
|
||||||
received: unknown,
|
|
||||||
expected: unknown,
|
|
||||||
nativeFunctions: Record<string, Function> = {}
|
|
||||||
) {
|
|
||||||
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
|
assert(typeof received === 'string', 'toEvaluateTo can only be used with string values')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -109,13 +108,10 @@ expect.extend({
|
||||||
if (expected instanceof RegExp) expected = String(expected)
|
if (expected instanceof RegExp) expected = String(expected)
|
||||||
if (value instanceof RegExp) value = String(value)
|
if (value instanceof RegExp) value = String(value)
|
||||||
|
|
||||||
if (value === expected) {
|
expect(value).toEqual(expected)
|
||||||
return { pass: true }
|
|
||||||
} else {
|
|
||||||
return {
|
return {
|
||||||
message: () => `Expected evaluation to be ${expected}, but got ${value}`,
|
message: () => `Expected evaluation to be ${expected}, but got ${value}`,
|
||||||
pass: false,
|
pass: true,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -125,7 +121,7 @@ expect.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async toFailEvaluation(received: unknown) {
|
async toFailEvaluation(received) {
|
||||||
assert(typeof received === 'string', 'toFailEvaluation can only be used with string values')
|
assert(typeof received === 'string', 'toFailEvaluation can only be used with string values')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user