working_set 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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