rbgrep 0.1.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/LICENSE.txt +21 -0
- data/README.md +72 -0
- data/exe/rbgrep +5 -0
- data/instruction.md +44 -0
- data/lib/rbgrep/cli.rb +235 -0
- data/lib/rbgrep/pattern.rb +30 -0
- data/lib/rbgrep/version.rb +3 -0
- data/lib/rbgrep/window.rb +57 -0
- data/lib/rbgrep.rb +9 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 13fc98cb1e9f142256df88ad75264dfba4f3133cd86782f69562e9e4f3b73f9d
|
|
4
|
+
data.tar.gz: 95f53537ba8c4fb6f885be383e1b78782309c751cd3313e24ab612dbb9f6e7ba
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7085f872add33bf241d0e043f95b70806ef9a09085929831b416fbab78f2deda61b19bcfe4d2721c3687a76b379b4ecc5226058caf69480e15d2dae4fa2158b2
|
|
7
|
+
data.tar.gz: 58f543d9979226be7e4c134047b0aafc11739df7c1ad7a03a8e78103667a2b4714a2ab8c9817b7d11631f9364b65e33ca2367746caaab37293d4ddb6338692ee
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuke Endoh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# rbgrep
|
|
2
|
+
|
|
3
|
+
A mostly grep-compatible search tool for Ruby codebases.
|
|
4
|
+
|
|
5
|
+
By default, `rbgrep` takes the same flags as `grep` and prints the same
|
|
6
|
+
kind of output: a filename heading, then `NN:` hit lines. Add
|
|
7
|
+
**`--context`** and each hit in a `.rb` file is shown together with the
|
|
8
|
+
method definition and class that enclose it, located with the
|
|
9
|
+
[Prism](https://github.com/ruby/prism) parser.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
gem install rbgrep
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
rbgrep [-rn -w -i -E -F -A/-B/-C --include=GLOB] PATTERN [PATH...]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Plain search works like grep (including `\|` alternation in basic
|
|
24
|
+
regexps, or `-E` for extended ones):
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
$ rbgrep -n 'def visible?' app/models/news.rb
|
|
28
|
+
app/models/news.rb
|
|
29
|
+
55: def visible?(user=User.current)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With `--context`, the hit arrives with the code that gives it meaning —
|
|
33
|
+
the enclosing method body and the class frame, with `--` marking elided
|
|
34
|
+
stretches:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
$ rbgrep -n --context 'def visible?' app/models/news.rb
|
|
38
|
+
app/models/news.rb
|
|
39
|
+
20- class News < ApplicationRecord
|
|
40
|
+
--
|
|
41
|
+
55: def visible?(user=User.current)
|
|
42
|
+
56- !user.nil? && user.allowed_to?(:view_news, project)
|
|
43
|
+
57- end
|
|
44
|
+
--
|
|
45
|
+
105- end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A hit on a method definition shows the body (capped); a hit that merely
|
|
49
|
+
references something shows a few surrounding lines. Files under `test/`
|
|
50
|
+
and `spec/` directories, and non-Ruby files, print plain hits.
|
|
51
|
+
|
|
52
|
+
`--head N` caps output by hits, returns each hit whole, and reports how
|
|
53
|
+
many were cut — unlike `| head`, you always know whether there was more:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
$ rbgrep -rn --head 3 'def initialize' lib
|
|
57
|
+
...
|
|
58
|
+
... 48 more hits (raise --head to see them)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
When reading from a pipe, `rbgrep` behaves exactly like `grep` — no
|
|
62
|
+
headings, no context — so `... | rbgrep -v foo` works as expected.
|
|
63
|
+
|
|
64
|
+
## Use with coding agents
|
|
65
|
+
|
|
66
|
+
`rbgrep` is designed to be handed to an AI coding agent as its search
|
|
67
|
+
tool. [instruction.md](instruction.md) is a ready-made instruction sheet:
|
|
68
|
+
tell your agent to read and follow it.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/exe/rbgrep
ADDED
data/instruction.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# rbgrep
|
|
2
|
+
|
|
3
|
+
**Use `rbgrep` to search the repository you are working in — Ruby sources and templates alike.**
|
|
4
|
+
If `rbgrep` is not installed, run `gem install rbgrep` first.
|
|
5
|
+
|
|
6
|
+
The arguments are the same as grep's, and by default so is the output:
|
|
7
|
+
a filename heading, then `NN:` hit lines. **Using `grep` / `rg` to search
|
|
8
|
+
files in the repository is prohibited.**
|
|
9
|
+
The following remain free:
|
|
10
|
+
|
|
11
|
+
- narrowing **downstream of a pipe** (`... | grep -v foo`)
|
|
12
|
+
- **reading files** (`cat` / `sed -n` / Read / `head` / `tail`)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
rbgrep [-rn -w -i -E -F -A/-B/-C --include=GLOB] PATTERN [PATH...]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Search. Multi-term search is written the same way as in grep:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
rbgrep -rn 'Payment\|payment_method' app lib
|
|
22
|
+
rbgrep -rnE 'Payment|PaymentMethod' app lib
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Add **`--context`** to see where hits sit: each hit in a `.rb` file is then
|
|
26
|
+
shown together with its **enclosing method definition and class** as `NN-`
|
|
27
|
+
context lines (`--` marks elided stretches). **Reach for it whenever a hit
|
|
28
|
+
is in a file you haven't read yet** — it usually saves a follow-up read.
|
|
29
|
+
Plain output is for counting and locating.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
rbgrep --head N PATTERN [PATH...]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Piping `rbgrep` output into `| head` is prohibited. Use `--head N` instead.**
|
|
36
|
+
`--head` caps by hits, returns each hit whole, and when results are cut it
|
|
37
|
+
says so, so you know whether to raise N.
|
|
38
|
+
|
|
39
|
+
**Warm-up (once, before you start)**: to get used to the `--context` output,
|
|
40
|
+
pick one small Ruby file (any file under `lib/` will do) and search it:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
rbgrep -n --context 'def ' path/to/that/file.rb
|
|
44
|
+
```
|
data/lib/rbgrep/cli.rb
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
module Rbgrep
|
|
2
|
+
# The command line: parse grep-shaped arguments, search, print.
|
|
3
|
+
class CLI
|
|
4
|
+
def self.run(argv, stdout: $stdout, stdin: $stdin)
|
|
5
|
+
new(stdout: stdout, stdin: stdin).run(argv)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(stdout: $stdout, stdin: $stdin)
|
|
9
|
+
@out = stdout
|
|
10
|
+
@in = stdin
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(argv)
|
|
14
|
+
o = parse(argv)
|
|
15
|
+
if o[:patterns].empty?
|
|
16
|
+
warn "usage: rbgrep [options] PATTERN [FILE...]"
|
|
17
|
+
return 1
|
|
18
|
+
end
|
|
19
|
+
regexp = Pattern.build(o[:patterns], fixed: o[:fixed], extended: o[:ere],
|
|
20
|
+
word: o[:word], ignore_case: o[:ignore_case])
|
|
21
|
+
return filter_stdin(regexp, o) if o[:paths].empty? && !@in.tty?
|
|
22
|
+
|
|
23
|
+
search_files(regexp, o)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def parse(argv)
|
|
29
|
+
o = {
|
|
30
|
+
patterns: [], paths: [], recursive: false, line_numbers: false,
|
|
31
|
+
ignore_case: false, word: false, invert: false, fixed: false,
|
|
32
|
+
list: false, count: false, quiet: false, no_filename: nil,
|
|
33
|
+
before: 0, after: 0, include: [], exclude: [], exclude_dir: [],
|
|
34
|
+
max: nil, ere: false, context: false, head: nil,
|
|
35
|
+
}
|
|
36
|
+
argv = argv.dup
|
|
37
|
+
until argv.empty?
|
|
38
|
+
a = argv.shift
|
|
39
|
+
case a
|
|
40
|
+
when "--" then o[:paths].concat(argv); break
|
|
41
|
+
when /\A--include=(.+)/ then o[:include] << $1
|
|
42
|
+
when "--include" then o[:include] << argv.shift
|
|
43
|
+
when /\A--exclude=(.+)/ then o[:exclude] << $1
|
|
44
|
+
when "--exclude" then o[:exclude] << argv.shift
|
|
45
|
+
when /\A--exclude-dir=(.+)/ then o[:exclude_dir] << $1
|
|
46
|
+
when "--exclude-dir" then o[:exclude_dir] << argv.shift
|
|
47
|
+
when "--color", "--colour", /\A--colou?r=./ then nil # output is never colored
|
|
48
|
+
when "--no-filename" then o[:no_filename] = true
|
|
49
|
+
when "--with-filename" then o[:no_filename] = false
|
|
50
|
+
when "--line-number" then o[:line_numbers] = true
|
|
51
|
+
when "--recursive" then o[:recursive] = true
|
|
52
|
+
when "--invert-match" then o[:invert] = true
|
|
53
|
+
when "--files-with-matches" then o[:list] = true
|
|
54
|
+
when "--count" then o[:count] = true
|
|
55
|
+
when "--word-regexp" then o[:word] = true
|
|
56
|
+
when "--ignore-case" then o[:ignore_case] = true
|
|
57
|
+
when "--fixed-strings" then o[:fixed] = true
|
|
58
|
+
when "--extended-regexp" then o[:ere] = true
|
|
59
|
+
when "--context" then o[:context] = true
|
|
60
|
+
when /\A--head=(\d+)\z/ then o[:head] = $1.to_i
|
|
61
|
+
when "--head" then o[:head] = argv.shift.to_i
|
|
62
|
+
when /\A-([A-Za-z]+)(\d*)\z/
|
|
63
|
+
letters = $1
|
|
64
|
+
trailing = $2
|
|
65
|
+
until letters.empty?
|
|
66
|
+
c = letters.slice!(0)
|
|
67
|
+
case c
|
|
68
|
+
when "r", "R" then o[:recursive] = true
|
|
69
|
+
when "n" then o[:line_numbers] = true
|
|
70
|
+
when "i" then o[:ignore_case] = true
|
|
71
|
+
when "w" then o[:word] = true
|
|
72
|
+
when "v" then o[:invert] = true
|
|
73
|
+
when "l" then o[:list] = true
|
|
74
|
+
when "c" then o[:count] = true
|
|
75
|
+
when "q" then o[:quiet] = true
|
|
76
|
+
when "h" then o[:no_filename] = true
|
|
77
|
+
when "H" then o[:no_filename] = false
|
|
78
|
+
when "F" then o[:fixed] = true
|
|
79
|
+
when "E", "P" then o[:ere] = true
|
|
80
|
+
when "s", "a", "o" then nil # -o is approximated as a normal match
|
|
81
|
+
when "e" then o[:patterns] << (letters.empty? ? argv.shift : letters.slice!(0..-1))
|
|
82
|
+
when "A", "B", "C", "m"
|
|
83
|
+
n = letters.empty? ? (trailing.empty? ? argv.shift : trailing) : letters.slice!(0..-1)
|
|
84
|
+
trailing = ""
|
|
85
|
+
n = n.to_i
|
|
86
|
+
case c
|
|
87
|
+
when "A" then o[:after] = n
|
|
88
|
+
when "B" then o[:before] = n
|
|
89
|
+
when "C" then o[:after] = o[:before] = n
|
|
90
|
+
when "m" then o[:max] = n
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
o[:patterns].empty? ? o[:patterns] << a : o[:paths] << a
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
o
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# piped input: behave exactly like grep — no headings, no context
|
|
102
|
+
def filter_stdin(regexp, o)
|
|
103
|
+
hits = 0
|
|
104
|
+
@in.each_line do |line|
|
|
105
|
+
next unless regexp.match?(line) ^ o[:invert]
|
|
106
|
+
|
|
107
|
+
hits += 1
|
|
108
|
+
break if o[:max] && hits > o[:max]
|
|
109
|
+
|
|
110
|
+
@out.print line unless o[:count] || o[:quiet]
|
|
111
|
+
end
|
|
112
|
+
@out.puts hits if o[:count]
|
|
113
|
+
hits > 0 ? 0 : 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def collect_files(paths, o)
|
|
117
|
+
out = []
|
|
118
|
+
paths = ["."] if paths.empty?
|
|
119
|
+
paths.each do |p|
|
|
120
|
+
if File.directory?(p)
|
|
121
|
+
Dir.glob(File.join(p, "**", "*")).each do |f|
|
|
122
|
+
next unless File.file?(f)
|
|
123
|
+
next if o[:exclude_dir].any? { |g| f.split("/").any? { |seg| File.fnmatch?(g, seg) } }
|
|
124
|
+
|
|
125
|
+
out << f
|
|
126
|
+
end
|
|
127
|
+
elsif File.file?(p)
|
|
128
|
+
out << p
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
out = out.select { |f| o[:include].any? { |g| File.fnmatch?(g, File.basename(f)) } } if o[:include].any?
|
|
132
|
+
out = out.reject { |f| o[:exclude].any? { |g| File.fnmatch?(g, File.basename(f)) } } if o[:exclude].any?
|
|
133
|
+
out.reject { |f| f.include?("/.git/") }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def search_files(regexp, o)
|
|
137
|
+
files = collect_files(o[:paths], o)
|
|
138
|
+
|
|
139
|
+
results = []
|
|
140
|
+
total = 0
|
|
141
|
+
found = false
|
|
142
|
+
files.each do |path|
|
|
143
|
+
body = begin
|
|
144
|
+
File.read(path, encoding: "UTF-8")
|
|
145
|
+
rescue StandardError
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
next unless body.valid_encoding?
|
|
149
|
+
|
|
150
|
+
lines = body.lines
|
|
151
|
+
hits = lines.each_index.select { |i| regexp.match?(lines[i]) ^ o[:invert] }.map { |i| i + 1 }
|
|
152
|
+
next if hits.empty?
|
|
153
|
+
|
|
154
|
+
found = true
|
|
155
|
+
total += hits.size
|
|
156
|
+
results << [path, body, lines, hits]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
truncated = nil
|
|
160
|
+
if o[:head]
|
|
161
|
+
left = o[:head]
|
|
162
|
+
capped = []
|
|
163
|
+
results.each do |path, body, lines, hits|
|
|
164
|
+
break if left <= 0
|
|
165
|
+
|
|
166
|
+
take = hits.first(left)
|
|
167
|
+
left -= take.size
|
|
168
|
+
capped << [path, body, lines, take]
|
|
169
|
+
end
|
|
170
|
+
truncated = total - o[:head]
|
|
171
|
+
truncated = nil unless truncated > 0
|
|
172
|
+
results = capped
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
return found ? 0 : 1 if o[:quiet]
|
|
176
|
+
return list_output(results) || (found ? 0 : 1) if o[:list]
|
|
177
|
+
return count_output(results, files, o) || (found ? 0 : 1) if o[:count]
|
|
178
|
+
|
|
179
|
+
text_output(results, o, truncated)
|
|
180
|
+
found ? 0 : 1
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def list_output(results)
|
|
184
|
+
results.each { |path, _, _, _| @out.puts path }
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def count_output(results, files, o)
|
|
189
|
+
show_name = o[:no_filename].nil? ? (files.size > 1 || o[:recursive]) : !o[:no_filename]
|
|
190
|
+
results.each do |path, _, _, hits|
|
|
191
|
+
@out.puts show_name ? "#{path}:#{hits.size}" : hits.size.to_s
|
|
192
|
+
end
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# a filename heading, then "NN:" hits and "NN-" context, "--" for elisions
|
|
197
|
+
def text_output(results, o, truncated)
|
|
198
|
+
first = true
|
|
199
|
+
results.each do |path, body, lines, hits|
|
|
200
|
+
keep = select_lines(path, body, lines, hits, o)
|
|
201
|
+
shown = keep.keys.select { |l| l >= 1 && l <= lines.size }.sort
|
|
202
|
+
bare = shown.all? { |l| keep[l] == :hit }
|
|
203
|
+
@out.puts "" unless first
|
|
204
|
+
first = false
|
|
205
|
+
@out.puts path
|
|
206
|
+
prev = 0
|
|
207
|
+
shown.each do |ln|
|
|
208
|
+
@out.puts "--" if !bare && prev > 0 && ln > prev + 1
|
|
209
|
+
@out.puts "#{ln}#{keep[ln] == :hit ? ':' : '-'} #{lines[ln - 1].to_s.chomp}"
|
|
210
|
+
prev = ln
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
@out.puts "... #{truncated} more hits (raise --head to see them)" if truncated
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def select_lines(path, body, lines, hits, o)
|
|
217
|
+
if o[:context] && windowable?(path, o)
|
|
218
|
+
parsed = Prism.parse(body)
|
|
219
|
+
return Window.select(parsed.value, hits, o[:before], o[:after]) if parsed.success?
|
|
220
|
+
end
|
|
221
|
+
ctx = [o[:before], o[:after]].max
|
|
222
|
+
keep = {}
|
|
223
|
+
hits.each do |ln|
|
|
224
|
+
keep[ln] = :hit
|
|
225
|
+
((ln - ctx)..(ln + ctx)).each { |i| keep[i] ||= :ctx if i >= 1 && i <= lines.size }
|
|
226
|
+
end
|
|
227
|
+
keep
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# windows are for Ruby source; tests and non-.rb files print plain hits
|
|
231
|
+
def windowable?(path, o)
|
|
232
|
+
path.end_with?(".rb") && !o[:invert] && path !~ %r{(\A|/)(test|spec)/}
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Rbgrep
|
|
2
|
+
# Builds the Regexp for a search the way grep would read it.
|
|
3
|
+
module Pattern
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
# grep's basic regexps spell alternation "\|" and grouping "\(...\)";
|
|
7
|
+
# translate to Ruby: escaped metacharacters active, bare ones literal.
|
|
8
|
+
def from_bre(pattern)
|
|
9
|
+
out = +""
|
|
10
|
+
i = 0
|
|
11
|
+
while i < pattern.length
|
|
12
|
+
if pattern[i] == "\\" && i + 1 < pattern.length && "|(){}+?".include?(pattern[i + 1])
|
|
13
|
+
out << pattern[i + 1]
|
|
14
|
+
i += 2
|
|
15
|
+
else
|
|
16
|
+
out << "\\" if "|(){}+?".include?(pattern[i])
|
|
17
|
+
out << pattern[i]
|
|
18
|
+
i += 1
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
out
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build(patterns, fixed: false, extended: false, word: false, ignore_case: false)
|
|
25
|
+
src = patterns.map { |p| fixed ? Regexp.escape(p) : (extended ? p : from_bre(p)) }.join("|")
|
|
26
|
+
src = "\\b(?:#{src})\\b" if word
|
|
27
|
+
Regexp.new(src, ignore_case ? Regexp::IGNORECASE : nil)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Rbgrep
|
|
2
|
+
# Line selection for --context: show each hit together with the enclosing
|
|
3
|
+
# method definition and class/module frame.
|
|
4
|
+
module Window
|
|
5
|
+
BODY_CAP = 10 # body lines shown when the hit is the `def` line itself
|
|
6
|
+
REF_MARGIN = 3 # lines around a hit inside a body (a mere reference)
|
|
7
|
+
EDGE_SLACK = 4 # show this many lines rather than eliding them at a def edge
|
|
8
|
+
FRAME_NODES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Nodes enclosing the line, outermost first.
|
|
13
|
+
def ancestors(node, lineno, acc = [])
|
|
14
|
+
return acc unless node.is_a?(Prism::Node)
|
|
15
|
+
|
|
16
|
+
loc = node.location
|
|
17
|
+
return acc unless loc.start_line <= lineno && lineno <= loc.end_line
|
|
18
|
+
|
|
19
|
+
acc << node
|
|
20
|
+
node.compact_child_nodes.each { |c| ancestors(c, lineno, acc) }
|
|
21
|
+
acc
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# => { lineno => :hit | :body | :frame | :ctx }
|
|
25
|
+
def select(ast, hits, before, after)
|
|
26
|
+
cap_b = [BODY_CAP, before].max
|
|
27
|
+
cap_a = [BODY_CAP, after].max
|
|
28
|
+
ctx_b = [REF_MARGIN, before].max
|
|
29
|
+
ctx_a = [REF_MARGIN, after].max
|
|
30
|
+
keep = {}
|
|
31
|
+
hits.each do |ln|
|
|
32
|
+
keep[ln] = :hit
|
|
33
|
+
((ln - before)..(ln + after)).each { |i| keep[i] ||= :ctx if i >= 1 }
|
|
34
|
+
stack = ancestors(ast, ln)
|
|
35
|
+
if (defn = stack.find { |n| n.is_a?(Prism::DefNode) })
|
|
36
|
+
keep[defn.location.start_line] ||= :frame
|
|
37
|
+
keep[defn.location.end_line] ||= :frame
|
|
38
|
+
b, a = defn.location.start_line == ln ? [cap_b, cap_a] : [ctx_b, ctx_a]
|
|
39
|
+
lo = [defn.location.start_line, ln - b].max
|
|
40
|
+
hi = [defn.location.end_line, ln + a].min
|
|
41
|
+
lo = defn.location.start_line if lo - defn.location.start_line <= EDGE_SLACK
|
|
42
|
+
hi = defn.location.end_line if defn.location.end_line - hi <= EDGE_SLACK
|
|
43
|
+
(lo..hi).each { |i| keep[i] ||= :body }
|
|
44
|
+
else
|
|
45
|
+
((ln - ctx_b)..(ln + ctx_a)).each { |i| keep[i] ||= :body if i >= 1 }
|
|
46
|
+
end
|
|
47
|
+
stack.each do |n|
|
|
48
|
+
next unless FRAME_NODES.any? { |k| n.is_a?(k) }
|
|
49
|
+
|
|
50
|
+
keep[n.location.start_line] ||= :frame
|
|
51
|
+
keep[n.location.end_line] ||= :frame
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
keep
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/rbgrep.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rbgrep
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yusuke Endoh
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
-
|
|
17
|
+
- ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: "0.24"
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
-
|
|
25
|
+
- ">="
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: "0.24"
|
|
28
|
+
description: |
|
|
29
|
+
rbgrep is a mostly grep-compatible search tool for Ruby codebases.
|
|
30
|
+
By default it behaves like grep. With --context, each hit in a Ruby
|
|
31
|
+
file is shown together with the method definition and class that
|
|
32
|
+
enclose it, located with the Prism parser.
|
|
33
|
+
email:
|
|
34
|
+
- y.endoh@gmail.com
|
|
35
|
+
executables:
|
|
36
|
+
- rbgrep
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- exe/rbgrep
|
|
43
|
+
- instruction.md
|
|
44
|
+
- lib/rbgrep.rb
|
|
45
|
+
- lib/rbgrep/cli.rb
|
|
46
|
+
- lib/rbgrep/pattern.rb
|
|
47
|
+
- lib/rbgrep/version.rb
|
|
48
|
+
- lib/rbgrep/window.rb
|
|
49
|
+
homepage: "https://github.com/mame/rbgrep"
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
homepage_uri: "https://github.com/mame/rbgrep"
|
|
54
|
+
source_code_uri: "https://github.com/mame/rbgrep"
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
-
|
|
61
|
+
- ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: "3.2"
|
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
-
|
|
67
|
+
- ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: "0"
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 4.1.0.dev
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: "grep for Ruby codebases: grep-compatible search, syntax-aware context"
|
|
74
|
+
test_files: []
|