rufio 1.1.0 → 1.4.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 +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +26 -297
- data/README_ja.md +24 -261
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/docs/guide.md +410 -0
- data/docs/guide_ja.md +410 -0
- data/info/help.md +54 -17
- data/info/keybindings.md +50 -22
- data/info/welcome.md +14 -5
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +28 -2
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +60 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +4 -11
- data/lib/rufio/dialog_renderer.rb +90 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_operations.rb +28 -6
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +125 -13
- data/lib/rufio/help_menu.rb +19 -7
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +83 -116
- data/lib/rufio/local_script_scanner.rb +6 -18
- data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native_content_search.rb +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +20 -26
- data/lib/rufio/path_jump.rb +55 -0
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +84 -298
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +73 -45
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +6 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +128 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'dialog_support'
|
|
4
|
+
|
|
3
5
|
require 'shellwords'
|
|
4
6
|
require_relative 'file_opener'
|
|
5
7
|
require_relative 'filter_manager'
|
|
@@ -11,11 +13,14 @@ require_relative 'selection_manager'
|
|
|
11
13
|
require_relative 'file_operations'
|
|
12
14
|
require_relative 'bookmark_manager'
|
|
13
15
|
require_relative 'zoxide_integration'
|
|
16
|
+
require_relative 'archive_controller'
|
|
14
17
|
require_relative 'dialog_renderer'
|
|
15
18
|
require_relative 'logger'
|
|
16
19
|
|
|
17
20
|
module Rufio
|
|
18
21
|
class KeybindHandler
|
|
22
|
+
include DialogSupport
|
|
23
|
+
|
|
19
24
|
# NavigationController への委譲
|
|
20
25
|
def current_index
|
|
21
26
|
@nav_controller.current_index
|
|
@@ -49,9 +54,7 @@ module Rufio
|
|
|
49
54
|
return false unless @directory_listing
|
|
50
55
|
|
|
51
56
|
FileUtils.mkdir_p(config_directory)
|
|
52
|
-
|
|
53
|
-
exit_help_mode if @in_help_mode
|
|
54
|
-
exit_job_mode if @job_mode.active?
|
|
57
|
+
leave_other_modes(:config)
|
|
55
58
|
@nav_controller.unfocus_preview_pane
|
|
56
59
|
navigate_to_directory(config_directory)
|
|
57
60
|
rescue SystemCallError => e
|
|
@@ -59,6 +62,23 @@ module Rufio
|
|
|
59
62
|
false
|
|
60
63
|
end
|
|
61
64
|
|
|
65
|
+
# モード遷移の入口。ヘルプ・検索のオーバーレイは元のモードを保持する。
|
|
66
|
+
def switch_mode(mode)
|
|
67
|
+
case mode
|
|
68
|
+
when :files then leave_other_modes(:files)
|
|
69
|
+
when :logs then enter_log_viewer_mode
|
|
70
|
+
when :jobs then enter_job_mode
|
|
71
|
+
when :config then enter_config_mode
|
|
72
|
+
when :global then start_global_search
|
|
73
|
+
when :help then show_help_dialog
|
|
74
|
+
else false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def navigate_to_directory(path)
|
|
79
|
+
@nav_controller.navigate_to_directory(path)
|
|
80
|
+
end
|
|
81
|
+
|
|
62
82
|
def preview_scroll_offset
|
|
63
83
|
@nav_controller.preview_scroll_offset
|
|
64
84
|
end
|
|
@@ -112,6 +132,11 @@ module Rufio
|
|
|
112
132
|
@script_path_manager, @notification_manager, @zoxide_integration
|
|
113
133
|
)
|
|
114
134
|
|
|
135
|
+
# ArchiveController(選択アイテムの ZIP 圧縮・展開を担当)
|
|
136
|
+
@archive_controller = ArchiveController.new(
|
|
137
|
+
nil, @dialog_renderer, @selection_manager, @job_manager, @notification_manager
|
|
138
|
+
)
|
|
139
|
+
|
|
115
140
|
# SearchController(fzf・rga検索を担当)
|
|
116
141
|
@search_controller = SearchController.new(nil, @file_opener)
|
|
117
142
|
|
|
@@ -134,12 +159,14 @@ module Rufio
|
|
|
134
159
|
@file_op_controller.set_directory_listing(directory_listing)
|
|
135
160
|
@bookmark_controller.set_directory_listing(directory_listing)
|
|
136
161
|
@search_controller.set_directory_listing(directory_listing)
|
|
162
|
+
@archive_controller.set_directory_listing(directory_listing)
|
|
137
163
|
end
|
|
138
164
|
|
|
139
165
|
def set_terminal_ui(terminal_ui)
|
|
140
166
|
@terminal_ui = terminal_ui
|
|
141
167
|
@nav_controller.set_terminal_ui(terminal_ui)
|
|
142
168
|
@file_op_controller.set_terminal_ui(terminal_ui)
|
|
169
|
+
@archive_controller.set_terminal_ui(terminal_ui)
|
|
143
170
|
@bookmark_controller.set_terminal_ui(terminal_ui)
|
|
144
171
|
# terminal_ui が設定されたら、bookmark_manager と zoxide_integration にも渡す
|
|
145
172
|
@bookmark_manager.set_terminal_ui(terminal_ui)
|
|
@@ -201,7 +228,7 @@ module Rufio
|
|
|
201
228
|
|
|
202
229
|
entries = get_active_entries
|
|
203
230
|
|
|
204
|
-
# Enter
|
|
231
|
+
# Enter キー(ディレクトリ移動)
|
|
205
232
|
if key == "\r" || key == "\n"
|
|
206
233
|
return @nav_controller.handle_enter_key(current_entry, @in_help_mode, @in_log_viewer_mode)
|
|
207
234
|
end
|
|
@@ -236,6 +263,10 @@ module Rufio
|
|
|
236
263
|
entries[@nav_controller.current_index]
|
|
237
264
|
end
|
|
238
265
|
|
|
266
|
+
def filter_mode?
|
|
267
|
+
@filter_manager.filter_mode
|
|
268
|
+
end
|
|
269
|
+
|
|
239
270
|
def filter_active?
|
|
240
271
|
@filter_manager.filter_active?
|
|
241
272
|
end
|
|
@@ -265,11 +296,6 @@ module Rufio
|
|
|
265
296
|
return false unless @directory_listing
|
|
266
297
|
return true if @in_help_mode
|
|
267
298
|
|
|
268
|
-
exit_log_viewer_mode if @in_log_viewer_mode
|
|
269
|
-
|
|
270
|
-
# 現在のディレクトリを保存
|
|
271
|
-
@pre_help_directory = @directory_listing.current_path
|
|
272
|
-
|
|
273
299
|
# info ディレクトリに移動
|
|
274
300
|
rufio_root = File.expand_path('../..', __dir__)
|
|
275
301
|
info_dir = File.join(rufio_root, 'info')
|
|
@@ -277,6 +303,9 @@ module Rufio
|
|
|
277
303
|
# info ディレクトリが存在することを確認
|
|
278
304
|
return false unless Dir.exist?(info_dir)
|
|
279
305
|
|
|
306
|
+
leave_other_modes(:help_directory)
|
|
307
|
+
@pre_help_directory = @directory_listing.current_path
|
|
308
|
+
|
|
280
309
|
# ヘルプモードを有効化
|
|
281
310
|
@in_help_mode = true
|
|
282
311
|
|
|
@@ -315,14 +344,10 @@ module Rufio
|
|
|
315
344
|
return false unless @directory_listing
|
|
316
345
|
return true if @in_log_viewer_mode
|
|
317
346
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
# 現在のディレクトリを保存
|
|
347
|
+
FileUtils.mkdir_p(@log_dir)
|
|
348
|
+
leave_other_modes(:logs)
|
|
321
349
|
@pre_log_viewer_directory = @directory_listing.current_path
|
|
322
350
|
|
|
323
|
-
# log ディレクトリを作成(存在しない場合)
|
|
324
|
-
FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
|
|
325
|
-
|
|
326
351
|
# log ディレクトリに移動
|
|
327
352
|
navigate_to_directory(@log_dir)
|
|
328
353
|
|
|
@@ -351,45 +376,19 @@ module Rufio
|
|
|
351
376
|
|
|
352
377
|
# ヘルプモード時の制限付き親ディレクトリナビゲーション
|
|
353
378
|
def navigate_parent_with_restriction
|
|
354
|
-
if @in_help_mode
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
379
|
+
restricted_root = if @in_help_mode
|
|
380
|
+
File.join(File.expand_path('../..', __dir__), 'info')
|
|
381
|
+
elsif @in_log_viewer_mode
|
|
382
|
+
@log_dir
|
|
383
|
+
end
|
|
358
384
|
|
|
385
|
+
if restricted_root
|
|
359
386
|
current_path = @directory_listing.current_path
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
unless current_path.start_with?(info_dir)
|
|
363
|
-
return false
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
# 現在のパスが info ディレクトリそのものの場合は移動を許可しない
|
|
367
|
-
if current_path == info_dir
|
|
368
|
-
return false
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
# info ディレクトリ配下であれば、通常のナビゲーションを実行
|
|
372
|
-
navigate_parent
|
|
373
|
-
elsif @in_log_viewer_mode
|
|
374
|
-
# log ディレクトリより上には移動できない
|
|
375
|
-
current_path = @directory_listing.current_path
|
|
376
|
-
|
|
377
|
-
# 現在のパスが log ディレクトリ以下でない場合は移動を許可しない
|
|
378
|
-
unless current_path.start_with?(@log_dir)
|
|
379
|
-
return false
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
# 現在のパスが log ディレクトリそのものの場合は移動を許可しない
|
|
383
|
-
if current_path == @log_dir
|
|
384
|
-
return false
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
# log ディレクトリ配下であれば、通常のナビゲーションを実行
|
|
388
|
-
navigate_parent
|
|
389
|
-
else
|
|
390
|
-
# ヘルプモード・ログビューワモード外では通常のナビゲーション
|
|
391
|
-
navigate_parent
|
|
387
|
+
return false unless current_path.start_with?(restricted_root)
|
|
388
|
+
return false if current_path == restricted_root
|
|
392
389
|
end
|
|
390
|
+
|
|
391
|
+
navigate_parent
|
|
393
392
|
end
|
|
394
393
|
|
|
395
394
|
# プレビューペイン関連メソッド(NavigationController に委譲)
|
|
@@ -444,52 +443,12 @@ module Rufio
|
|
|
444
443
|
|
|
445
444
|
private
|
|
446
445
|
|
|
447
|
-
#
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
# @return [String] 入力されたキー
|
|
454
|
-
def show_overlay_dialog(title, content_lines, options = {}, &block)
|
|
455
|
-
# terminal_ui が利用可能で、screen と renderer が存在する場合のみオーバーレイを使用
|
|
456
|
-
use_overlay = @terminal_ui &&
|
|
457
|
-
@terminal_ui.respond_to?(:screen) &&
|
|
458
|
-
@terminal_ui.respond_to?(:renderer) &&
|
|
459
|
-
@terminal_ui.screen &&
|
|
460
|
-
@terminal_ui.renderer
|
|
461
|
-
|
|
462
|
-
if use_overlay
|
|
463
|
-
# オーバーレイを使用
|
|
464
|
-
@terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
|
|
465
|
-
else
|
|
466
|
-
# フォールバック: 従来の方法
|
|
467
|
-
width = options[:width]
|
|
468
|
-
height = options[:height]
|
|
469
|
-
|
|
470
|
-
unless width && height
|
|
471
|
-
width, height = @dialog_renderer.calculate_dimensions(content_lines, {
|
|
472
|
-
title: title,
|
|
473
|
-
min_width: options[:min_width] || 40,
|
|
474
|
-
max_width: options[:max_width] || 80
|
|
475
|
-
})
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
x, y = @dialog_renderer.calculate_center(width, height)
|
|
479
|
-
|
|
480
|
-
@dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
|
|
481
|
-
border_color: options[:border_color] || "\e[37m",
|
|
482
|
-
title_color: options[:title_color] || "\e[1;33m",
|
|
483
|
-
content_color: options[:content_color] || "\e[37m"
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
key = block_given? ? yield : STDIN.getch
|
|
487
|
-
|
|
488
|
-
@dialog_renderer.clear_area(x, y, width, height)
|
|
489
|
-
@terminal_ui&.refresh_display
|
|
490
|
-
|
|
491
|
-
key
|
|
492
|
-
end
|
|
446
|
+
# 別モードへ入る際の終了順序はここだけで管理する。
|
|
447
|
+
def leave_other_modes(target)
|
|
448
|
+
exit_help_mode if target != :help_directory && help_mode?
|
|
449
|
+
exit_log_viewer_mode if target != :logs && log_viewer_mode?
|
|
450
|
+
exit_job_mode if target != :jobs && in_job_mode?
|
|
451
|
+
true
|
|
493
452
|
end
|
|
494
453
|
|
|
495
454
|
def navigate_parent
|
|
@@ -517,6 +476,7 @@ module Rufio
|
|
|
517
476
|
when :top then @nav_controller.move_to_top
|
|
518
477
|
when :bottom then @nav_controller.move_to_bottom(entries)
|
|
519
478
|
when :refresh then @nav_controller.refresh
|
|
479
|
+
when :preview then focus_preview_pane
|
|
520
480
|
when :open_file then @nav_controller.open_current_file(current_entry)
|
|
521
481
|
when :open_explorer then @nav_controller.open_directory_in_explorer
|
|
522
482
|
when :rename then @file_op_controller.rename_current_file(current_entry)
|
|
@@ -537,13 +497,14 @@ module Rufio
|
|
|
537
497
|
when :rga_search then rga_search
|
|
538
498
|
when :add_bookmark then add_bookmark
|
|
539
499
|
when :bookmark_menu then show_bookmark_menu
|
|
540
|
-
when :zoxide then
|
|
500
|
+
when :zoxide then archive_or_zoxide
|
|
541
501
|
when :start_dir then goto_start_directory
|
|
542
502
|
when :job_mode then enter_job_mode
|
|
543
503
|
when :help then show_help_dialog
|
|
544
504
|
when :log_viewer then enter_log_viewer_mode
|
|
545
505
|
when :command_mode then activate_command_mode
|
|
546
506
|
when :quit then exit_request
|
|
507
|
+
when :quit_cd then quit_to_current_directory
|
|
547
508
|
else
|
|
548
509
|
false
|
|
549
510
|
end
|
|
@@ -553,6 +514,14 @@ module Rufio
|
|
|
553
514
|
@file_op_controller.show_exit_confirmation
|
|
554
515
|
end
|
|
555
516
|
|
|
517
|
+
# 確認なしで終了し、シェル関数(rufio --shell-init)が移動する先として現在のディレクトリを伝える。
|
|
518
|
+
# ヘルプ・ログビューワは rufio 内部のディレクトリを表示しているため対象外。
|
|
519
|
+
def quit_to_current_directory
|
|
520
|
+
return false if @in_help_mode || @in_log_viewer_mode
|
|
521
|
+
|
|
522
|
+
:quit_cd
|
|
523
|
+
end
|
|
524
|
+
|
|
556
525
|
def fzf_search
|
|
557
526
|
@search_controller.fzf_search
|
|
558
527
|
end
|
|
@@ -590,8 +559,6 @@ module Rufio
|
|
|
590
559
|
true
|
|
591
560
|
end
|
|
592
561
|
|
|
593
|
-
|
|
594
|
-
|
|
595
562
|
def goto_start_directory
|
|
596
563
|
@bookmark_controller.goto_start_directory
|
|
597
564
|
end
|
|
@@ -615,17 +582,11 @@ module Rufio
|
|
|
615
582
|
@bookmark_controller.show_script_paths_manager
|
|
616
583
|
end
|
|
617
584
|
|
|
618
|
-
|
|
619
|
-
|
|
585
|
+
# 選択中のアイテムがあれば ZIP 圧縮・展開、なければ zoxide
|
|
586
|
+
def archive_or_zoxide
|
|
587
|
+
return @archive_controller.archive_selected if @selection_manager.any? && !@in_help_mode && !@in_log_viewer_mode
|
|
620
588
|
|
|
621
|
-
|
|
622
|
-
if result
|
|
623
|
-
@nav_controller.select_index(0)
|
|
624
|
-
@nav_controller.clear_filter_mode
|
|
625
|
-
true
|
|
626
|
-
else
|
|
627
|
-
false
|
|
628
|
-
end
|
|
589
|
+
show_zoxide_menu
|
|
629
590
|
end
|
|
630
591
|
|
|
631
592
|
# zoxide 機能
|
|
@@ -641,11 +602,9 @@ module Rufio
|
|
|
641
602
|
|
|
642
603
|
# カーソルを画面下部の入力行に移動
|
|
643
604
|
def move_to_input_line
|
|
644
|
-
|
|
645
|
-
#
|
|
646
|
-
|
|
647
|
-
print "\e[#{screen_height};1H" # 最終行の先頭にカーソル移動
|
|
648
|
-
print "\e[2K" # 行全体をクリア
|
|
605
|
+
screen_height = @terminal_ui&.screen_height || 24
|
|
606
|
+
print "\e[#{screen_height};1H"
|
|
607
|
+
print "\e[2K"
|
|
649
608
|
end
|
|
650
609
|
|
|
651
610
|
# Escキーでキャンセル可能な入力処理
|
|
@@ -655,7 +614,7 @@ module Rufio
|
|
|
655
614
|
input = []
|
|
656
615
|
|
|
657
616
|
loop do
|
|
658
|
-
char =
|
|
617
|
+
char = KeyInput.getch
|
|
659
618
|
|
|
660
619
|
case char
|
|
661
620
|
when "\e" # Escape
|
|
@@ -699,6 +658,9 @@ module Rufio
|
|
|
699
658
|
|
|
700
659
|
# ジョブモードに入る
|
|
701
660
|
def enter_job_mode
|
|
661
|
+
return true if @job_mode.active?
|
|
662
|
+
|
|
663
|
+
leave_other_modes(:jobs)
|
|
702
664
|
@job_mode.activate
|
|
703
665
|
@terminal_ui&.set_job_mode(@job_mode, @job_manager, @notification_manager) if @terminal_ui
|
|
704
666
|
true
|
|
@@ -737,6 +699,11 @@ module Rufio
|
|
|
737
699
|
end
|
|
738
700
|
|
|
739
701
|
# ジョブがあるかどうか
|
|
702
|
+
# バックグラウンドの圧縮・展開ジョブの進捗・完了を反映する(メインループから呼ぶ)
|
|
703
|
+
def poll_archive_jobs
|
|
704
|
+
@archive_controller.poll
|
|
705
|
+
end
|
|
706
|
+
|
|
740
707
|
def has_jobs?
|
|
741
708
|
@job_manager.has_jobs?
|
|
742
709
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'script_file'
|
|
4
|
+
|
|
3
5
|
module Rufio
|
|
4
6
|
# 閲覧中ディレクトリのスクリプトファイルを検出するスキャナー
|
|
5
7
|
# カレントディレクトリ直下のスクリプトファイル(.sh, .rb, .py等)を検出し、
|
|
6
8
|
# コマンドモードから @script.sh 形式で実行可能にする
|
|
7
9
|
class LocalScriptScanner
|
|
8
10
|
# サポートするスクリプト拡張子
|
|
9
|
-
SUPPORTED_EXTENSIONS =
|
|
11
|
+
SUPPORTED_EXTENSIONS = ScriptFile::EXTENSIONS
|
|
10
12
|
|
|
11
13
|
# @param directory [String, nil] スキャン対象ディレクトリ
|
|
12
14
|
def initialize(directory = nil)
|
|
@@ -33,27 +35,14 @@ module Rufio
|
|
|
33
35
|
# @param name [String] スクリプト名(拡張子あり/なし)
|
|
34
36
|
# @return [Hash, nil] スクリプト情報 { name:, path:, dir: } または nil
|
|
35
37
|
def find_script(name)
|
|
36
|
-
|
|
37
|
-
script = available_scripts.find { |s| s[:name] == name }
|
|
38
|
-
return script if script
|
|
39
|
-
|
|
40
|
-
# 拡張子なしで検索
|
|
41
|
-
SUPPORTED_EXTENSIONS.each do |ext|
|
|
42
|
-
script = available_scripts.find { |s| s[:name] == "#{name}#{ext}" }
|
|
43
|
-
return script if script
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
nil
|
|
38
|
+
ScriptFile.find(available_scripts, name)
|
|
47
39
|
end
|
|
48
40
|
|
|
49
41
|
# Tab補完候補を取得
|
|
50
42
|
# @param prefix [String] 入力中の文字列
|
|
51
43
|
# @return [Array<String>] 補完候補のスクリプト名
|
|
52
44
|
def complete(prefix)
|
|
53
|
-
available_scripts
|
|
54
|
-
.map { |s| s[:name] }
|
|
55
|
-
.select { |name| name.start_with?(prefix) }
|
|
56
|
-
.sort
|
|
45
|
+
ScriptFile.complete(available_scripts, prefix)
|
|
57
46
|
end
|
|
58
47
|
|
|
59
48
|
private
|
|
@@ -85,8 +74,7 @@ module Rufio
|
|
|
85
74
|
# @param filename [String] ファイル名
|
|
86
75
|
# @return [Boolean]
|
|
87
76
|
def script_file?(filename)
|
|
88
|
-
|
|
89
|
-
SUPPORTED_EXTENSIONS.include?(ext)
|
|
77
|
+
ScriptFile.supported?(filename)
|
|
90
78
|
end
|
|
91
79
|
end
|
|
92
80
|
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle'
|
|
4
|
+
require_relative 'native_scanner'
|
|
5
|
+
require_relative 'native_scanner_zig'
|
|
6
|
+
|
|
7
|
+
module Rufio
|
|
8
|
+
# Zig 実装のコンテンツ検索(G mode 用)。
|
|
9
|
+
# Ruby 実装(GlobalSearch#scan_content)と同じ結果を、並列走査と SIMD 検索で返す。
|
|
10
|
+
class NativeContentSearch
|
|
11
|
+
HEADER_SIZE = 25
|
|
12
|
+
HEADER_FORMAT = 'CL<Q<L<L<L<'
|
|
13
|
+
STATE_SCANNING = 1
|
|
14
|
+
KINDS = { 1 => :match, 2 => :document, 3 => :error }.freeze
|
|
15
|
+
MAX_QUERY_BYTES = 1024
|
|
16
|
+
POLL_INTERVAL = 0.001
|
|
17
|
+
|
|
18
|
+
# 検索をまたいでファイル内容を保持するキャッシュ(mtime/inode/サイズで毎回検証する)
|
|
19
|
+
class Cache
|
|
20
|
+
DEFAULT_MAX_BYTES = 256 * 1024 * 1024
|
|
21
|
+
|
|
22
|
+
attr_reader :handle
|
|
23
|
+
|
|
24
|
+
def self.finalizer(handle)
|
|
25
|
+
proc { NativeScannerZigFFI.core_cache_destroy(handle) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(max_bytes: DEFAULT_MAX_BYTES)
|
|
29
|
+
@handle = NativeScannerZigFFI.core_cache_create(max_bytes)
|
|
30
|
+
raise StandardError, 'Failed to create search cache' if @handle.zero?
|
|
31
|
+
|
|
32
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@handle))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cached_bytes
|
|
36
|
+
@handle.zero? ? 0 : NativeScannerZigFFI.core_cache_bytes(@handle)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# このキャッシュを使う検索をすべて close してから呼ぶこと
|
|
40
|
+
def close
|
|
41
|
+
return if @handle.zero?
|
|
42
|
+
|
|
43
|
+
ObjectSpace.undefine_finalizer(self)
|
|
44
|
+
NativeScannerZigFFI.core_cache_destroy(@handle)
|
|
45
|
+
@handle = 0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.available?
|
|
50
|
+
NativeScannerZigFFI.search_available?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Zig 側は ASCII のみ大文字小文字を畳み込むため、大文字小文字を持つ非 ASCII 文字は Ruby 実装に任せる
|
|
54
|
+
def self.supports_query?(query)
|
|
55
|
+
return false if query.empty? || query.bytesize > MAX_QUERY_BYTES
|
|
56
|
+
|
|
57
|
+
query.each_char.all? { |char| char.ascii_only? || (char.upcase == char && char.downcase == char) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# マッチ周辺のバイト列から Ruby 実装と同じスニペット(30 文字前から 200 文字)を作る
|
|
61
|
+
def self.snippet(window, offset)
|
|
62
|
+
line = window.dup.force_encoding(Encoding::UTF_8).scrub
|
|
63
|
+
match_index = window.byteslice(0, offset).force_encoding(Encoding::UTF_8).scrub.length
|
|
64
|
+
line[[match_index - 30, 0].max, 200].to_s.strip
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def initialize(roots:, query:, excluded_extensions: [], document_extensions: [], cache: nil, threads: 0)
|
|
68
|
+
raise StandardError, 'Zig content search is not available' unless self.class.available?
|
|
69
|
+
|
|
70
|
+
@cache = cache
|
|
71
|
+
@path_encoding = Encoding.find('filesystem')
|
|
72
|
+
roots_buf = pack(roots)
|
|
73
|
+
query_buf = query.b
|
|
74
|
+
excluded_buf = pack(excluded_extensions)
|
|
75
|
+
documents_buf = pack(document_extensions)
|
|
76
|
+
@handle = NativeScannerZigFFI.core_search_create(
|
|
77
|
+
roots_buf, roots_buf.bytesize, query_buf, query_buf.bytesize,
|
|
78
|
+
excluded_buf, excluded_buf.bytesize, documents_buf, documents_buf.bytesize,
|
|
79
|
+
threads, cache ? cache.handle : 0
|
|
80
|
+
)
|
|
81
|
+
raise StandardError, 'Failed to start native search' if @handle.zero?
|
|
82
|
+
|
|
83
|
+
@length = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
84
|
+
@skipped = 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# [:match, root_index, path, line_number, snippet] / [:document, root_index, path] /
|
|
88
|
+
# [:error, root_index, message] / [:skipped, count] を検索終了まで順に返す
|
|
89
|
+
def each_event(&block)
|
|
90
|
+
return enum_for(:each_event) unless block_given?
|
|
91
|
+
|
|
92
|
+
each_batch { |batch| batch.each(&block) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 1 回の取り出しごとにイベント配列を返す(UI へまとめて渡すため)
|
|
96
|
+
def each_batch
|
|
97
|
+
return enum_for(:each_batch) unless block_given?
|
|
98
|
+
|
|
99
|
+
loop do
|
|
100
|
+
state = NativeScannerZigFFI.core_search_state(@handle)
|
|
101
|
+
data = drain
|
|
102
|
+
batch = []
|
|
103
|
+
each_record(data) { |event| batch << event }
|
|
104
|
+
skipped = NativeScannerZigFFI.core_search_skipped(@handle)
|
|
105
|
+
if skipped > @skipped
|
|
106
|
+
batch << [:skipped, skipped - @skipped]
|
|
107
|
+
@skipped = skipped
|
|
108
|
+
end
|
|
109
|
+
yield batch unless batch.empty?
|
|
110
|
+
break if data.empty? && state != STATE_SCANNING
|
|
111
|
+
|
|
112
|
+
sleep POLL_INTERVAL if data.empty?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def close
|
|
117
|
+
return if @handle.nil? || @handle.zero?
|
|
118
|
+
|
|
119
|
+
NativeScannerZigFFI.core_search_destroy(@handle)
|
|
120
|
+
@handle = 0
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def pack(values)
|
|
126
|
+
values.map { |value| value.to_s.b }.join("\0")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def drain
|
|
130
|
+
pointer = NativeScannerZigFFI.core_search_drain(@handle, @length)
|
|
131
|
+
length = @length[0, 8].unpack1('Q')
|
|
132
|
+
length.zero? ? ''.b : pointer.to_str(length)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def each_record(data)
|
|
136
|
+
position = 0
|
|
137
|
+
while position + HEADER_SIZE <= data.bytesize
|
|
138
|
+
kind, root, line, offset, path_length, extra_length = data.byteslice(position, HEADER_SIZE).unpack(HEADER_FORMAT)
|
|
139
|
+
position += HEADER_SIZE
|
|
140
|
+
path = data.byteslice(position, path_length).force_encoding(@path_encoding)
|
|
141
|
+
position += path_length
|
|
142
|
+
extra = data.byteslice(position, extra_length)
|
|
143
|
+
position += extra_length
|
|
144
|
+
|
|
145
|
+
case KINDS[kind]
|
|
146
|
+
when :match then yield [:match, root, path, line, self.class.snippet(extra, offset)]
|
|
147
|
+
when :document then yield [:document, root, path]
|
|
148
|
+
when :error then yield [:error, root, "#{path}: #{extra.force_encoding(Encoding::UTF_8).scrub}"]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|