Class: SwarmCLI::V3::FileCompleter

Inherits:
Object
  • Object
show all
Defined in:
lib/swarm_cli/v3/file_completer.rb

Overview

File path completion logic for autocomplete.

FileCompleter provides methods to extract completion targets from text buffers and find matching file paths using ripgrep (rg). It has no knowledge of the UI, keyboard input, or display rendering - it only handles the file matching logic.

Examples:

Extracting completion word

buffer = "check @lib/swa"
cursor = 14  # After 'swa'
result = FileCompleter.extract_completion_word(buffer, cursor)
# => ["check ", "@lib/swa", ""]

Finding matches

matches = FileCompleter.find_matches("@lib/swa", max: 5)
# => ["@lib/swarm_sdk/", "@lib/swarm_cli/", ...]

Class Method Summary collapse

Class Method Details

.extract_completion_word(buffer, cursor) ⇒ Array<String>?

Extract the word at cursor position that should trigger completion.

Searches backward from cursor to find the nearest @ symbol, then extracts from that @ to the cursor position. Returns nil if no @ is found before cursor.

Examples:

Cursor at end of word

extract_completion_word("check @README.md", 16)
# => ["check ", "@README.md", ""]

Cursor in middle of word

extract_completion_word("check @README.md", 10)
# => ["check ", "@READ", "ME.md"]

No @ before cursor

extract_completion_word("check file.txt", 10)
# => nil

Parameters:

  • buffer (String)

    the full text buffer

  • cursor (Integer)

    the cursor position (0-based index)

Returns:

  • (Array<String>, nil)
    pre, target, post

    or nil if no @ found

    • pre: text before the @

    • target: the @ and characters up to cursor

    • post: text after cursor



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/swarm_cli/v3/file_completer.rb', line 46

def extract_completion_word(buffer, cursor)
  # Find @ symbol before cursor
  before_cursor = buffer[0...cursor]
  at_index = before_cursor.rindex("@")
  return unless at_index

  # Extract target from @ to cursor
  target_start = at_index
  target_end = cursor

  # Find end of word after cursor (next space or end of buffer)
  target_end += 1 while target_end < buffer.length && buffer[target_end] !~ /\s/

  pre = buffer[0...target_start]
  target = buffer[target_start...cursor]
  post = buffer[cursor...target_end] || ""

  [pre, target, post]
end

.find_matches(target, max: 5) ⇒ Array<String>

Note:

Requires ripgrep (rg) to be installed. Returns [] if not available.

Find matching files for a query (with @ prefix).

Uses ripgrep (rg) for fast file searching with fuzzy case-insensitive matching. Falls back to empty array if rg is not available or fails.

Examples:

Empty query shows current directory

find_matches("@", max: 5)
# => ["@README.md", "@Gemfile", "@lib/", "@test/", "@bin/"]

Fuzzy matching

find_matches("@lib/swa", max: 5)
# => ["@lib/swarm_sdk/", "@lib/swarm_cli/", ...]

Parameters:

  • target (String)

    the search target (must start with @)

  • max (Integer) (defaults to: 5)

    maximum number of results to return

Returns:

  • (Array<String>)

    array of completion strings (with @ prefix)



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/swarm_cli/v3/file_completer.rb', line 85

def find_matches(target, max: 5)
  return [] unless target.start_with?("@")

  query = target[1..] # Strip @

  # Empty query -> show current directory contents (non-hidden)
  if query.empty?
    matches = %x(rg --files --max-count=#{max} 2>/dev/null).split("\n")
    return matches.first(max).map { |p| "@#{p}" }
  end

  # Use rg --files with grep for fuzzy matching
  # Get more results than requested so we can sort by quality
  escaped_query = Regexp.escape(query)
  matches = %x(rg --files 2>/dev/null | rg -i '#{escaped_query}' --max-count=#{max * 3}).split("\n")

  # Sort by match quality (exact matches first, then by relevance)
  sorted = matches.sort_by { |path| -score_match(path, query) }
  sorted.first(max).map { |p| "@#{p}" }
rescue StandardError => _e
  # Fallback to empty if rg fails
  []
end

.score_match(path, query) ⇒ Integer

Score a match based on quality (higher = better). Prioritizes exact matches, then start-of-filename matches, then shorter/shallower paths.

Parameters:

  • path (String)

    the file path

  • query (String)

    the search query (without @)

Returns:

  • (Integer)

    match quality score (higher is better)



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/swarm_cli/v3/file_completer.rb', line 116

def score_match(path, query)
  score = 0
  basename = File.basename(path)
  query_lower = query.downcase
  basename_lower = basename.downcase

  # Exact match gets highest priority
  score += 1000 if basename_lower == query_lower

  # Match at start of filename gets high priority
  score += 500 if basename_lower.start_with?(query_lower)

  # Prefer shorter filenames (more specific/complete matches)
  score += (100 - basename.length).clamp(0, 100)

  # Prefer shallower paths (current directory over subdirectories)
  depth = path.count("/")
  score -= depth * 20

  # Small bonus for any match (already filtered by rg)
  score += 10

  score
end