Module: Space::Src::Nav
- Defined in:
- lib/space_src/nav.rb
Overview
Fuzzy navigator over on-disk source checkouts.
Checkouts live at base_dir/<host>/<owner>/<name> (depth-3 dirs). Matching is a case-insensitive SUBSEQUENCE against “owner/name” —host is excluded from the match but is part of the resolved path.
Ranking is fzf-inspired: contiguity bonus, word-boundary bonus, earliness (earlier first match wins). Ties break by target string asc then host asc — deterministic total order.
Class Method Summary collapse
-
.dispatch(query, stdout, stderr, base_dir) ⇒ Object
Cd-contract executor.
-
.match_positions(query, target) ⇒ Object
Pure: find the leftmost match positions of query chars (case-insensitive) as a subsequence into target.
-
.rank(entries, query) ⇒ Object
Pure: match and rank a list of entry hashes against query.
-
.scan(base_dir) ⇒ Object
Enumerate all depth-3 directories under base_dir.
-
.score_match(positions, target_lower) ⇒ Object
Pure: compute a score for a set of match positions within target_lower.
Class Method Details
.dispatch(query, stdout, stderr, base_dir) ⇒ Object
Cd-contract executor. Scans base_dir for checkouts, fuzzy-matches query, applies the 0/1/many contract:
- exactly one match → absolute path on last stdout line, returns 0
- zero matches → message on stderr, returns 1
- multiple matches → ranked candidates on stdout, returns 1
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/space_src/nav.rb', line 81 def self.dispatch(query, stdout, stderr, base_dir) entries = scan(base_dir) matches = rank(entries, query) case matches.length when 0 stderr.puts "src: no match for '#{query}'" 1 when 1 stdout.puts matches.first[:path] 0 else matches.each { |m| stdout.puts "#{m[:host]}/#{m[:target]}" } 1 end end |
.match_positions(query, target) ⇒ Object
Pure: find the leftmost match positions of query chars (case-insensitive) as a subsequence into target. Returns an array of integer indices, or nil if the query is not a subsequence of the target.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/space_src/nav.rb', line 32 def self.match_positions(query, target) q = query.downcase t = target.downcase positions = [] qi = 0 t.each_char.with_index do |c, i| if c == q[qi] positions << i qi += 1 return positions if qi == q.length end end nil end |
.rank(entries, query) ⇒ Object
Pure: match and rank a list of entry hashes against query. Returns entries annotated with :score, sorted best-first. Tie-break: target asc, then host asc.
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/space_src/nav.rb', line 65 def self.rank(entries, query) scored = entries.filter_map do |e| t = e[:target].downcase positions = match_positions(query, t) next unless positions score = score_match(positions, t) e.merge(score:) end scored.sort_by { |e| [-e[:score], e[:target], e[:host]] } end |
.scan(base_dir) ⇒ Object
Enumerate all depth-3 directories under base_dir. Returns array of hashes: owner:, name:, target:, path:.
16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/space_src/nav.rb', line 16 def self.scan(base_dir) pattern = File.join(base_dir, "*", "*", "*") prefix = base_dir.chomp("/") + "/" Dir.glob(pattern).filter_map do |path| next unless File.directory?(path) relative = path.delete_prefix(prefix) parts = relative.split("/") next unless parts.length == 3 host, owner, name = parts {host:, owner:, name:, target: "#{owner}/#{name}", path:} end end |
.score_match(positions, target_lower) ⇒ Object
Pure: compute a score for a set of match positions within target_lower. Higher score = better match.
contiguity: each consecutive pair of matched indices scores +10
word boundary: each position at start of string or right after
'/', '-', '_' scores +5
earliness: subtract the first matched position (earlier = higher score)
53 54 55 56 57 58 59 60 |
# File 'lib/space_src/nav.rb', line 53 def self.score_match(positions, target_lower) contiguity = positions.each_cons(2).count { |a, b| b == a + 1 } * 10 boundary = positions.count do |p| p == 0 || "/\\-_".include?(target_lower[p - 1]) end * 5 earliness = -positions.first contiguity + boundary + earliness end |