girb 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/.rspec +3 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +253 -0
- data/README_ja.md +251 -0
- data/Rakefile +8 -0
- data/exe/girb +38 -0
- data/lib/girb/ai_client.rb +157 -0
- data/lib/girb/configuration.rb +39 -0
- data/lib/girb/context_builder.rb +134 -0
- data/lib/girb/conversation_history.rb +132 -0
- data/lib/girb/exception_capture.rb +54 -0
- data/lib/girb/irb_integration.rb +87 -0
- data/lib/girb/prompt_builder.rb +173 -0
- data/lib/girb/providers/base.rb +65 -0
- data/lib/girb/session_history.rb +197 -0
- data/lib/girb/tools/base.rb +61 -0
- data/lib/girb/tools/evaluate_code.rb +43 -0
- data/lib/girb/tools/find_file.rb +93 -0
- data/lib/girb/tools/get_source.rb +145 -0
- data/lib/girb/tools/inspect_object.rb +54 -0
- data/lib/girb/tools/list_methods.rb +66 -0
- data/lib/girb/tools/rails_tools.rb +141 -0
- data/lib/girb/tools/read_file.rb +124 -0
- data/lib/girb/tools/session_history_tool.rb +183 -0
- data/lib/girb/tools.rb +38 -0
- data/lib/girb/version.rb +5 -0
- data/lib/girb.rb +53 -0
- data/lib/irb/command/qq.rb +55 -0
- metadata +131 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../session_history"
|
|
5
|
+
|
|
6
|
+
module Girb
|
|
7
|
+
module Tools
|
|
8
|
+
class GetSource < Base
|
|
9
|
+
MAX_SOURCE_LINES = 50
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def description
|
|
13
|
+
"Get the source code of a method or class definition. Use 'Class#method' for instance methods, 'Class.method' for class methods."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parameters
|
|
17
|
+
{
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
target: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Class or method to get source for (e.g., 'User', 'User#save', 'User.find')"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
required: ["target"]
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def execute(binding, target:)
|
|
31
|
+
if target.include?("#")
|
|
32
|
+
get_instance_method_source(binding, target)
|
|
33
|
+
elsif target.include?(".")
|
|
34
|
+
get_class_method_source(binding, target)
|
|
35
|
+
else
|
|
36
|
+
get_class_info(binding, target)
|
|
37
|
+
end
|
|
38
|
+
rescue NameError => e
|
|
39
|
+
{ error: "Not found: #{e.message}" }
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def get_instance_method_source(binding, target)
|
|
47
|
+
class_name, method_name = target.split("#", 2)
|
|
48
|
+
klass = binding.eval(class_name)
|
|
49
|
+
method = klass.instance_method(method_name.to_sym)
|
|
50
|
+
|
|
51
|
+
extract_method_info(method, target)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def get_class_method_source(binding, target)
|
|
55
|
+
class_name, method_name = target.split(".", 2)
|
|
56
|
+
klass = binding.eval(class_name)
|
|
57
|
+
method = klass.method(method_name.to_sym)
|
|
58
|
+
|
|
59
|
+
extract_method_info(method, target)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def extract_method_info(method, target)
|
|
63
|
+
location = method.source_location
|
|
64
|
+
method_name = method.name.to_s
|
|
65
|
+
|
|
66
|
+
if location
|
|
67
|
+
file, line = location
|
|
68
|
+
|
|
69
|
+
# IRBで定義されたメソッドの場合、SessionHistoryから取得
|
|
70
|
+
if file == "(irb)" || file&.start_with?("(irb)")
|
|
71
|
+
session_method = SessionHistory.find_method(method_name)
|
|
72
|
+
if session_method
|
|
73
|
+
return {
|
|
74
|
+
target: target,
|
|
75
|
+
file: "(irb)",
|
|
76
|
+
line: "#{session_method.start_line}-#{session_method.end_line}",
|
|
77
|
+
source: session_method.code,
|
|
78
|
+
parameters: method.parameters.map { |type, name| "#{type}: #{name}" },
|
|
79
|
+
defined_in_session: true
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
source = read_source(file, line)
|
|
85
|
+
{
|
|
86
|
+
target: target,
|
|
87
|
+
file: file,
|
|
88
|
+
line: line,
|
|
89
|
+
source: source,
|
|
90
|
+
parameters: method.parameters.map { |type, name| "#{type}: #{name}" }
|
|
91
|
+
}
|
|
92
|
+
else
|
|
93
|
+
{
|
|
94
|
+
target: target,
|
|
95
|
+
error: "Source not available (native or C extension method)",
|
|
96
|
+
parameters: method.parameters.map { |type, name| "#{type}: #{name}" }
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get_class_info(binding, class_name)
|
|
102
|
+
klass = binding.eval(class_name)
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
name: klass.name,
|
|
106
|
+
type: klass.class.name,
|
|
107
|
+
ancestors: klass.ancestors.first(10).map(&:to_s),
|
|
108
|
+
instance_methods: klass.instance_methods(false).sort.first(30),
|
|
109
|
+
class_methods: (klass.methods - Class.methods).sort.first(30),
|
|
110
|
+
constants: klass.constants.first(20)
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_source(file, start_line)
|
|
115
|
+
return nil unless File.exist?(file)
|
|
116
|
+
|
|
117
|
+
lines = File.readlines(file)
|
|
118
|
+
end_line = find_method_end(lines, start_line - 1)
|
|
119
|
+
lines[(start_line - 1)..end_line].join
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
"(Failed to read source: #{e.message})"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def find_method_end(lines, start_index)
|
|
125
|
+
# 簡易的なインデント解析でメソッド終端を探す
|
|
126
|
+
return [start_index + MAX_SOURCE_LINES, lines.length - 1].min if start_index >= lines.length
|
|
127
|
+
|
|
128
|
+
base_indent = lines[start_index][/^\s*/].length
|
|
129
|
+
end_keywords = %w[end]
|
|
130
|
+
|
|
131
|
+
(start_index + 1).upto([start_index + MAX_SOURCE_LINES, lines.length - 1].min) do |i|
|
|
132
|
+
line = lines[i]
|
|
133
|
+
next if line.strip.empty? || line.strip.start_with?("#")
|
|
134
|
+
|
|
135
|
+
current_indent = line[/^\s*/].length
|
|
136
|
+
if current_indent <= base_indent && end_keywords.any? { |kw| line.strip.start_with?(kw) }
|
|
137
|
+
return i
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
[start_index + MAX_SOURCE_LINES, lines.length - 1].min
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
module Tools
|
|
7
|
+
class InspectObject < Base
|
|
8
|
+
class << self
|
|
9
|
+
def description
|
|
10
|
+
"Inspect a variable or expression in the current context. Returns detailed information about the object."
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parameters
|
|
14
|
+
{
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
expression: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "The variable name or Ruby expression to inspect (e.g., 'user', 'user.errors', '@items.first')"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
required: ["expression"]
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute(binding, expression:)
|
|
28
|
+
result = binding.eval(expression)
|
|
29
|
+
{
|
|
30
|
+
expression: expression,
|
|
31
|
+
class: result.class.name,
|
|
32
|
+
value: safe_inspect(result),
|
|
33
|
+
instance_variables: extract_instance_variables(result),
|
|
34
|
+
methods_count: result.methods.count
|
|
35
|
+
}
|
|
36
|
+
rescue SyntaxError => e
|
|
37
|
+
{ error: "Syntax error: #{e.message}" }
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def extract_instance_variables(obj)
|
|
45
|
+
obj.instance_variables.to_h do |var|
|
|
46
|
+
value = obj.instance_variable_get(var)
|
|
47
|
+
[var, safe_inspect(value, max_length: 200)]
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError
|
|
50
|
+
{}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
module Tools
|
|
7
|
+
class ListMethods < Base
|
|
8
|
+
class << self
|
|
9
|
+
def description
|
|
10
|
+
"List methods available on an object or class. Can filter by pattern."
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parameters
|
|
14
|
+
{
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
expression: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "The variable name or expression to list methods for"
|
|
20
|
+
},
|
|
21
|
+
pattern: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Optional regex pattern to filter method names (e.g., 'valid', 'save')"
|
|
24
|
+
},
|
|
25
|
+
include_inherited: {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
description: "Whether to include inherited methods (default: false)"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: ["expression"]
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def execute(binding, expression:, pattern: nil, include_inherited: false)
|
|
36
|
+
obj = binding.eval(expression)
|
|
37
|
+
|
|
38
|
+
methods = if obj.is_a?(Class) || obj.is_a?(Module)
|
|
39
|
+
{
|
|
40
|
+
instance_methods: obj.instance_methods(!include_inherited),
|
|
41
|
+
class_methods: obj.methods(!include_inherited)
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
{
|
|
45
|
+
methods: obj.methods(!include_inherited)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# パターンでフィルタ
|
|
50
|
+
if pattern && !pattern.empty?
|
|
51
|
+
regex = Regexp.new(pattern, Regexp::IGNORECASE)
|
|
52
|
+
methods = methods.transform_values do |list|
|
|
53
|
+
list.select { |m| m.to_s.match?(regex) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ソートして返す
|
|
58
|
+
methods.transform_values(&:sort)
|
|
59
|
+
rescue RegexpError => e
|
|
60
|
+
{ error: "Invalid pattern: #{e.message}" }
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
module Tools
|
|
7
|
+
class RailsModelInfo < Base
|
|
8
|
+
class << self
|
|
9
|
+
def available?
|
|
10
|
+
defined?(ActiveRecord::Base)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def description
|
|
14
|
+
"Get Rails ActiveRecord model information including associations, validations, callbacks, and scopes."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parameters
|
|
18
|
+
{
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
model_name: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "The model class name (e.g., 'User', 'Order', 'Product')"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
required: ["model_name"]
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def execute(binding, model_name:)
|
|
32
|
+
klass = binding.eval(model_name)
|
|
33
|
+
|
|
34
|
+
unless klass < ActiveRecord::Base
|
|
35
|
+
return { error: "#{model_name} is not an ActiveRecord model" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
model: model_name,
|
|
40
|
+
table_name: klass.table_name,
|
|
41
|
+
primary_key: klass.primary_key,
|
|
42
|
+
columns: get_columns(klass),
|
|
43
|
+
associations: get_associations(klass),
|
|
44
|
+
validations: get_validations(klass),
|
|
45
|
+
callbacks: get_callbacks(klass),
|
|
46
|
+
scopes: get_scopes(klass)
|
|
47
|
+
}
|
|
48
|
+
rescue NameError => e
|
|
49
|
+
{ error: "Model not found: #{e.message}" }
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def get_columns(klass)
|
|
57
|
+
klass.columns.map do |col|
|
|
58
|
+
{
|
|
59
|
+
name: col.name,
|
|
60
|
+
type: col.type,
|
|
61
|
+
null: col.null,
|
|
62
|
+
default: col.default
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
rescue StandardError
|
|
66
|
+
[]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def get_associations(klass)
|
|
70
|
+
klass.reflect_on_all_associations.map do |assoc|
|
|
71
|
+
info = {
|
|
72
|
+
name: assoc.name,
|
|
73
|
+
type: assoc.macro,
|
|
74
|
+
class_name: assoc.class_name
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# オプションがあれば追加
|
|
78
|
+
%i[dependent through source foreign_key].each do |opt|
|
|
79
|
+
value = assoc.options[opt]
|
|
80
|
+
info[opt] = value if value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
info
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError
|
|
86
|
+
[]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_validations(klass)
|
|
90
|
+
klass.validators.map do |validator|
|
|
91
|
+
{
|
|
92
|
+
attributes: validator.attributes,
|
|
93
|
+
kind: validator.kind,
|
|
94
|
+
options: validator.options.reject { |k, _| %i[if unless].include?(k) }
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError
|
|
98
|
+
[]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get_callbacks(klass)
|
|
102
|
+
callback_types = %i[
|
|
103
|
+
before_validation after_validation
|
|
104
|
+
before_save after_save around_save
|
|
105
|
+
before_create after_create around_create
|
|
106
|
+
before_update after_update around_update
|
|
107
|
+
before_destroy after_destroy around_destroy
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
callback_types.to_h do |callback_type|
|
|
111
|
+
callbacks = begin
|
|
112
|
+
klass.send("_#{callback_type}_callbacks").map do |cb|
|
|
113
|
+
{ filter: cb.filter.to_s, kind: cb.kind }
|
|
114
|
+
end
|
|
115
|
+
rescue StandardError
|
|
116
|
+
[]
|
|
117
|
+
end
|
|
118
|
+
[callback_type, callbacks]
|
|
119
|
+
end.reject { |_, v| v.empty? }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def get_scopes(klass)
|
|
123
|
+
# Rails doesn't expose scope names directly, but we can check for scope methods
|
|
124
|
+
# that are defined on the class but not on ActiveRecord::Base
|
|
125
|
+
base_methods = ActiveRecord::Base.methods
|
|
126
|
+
scope_candidates = (klass.methods - base_methods).select do |method_name|
|
|
127
|
+
# スコープは通常、ActiveRecord::Relationを返す
|
|
128
|
+
begin
|
|
129
|
+
klass.respond_to?(method_name) &&
|
|
130
|
+
klass.method(method_name).arity <= 0
|
|
131
|
+
rescue StandardError
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
scope_candidates.sort.first(20)
|
|
136
|
+
rescue StandardError
|
|
137
|
+
[]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
module Tools
|
|
7
|
+
class ReadFile < Base
|
|
8
|
+
MAX_FILE_SIZE = 100_000 # 100KB
|
|
9
|
+
MAX_LINES = 500
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def description
|
|
13
|
+
"Read source code from a file in the application. " \
|
|
14
|
+
"Can read models, controllers, views, configs, or any Ruby/text file. " \
|
|
15
|
+
"Optionally specify line range to read specific sections."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parameters
|
|
19
|
+
{
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
path: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "File path (relative to app root or absolute). Examples: 'app/models/user.rb', 'config/routes.rb'"
|
|
25
|
+
},
|
|
26
|
+
start_line: {
|
|
27
|
+
type: "integer",
|
|
28
|
+
description: "Start line number (1-indexed, optional)"
|
|
29
|
+
},
|
|
30
|
+
end_line: {
|
|
31
|
+
type: "integer",
|
|
32
|
+
description: "End line number (1-indexed, optional)"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
required: ["path"]
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execute(binding, path:, start_line: nil, end_line: nil)
|
|
41
|
+
full_path = resolve_path(path)
|
|
42
|
+
|
|
43
|
+
unless File.exist?(full_path)
|
|
44
|
+
return { error: "File not found: #{path}", searched_path: full_path }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
unless File.readable?(full_path)
|
|
48
|
+
return { error: "File not readable: #{path}" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if File.size(full_path) > MAX_FILE_SIZE
|
|
52
|
+
return { error: "File too large (max #{MAX_FILE_SIZE / 1000}KB): #{path}" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
read_file_content(full_path, path, start_line, end_line)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
{ error: "#{e.class}: #{e.message}" }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def resolve_path(path)
|
|
63
|
+
return path if path.start_with?("/")
|
|
64
|
+
|
|
65
|
+
# Rails.root があればそこから
|
|
66
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
67
|
+
return File.join(Rails.root, path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Bundler.root があればそこから
|
|
71
|
+
if defined?(Bundler) && Bundler.respond_to?(:root) && Bundler.root
|
|
72
|
+
return File.join(Bundler.root, path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# カレントディレクトリから
|
|
76
|
+
File.expand_path(path, Dir.pwd)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def read_file_content(full_path, original_path, start_line, end_line)
|
|
80
|
+
lines = File.readlines(full_path)
|
|
81
|
+
total_lines = lines.length
|
|
82
|
+
|
|
83
|
+
# 行範囲の指定がある場合
|
|
84
|
+
if start_line || end_line
|
|
85
|
+
start_idx = [(start_line || 1) - 1, 0].max
|
|
86
|
+
end_idx = [(end_line || total_lines) - 1, total_lines - 1].min
|
|
87
|
+
end_idx = [end_idx, start_idx + MAX_LINES - 1].min
|
|
88
|
+
|
|
89
|
+
selected_lines = lines[start_idx..end_idx]
|
|
90
|
+
content = selected_lines.map.with_index(start_idx + 1) { |line, num| "#{num}: #{line}" }.join
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
path: original_path,
|
|
94
|
+
full_path: full_path,
|
|
95
|
+
lines: "#{start_idx + 1}-#{end_idx + 1}",
|
|
96
|
+
total_lines: total_lines,
|
|
97
|
+
content: content
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
# 全体を読む(MAX_LINES制限あり)
|
|
101
|
+
if lines.length > MAX_LINES
|
|
102
|
+
content = lines.first(MAX_LINES).map.with_index(1) { |line, num| "#{num}: #{line}" }.join
|
|
103
|
+
{
|
|
104
|
+
path: original_path,
|
|
105
|
+
full_path: full_path,
|
|
106
|
+
lines: "1-#{MAX_LINES}",
|
|
107
|
+
total_lines: total_lines,
|
|
108
|
+
truncated: true,
|
|
109
|
+
content: content
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
content = lines.map.with_index(1) { |line, num| "#{num}: #{line}" }.join
|
|
113
|
+
{
|
|
114
|
+
path: original_path,
|
|
115
|
+
full_path: full_path,
|
|
116
|
+
total_lines: total_lines,
|
|
117
|
+
content: content
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|