From d195c5321c63bc227ad13533a81fb88961f6371a Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 31 Oct 2025 10:00:06 -0700 Subject: [PATCH] wip --- src/editor/autocomplete.test.ts | 44 +++++++++++++++++++++++++++++---- src/editor/autocomplete.ts | 39 +++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/editor/autocomplete.test.ts b/src/editor/autocomplete.test.ts index ac1cca6..dd64924 100644 --- a/src/editor/autocomplete.test.ts +++ b/src/editor/autocomplete.test.ts @@ -90,7 +90,7 @@ describe('autocomplete function names', () => { }) }) -describe('autocomplete function arguments', () => { +describe('autocomplete positional arguments', () => { test('shows args for shrimp function', () => { const args = getArgsInScope(` add = do x y: x + y end @@ -123,6 +123,44 @@ describe('autocomplete function arguments', () => { }) }) +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 + `) + // 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 + `) + // 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 + `) + // 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 + `) + // bravo is named, 10 fills alpha (first positional slot) + expect(args).toEqual(['alpha', 'charlie']) + }) +}) + // Helper functions const getVarsInScope = (codeWithCursor: string): CompletionItem[] => { @@ -163,7 +201,3 @@ const getArgsInScope = (codeWithCursor: string): string[] => { return items.map((v) => v.name) } - -const getArgItems = (codeWithCursor: string): CompletionItem[] => { - return getVarsInScope(codeWithCursor).filter((v) => v.kind === 'arg') -} diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index a4d19bc..75b464c 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -42,16 +42,19 @@ const autocompleteArg = (view: EditorView, context: FunctionCallContext): Comple return [] } - // Count how many positional args have already been used + // Collect used args const usedPositionalArgs = countPositionalArgs(context.fnCallNode, cursor) + const usedNamedArgs = collectUsedNamedArgs(context.fnCallNode, cursor, view) if (fn.kind === 'shrimp-fn') { - // Skip params that have already been filled positionally - const remainingParams = fn.params.slice(usedPositionalArgs) + // 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') { - // Skip params that have already been filled positionally - const remainingParams = fn.def.signature.params.slice(usedPositionalArgs) + // 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, @@ -266,6 +269,32 @@ const countPositionalArgs = (fnCallNode: SyntaxNode, cursor: number): number => return count } +const collectUsedNamedArgs = ( + fnCallNode: SyntaxNode, + cursor: number, + view: EditorView +): Set => { + const usedNames = new Set() + 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