pretty_irb 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.
@@ -0,0 +1,97 @@
1
+ require "time"
2
+
3
+ module PrettyIRB
4
+ class HistoryManager
5
+ attr_reader :entries
6
+
7
+ def initialize
8
+ @entries = []
9
+ end
10
+
11
+ # Add entry to history
12
+ def add(code, result = nil, error = nil)
13
+ @entries << {
14
+ code: code,
15
+ result: result,
16
+ error: error,
17
+ timestamp: Time.now,
18
+ line_number: @entries.length + 1
19
+ }
20
+ end
21
+
22
+ # Search history by keyword
23
+ def search(keyword)
24
+ results = @entries.select { |entry| entry[:code].include?(keyword) }
25
+ format_search_results(results)
26
+ end
27
+
28
+ # Get last N entries
29
+ def last(n = 10)
30
+ format_history(@entries.last(n))
31
+ end
32
+
33
+ # Get all history
34
+ def all
35
+ format_history(@entries)
36
+ end
37
+
38
+ # Get entry by line number
39
+ def get(line_number)
40
+ entry = @entries[line_number - 1]
41
+ return "" unless entry
42
+ "#{entry[:line_number]}: #{entry[:code]}\n=> #{entry[:result]}"
43
+ end
44
+
45
+ # Clear history
46
+ def clear
47
+ @entries = []
48
+ "✓ History cleared"
49
+ end
50
+
51
+ # Export history to file
52
+ def export(filename)
53
+ File.write(filename, format_for_export)
54
+ "✓ History exported to #{filename}"
55
+ end
56
+
57
+ private
58
+
59
+ def format_history(entries)
60
+ return "📋 No history" if entries.empty?
61
+
62
+ output = "📋 History (#{entries.length} entries):\n\n"
63
+ entries.each do |entry|
64
+ output += "#{entry[:line_number]}: #{entry[:code]}\n"
65
+ if entry[:result]
66
+ output += " => #{entry[:result]}\n"
67
+ elsif entry[:error]
68
+ output += " ✗ #{entry[:error]}\n"
69
+ end
70
+ output += "\n"
71
+ end
72
+ output
73
+ end
74
+
75
+ def format_search_results(results)
76
+ return "🔍 No matches found" if results.empty?
77
+
78
+ output = "🔍 Found #{results.length} matches:\n\n"
79
+ results.each do |entry|
80
+ output += "#{entry[:line_number]}: #{entry[:code]}\n"
81
+ output += " Time: #{entry[:timestamp].strftime('%H:%M:%S')}\n\n"
82
+ end
83
+ output
84
+ end
85
+
86
+ def format_for_export
87
+ output = "# Pretty IRB History\n"
88
+ output += "# Generated: #{Time.now}\n\n"
89
+ @entries.each do |entry|
90
+ output += "# Line #{entry[:line_number]} - #{entry[:timestamp]}\n"
91
+ output += entry[:code] + "\n"
92
+ output += "# => #{entry[:result]}\n\n" if entry[:result]
93
+ end
94
+ output
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,387 @@
1
+ require "irb"
2
+ require "reline"
3
+
4
+ module PrettyIRB
5
+ class Shell
6
+ BANNER = <<~BANNER
7
+ ╔═══════════════════════════════════════════════════════════╗
8
+ ║ 🎨 Welcome to Pretty IRB 🎨 ║
9
+ ║ ║
10
+ ║ Enhanced Interactive Ruby Shell with Auto-Correct ║
11
+ ║ Type 'help' for commands or 'exit' to quit ║
12
+ ╚═══════════════════════════════════════════════════════════╝
13
+ BANNER
14
+
15
+ def initialize(config = {})
16
+ @config = config
17
+ @history_manager = HistoryManager.new
18
+ @snippet_manager = SnippetManager.new
19
+ @variable_inspector = VariableInspector.new(TOPLEVEL_BINDING)
20
+ @input_buffer = ""
21
+ @binding = TOPLEVEL_BINDING
22
+ end
23
+
24
+ def run
25
+ puts BANNER.light_magenta
26
+ start_repl
27
+ end
28
+
29
+ private
30
+
31
+ def start_repl
32
+ loop do
33
+ begin
34
+ input = read_input
35
+ break if input.nil? || input.downcase == "exit"
36
+
37
+ if input.downcase == "help"
38
+ show_help
39
+ next
40
+ end
41
+
42
+ # Handle special commands
43
+ if input.downcase == "clear"
44
+ system("clear") || system("cls")
45
+ next
46
+ end
47
+
48
+ # Handle history commands
49
+ if input.start_with?("history")
50
+ handle_history_command(input)
51
+ next
52
+ end
53
+
54
+ # Handle snippet commands
55
+ if input.start_with?("snippet")
56
+ handle_snippet_command(input)
57
+ next
58
+ end
59
+
60
+ # Handle variable commands
61
+ if input.start_with?("vars")
62
+ handle_variable_command(input)
63
+ next
64
+ end
65
+
66
+ # Handle benchmark commands
67
+ if input.start_with?("benchmark") || input.start_with?("bench")
68
+ handle_benchmark_command(input)
69
+ next
70
+ end
71
+
72
+ # Handle cheatsheet commands
73
+ if input.start_with?("cheat")
74
+ handle_cheatsheet_command(input)
75
+ next
76
+ end
77
+
78
+ # Handle AI help commands
79
+ if input.start_with?("?")
80
+ handle_ai_command(input)
81
+ next
82
+ end
83
+
84
+ execute_code(input)
85
+ @history_manager.add(input)
86
+
87
+ rescue Interrupt
88
+ puts "\n" + Formatter.format_warning("Interrupted. Type 'exit' to quit.")
89
+ rescue EOFError
90
+ puts "\n" + Formatter.format_success("Goodbye!")
91
+ break
92
+ rescue StandardError => e
93
+ handle_error(e)
94
+ end
95
+ end
96
+ end
97
+
98
+ def read_input
99
+ prompt = "pretty_irb".light_magenta + " >> ".light_magenta
100
+ input = Reline.readline(prompt, true)
101
+ return nil if input.nil?
102
+
103
+ # If the line appears to start a block or otherwise is incomplete,
104
+ # collect additional lines until the code is complete.
105
+ if incomplete_code?(input)
106
+ buffer = input + "\n"
107
+ while (more = Reline.readline("... ".light_magenta, true))
108
+ break if more.nil?
109
+ buffer += more + "\n"
110
+ break unless incomplete_code?(buffer)
111
+ end
112
+
113
+ return buffer
114
+ end
115
+
116
+ input
117
+ end
118
+
119
+ # Heuristic to detect incomplete Ruby code.
120
+ # Returns true if there are unclosed block tokens, brackets, or quotes.
121
+ def incomplete_code?(code)
122
+ return true if code.nil? || code.strip.empty?
123
+
124
+ # Count block starters and 'end'
125
+ opens = 0
126
+ opens += code.scan(/\b(do|def|class|module|if|case|begin|for|while|until|unless)\b/).size
127
+ closes = code.scan(/\bend\b/).size
128
+ return true if opens > closes
129
+
130
+ # Brackets / braces / parentheses
131
+ return true if code.count('(') > code.count(')')
132
+ return true if code.count('[') > code.count(']')
133
+ return true if code.count('{') > code.count('}')
134
+
135
+ # Unclosed quotes (simple heuristic, ignores escaped quotes)
136
+ single_quotes = code.scan(/(?<!\\)'/).size
137
+ double_quotes = code.scan(/(?<!\\)"/).size
138
+ return true if single_quotes.odd? || double_quotes.odd?
139
+
140
+ # Line continuation with trailing backslash
141
+ return true if code.rstrip.end_with?('\\')
142
+
143
+ false
144
+ end
145
+
146
+ def execute_code(code)
147
+ # Auto-correct the code
148
+ corrected_code = AutoCorrector.auto_correct_code(code)
149
+
150
+ # Show if code was corrected
151
+ if corrected_code != code
152
+ puts Formatter.format_warning("↻ Auto-corrected: #{corrected_code}")
153
+ end
154
+
155
+ begin
156
+ # Evaluate the code
157
+ result = eval(corrected_code, @binding)
158
+
159
+ # Display the result
160
+ unless result.nil?
161
+ puts Formatter.format_output(result)
162
+ end
163
+
164
+ rescue NameError => e
165
+ handle_name_error(e)
166
+ rescue NoMethodError => e
167
+ handle_method_error(e)
168
+ rescue SyntaxError => e
169
+ handle_syntax_error(e)
170
+ rescue StandardError => e
171
+ handle_error(e)
172
+ end
173
+ end
174
+
175
+ def handle_name_error(error)
176
+ puts Formatter.format_error("NameError: #{error.message}")
177
+ suggestions = AutoCorrector.suggest_corrections(error)
178
+ puts suggestions if suggestions && !suggestions.empty?
179
+ end
180
+
181
+ def handle_method_error(error)
182
+ puts Formatter.format_error("NoMethodError: #{error.message}")
183
+ suggestions = AutoCorrector.suggest_corrections(error)
184
+ puts suggestions if suggestions && !suggestions.empty?
185
+ end
186
+
187
+ def handle_syntax_error(error)
188
+ puts Formatter.format_error("SyntaxError: #{error.message}")
189
+ suggestions = AutoCorrector.suggest_corrections(error)
190
+ puts suggestions if suggestions && !suggestions.empty?
191
+ end
192
+
193
+ def handle_error(error)
194
+ puts Formatter.format_error("#{error.class}: #{error.message}")
195
+ if error.backtrace
196
+ puts Formatter.format_warning("Backtrace:")
197
+ error.backtrace.first(5).each do |line|
198
+ puts " #{line}"
199
+ end
200
+ end
201
+ end
202
+
203
+ def show_help
204
+ help_text = <<~HELP
205
+ #{'=== Pretty IRB Commands ==='.light_magenta}
206
+
207
+ exit, quit Exit the shell
208
+ help Show this help message
209
+ clear Clear the screen
210
+
211
+ #{'=== History Commands ==='.light_magenta}
212
+
213
+ history Show all command history
214
+ history search KEYWORD Search history
215
+ history last N Show last N commands
216
+ history export FILE Export to file
217
+ history clear Clear history
218
+
219
+ #{'=== Snippet Commands ==='.light_magenta}
220
+
221
+ snippet list List all snippets
222
+ snippet save NAME CODE Save code snippet
223
+ snippet load NAME Load and execute snippet
224
+ snippet show NAME Show snippet code
225
+ snippet delete NAME Delete snippet
226
+ snippet search KEYWORD Search snippets
227
+
228
+ #{'=== Variable Commands ==='.light_magenta}
229
+
230
+ vars List all variables
231
+ vars VARNAME Inspect variable
232
+ vars type:TYPE Variables of type
233
+ vars search:KEYWORD Search variables
234
+ vars memory Memory usage
235
+
236
+ #{'=== Benchmark Commands ==='.light_magenta}
237
+
238
+ bench CODE Benchmark code
239
+ bench compare CODE1 vs CODE2 Compare two snippets
240
+ bench memory CODE Profile memory
241
+
242
+ #{'=== Cheatsheet Commands ==='.light_magenta}
243
+
244
+ cheat Show Ruby cheatsheet
245
+ cheat TOPIC Cheatsheet for topic
246
+ (array, hash, string, enumerable, file, regex, date)
247
+
248
+ #{'=== AI Help Commands ==='.light_magenta}
249
+
250
+ ?explain(method) Explain a Ruby method
251
+ ?example(topic) Get code examples
252
+ ?debug(code) Analyze code for issues
253
+ ?practices(topic) Learn best practices
254
+ ?ref(keyword) Quick reference
255
+
256
+ #{'You can type any valid Ruby code and it will be executed!'.light_green}
257
+ HELP
258
+ puts help_text
259
+ end
260
+
261
+ def handle_history_command(input)
262
+ case input.downcase
263
+ when "history"
264
+ puts @history_manager.all
265
+ when /^history\s+search\s+(.+)$/
266
+ keyword = $1.strip
267
+ puts @history_manager.search(keyword)
268
+ when /^history\s+last\s+(\d+)$/
269
+ n = $1.to_i
270
+ puts @history_manager.last(n)
271
+ when /^history\s+export\s+(.+)$/
272
+ filename = $1.strip
273
+ puts @history_manager.export(filename)
274
+ when "history clear"
275
+ puts @history_manager.clear
276
+ else
277
+ puts Formatter.format_warning("Usage: history | history search KEYWORD | history last N | history export FILE | history clear")
278
+ end
279
+ end
280
+
281
+ def handle_snippet_command(input)
282
+ case input.downcase
283
+ when "snippet list"
284
+ puts @snippet_manager.list
285
+ when /^snippet\s+save\s+(\w+)\s+(.+)$/
286
+ name = $1
287
+ code = $2
288
+ puts @snippet_manager.save(name, code)
289
+ when /^snippet\s+load\s+(\w+)$/
290
+ name = $1
291
+ code = @snippet_manager.load(name)
292
+ if code
293
+ puts Formatter.format_success("📌 Loaded snippet: #{name}")
294
+ puts code
295
+ execute_code(code)
296
+ else
297
+ puts code
298
+ end
299
+ when /^snippet\s+show\s+(\w+)$/
300
+ name = $1
301
+ puts @snippet_manager.show(name)
302
+ when /^snippet\s+delete\s+(\w+)$/
303
+ name = $1
304
+ puts @snippet_manager.delete(name)
305
+ when /^snippet\s+search\s+(.+)$/
306
+ keyword = $1.strip
307
+ puts @snippet_manager.search(keyword)
308
+ else
309
+ puts Formatter.format_warning("Usage: snippet list | snippet save NAME CODE | snippet load NAME | snippet show NAME | snippet delete NAME | snippet search KEYWORD")
310
+ end
311
+ end
312
+
313
+ def handle_variable_command(input)
314
+ case input.downcase
315
+ when "vars"
316
+ puts @variable_inspector.list_variables
317
+ when /^vars\s+(.+)$/
318
+ var_name = $1.strip
319
+ if var_name == "memory"
320
+ puts @variable_inspector.memory_usage
321
+ elsif var_name.start_with?("type:")
322
+ type = var_name.sub("type:", "").strip
323
+ puts @variable_inspector.by_type(type)
324
+ elsif var_name.start_with?("search:")
325
+ keyword = var_name.sub("search:", "").strip
326
+ puts @variable_inspector.search(keyword)
327
+ else
328
+ puts @variable_inspector.inspect_var(var_name)
329
+ end
330
+ else
331
+ puts Formatter.format_warning("Usage: vars | vars VARNAME | vars type:TYPE | vars search:KEYWORD | vars memory")
332
+ end
333
+ end
334
+
335
+ def handle_benchmark_command(input)
336
+ case input
337
+ when /^bench(?:mark)?\s+(.+)$/
338
+ code = $1
339
+ puts Benchmarker.benchmark(code)
340
+ when /^bench(?:mark)?\s+compare\s+(.+?)\s+vs\s+(.+)$/
341
+ code1 = $1
342
+ code2 = $2
343
+ puts Benchmarker.compare(code1, code2)
344
+ when /^bench(?:mark)?\s+memory\s+(.+)$/
345
+ code = $1
346
+ puts Benchmarker.profile_memory(code)
347
+ else
348
+ puts Formatter.format_warning("Usage: bench CODE | bench compare CODE1 vs CODE2 | bench memory CODE")
349
+ end
350
+ end
351
+
352
+ def handle_cheatsheet_command(input)
353
+ case input.downcase
354
+ when "cheat"
355
+ puts CheatSheet.show
356
+ when /^cheat\s+(\w+)$/
357
+ topic = $1
358
+ puts CheatSheet.show(topic)
359
+ else
360
+ puts Formatter.format_warning("Usage: cheat | cheat TOPIC")
361
+ puts "Topics: array, hash, string, enumerable, file, regex, date"
362
+ end
363
+ end
364
+
365
+ def handle_ai_command(input)
366
+ case input
367
+ when /^\?explain\((.*?)\)$/
368
+ method_name = $1.gsub(/'|"/, "")
369
+ puts AIHelper.explain_method(method_name)
370
+ when /^\?example\((.*?)\)$/
371
+ topic = $1.gsub(/'|"/, "")
372
+ puts AIHelper.get_example(topic)
373
+ when /^\?debug\((.*)\)$/
374
+ code_snippet = $1
375
+ puts AIHelper.debug_code(code_snippet)
376
+ when /^\?practices\((.*?)\)$/
377
+ topic = $1.gsub(/'|"/, "")
378
+ puts AIHelper.best_practices(topic)
379
+ when /^\?ref\((.*?)\)$/
380
+ keyword = $1.gsub(/'|"/, "")
381
+ puts AIHelper.quick_reference(keyword)
382
+ else
383
+ puts Formatter.format_warning("❓ Unknown AI command. Type 'help' for available commands.")
384
+ end
385
+ end
386
+ end
387
+ end
@@ -0,0 +1,119 @@
1
+ require "fileutils"
2
+ require "json"
3
+
4
+ module PrettyIRB
5
+ class SnippetManager
6
+ SNIPPET_DIR = File.expand_path("~/.pretty_irb_snippets")
7
+
8
+ def initialize
9
+ ensure_snippet_dir
10
+ end
11
+
12
+ # Save a code snippet
13
+ def save(name, code, description = "")
14
+ filename = snippet_path(name)
15
+
16
+ snippet = {
17
+ name: name,
18
+ code: code,
19
+ description: description,
20
+ created_at: Time.now.to_s,
21
+ tags: extract_tags(code)
22
+ }
23
+
24
+ File.write(filename, JSON.pretty_generate(snippet))
25
+ "✓ Snippet '#{name}' saved"
26
+ end
27
+
28
+ # Load a snippet
29
+ def load(name)
30
+ filename = snippet_path(name)
31
+ return "❌ Snippet '#{name}' not found" unless File.exist?(filename)
32
+
33
+ snippet = JSON.parse(File.read(filename))
34
+ snippet["code"]
35
+ end
36
+
37
+ # List all snippets
38
+ def list
39
+ return "📋 No snippets saved yet" unless Dir.exist?(SNIPPET_DIR)
40
+
41
+ snippets = Dir.glob("#{SNIPPET_DIR}/*.json").map do |file|
42
+ data = JSON.parse(File.read(file))
43
+ " • #{data['name']}: #{data['description']}"
44
+ end
45
+
46
+ return "📋 No snippets saved yet" if snippets.empty?
47
+
48
+ "📋 Saved Snippets:\n#{snippets.join("\n")}"
49
+ end
50
+
51
+ # Search snippets by tag or keyword
52
+ def search(keyword)
53
+ return "📋 No snippets saved yet" unless Dir.exist?(SNIPPET_DIR)
54
+
55
+ results = Dir.glob("#{SNIPPET_DIR}/*.json").select do |file|
56
+ data = JSON.parse(File.read(file))
57
+ data["name"].include?(keyword) ||
58
+ data["code"].include?(keyword) ||
59
+ data["tags"].any? { |tag| tag.include?(keyword) }
60
+ end
61
+
62
+ return "🔍 No matches found" if results.empty?
63
+
64
+ output = "🔍 Found #{results.length} snippet(s):\n\n"
65
+ results.each do |file|
66
+ data = JSON.parse(File.read(file))
67
+ output += "📌 #{data['name']}\n"
68
+ output += " Description: #{data['description']}\n"
69
+ output += " Tags: #{data['tags'].join(', ')}\n\n"
70
+ end
71
+ output
72
+ end
73
+
74
+ # Delete a snippet
75
+ def delete(name)
76
+ filename = snippet_path(name)
77
+ return "❌ Snippet '#{name}' not found" unless File.exist?(filename)
78
+
79
+ File.delete(filename)
80
+ "✓ Snippet '#{name}' deleted"
81
+ end
82
+
83
+ # Show snippet details
84
+ def show(name)
85
+ filename = snippet_path(name)
86
+ return "❌ Snippet '#{name}' not found" unless File.exist?(filename)
87
+
88
+ snippet = JSON.parse(File.read(filename))
89
+ output = "📌 #{snippet['name']}\n"
90
+ output += "Description: #{snippet['description']}\n"
91
+ output += "Created: #{snippet['created_at']}\n"
92
+ output += "Tags: #{snippet['tags'].join(', ')}\n\n"
93
+ output += "Code:\n#{snippet['code']}\n"
94
+ output
95
+ end
96
+
97
+ private
98
+
99
+ def ensure_snippet_dir
100
+ FileUtils.mkdir_p(SNIPPET_DIR) unless Dir.exist?(SNIPPET_DIR)
101
+ end
102
+
103
+ def snippet_path(name)
104
+ File.join(SNIPPET_DIR, "#{name}.json")
105
+ end
106
+
107
+ def extract_tags(code)
108
+ tags = []
109
+ tags << "class" if code.include?("class ")
110
+ tags << "method" if code.include?("def ")
111
+ tags << "loop" if code.match?(/\.(each|map|select|reduce)/)
112
+ tags << "string" if code.match?(/".*"|'.*'/)
113
+ tags << "array" if code.match?(/\[.*\]/)
114
+ tags << "hash" if code.match?(/\{.*\}/)
115
+ tags << "error-handling" if code.include?("begin")
116
+ tags
117
+ end
118
+ end
119
+ end