working_set 1.0.1

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,2 @@
1
+ module SetBuilderAdapter
2
+ end
@@ -0,0 +1,132 @@
1
+ require 'open3'
2
+
3
+ module SetBuilderAdapter
4
+ class Ag
5
+
6
+ class ParserError < StandardError; end
7
+
8
+ class Parser
9
+ attr_accessor :input, :parsed_items, :current_item
10
+
11
+ def initialize(input)
12
+ self.input = input
13
+ self.parsed_items = []
14
+ end
15
+
16
+ def parse
17
+ input.split("\n").each do |line|
18
+ parse_line line
19
+ end
20
+ record_existing_item
21
+ parsed_items
22
+ end
23
+
24
+ def learn(key, value)
25
+ self.current_item[key] = value
26
+ end
27
+
28
+ def add(list_key, value)
29
+ self.current_item[list_key] ||= []
30
+ self.current_item[list_key].push value
31
+ end
32
+
33
+ def record_existing_item
34
+ if current_item
35
+ parsed_items << current_item
36
+ end
37
+ end
38
+
39
+ def new_item_with_file_path(file_path)
40
+ record_existing_item
41
+ self.current_item = { }
42
+ learn :file_path, file_path
43
+ end
44
+
45
+ def parse_line(line)
46
+ # A new item is started when the current item has no path, or if the
47
+ # path for the current result line doesn't match the current item's file
48
+ # path.
49
+ if current_item == nil or (line != "--" and line != "" and not line.start_with?(current_item[:file_path]))
50
+ line =~ /^(.*?):\d/
51
+ new_item_with_file_path $1
52
+ end
53
+
54
+ # The line can be either a pre or post match for the current item
55
+ if line =~ /^(.*?):(\d+)-(.*)/
56
+ if current_item[:match_line]
57
+ add :post_match_lines, $3
58
+ else
59
+ add :pre_match_lines, $3
60
+ end
61
+
62
+ # The line can be the actual match itself
63
+ elsif line =~ /^(.*?):(\d+):(\d+):(.*)/ # match line
64
+ if current_item[:match_line]
65
+ new_item_with_file_path current_item[:file_path]
66
+ end
67
+ learn :row, $2
68
+ learn :column, $3
69
+ learn :match_line, $4
70
+
71
+ # Finally, the item can be the inter-file match separator
72
+ elsif line =~ /--/
73
+ new_item_with_file_path current_item[:file_path]
74
+
75
+ # Weird exception: a blank line will be ignored.
76
+ elsif line == ""
77
+
78
+ # Otherwise big fat fail.
79
+ else
80
+ debug_message "Parse failed for:"
81
+ debug_message line.inspect
82
+ raise ParserError.new("parse_line failed for: #{line.inspect}")
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ def command_bin
89
+ "ag"
90
+ end
91
+
92
+ def command_options(options)
93
+ %W(--search-files -C#{$CONTEXT_LINES} --numbers --column --nogroup --literal) + map_external_options(options)
94
+ end
95
+
96
+ def map_external_options(options)
97
+ [].tap do |ary|
98
+ ary << "--word-regexp" if options["whole_word"]
99
+ end
100
+ end
101
+
102
+ def command_parts(search_term, options)
103
+ [ command_bin, *command_options(options), "--", search_term ]
104
+ end
105
+
106
+ def parse_results(results)
107
+ debug_message "Ag Results:\n#{results}"
108
+ Parser.new(results).parse
109
+ rescue ParserError => e
110
+ STDERR.puts e
111
+ raise e
112
+ end
113
+
114
+ def build_working_set(search, options)
115
+ debug_message "search command: #{command_parts(search, options)}"
116
+
117
+ stdout, stderr, status = Open3.capture3(*command_parts(search, options))
118
+
119
+ # Ag exits 0 when results found
120
+ # Ag exits 1 when zero results found
121
+ # ... It also exits 1 when there's a problem with options.
122
+ if status.exitstatus == 0 || status.exitstatus == 1
123
+ WorkingSet.new search, options, parse_results(stdout)
124
+ else
125
+ raise "ag command failed: #{stdout} #{stderr}"
126
+ # raise "ag command failed with status #{$?.exitstatus.inspect}: #{stdout}"
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,146 @@
1
+ class SetViewerActor
2
+ include BasicActor
3
+
4
+ def initialize
5
+ subscribe "tell_selected_item", :tell_selected_item
6
+ subscribe "copy_selected_item", :copy_selected_item
7
+ subscribe "tell_selected_item_content", :tell_selected_item_content
8
+ subscribe "set_build_finished", :refresh_view
9
+ subscribe "set_build_failed", :show_error
10
+ subscribe "scroll_changed", :scroll
11
+ subscribe "context_lines_changed", :update_context_lines
12
+ subscribe "refresh", :refresh
13
+ subscribe "show_match_lines_toggled", :toggle_match_lines
14
+ subscribe "select_next_file", :select_next_file
15
+ subscribe "select_prev_file", :select_prev_file
16
+ subscribe "select_next_item", :select_next_item
17
+ subscribe "select_prev_item", :select_prev_item
18
+ subscribe "welcome_user", :welcome_user
19
+ subscribe "display_help", :display_help
20
+ subscribe "display_working_set", :display_working_set
21
+ welcome_user
22
+ end
23
+
24
+ def welcome_user(_=nil)
25
+ debug_message "displaying welcome_user!"
26
+ View::WelcomeUser.render
27
+ end
28
+
29
+ def display_help(_)
30
+ debug_message "displaying help!"
31
+ View::Help.render
32
+ end
33
+
34
+ def display_working_set(_)
35
+ debug_message "displaying working_set!"
36
+ @working_set_view&.render
37
+ end
38
+
39
+ def refresh_view(_, working_set)
40
+ prev_wsv = @working_set_view
41
+ @working_set_view = View::WorkingSet.new(working_set)
42
+ if prev_wsv&.working_set&.search == working_set.search
43
+ @working_set_view.restore_selection_state(prev_wsv)
44
+ end
45
+ @working_set_view.render
46
+ end
47
+
48
+ def items_present?
49
+ (@working_set_view&.working_set&.items&.size || 0) > 0
50
+ end
51
+
52
+ def scroll(_, delta)
53
+ return unless items_present?
54
+ @working_set_view.scroll(delta)
55
+ end
56
+
57
+ def update_context_lines(_, delta)
58
+ $CONTEXT_LINES += delta
59
+ $CONTEXT_LINES = 0 if $CONTEXT_LINES < 0
60
+ debug_message "context lines set to #{$CONTEXT_LINES}"
61
+ refresh
62
+ end
63
+
64
+ def refresh(_=nil)
65
+ return unless @working_set_view
66
+ # triggers search again without changing search term
67
+ ws = @working_set_view.working_set
68
+
69
+ publish "search_changed", ws.search, ws.options
70
+ end
71
+
72
+ def toggle_match_lines(_)
73
+ return unless items_present?
74
+ @working_set_view.toggle_match_lines
75
+ end
76
+
77
+ def select_next_file(_)
78
+ return unless items_present?
79
+ @working_set_view.select_next_file
80
+ unless @working_set_view.selected_item_in_view?
81
+ publish "scroll_changed", @working_set_view.selected_item_scroll_delta
82
+ end
83
+ end
84
+
85
+ def select_prev_file(_)
86
+ return unless items_present?
87
+ @working_set_view.select_prev_file
88
+ unless @working_set_view.selected_item_in_view?
89
+ publish "scroll_changed", @working_set_view.selected_item_scroll_delta
90
+ end
91
+ end
92
+
93
+ def select_next_item(_)
94
+ return unless items_present?
95
+ @working_set_view.select_next_item
96
+ unless @working_set_view.selected_item_in_view?
97
+ publish "scroll_changed", @working_set_view.selected_item_scroll_delta
98
+ end
99
+ end
100
+
101
+ def select_prev_item(_)
102
+ return unless items_present?
103
+ @working_set_view.select_prev_item
104
+ unless @working_set_view.selected_item_in_view?
105
+ publish "scroll_changed", @working_set_view.selected_item_scroll_delta
106
+ end
107
+ end
108
+
109
+ def tell_selected_item(_)
110
+ if items_present?
111
+ item = @working_set_view.selected_item
112
+ publish "respond_client", "selected_item", {
113
+ file_path: item.file_path,
114
+ row: item.row,
115
+ column: item.column
116
+ }
117
+ end
118
+ end
119
+
120
+ def tell_selected_item_content(_)
121
+ if items_present?
122
+ item = @working_set_view.selected_item
123
+ publish "respond_client", "selected_item_content", {
124
+ data: item.match_line
125
+ }
126
+ end
127
+ end
128
+
129
+ def copy_selected_item(_, include_context=false)
130
+ if items_present?
131
+ item = @working_set_view.selected_item
132
+ if include_context
133
+ Clipboard.copy item.full_body
134
+ else
135
+ Clipboard.copy item.match_line
136
+ end
137
+ end
138
+ end
139
+
140
+ def show_error(_, error)
141
+ debug_message error
142
+ # Ncurses.stdscr.mvaddstr 0, 0, "SetViewerActor#show_error: #{error.backtrace}"
143
+ # Ncurses.stdscr.refresh
144
+ end
145
+
146
+ end
@@ -0,0 +1,177 @@
1
+ class UserInputActor
2
+ include BasicActor
3
+
4
+ finalizer :clean_up
5
+
6
+ DEFAULT_SCROLL_STEP = 5
7
+
8
+ def self.user_input_mode
9
+ @user_input_mode
10
+ end
11
+
12
+ def self.prev_user_input_modes
13
+ @prev_user_input_modes ||= []
14
+ end
15
+
16
+ def self.set_user_input_mode(mode)
17
+ debug_message "set input mode: #{mode.inspect}"
18
+ push_mode(mode)
19
+ end
20
+
21
+ def self.push_mode(mode)
22
+ prev_user_input_modes << @user_input_mode if @user_input_mode
23
+ @user_input_mode = mode
24
+ end
25
+
26
+ def self.pop_mode
27
+ @user_input_mode = prev_user_input_modes.pop
28
+ end
29
+
30
+ def user_input_mode
31
+ self.class.user_input_mode
32
+ end
33
+
34
+ def pop_mode
35
+ self.class.pop_mode
36
+ end
37
+
38
+ def initialize
39
+ async.watch_input
40
+ end
41
+
42
+ def watch_input
43
+
44
+ # Creating this otherwise unused window so that I can run getch() without
45
+ # the implicit call to stdscr.refresh that it apparently precipitates.
46
+ trash_win = Ncurses.newwin(1, 1, 0, 0)
47
+ trash_win.keypad(true)
48
+
49
+ catch(:shutdown) do
50
+ while(ch = trash_win.getch)
51
+ debug_message "getch: #{ch}"
52
+ handle_modal_input(ch)
53
+ end
54
+ end
55
+ debug_message "Caught :shutdown"
56
+ $supervisor.do_shutdown
57
+ end
58
+
59
+ def handle_modal_input(ch)
60
+ case user_input_mode
61
+ when :welcome_user then handle_welcome_user_input(ch)
62
+ when :help then handle_help_input(ch)
63
+ when :working_set then handle_working_set_input(ch)
64
+ else
65
+ debug_message "Uncrecognized mode: #{user_input_mode.inspect}"
66
+ throw :shutdown
67
+ end
68
+ end
69
+
70
+ def handle_help_input(ch)
71
+ case ch
72
+ when ?q.ord
73
+ mode = pop_mode
74
+ case mode
75
+ when :welcome_user then publish "welcome_user"
76
+ when :working_set then publish "display_working_set"
77
+ else
78
+ debug_message "Unrecognized mode from pop: #{mode.inspect}"
79
+ throw :shutdown
80
+ end
81
+ else
82
+ debug_message "Unhandled user input: #{ch}"
83
+ end
84
+ end
85
+
86
+ def handle_welcome_user_input(ch)
87
+ case ch
88
+ when ?q.ord
89
+ throw :shutdown
90
+ when ??.ord
91
+ publish "display_help"
92
+ else
93
+ debug_message "Unhandled user input: #{ch}"
94
+ end
95
+ end
96
+
97
+ USER_INPUT_MAPPINGS = {
98
+ "?" => {
99
+ desc: "display help",
100
+ action: -> { publish "display_help" }
101
+ },
102
+ "q" => {
103
+ desc: "quit",
104
+ action: -> { throw :shutdown }
105
+ },
106
+ "j" => {
107
+ desc: "select next match",
108
+ action: -> { publish "select_next_item" }
109
+ },
110
+ "k" => {
111
+ desc: "select previous match",
112
+ action: -> { publish "select_prev_item" }
113
+ },
114
+ 14 => {
115
+ key_desc: "ctrl-n",
116
+ desc: "select first match in next file",
117
+ action: -> { publish "select_next_file" }
118
+ },
119
+ 16 => {
120
+ key_desc: "ctrl-p",
121
+ desc: "select first match in previous file",
122
+ action: -> { publish "select_prev_file" }
123
+ },
124
+ 13 => {
125
+ key_desc: "enter",
126
+ desc: "Tell editor to jump to match",
127
+ action: -> { publish "tell_selected_item" }
128
+ },
129
+ Ncurses::KEY_DOWN => {
130
+ key_desc: "down arrow",
131
+ desc: "scroll down without changing selection",
132
+ action: -> { publish "scroll_changed", DEFAULT_SCROLL_STEP }
133
+ },
134
+ Ncurses::KEY_UP => {
135
+ key_desc: "up arrow",
136
+ desc: "scroll up without changing selection",
137
+ action: -> { publish "scroll_changed", DEFAULT_SCROLL_STEP * -1 }
138
+ },
139
+ "r" => {
140
+ desc: "refresh search results",
141
+ action: -> { publish "refresh" }
142
+ },
143
+ "[" => {
144
+ desc: "decrease context lines",
145
+ action: -> { publish "context_lines_changed", -1 }
146
+ },
147
+ "]" => {
148
+ desc: "increase context lines",
149
+ action: -> { publish "context_lines_changed", 1 }
150
+ },
151
+ "z" => {
152
+ desc: "toggle showing match lines vs just matched files",
153
+ action: -> { publish "show_match_lines_toggled" }
154
+ },
155
+ "y" => {
156
+ desc: "copy selected match to system clipboard",
157
+ action: -> { publish "copy_selected_item" }
158
+ },
159
+ "Y" => {
160
+ desc: "copy selected match + context to system clipboard",
161
+ action: -> { publish "copy_selected_item", true }
162
+ },
163
+ }
164
+
165
+ def handle_working_set_input(ch)
166
+ mapping = USER_INPUT_MAPPINGS[ch] || USER_INPUT_MAPPINGS[ch.chr]
167
+ if mapping
168
+ instance_exec(&mapping[:action])
169
+ end
170
+ rescue RangeError # ignore when .chr is out of range. Just means it's not input we care about anyways.
171
+ end
172
+
173
+ def clean_up
174
+ debug_message "done user input"
175
+ end
176
+
177
+ end