wip
This commit is contained in:
parent
a1693078f9
commit
9b57304b87
169
src/editor/autocomplete.test.ts
Normal file
169
src/editor/autocomplete.test.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
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 function 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([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getArgItems = (codeWithCursor: string): CompletionItem[] => {
|
||||||
|
return getVarsInScope(codeWithCursor).filter((v) => v.kind === 'arg')
|
||||||
|
}
|
||||||
305
src/editor/autocomplete.ts
Normal file
305
src/editor/autocomplete.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
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 []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count how many positional args have already been used
|
||||||
|
const usedPositionalArgs = countPositionalArgs(context.fnCallNode, cursor)
|
||||||
|
|
||||||
|
if (fn.kind === 'shrimp-fn') {
|
||||||
|
// Skip params that have already been filled positionally
|
||||||
|
const remainingParams = fn.params.slice(usedPositionalArgs)
|
||||||
|
return remainingParams.map((paramName) => ({ kind: 'arg', name: paramName }))
|
||||||
|
} else if (fn.kind === 'nose-command') {
|
||||||
|
// Skip params that have already been filled positionally
|
||||||
|
const remainingParams = fn.def.signature.params.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
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
@ -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) => {
|
||||||
view.dispatch({
|
// if only whitespace is before the cursor on the line, insert two spaces
|
||||||
changes: { from: view.state.selection.main.from, insert: ' ' },
|
const line = view.state.doc.lineAt(view.state.selection.main.from)
|
||||||
selection: { anchor: view.state.selection.main.from + 2 },
|
const textBeforeCursor = line.text.slice(0, view.state.selection.main.from - line.from)
|
||||||
})
|
if (textBeforeCursor.trim() === '') {
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: view.state.selection.main.from, insert: ' ' },
|
||||||
|
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,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user