Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/images/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions docs/tapes/demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ Enter

Sleep 2.5s

Type "s"
Sleep 2s

Type ":"
Sleep 1.5s
Type "vpc/subnets"
Expand Down
55 changes: 36 additions & 19 deletions internal/view/command_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,14 @@ func (c *CommandInput) resolveDestination(input string) string {

// If input contains "/", try ParseServiceResource for full path
if strings.Contains(input, "/") {
parts := strings.SplitN(input, "/", 2)
resourcePart := ""
if len(parts) > 1 {
resourcePart = parts[1]
}
if service, resourceType, err := c.registry.ParseServiceResource(input); err == nil {
if resourceType != "" {
// Only show resource if user typed something after "/"
if resourcePart != "" && resourceType != "" {
return service + "/" + resourceType
}
return service
Expand All @@ -286,8 +292,7 @@ func (c *CommandInput) resolveDestination(input string) string {

// Fallback: prefix match on service/alias
if svc, res, ok := c.resolvePrefixMatch(input); ok {
// Only show resource if user explicitly typed "/"
if strings.Contains(input, "/") && res != "" {
if res != "" {
return svc + "/" + res
}
return svc
Expand All @@ -296,8 +301,8 @@ func (c *CommandInput) resolveDestination(input string) string {
return ""
}

// resolvePrefixMatch tries prefix match on services and aliases, returns resolved service/resource.
// Returns empty strings if no match found.
// resolvePrefixMatch tries prefix match on services, aliases, and resources.
// Returns resolved service/resource. Empty strings if no match found.
func (c *CommandInput) resolvePrefixMatch(input string) (service, resource string, ok bool) {
parts := strings.SplitN(input, "/", 2)
servicePart := parts[0]
Expand All @@ -307,38 +312,47 @@ func (c *CommandInput) resolvePrefixMatch(input string) (service, resource strin
}

// Try prefix match on service name
var matched string
var matchedService string
for _, svc := range c.registry.ListServices() {
if strings.HasPrefix(svc, servicePart) {
matched = svc
matchedService = svc
break
}
}

// Try prefix match on alias if no service matched
if matched == "" {
var aliasResource string
if matchedService == "" {
for _, alias := range c.registry.GetAliases() {
if strings.HasPrefix(alias, servicePart) {
matched = alias
break
// Resolve alias to service (and resource if alias includes it)
if resolved, res, resolveOK := c.registry.ResolveAlias(alias); resolveOK {
matchedService = resolved
aliasResource = res
break
}
}
}
}

if matched == "" {
if matchedService == "" {
return "", "", false
}

// Build full path and parse via ParseServiceResource (handles alias resolution)
fullPath := matched
if resourcePart != "" {
fullPath = matched + "/" + resourcePart
// If no resource part specified, use alias resource (if any) or let caller use default
if resourcePart == "" {
return matchedService, aliasResource, true
}
svc, res, err := c.registry.ParseServiceResource(fullPath)
if err != nil {
return "", "", false

// Try prefix match on resource name (sorted, so first match = alphabetically first)
for _, res := range c.registry.ListResources(matchedService) {
if strings.HasPrefix(res, resourcePart) {
return matchedService, res, true
}
}
return svc, res, true

// No matching resource found
return "", "", false
}

// SetTagProvider sets the tag completion provider
Expand Down Expand Up @@ -484,6 +498,9 @@ func (c *CommandInput) executeCommand() (tea.Cmd, *NavigateMsg) {

// Fallback: prefix matching for partial input
if svc, res, ok := c.resolvePrefixMatch(input); ok {
if res == "" {
res = c.registry.DefaultResource(svc)
}
browser := NewResourceBrowserWithType(c.ctx, c.registry, svc, res)
return nil, &NavigateMsg{View: browser}
}
Expand Down
56 changes: 56 additions & 0 deletions internal/view/command_input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,3 +690,59 @@ func TestCommandInput_TabCompletionSingleMatch(t *testing.T) {
t.Errorf("After Tab with single match, got %q, want 'bedrock'", got)
}
}

func TestCommandInput_ResourcePrefixMatching(t *testing.T) {
ctx := context.Background()
reg := registry.New()

// Register ec2 with multiple resources
reg.RegisterCustom("ec2", "instances", registry.Entry{})
reg.RegisterCustom("ec2", "images", registry.Entry{})
reg.RegisterCustom("ec2", "volumes", registry.Entry{})

ci := NewCommandInput(ctx, reg)
ci.Activate()

tests := []struct {
input string
wantDest string
}{
{"ec2/in", "ec2/instances"}, // prefix match "in" -> "instances"
{"ec2/im", "ec2/images"}, // prefix match "im" -> "images"
{"ec2/vol", "ec2/volumes"}, // prefix match "vol" -> "volumes"
{"ec2/instances", "ec2/instances"}, // exact match
}

for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := ci.resolveDestination(tt.input)
if got != tt.wantDest {
t.Errorf("resolveDestination(%q) = %q, want %q", tt.input, got, tt.wantDest)
}
})
}
}

func TestCommandInput_AliasResourcePreservation(t *testing.T) {
ctx := context.Background()
reg := registry.New()

// Register cloudwatch with log-groups (the "logs" alias points here)
reg.RegisterCustom("cloudwatch", "log-groups", registry.Entry{})
reg.RegisterCustom("cloudwatch", "metrics", registry.Entry{})

ci := NewCommandInput(ctx, reg)
ci.Activate()

// "logs" alias resolves to "cloudwatch/log-groups"
dest := ci.resolveDestination("logs")
if dest != "cloudwatch/log-groups" {
t.Errorf("resolveDestination('logs') = %q, want 'cloudwatch/log-groups'", dest)
}

// Prefix match on alias should also work
dest = ci.resolveDestination("log")
if dest != "cloudwatch/log-groups" {
t.Errorf("resolveDestination('log') = %q, want 'cloudwatch/log-groups'", dest)
}
}