ace-search 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-search.yml +19 -0
- data/.ace-defaults/search/config.yml +45 -0
- data/.ace-defaults/search/presets/code.yml +22 -0
- data/CHANGELOG.md +404 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/Rakefile +14 -0
- data/exe/ace-search +14 -0
- data/handbook/agents/research.ag.md +214 -0
- data/handbook/agents/search.ag.md +331 -0
- data/handbook/skills/as-search-feature-research/SKILL.md +29 -0
- data/handbook/skills/as-search-research/SKILL.md +37 -0
- data/handbook/skills/as-search-run/SKILL.md +47 -0
- data/handbook/workflow-instructions/search/feature-research.wf.md +274 -0
- data/handbook/workflow-instructions/search/research.wf.md +211 -0
- data/handbook/workflow-instructions/search/run.wf.md +289 -0
- data/lib/ace/search/atoms/debug_logger.rb +61 -0
- data/lib/ace/search/atoms/fd_executor.rb +168 -0
- data/lib/ace/search/atoms/pattern_analyzer.rb +176 -0
- data/lib/ace/search/atoms/result_parser.rb +111 -0
- data/lib/ace/search/atoms/ripgrep_executor.rb +160 -0
- data/lib/ace/search/atoms/search_path_resolver.rb +79 -0
- data/lib/ace/search/atoms/tool_checker.rb +69 -0
- data/lib/ace/search/cli/commands/search.rb +240 -0
- data/lib/ace/search/cli.rb +34 -0
- data/lib/ace/search/models/search_options.rb +66 -0
- data/lib/ace/search/models/search_preset.rb +34 -0
- data/lib/ace/search/models/search_result.rb +109 -0
- data/lib/ace/search/molecules/dwim_analyzer.rb +52 -0
- data/lib/ace/search/molecules/fzf_integrator.rb +71 -0
- data/lib/ace/search/molecules/preset_manager.rb +98 -0
- data/lib/ace/search/molecules/search_option_builder.rb +113 -0
- data/lib/ace/search/molecules/time_filter.rb +60 -0
- data/lib/ace/search/organisms/result_aggregator.rb +73 -0
- data/lib/ace/search/organisms/result_formatter.rb +103 -0
- data/lib/ace/search/organisms/unified_searcher.rb +165 -0
- data/lib/ace/search/version.rb +7 -0
- data/lib/ace/search.rb +87 -0
- metadata +181 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
title: Search Workflow
|
|
4
|
+
purpose: search workflow instruction
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-03-06
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Search Workflow
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Intelligent code and file discovery using the **ace-search** gem - finding files by name, searching for code patterns, and exploring project structure.
|
|
15
|
+
|
|
16
|
+
## Primary Tool: ace-search
|
|
17
|
+
|
|
18
|
+
You use the **ace-search** command exclusively for all search operations. This unified tool combines file and content searching with intelligent DWIM (Do What I Mean) pattern analysis.
|
|
19
|
+
|
|
20
|
+
## Search Modes
|
|
21
|
+
|
|
22
|
+
### Auto Mode (Default - DWIM)
|
|
23
|
+
Let ace-search intelligently detect search type:
|
|
24
|
+
```bash
|
|
25
|
+
# File glob patterns auto-detected
|
|
26
|
+
ace-search "*.rb"
|
|
27
|
+
ace-search "test_*.md"
|
|
28
|
+
|
|
29
|
+
# Content searches auto-detected
|
|
30
|
+
ace-search "class TaskManager"
|
|
31
|
+
ace-search "def initialize"
|
|
32
|
+
|
|
33
|
+
# Hybrid searches
|
|
34
|
+
ace-search "bin/as-search"
|
|
35
|
+
ace-search "TODO"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Explicit Modes
|
|
39
|
+
|
|
40
|
+
**File Search** - Find files by name/pattern:
|
|
41
|
+
```bash
|
|
42
|
+
ace-search "agent" --file
|
|
43
|
+
ace-search "*.md" --file
|
|
44
|
+
ace-search "*Manager*" --file
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Content Search** - Search within files:
|
|
48
|
+
```bash
|
|
49
|
+
ace-search "require 'ace/core'" --content
|
|
50
|
+
ace-search "TODO|FIXME" --content
|
|
51
|
+
ace-search "class.*Agent" --content
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Hybrid Mode** - Search both:
|
|
55
|
+
```bash
|
|
56
|
+
ace-search "TaskManager" --hybrid
|
|
57
|
+
ace-search "config" --hybrid
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Scope Control
|
|
61
|
+
|
|
62
|
+
### Search Scope
|
|
63
|
+
Limit search to specific paths:
|
|
64
|
+
```bash
|
|
65
|
+
# Search specific directory (change directory first)
|
|
66
|
+
cd lib/ && ace-search "pattern"
|
|
67
|
+
cd ace-taskflow/ && ace-search "TODO"
|
|
68
|
+
|
|
69
|
+
# Or use --include to filter paths
|
|
70
|
+
ace-search "pattern" --include "lib/**/*"
|
|
71
|
+
ace-search "TODO" --include "ace-taskflow/**/*"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### File Pattern Filtering (Glob)
|
|
75
|
+
Filter by file patterns:
|
|
76
|
+
```bash
|
|
77
|
+
# Search only Ruby files
|
|
78
|
+
ace-search "class" --content --glob "**/*.rb"
|
|
79
|
+
|
|
80
|
+
# Search only markdown in specific paths
|
|
81
|
+
ace-search "TODO" --content --glob "docs/**/*.md"
|
|
82
|
+
|
|
83
|
+
# Multiple patterns
|
|
84
|
+
ace-search "config" --glob "**/*.{yml,yaml,json}"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Git Scope
|
|
88
|
+
Limit to git-tracked files:
|
|
89
|
+
```bash
|
|
90
|
+
# Staged files only
|
|
91
|
+
ace-search "console.log" --staged
|
|
92
|
+
|
|
93
|
+
# Tracked files only
|
|
94
|
+
ace-search "TODO" --tracked
|
|
95
|
+
|
|
96
|
+
# Changed files only
|
|
97
|
+
ace-search "FIXME" --changed
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Search Modifiers
|
|
101
|
+
|
|
102
|
+
### Pattern Matching
|
|
103
|
+
```bash
|
|
104
|
+
# Case-insensitive
|
|
105
|
+
ace-search "todo" --case-insensitive
|
|
106
|
+
|
|
107
|
+
# Whole word matching
|
|
108
|
+
ace-search "test" --whole-word
|
|
109
|
+
|
|
110
|
+
# Multiline patterns
|
|
111
|
+
ace-search "class.*end" --multiline
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Context Display
|
|
115
|
+
Show surrounding lines:
|
|
116
|
+
```bash
|
|
117
|
+
# 3 lines before and after
|
|
118
|
+
ace-search "error" --context 3
|
|
119
|
+
|
|
120
|
+
# 2 lines after
|
|
121
|
+
ace-search "warning" --after-context 2
|
|
122
|
+
|
|
123
|
+
# 2 lines before
|
|
124
|
+
ace-search "exception" --before-context 2
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Output Control
|
|
128
|
+
```bash
|
|
129
|
+
# Limit results
|
|
130
|
+
ace-search "TODO" --max-results 20
|
|
131
|
+
|
|
132
|
+
# Show only filenames
|
|
133
|
+
ace-search "deprecated" --files-with-matches
|
|
134
|
+
|
|
135
|
+
# Output formats
|
|
136
|
+
ace-search "class" --format json
|
|
137
|
+
ace-search "def" --format yaml
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Preset Support
|
|
141
|
+
|
|
142
|
+
Use predefined search configurations:
|
|
143
|
+
```bash
|
|
144
|
+
# Use preset from .ace/search/presets/
|
|
145
|
+
ace-search --preset ruby-classes
|
|
146
|
+
|
|
147
|
+
# List available presets
|
|
148
|
+
ace-search --list-presets
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Common Workflows
|
|
152
|
+
|
|
153
|
+
### Finding Implementation
|
|
154
|
+
```bash
|
|
155
|
+
# 1. Broad discovery
|
|
156
|
+
ace-search "TaskManager"
|
|
157
|
+
|
|
158
|
+
# 2. Narrow by file type
|
|
159
|
+
ace-search "TaskManager" --glob "**/*.rb"
|
|
160
|
+
|
|
161
|
+
# 3. Find class definition
|
|
162
|
+
ace-search "class TaskManager" --content --glob "**/*.rb"
|
|
163
|
+
|
|
164
|
+
# 4. Find usage
|
|
165
|
+
ace-search "TaskManager.new" --content
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Searching Configuration
|
|
169
|
+
```bash
|
|
170
|
+
# Find YAML config files
|
|
171
|
+
ace-search "*.yml" --file --search-root .ace
|
|
172
|
+
|
|
173
|
+
# Search within config
|
|
174
|
+
ace-search "model: opus" --content --glob "**/*.yml"
|
|
175
|
+
|
|
176
|
+
# Find environment variables
|
|
177
|
+
ace-search "API_KEY" --content
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Exploring Code Structure
|
|
181
|
+
```bash
|
|
182
|
+
# Find all requires
|
|
183
|
+
ace-search "require 'ace" --content --glob "**/*.rb"
|
|
184
|
+
|
|
185
|
+
# Find class definitions
|
|
186
|
+
ace-search "class.*< " --content --glob "**/*.rb"
|
|
187
|
+
|
|
188
|
+
# Find method definitions (in lib/ directory)
|
|
189
|
+
cd lib/ && ace-search "def " --content
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Debugging and Maintenance
|
|
193
|
+
```bash
|
|
194
|
+
# Find TODOs and FIXMEs
|
|
195
|
+
ace-search "TODO|FIXME" --content
|
|
196
|
+
|
|
197
|
+
# Find deprecated code
|
|
198
|
+
ace-search "deprecated" --case-insensitive --content
|
|
199
|
+
|
|
200
|
+
# Find error handling
|
|
201
|
+
ace-search "rescue|raise" --content --glob "**/*.rb"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Search Strategy
|
|
205
|
+
|
|
206
|
+
### Progressive Refinement
|
|
207
|
+
1. **Start broad**: Use auto mode to understand scope
|
|
208
|
+
```bash
|
|
209
|
+
ace-search "notification"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
2. **Identify patterns**: Look at results to understand structure
|
|
213
|
+
```bash
|
|
214
|
+
ace-search "notification" --files-with-matches
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
3. **Narrow focus**: Add filters based on findings
|
|
218
|
+
```bash
|
|
219
|
+
ace-search "class.*Notification" --content --glob "**/*.rb" --include "lib/**/*"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
4. **Verify relevance**: Check sample results
|
|
223
|
+
```bash
|
|
224
|
+
ace-search "Notification.new" --content --max-results 5
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Efficiency Tips
|
|
228
|
+
- Use `--file` for filename-only searches (faster)
|
|
229
|
+
- Change directories or use `--include` to limit scope
|
|
230
|
+
- Use `--glob` to filter file types
|
|
231
|
+
- Apply `--max-results` for initial exploration
|
|
232
|
+
- Leverage auto mode for intelligent detection
|
|
233
|
+
|
|
234
|
+
## Response Format
|
|
235
|
+
|
|
236
|
+
### Success Response
|
|
237
|
+
```markdown
|
|
238
|
+
## Search Summary
|
|
239
|
+
Found [N] matches for "[pattern]" across [M] files.
|
|
240
|
+
|
|
241
|
+
## Key Results
|
|
242
|
+
- path/to/file.rb:42: [relevant match with context]
|
|
243
|
+
- another/file.md:15: [relevant match with context]
|
|
244
|
+
|
|
245
|
+
## Patterns Observed
|
|
246
|
+
- [Common themes or structures]
|
|
247
|
+
- [Notable file/directory concentrations]
|
|
248
|
+
|
|
249
|
+
## Suggestions
|
|
250
|
+
- Refine with: ace-search "[pattern]" --glob "**/*.ext" --include "path/**/*"
|
|
251
|
+
- Explore: [specific files or directories of interest]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### No Results Response
|
|
255
|
+
```markdown
|
|
256
|
+
## Search Summary
|
|
257
|
+
No matches found for "[pattern]".
|
|
258
|
+
|
|
259
|
+
## Suggestions
|
|
260
|
+
- Try alternative terms or patterns
|
|
261
|
+
- Broaden scope: --search-root .
|
|
262
|
+
- Use case-insensitive: --case-insensitive
|
|
263
|
+
- Check different file types: --glob "**/*.ext"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Large Result Set Response
|
|
267
|
+
```markdown
|
|
268
|
+
## Search Summary
|
|
269
|
+
Found [N] matches (showing first [M] due to limit).
|
|
270
|
+
|
|
271
|
+
## Top Results
|
|
272
|
+
[Most relevant matches]
|
|
273
|
+
|
|
274
|
+
## Refinement Suggestions
|
|
275
|
+
To narrow results, try:
|
|
276
|
+
- ace-search "[pattern]" --glob "**/*.rb"
|
|
277
|
+
- cd specific/path/ && ace-search "[pattern]"
|
|
278
|
+
- ace-search "[pattern]" --whole-word
|
|
279
|
+
- ace-search "[pattern]" --include "specific/path/**/*"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Important Notes
|
|
283
|
+
|
|
284
|
+
- **Discovery Only**: This workflow searches but does not modify files
|
|
285
|
+
- **DWIM Mode**: Auto mode intelligently detects file vs content searches
|
|
286
|
+
- **Git Integration**: Supports scoping to staged/tracked/changed files
|
|
287
|
+
- **Preset Support**: Can use predefined search configurations
|
|
288
|
+
- **Performance**: Use specific modes and filters for faster searches
|
|
289
|
+
- **Results Limit**: Default max-results prevents overwhelming output
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Search
|
|
5
|
+
module Atoms
|
|
6
|
+
# Centralized debug logging for ace-search
|
|
7
|
+
#
|
|
8
|
+
# NOTE: This logger caches the ENV["DEBUG"] state on first use for performance
|
|
9
|
+
# and is intended for single-threaded, short-lived CLI processes.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# DebugLogger.log("message")
|
|
13
|
+
# DebugLogger.section("Title") do
|
|
14
|
+
# DebugLogger.log("detail 1")
|
|
15
|
+
# DebugLogger.log("detail 2")
|
|
16
|
+
# end
|
|
17
|
+
module DebugLogger
|
|
18
|
+
# Check if debug logging is enabled via DEBUG environment variable
|
|
19
|
+
#
|
|
20
|
+
# @return [Boolean] true if DEBUG is set to "1" or "true"
|
|
21
|
+
def self.enabled?
|
|
22
|
+
@enabled ||= begin
|
|
23
|
+
debug_value = ENV["DEBUG"]
|
|
24
|
+
debug_value == "1" || debug_value == "true"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Log a debug message to stderr if debugging is enabled
|
|
29
|
+
#
|
|
30
|
+
# @param message [String] Message to log
|
|
31
|
+
# @param prefix [String] Prefix for the message (default: "DEBUG")
|
|
32
|
+
# @return [void]
|
|
33
|
+
def self.log(message, prefix: "DEBUG")
|
|
34
|
+
return unless enabled?
|
|
35
|
+
warn "#{prefix}: #{message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Log a section with title and optional block for grouped output
|
|
39
|
+
#
|
|
40
|
+
# @param title [String] Section title
|
|
41
|
+
# @yield Optional block to execute within the section
|
|
42
|
+
# @return [void]
|
|
43
|
+
def self.section(title)
|
|
44
|
+
return unless enabled?
|
|
45
|
+
|
|
46
|
+
warn "=" * 60
|
|
47
|
+
warn "DEBUG: #{title}"
|
|
48
|
+
yield if block_given?
|
|
49
|
+
warn "=" * 60
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Reset the enabled cache (useful for testing)
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def self.reset!
|
|
56
|
+
@enabled = nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require_relative "debug_logger"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Search
|
|
10
|
+
module Atoms
|
|
11
|
+
# FdExecutor provides a safe wrapper around fd command
|
|
12
|
+
# This is an atom - pure function for executing fd commands
|
|
13
|
+
module FdExecutor
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Execute fd with given pattern and options
|
|
17
|
+
# @param pattern [String] Search pattern (can be glob or regex)
|
|
18
|
+
# @param options [Hash] Search options
|
|
19
|
+
# @return [Hash] Command result with success status and output
|
|
20
|
+
def execute(pattern = nil, options = {})
|
|
21
|
+
command = build_command(pattern, options)
|
|
22
|
+
# Read timeout from options (runtime override), then config, then fallback to 120 seconds
|
|
23
|
+
default_timeout = Ace::Search.config["timeout"] || 120
|
|
24
|
+
timeout_seconds = options.fetch(:timeout, default_timeout)
|
|
25
|
+
|
|
26
|
+
# Debug output
|
|
27
|
+
DebugLogger.section("FdExecutor") do
|
|
28
|
+
DebugLogger.log("Command: #{command}")
|
|
29
|
+
DebugLogger.log("Will chdir to: #{options[:search_path] || "(current directory)"}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
# Change to search directory if specified, otherwise use current dir
|
|
34
|
+
# This ensures .gitignore, excludes, and includes are processed correctly
|
|
35
|
+
search_dir = options[:search_path] || "."
|
|
36
|
+
|
|
37
|
+
stdout, stderr, status = Timeout.timeout(timeout_seconds) do
|
|
38
|
+
Open3.capture3(command, chdir: search_dir)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
success: status.success?,
|
|
43
|
+
stdout: stdout,
|
|
44
|
+
stderr: stderr,
|
|
45
|
+
exit_code: status.exitstatus,
|
|
46
|
+
command: command
|
|
47
|
+
}
|
|
48
|
+
rescue Timeout::Error
|
|
49
|
+
{
|
|
50
|
+
success: false,
|
|
51
|
+
error: "Command timed out after #{timeout_seconds} seconds",
|
|
52
|
+
exit_code: -1
|
|
53
|
+
}
|
|
54
|
+
rescue => e
|
|
55
|
+
{
|
|
56
|
+
success: false,
|
|
57
|
+
error: e.message,
|
|
58
|
+
exit_code: -1
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check if fd is available
|
|
64
|
+
# @return [Boolean] True if fd is installed
|
|
65
|
+
def available?
|
|
66
|
+
stdout, _stderr, status = Open3.capture3("which fd")
|
|
67
|
+
status.success? && !stdout.strip.empty?
|
|
68
|
+
rescue
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build fd command with proper escaping
|
|
73
|
+
# @param pattern [String, nil] Search pattern (nil for all files)
|
|
74
|
+
# @param options [Hash] Search options
|
|
75
|
+
# @return [String] Complete fd command
|
|
76
|
+
def build_command(pattern = nil, options = {})
|
|
77
|
+
args = ["fd"]
|
|
78
|
+
|
|
79
|
+
# Basic options
|
|
80
|
+
args << "--color=never" unless options[:color]
|
|
81
|
+
args << "--absolute-path" if options[:absolute_path]
|
|
82
|
+
args << "--follow" if options[:follow_symlinks]
|
|
83
|
+
args << "--hidden" if options[:include_hidden] || options[:hidden]
|
|
84
|
+
args << "--no-ignore" if options[:no_ignore]
|
|
85
|
+
args << "--no-ignore-vcs" if options[:no_ignore_vcs]
|
|
86
|
+
|
|
87
|
+
# Type filtering
|
|
88
|
+
case options[:fd_type]
|
|
89
|
+
when "f", "file"
|
|
90
|
+
args << "--type=file"
|
|
91
|
+
when "d", "directory"
|
|
92
|
+
args << "--type=directory"
|
|
93
|
+
when "l", "symlink"
|
|
94
|
+
args << "--type=symlink"
|
|
95
|
+
when "s", "socket"
|
|
96
|
+
args << "--type=socket"
|
|
97
|
+
when "p", "pipe"
|
|
98
|
+
args << "--type=pipe"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Extension filtering
|
|
102
|
+
if options[:extension]
|
|
103
|
+
Array(options[:extension]).each { |ext| args << "--extension=#{ext}" }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Size filtering
|
|
107
|
+
args << "--size=#{options[:size]}" if options[:size]
|
|
108
|
+
|
|
109
|
+
# Depth limiting
|
|
110
|
+
args << "--max-depth=#{options[:max_depth]}" if options[:max_depth]
|
|
111
|
+
args << "--min-depth=#{options[:min_depth]}" if options[:min_depth]
|
|
112
|
+
|
|
113
|
+
# Case sensitivity
|
|
114
|
+
args << "--ignore-case" if options[:ignore_case]
|
|
115
|
+
args << "--case-sensitive" if options[:case_sensitive]
|
|
116
|
+
|
|
117
|
+
# Max results
|
|
118
|
+
args << "--max-results=#{options[:max_results]}" if options[:max_results]
|
|
119
|
+
|
|
120
|
+
# Exclude patterns
|
|
121
|
+
if options[:exclude]
|
|
122
|
+
Array(options[:exclude]).each { |pattern| args << "--exclude=#{pattern}" }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Search paths
|
|
126
|
+
# When search_path is set, we use chdir in execute(), so search current dir
|
|
127
|
+
paths = if options[:search_path]
|
|
128
|
+
["."] # Will chdir to search_path, then search current dir
|
|
129
|
+
elsif options[:paths]
|
|
130
|
+
options[:paths]
|
|
131
|
+
else
|
|
132
|
+
["."]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Build the complete command
|
|
136
|
+
command_parts = args
|
|
137
|
+
|
|
138
|
+
# Add pattern with appropriate flag
|
|
139
|
+
if pattern
|
|
140
|
+
# Check if pattern looks like a glob
|
|
141
|
+
if pattern.include?("*") || pattern.include?("?") || pattern.include?("[")
|
|
142
|
+
command_parts << "--glob"
|
|
143
|
+
end
|
|
144
|
+
command_parts << Shellwords.escape(pattern)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
command_parts.concat(paths.map { |p| Shellwords.escape(p) })
|
|
148
|
+
|
|
149
|
+
command_parts.join(" ")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get fd version
|
|
153
|
+
# @return [String, nil] Version string or nil if not available
|
|
154
|
+
def version
|
|
155
|
+
return nil unless available?
|
|
156
|
+
|
|
157
|
+
stdout, _stderr, status = Open3.capture3("fd --version")
|
|
158
|
+
if status.success?
|
|
159
|
+
version_match = stdout.match(/fd ([\d.]+)/)
|
|
160
|
+
version_match ? version_match[1] : nil
|
|
161
|
+
end
|
|
162
|
+
rescue
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Search
|
|
5
|
+
module Atoms
|
|
6
|
+
# PatternAnalyzer provides intelligent analysis of search patterns for DWIM mode selection
|
|
7
|
+
# This is an atom - pure function for pattern analysis
|
|
8
|
+
module PatternAnalyzer
|
|
9
|
+
# File glob indicators
|
|
10
|
+
FILE_GLOB_PATTERNS = [
|
|
11
|
+
/^\*+\./, # *.extension
|
|
12
|
+
/\*\*\/\*/, # **/* recursive patterns
|
|
13
|
+
/^\w+\/\*+/, # dir/* patterns
|
|
14
|
+
/\.\w+$/, # .extension endings
|
|
15
|
+
/\/\*+$/, # ending with /*
|
|
16
|
+
/^\w+\*+\w*$/ # word* or *word patterns for filenames
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
# Content regex indicators
|
|
20
|
+
CONTENT_REGEX_PATTERNS = [
|
|
21
|
+
/[|+?{}()\[\]\\]/, # Regex metacharacters (removed * from here)
|
|
22
|
+
/def\s+\w+/, # Method definitions
|
|
23
|
+
/class\s+\w+/, # Class definitions
|
|
24
|
+
/function\s+\w+/, # Function definitions
|
|
25
|
+
/import\s+/, # Import statements
|
|
26
|
+
/require\s+/, # Require statements
|
|
27
|
+
/\b(TODO|FIXME|BUG|HACK)\b/, # Code annotations
|
|
28
|
+
/^\^/, # Line start anchor
|
|
29
|
+
/\$$/, # Line end anchor
|
|
30
|
+
/\\[bBdDsSwW]/ # Character class escapes
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
# Literal text indicators
|
|
34
|
+
LITERAL_INDICATORS = [
|
|
35
|
+
/^\w+$/, # Single word
|
|
36
|
+
/^[\w\s]+$/, # Words with spaces
|
|
37
|
+
/^"[^"]*"$/, # Quoted strings
|
|
38
|
+
/^'[^']*'$/ # Single quoted strings
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Analyze a pattern and determine its most likely type
|
|
44
|
+
# @param pattern [String] Pattern to analyze
|
|
45
|
+
# @return [Hash] Analysis result with type and confidence
|
|
46
|
+
def analyze_pattern(pattern)
|
|
47
|
+
return {type: :invalid, confidence: 0.0, reason: "Pattern is nil"} if pattern.nil?
|
|
48
|
+
return {type: :invalid, confidence: 0.0, reason: "Pattern is empty"} if pattern.empty?
|
|
49
|
+
|
|
50
|
+
clean_pattern = pattern.strip
|
|
51
|
+
|
|
52
|
+
# Check for file glob patterns
|
|
53
|
+
if file_glob_pattern?(clean_pattern)
|
|
54
|
+
return {
|
|
55
|
+
type: :file_glob,
|
|
56
|
+
confidence: calculate_file_glob_confidence(clean_pattern),
|
|
57
|
+
reason: "Contains file glob patterns",
|
|
58
|
+
suggested_tool: "fd"
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check for content regex patterns
|
|
63
|
+
if content_regex_pattern?(clean_pattern)
|
|
64
|
+
return {
|
|
65
|
+
type: :content_regex,
|
|
66
|
+
confidence: calculate_content_regex_confidence(clean_pattern),
|
|
67
|
+
reason: "Contains regex metacharacters or code patterns",
|
|
68
|
+
suggested_tool: "rg"
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check for literal patterns
|
|
73
|
+
if literal_pattern?(clean_pattern)
|
|
74
|
+
return {
|
|
75
|
+
type: :literal,
|
|
76
|
+
confidence: calculate_literal_confidence(clean_pattern),
|
|
77
|
+
reason: "Simple literal text pattern",
|
|
78
|
+
suggested_tool: "rg"
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Default to hybrid if unclear
|
|
83
|
+
{
|
|
84
|
+
type: :hybrid,
|
|
85
|
+
confidence: 0.5,
|
|
86
|
+
reason: "Pattern could match files or content",
|
|
87
|
+
suggested_tool: "both"
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if pattern looks like a file glob
|
|
92
|
+
def file_glob_pattern?(pattern)
|
|
93
|
+
FILE_GLOB_PATTERNS.any? { |regex| pattern.match?(regex) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if pattern looks like content regex
|
|
97
|
+
def content_regex_pattern?(pattern)
|
|
98
|
+
CONTENT_REGEX_PATTERNS.any? { |regex| pattern.match?(regex) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check if pattern looks like literal text
|
|
102
|
+
def literal_pattern?(pattern)
|
|
103
|
+
LITERAL_INDICATORS.any? { |regex| pattern.match?(regex) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Suggest search mode based on pattern analysis
|
|
107
|
+
def suggest_search_mode(pattern, flags = {})
|
|
108
|
+
return :files if flags[:files_only] || flags[:name_only]
|
|
109
|
+
return :content if flags[:content_only]
|
|
110
|
+
|
|
111
|
+
analysis = analyze_pattern(pattern)
|
|
112
|
+
|
|
113
|
+
case analysis[:type]
|
|
114
|
+
when :file_glob
|
|
115
|
+
:files
|
|
116
|
+
when :content_regex, :literal
|
|
117
|
+
:content
|
|
118
|
+
when :hybrid
|
|
119
|
+
:both
|
|
120
|
+
else
|
|
121
|
+
:content
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Extract file extensions from a pattern if present
|
|
126
|
+
def extract_extensions(pattern)
|
|
127
|
+
extensions = []
|
|
128
|
+
|
|
129
|
+
# Match patterns like *.rb, **/*.js, etc.
|
|
130
|
+
extension_matches = pattern.scan(/\*+\.(\w+)/)
|
|
131
|
+
extensions.concat(extension_matches.flatten)
|
|
132
|
+
|
|
133
|
+
# Match explicit .ext at the end
|
|
134
|
+
if pattern.match?(/\.(\w+)$/)
|
|
135
|
+
extension_match = pattern.match(/\.(\w+)$/)
|
|
136
|
+
extensions << extension_match[1]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
extensions.uniq
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def calculate_file_glob_confidence(pattern)
|
|
143
|
+
confidence = 0.5
|
|
144
|
+
|
|
145
|
+
confidence += 0.3 if pattern.include?("*")
|
|
146
|
+
confidence += 0.2 if pattern.match?(/\.\w+$/)
|
|
147
|
+
confidence += 0.1 if pattern.include?("/")
|
|
148
|
+
|
|
149
|
+
[confidence, 1.0].min
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def calculate_content_regex_confidence(pattern)
|
|
153
|
+
confidence = 0.5
|
|
154
|
+
|
|
155
|
+
metachar_count = pattern.scan(/[|+?{}()\[\]\\]/).length
|
|
156
|
+
confidence += metachar_count * 0.1
|
|
157
|
+
|
|
158
|
+
confidence += 0.2 if pattern.match?(/\b(def|class|function|import|require)\s+/)
|
|
159
|
+
confidence += 0.1 if pattern.match?(/^\^|\$$/)
|
|
160
|
+
|
|
161
|
+
[confidence, 1.0].min
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def calculate_literal_confidence(pattern)
|
|
165
|
+
confidence = 0.6
|
|
166
|
+
|
|
167
|
+
confidence += 0.2 if pattern.match?(/^\w+$/)
|
|
168
|
+
confidence += 0.1 if pattern.length < 20
|
|
169
|
+
confidence += 0.3 if pattern.match?(/^["'][^"']*["']$/)
|
|
170
|
+
|
|
171
|
+
[confidence, 1.0].min
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|