fantasy-cli 1.2.14 → 1.3.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,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsd
4
+ module LSP
5
+ # Symbols - Handles LSP document and workspace symbols
6
+ class Symbols
7
+ attr_reader :client, :cache, :cache_ttl
8
+
9
+ def initialize(client)
10
+ @client = client
11
+ @cache = {}
12
+ @cache_ttl = 30 # seconds
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ # Get document symbols
17
+ def document_symbols(uri)
18
+ return nil unless @client&.running?
19
+
20
+ cache_key = "doc:#{uri}"
21
+
22
+ # Check cache
23
+ cached = check_cache(cache_key)
24
+ return cached if cached
25
+
26
+ # Request from server
27
+ result = @client.document_symbol(uri)
28
+ return nil unless result
29
+
30
+ # Parse symbols
31
+ symbols = parse_symbols(result)
32
+
33
+ # Cache result
34
+ store_cache(cache_key, symbols)
35
+
36
+ symbols
37
+ end
38
+
39
+ # Get workspace symbols
40
+ def workspace_symbols(query = '')
41
+ return nil unless @client&.running?
42
+
43
+ cache_key = "ws:#{query}"
44
+
45
+ # Check cache
46
+ cached = check_cache(cache_key)
47
+ return cached if cached
48
+
49
+ # Request from server
50
+ result = @client.workspace_symbol(query)
51
+ return nil unless result
52
+
53
+ # Parse symbols
54
+ symbols = parse_symbols(result)
55
+
56
+ # Cache result
57
+ store_cache(cache_key, symbols)
58
+
59
+ symbols
60
+ end
61
+
62
+ # Get symbols at position
63
+ def symbol_at(symbols, line, character)
64
+ return nil unless symbols
65
+
66
+ symbols.find do |s|
67
+ range = s[:range]
68
+ next unless range
69
+
70
+ start_pos = range[:start]
71
+ end_pos = range[:end]
72
+
73
+ line >= start_pos[:line] && line <= end_pos[:line] &&
74
+ (line != start_pos[:line] || character >= start_pos[:character]) &&
75
+ (line != end_pos[:line] || character <= end_pos[:character])
76
+ end
77
+ end
78
+
79
+ # Filter symbols by name
80
+ def filter(symbols, pattern)
81
+ return symbols if pattern.empty?
82
+
83
+ regex = Regexp.new(pattern, Regexp::IGNORECASE)
84
+ symbols.select { |s| s[:name] =~ regex }
85
+ end
86
+
87
+ # Group symbols by kind
88
+ def group_by_kind(symbols)
89
+ symbols.group_by { |s| s[:kind] }
90
+ end
91
+
92
+ # Format symbol for display
93
+ def format_symbol(symbol, width: 50)
94
+ kind_icon = kind_to_icon(symbol[:kind])
95
+ name = symbol[:name]
96
+ detail = symbol[:detail]
97
+ line = symbol[:range]&.[]('start')&.[]('line') || symbol[:range]&.[](:start)&.[](:line)
98
+
99
+ text = "#{kind_icon} #{name}"
100
+ text += " (#{detail})" if detail && text.length + detail.length < width - 5
101
+ text += " L#{line + 1}" if line
102
+
103
+ text[0...width]
104
+ end
105
+
106
+ # Format symbol list for TUI
107
+ def format_list(symbols, selected_index: 0, width: 50, max_items: 15)
108
+ return [] if symbols.empty?
109
+
110
+ display_items = symbols.first(max_items)
111
+
112
+ display_items.map.with_index do |symbol, index|
113
+ prefix = index == selected_index ? '>' : ' '
114
+ formatted = format_symbol(symbol, width: width - 2)
115
+ "#{prefix} #{formatted}"
116
+ end
117
+ end
118
+
119
+ # Get breadcrumb path for symbol
120
+ def breadcrumb_path(symbols, line, character)
121
+ containing = symbols.select do |s|
122
+ range = s[:range]
123
+ next false unless range && s[:children]
124
+
125
+ start_pos = range[:start]
126
+ end_pos = range[:end]
127
+
128
+ line >= start_pos[:line] && line <= end_pos[:line] &&
129
+ (line != start_pos[:line] || character >= start_pos[:character]) &&
130
+ (line != end_pos[:line] || character <= end_pos[:character])
131
+ end
132
+
133
+ return [] if containing.empty?
134
+
135
+ # Find most specific container
136
+ innermost = containing.min_by do |s|
137
+ range = s[:range]
138
+ lines = range[:end][:line] - range[:start][:line]
139
+ chars = range[:end][:character] - range[:start][:character]
140
+ lines * 1000 + chars
141
+ end
142
+
143
+ path = [innermost]
144
+ path.concat(breadcrumb_path(innermost[:children] || [], line, character))
145
+ path
146
+ end
147
+
148
+ # Clear cache
149
+ def clear_cache
150
+ @mutex.synchronize do
151
+ @cache.clear
152
+ end
153
+ end
154
+
155
+ # Symbol outline (tree view)
156
+ def outline(symbols, indent: 0, max_depth: 5)
157
+ return [] if symbols.nil? || symbols.empty? || indent >= max_depth
158
+
159
+ lines = []
160
+
161
+ symbols.each do |symbol|
162
+ icon = kind_to_icon(symbol[:kind])
163
+ indent_str = ' ' * indent
164
+ name = symbol[:name]
165
+
166
+ lines << "#{indent_str}#{icon} #{name}"
167
+
168
+ # Add children
169
+ children = symbol[:children] || symbol['children']
170
+ if children && !children.empty?
171
+ lines.concat(outline(children, indent: indent + 1, max_depth: max_depth))
172
+ end
173
+ end
174
+
175
+ lines
176
+ end
177
+
178
+ private
179
+
180
+ def parse_symbols(result)
181
+ return [] unless result.is_a?(Array)
182
+
183
+ result.map { |s| normalize_symbol(s) }
184
+ end
185
+
186
+ def normalize_symbol(symbol)
187
+ {
188
+ name: symbol['name'] || symbol[:name],
189
+ kind: symbol['kind'] || symbol[:kind],
190
+ detail: symbol['detail'] || symbol[:detail],
191
+ range: symbol['location']&.[]('range') || symbol['range'] || symbol[:range],
192
+ uri: symbol['location']&.[]('uri') || symbol['uri'] || symbol[:uri],
193
+ children: parse_children(symbol['children'] || symbol[:children])
194
+ }
195
+ end
196
+
197
+ def parse_children(children)
198
+ return nil unless children.is_a?(Array)
199
+
200
+ children.map { |c| normalize_symbol(c) }
201
+ end
202
+
203
+ def check_cache(key)
204
+ @mutex.synchronize do
205
+ entry = @cache[key]
206
+ return nil unless entry
207
+
208
+ if Time.now - entry[:time] > @cache_ttl
209
+ @cache.delete(key)
210
+ nil
211
+ else
212
+ entry[:symbols]
213
+ end
214
+ end
215
+ end
216
+
217
+ def store_cache(key, symbols)
218
+ @mutex.synchronize do
219
+ @cache[key] = {
220
+ symbols: symbols,
221
+ time: Time.now
222
+ }
223
+
224
+ # Limit cache size
225
+ while @cache.size > 50
226
+ @cache.shift
227
+ end
228
+ end
229
+ end
230
+
231
+ def kind_to_icon(kind)
232
+ icons = {
233
+ 1 => '📄', # File
234
+ 2 => '📦', # Module
235
+ 3 => '📦', # Namespace
236
+ 4 => '📦', # Package
237
+ 5 => 'ⓒ', # Class
238
+ 6 => 'ⓜ', # Method
239
+ 7 => 'ⓟ', # Property
240
+ 8 => 'ⓕ', # Field
241
+ 9 => 'ⓒ', # Constructor
242
+ 10 => 'ⓔ', # Enum
243
+ 11 => 'ⓘ', # Interface
244
+ 12 => 'ⓕ', # Function
245
+ 13 => 'ⓥ', # Variable
246
+ 14 => 'ⓒ', # Constant
247
+ 15 => '"', # String
248
+ 16 => '#', # Number
249
+ 17 => '✓', # Boolean
250
+ 18 => '[]', # Array
251
+ 19 => '{}', # Object
252
+ 20 => 'ⓚ', # Key
253
+ 21 => 'ⓘ', # Null
254
+ 22 => 'ⓔ', # EnumMember
255
+ 23 => '📄', # Struct
256
+ 24 => '🔄', # Event
257
+ 25 => 'ⓞ', # Operator
258
+ 26 => 'ⓣ' # TypeParameter
259
+ }
260
+
261
+ icons[kind] || '❓'
262
+ end
263
+ end
264
+
265
+ # SymbolOutline - TUI panel for symbol outline
266
+ class SymbolOutline
267
+ attr_reader :visible, :width, :height, :symbols, :selected_index
268
+
269
+ def initialize(width: 40, height: 20)
270
+ @visible = false
271
+ @width = width
272
+ @height = height
273
+ @symbols = []
274
+ @selected_index = 0
275
+ @flattened = []
276
+ end
277
+
278
+ def show
279
+ @visible = true
280
+ end
281
+
282
+ def hide
283
+ @visible = false
284
+ end
285
+
286
+ def toggle
287
+ @visible = !@visible
288
+ end
289
+
290
+ def update(symbols)
291
+ @symbols = symbols || []
292
+ @selected_index = 0
293
+ flatten_symbols
294
+ end
295
+
296
+ def move_up
297
+ @selected_index = [@selected_index - 1, 0].max if @visible
298
+ end
299
+
300
+ def move_down
301
+ @selected_index = [@selected_index + 1, @flattened.length - 1].max if @visible
302
+ end
303
+
304
+ def current_symbol
305
+ @flattened[@selected_index]
306
+ end
307
+
308
+ def render
309
+ return [] unless @visible
310
+
311
+ lines = []
312
+ lines << '─' * @width
313
+ lines << ' Outline'.ljust(@width)
314
+ lines << '─' * @width
315
+
316
+ if @flattened.empty?
317
+ lines << ' No symbols'
318
+ else
319
+ display_items = @flattened.first(@height - 4)
320
+ display_items.each_with_index do |item, index|
321
+ prefix = (index + @selected_index.zero? ? '>' : ' ') rescue ' '
322
+ icon = kind_to_icon(item[:kind])
323
+ indent = ' ' * item[:depth]
324
+ name = item[:name][0...@width - indent.length - 5]
325
+ lines << "#{prefix} #{indent}#{icon} #{name}"
326
+ end
327
+ end
328
+
329
+ # Pad to height
330
+ while lines.length < @height
331
+ lines << ''
332
+ end
333
+
334
+ lines.first(@height)
335
+ end
336
+
337
+ private
338
+
339
+ def flatten_symbols
340
+ @flattened = []
341
+ flatten_recursive(@symbols, 0)
342
+ end
343
+
344
+ def flatten_recursive(symbols, depth)
345
+ return unless symbols
346
+
347
+ symbols.each do |s|
348
+ @flattened << s.merge(depth: depth)
349
+ children = s[:children] || s['children']
350
+ flatten_recursive(children, depth + 1) if children
351
+ end
352
+ end
353
+
354
+ def kind_to_icon(kind)
355
+ icons = {
356
+ 1 => '📄', 2 => '📦', 3 => '📦', 4 => '📦',
357
+ 5 => 'ⓒ', 6 => 'ⓜ', 7 => 'ⓟ', 8 => 'ⓕ',
358
+ 9 => 'ⓒ', 10 => 'ⓔ', 11 => 'ⓘ', 12 => 'ⓕ',
359
+ 13 => 'ⓥ', 14 => 'ⓒ', 15 => '"', 16 => '#',
360
+ 17 => '✓', 18 => '[]', 19 => '{}', 20 => 'ⓚ',
361
+ 21 => 'ⓘ', 22 => 'ⓔ', 23 => '📄', 24 => '🔄',
362
+ 25 => 'ⓞ', 26 => 'ⓣ'
363
+ }
364
+ icons[kind] || '❓'
365
+ end
366
+ end
367
+ end
368
+ end
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsd
4
+ module Plugins
5
+ # API - Interface pública para plugins
6
+ #
7
+ # Fornece métodos seguros para plugins interagirem com o sistema,
8
+ # sem expor detalhes internos perigosos.
9
+ class API
10
+ VERSION = '1.0.0'
11
+
12
+ def initialize(plugin_name)
13
+ @plugin_name = plugin_name
14
+ @registry = Registry.instance
15
+ @hooks = Hooks.instance
16
+ end
17
+
18
+ # === Informações do Sistema ===
19
+
20
+ # Versão da API
21
+ def api_version
22
+ VERSION
23
+ end
24
+
25
+ # Versão do Fantasy CLI
26
+ def cli_version
27
+ Gsd::VERSION
28
+ end
29
+
30
+ # Plataforma atual
31
+ def platform
32
+ RUBY_PLATFORM
33
+ end
34
+
35
+ # === Registro de Plugins ===
36
+
37
+ # Lista plugins instalados
38
+ def list_plugins
39
+ @registry.all.map(&:to_h)
40
+ end
41
+
42
+ # Verifica se plugin existe
43
+ def plugin_exists?(name)
44
+ @registry.exists?(name)
45
+ end
46
+
47
+ # Obtém informações de um plugin
48
+ def plugin_info(name)
49
+ plugin = @registry.get(name)
50
+ plugin&.to_h
51
+ end
52
+
53
+ # === Hooks e Eventos ===
54
+
55
+ # Registra handler para evento
56
+ def on(event, priority: 0, &block)
57
+ full_event = "#{@plugin_name}.#{event}"
58
+ @hooks.on(full_event, priority: priority, &block)
59
+ end
60
+
61
+ # Emite evento
62
+ def emit(event, *args)
63
+ full_event = "#{@plugin_name}.#{event}"
64
+ @hooks.trigger(full_event, *args)
65
+ end
66
+
67
+ # Escuta eventos de outros plugins
68
+ def listen_to(plugin, event, &block)
69
+ full_event = "#{plugin}.#{event}"
70
+ @hooks.on(full_event, &block)
71
+ end
72
+
73
+ # Escuta eventos globais
74
+ def listen_global(event, &block)
75
+ @hooks.on(event, &block)
76
+ end
77
+
78
+ # === Logging ===
79
+
80
+ # Log informativo
81
+ def log_info(message)
82
+ puts "[#{@plugin_name}] #{message}"
83
+ end
84
+
85
+ # Log de warning
86
+ def log_warn(message)
87
+ warn "[#{@plugin_name}] WARNING: #{message}"
88
+ end
89
+
90
+ # Log de erro
91
+ def log_error(message)
92
+ warn "[#{@plugin_name}] ERROR: #{message}"
93
+ end
94
+
95
+ # Log de debug (só em modo debug)
96
+ def log_debug(message)
97
+ return unless ENV['DEBUG']
98
+ puts "[#{@plugin_name}] DEBUG: #{message}"
99
+ end
100
+
101
+ # === Configuração ===
102
+
103
+ # Lê configuração do plugin
104
+ def get_config(key, default = nil)
105
+ path = config_path
106
+ return default unless File.exist?(path)
107
+
108
+ config = JSON.parse(File.read(path), symbolize_names: true)
109
+ config[key.to_sym] || default
110
+ rescue StandardError
111
+ default
112
+ end
113
+
114
+ # Salva configuração do plugin
115
+ def set_config(key, value)
116
+ path = config_path
117
+ config = {}
118
+
119
+ if File.exist?(path)
120
+ config = JSON.parse(File.read(path), symbolize_names: true)
121
+ end
122
+
123
+ config[key.to_sym] = value
124
+
125
+ FileUtils.mkdir_p(File.dirname(path))
126
+ File.write(path, JSON.pretty_generate(config))
127
+ rescue StandardError => e
128
+ log_error("Failed to save config: #{e.message}")
129
+ end
130
+
131
+ # === Utilidades ===
132
+
133
+ # Lê arquivo (dentro de ~/.gsd apenas)
134
+ def read_file(path)
135
+ safe_path = File.expand_path(path, File.join(Dir.home, '.gsd'))
136
+ return nil unless File.exist?(safe_path)
137
+ return nil unless safe_path.start_with?(File.join(Dir.home, '.gsd'))
138
+
139
+ File.read(safe_path)
140
+ rescue StandardError
141
+ nil
142
+ end
143
+
144
+ # Escreve arquivo (dentro de ~/.gsd apenas)
145
+ def write_file(path, content)
146
+ safe_path = File.expand_path(path, File.join(Dir.home, '.gsd'))
147
+ return false unless safe_path.start_with?(File.join(Dir.home, '.gsd'))
148
+
149
+ FileUtils.mkdir_p(File.dirname(safe_path))
150
+ File.write(safe_path, content)
151
+ true
152
+ rescue StandardError
153
+ false
154
+ end
155
+
156
+ # HTTP GET (com timeout)
157
+ def http_get(url, timeout: 10)
158
+ require 'net/http'
159
+ require 'uri'
160
+
161
+ uri = URI.parse(url)
162
+ http = Net::HTTP.new(uri.host, uri.port)
163
+ http.use_ssl = uri.scheme == 'https'
164
+ http.open_timeout = timeout
165
+ http.read_timeout = timeout
166
+
167
+ response = http.get(uri.request_uri)
168
+ response.body
169
+ rescue StandardError => e
170
+ log_error("HTTP GET failed: #{e.message}")
171
+ nil
172
+ end
173
+
174
+ # === UI (para plugins TUI) ===
175
+
176
+ # Adiciona mensagem ao output (se TUI estiver ativo)
177
+ def ui_message(text, type: :system)
178
+ return unless defined?(Gsd::TUI::App)
179
+
180
+ # Envia via hook para ser capturado pelo App
181
+ @hooks.trigger('tui.message', { text: text, type: type, plugin: @plugin_name })
182
+ end
183
+
184
+ # Adiciona comando à palette (se TUI estiver ativo)
185
+ def ui_command(name, description, &block)
186
+ @hooks.trigger('tui.register_command', {
187
+ name: name,
188
+ description: description,
189
+ plugin: @plugin_name,
190
+ callback: block
191
+ })
192
+ end
193
+
194
+ # === Timers ===
195
+
196
+ # Executa código após delay
197
+ def set_timeout(seconds, &block)
198
+ Thread.new do
199
+ sleep(seconds)
200
+ block.call
201
+ end
202
+ end
203
+
204
+ # Executa código periodicamente
205
+ def set_interval(seconds, &block)
206
+ Thread.new do
207
+ loop do
208
+ sleep(seconds)
209
+ block.call
210
+ end
211
+ end
212
+ end
213
+
214
+ # === Storage ===
215
+
216
+ # Salva dados (key-value store)
217
+ def storage_set(key, value)
218
+ storage_path = File.join(Dir.home, '.gsd', 'storage', "#{@plugin_name}.json")
219
+ data = {}
220
+
221
+ if File.exist?(storage_path)
222
+ data = JSON.parse(File.read(storage_path), symbolize_names: true)
223
+ end
224
+
225
+ data[key.to_sym] = value
226
+ FileUtils.mkdir_p(File.dirname(storage_path))
227
+ File.write(storage_path, JSON.pretty_generate(data))
228
+ end
229
+
230
+ # Lê dados
231
+ def storage_get(key, default = nil)
232
+ storage_path = File.join(Dir.home, '.gsd', 'storage', "#{@plugin_name}.json")
233
+ return default unless File.exist?(storage_path)
234
+
235
+ data = JSON.parse(File.read(storage_path), symbolize_names: true)
236
+ data.fetch(key.to_sym, default)
237
+ rescue StandardError
238
+ default
239
+ end
240
+
241
+ # Limpa storage
242
+ def storage_clear
243
+ storage_path = File.join(Dir.home, '.gsd', 'storage', "#{@plugin_name}.json")
244
+ File.delete(storage_path) if File.exist?(storage_path)
245
+ end
246
+
247
+ private
248
+
249
+ def config_path
250
+ File.join(Dir.home, '.gsd', 'plugins', @plugin_name, 'config.json')
251
+ end
252
+ end
253
+
254
+ # PluginContext - Contexto para execução de plugins
255
+ # Fornece um ambiente isolado onde o plugin roda
256
+ class PluginContext
257
+ SAFE_METHODS = %i[
258
+ puts print p puts print
259
+ map select reject each each_with_index
260
+ chomp split gsub sub strip
261
+ length size empty? nil?
262
+ to_s to_i to_f to_a to_h to_sym
263
+ freeze dup clone
264
+ upcase downcase capitalize
265
+ include? start_with? end_with?
266
+ + - * / % ** << >>
267
+ == != < > <= >= <=>
268
+ [] []= fetch dig
269
+ keys values has_key?
270
+ new create
271
+ sleep rand srand
272
+ time Time.now
273
+ ].freeze
274
+
275
+ def initialize(api)
276
+ @api = api
277
+ @exports = {}
278
+ end
279
+
280
+ # Executa plugin em contexto seguro
281
+ def run(code)
282
+ create_binding.instance_eval(code)
283
+ rescue StandardError => e
284
+ @api.log_error("Execution error: #{e.message}")
285
+ nil
286
+ end
287
+
288
+ # Exporta função para outros plugins
289
+ def export(name, &block)
290
+ @exports[name] = block
291
+ end
292
+
293
+ # Chama função exportada de outro plugin
294
+ def import(plugin_name, function_name, *args)
295
+ # Implementação via registry
296
+ plugin = Registry.instance.get(plugin_name)
297
+ return nil unless plugin
298
+
299
+ context = plugin.instance_variable_get(:@context)
300
+ return nil unless context
301
+
302
+ handler = context.instance_variable_get(:@exports)[function_name]
303
+ return nil unless handler
304
+
305
+ handler.call(*args)
306
+ end
307
+
308
+ private
309
+
310
+ def create_binding
311
+ binding_context = Object.new
312
+
313
+ # Define métodos seguros
314
+ SAFE_METHODS.each do |method|
315
+ define_safe_method(binding_context, method)
316
+ end
317
+
318
+ # Expõe API
319
+ binding_context.define_singleton_method(:api) { @api }
320
+ binding_context.define_singleton_method(:export) { |n, &b| export(n, &b) }
321
+ binding_context.define_singleton_method(:import) { |p, f, *a| import(p, f, *a) }
322
+
323
+ binding_context.instance_variable_set(:@api, @api)
324
+ binding_context.instance_variable_set(:@context, self)
325
+
326
+ binding_context
327
+ end
328
+
329
+ def define_safe_method(obj, method_name)
330
+ obj.define_singleton_method(method_name) do |*args, &block|
331
+ if args.first.respond_to?(method_name)
332
+ args.first.send(method_name, *args[1..-1], &block)
333
+ else
334
+ nil
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end