yap-shell 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.travis.lock +104 -0
- data/bin/yap +6 -0
- data/bin/yap-dev +37 -0
- data/lib/yap.rb +29 -39
- data/lib/yap/addon.rb +24 -0
- data/lib/yap/addon/base.rb +52 -0
- data/lib/yap/addon/export_as.rb +12 -0
- data/lib/yap/addon/loader.rb +84 -0
- data/lib/yap/addon/path.rb +56 -0
- data/lib/yap/addon/rc_file.rb +21 -0
- data/lib/yap/addon/reference.rb +22 -0
- data/lib/yap/cli.rb +4 -0
- data/lib/yap/cli/commands.rb +6 -0
- data/lib/yap/cli/commands/addon.rb +14 -0
- data/lib/yap/cli/commands/addon/disable.rb +35 -0
- data/lib/yap/cli/commands/addon/enable.rb +35 -0
- data/lib/yap/cli/commands/addon/list.rb +37 -0
- data/lib/yap/cli/commands/addon/search.rb +99 -0
- data/lib/yap/cli/commands/generate.rb +13 -0
- data/lib/yap/cli/commands/generate/addon.rb +258 -0
- data/lib/yap/cli/commands/generate/addonrb.template +22 -0
- data/lib/yap/cli/commands/generate/gemspec.template +25 -0
- data/lib/yap/cli/commands/generate/license.template +21 -0
- data/lib/yap/cli/commands/generate/rakefile.template +6 -0
- data/lib/yap/cli/commands/generate/readme.template +40 -0
- data/lib/yap/cli/options.rb +162 -0
- data/lib/yap/cli/options/addon.rb +64 -0
- data/lib/yap/cli/options/addon/disable.rb +62 -0
- data/lib/yap/cli/options/addon/enable.rb +63 -0
- data/lib/yap/cli/options/addon/list.rb +65 -0
- data/lib/yap/cli/options/addon/search.rb +76 -0
- data/lib/yap/cli/options/generate.rb +59 -0
- data/lib/yap/cli/options/generate/addon.rb +63 -0
- data/lib/yap/configuration.rb +10 -3
- data/lib/yap/gem_helper.rb +195 -0
- data/lib/yap/gem_tasks.rb +6 -0
- data/lib/yap/shell.rb +1 -1
- data/lib/yap/shell/repl.rb +1 -1
- data/lib/yap/shell/version.rb +1 -1
- data/lib/yap/world.rb +45 -7
- data/rcfiles/yaprc +90 -10
- data/spec/features/addons/generating_an_addon_spec.rb +55 -0
- data/spec/features/addons/using_an_addon_spec.rb +182 -0
- data/spec/features/aliases_spec.rb +6 -6
- data/spec/features/grouping_spec.rb +18 -18
- data/spec/features/line_editing_spec.rb +9 -1
- data/spec/features/redirection_spec.rb +12 -3
- data/spec/spec_helper.rb +21 -11
- data/spec/support/matchers/have_printed.rb +38 -0
- data/spec/support/yap_spec_dsl.rb +24 -6
- data/yap-shell.gemspec +6 -11
- metadata +51 -45
- data/addons/history/README.md +0 -16
- data/addons/history/history.rb +0 -58
- data/addons/history_search/history_search.rb +0 -197
- data/addons/keyboard_macros/keyboard_macros.rb +0 -425
- data/addons/keyboard_macros/lib/keyboard_macros/cycle.rb +0 -38
- data/addons/prompt/Gemfile +0 -1
- data/addons/prompt/right_prompt.rb +0 -17
- data/addons/prompt_updates/prompt_updates.rb +0 -28
- data/addons/tab_completion/Gemfile +0 -0
- data/addons/tab_completion/lib/tab_completion/basic_completion.rb +0 -151
- data/addons/tab_completion/lib/tab_completion/completer.rb +0 -62
- data/addons/tab_completion/lib/tab_completion/custom_completion.rb +0 -33
- data/addons/tab_completion/lib/tab_completion/dsl_methods.rb +0 -7
- data/addons/tab_completion/tab_completion.rb +0 -174
- data/lib/tasks/addons.rake +0 -97
- data/lib/yap/world/addons.rb +0 -181
data/addons/history/README.md
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
# History Addon
|
2
|
-
|
3
|
-
The History addon is provides primitive history capabilities:
|
4
|
-
|
5
|
-
* Loads history from ~/.yap/history when yap is loaded
|
6
|
-
* Saves history to ~/.yap/history when yap exits
|
7
|
-
* Saving is an append-only operation
|
8
|
-
* ~/.yap/history is in YAML format
|
9
|
-
|
10
|
-
## Shell Functions
|
11
|
-
|
12
|
-
The history addon provides the `history` shell function that prints the contents of the history.
|
13
|
-
|
14
|
-
## Limitations
|
15
|
-
|
16
|
-
The history addon currently does not support any kind of advanced history operations.
|
data/addons/history/history.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
class History < Addon
|
2
|
-
attr_reader :file
|
3
|
-
attr_reader :position
|
4
|
-
|
5
|
-
def initialize_world(world)
|
6
|
-
return unless world.configuration.use_history?
|
7
|
-
@world = world
|
8
|
-
|
9
|
-
@file = world.configuration.path_for('history')
|
10
|
-
@position = 0
|
11
|
-
|
12
|
-
load_history
|
13
|
-
|
14
|
-
world.func(:history) do |args:, stdin:, stdout:, stderr:|
|
15
|
-
history_length = @world.editor.history.length
|
16
|
-
first_arg = args.first.to_i
|
17
|
-
size = first_arg > 0 ? first_arg + 1 : history_length
|
18
|
-
|
19
|
-
# start from -2 since we don't want to include the current history
|
20
|
-
# command being run.
|
21
|
-
stdout.puts @world.editor.history[history_length-size..-2]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def save
|
26
|
-
debug_log "saving history file=#{file.inspect}"
|
27
|
-
File.open(file, "a") do |file|
|
28
|
-
# Don't write the YAML header because we're going to append to the
|
29
|
-
# history file, not overwrite. YAML works fine without it.
|
30
|
-
unwritten_history = @world.editor.history.to_a[@position..-1]
|
31
|
-
if unwritten_history.any?
|
32
|
-
contents = unwritten_history
|
33
|
-
.each_with_object([]) { |line, arr| arr << line unless line == arr.last }
|
34
|
-
.map { |str| str.respond_to?(:without_ansi) ? str.without_ansi : str }
|
35
|
-
.to_yaml
|
36
|
-
.gsub(/^---.*?^/m, '')
|
37
|
-
file.write contents
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def load_history
|
45
|
-
debug_log "loading history file=#{file.inspect}"
|
46
|
-
at_exit { save }
|
47
|
-
|
48
|
-
if File.exists?(file) && File.readable?(file)
|
49
|
-
history = YAML.load_file(file) || []
|
50
|
-
|
51
|
-
# History starts at the end of the history loaded from file.
|
52
|
-
@position = history.length
|
53
|
-
|
54
|
-
# Rely on the builtin history for now.
|
55
|
-
@world.editor.history.replace(history)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
@@ -1,197 +0,0 @@
|
|
1
|
-
class HistorySearch < Addon
|
2
|
-
attr_reader :editor
|
3
|
-
|
4
|
-
def initialize_world(world)
|
5
|
-
@editor = world.editor
|
6
|
-
end
|
7
|
-
|
8
|
-
def prompt_user_to_search
|
9
|
-
label_text = "(reverse-search): "
|
10
|
-
search_label = ::TerminalLayout::Box.new(
|
11
|
-
content: label_text,
|
12
|
-
style: {
|
13
|
-
display: :inline,
|
14
|
-
height: 1
|
15
|
-
}
|
16
|
-
)
|
17
|
-
search_box = ::TerminalLayout::InputBox.new(
|
18
|
-
content: "",
|
19
|
-
style: {
|
20
|
-
display: :inline,
|
21
|
-
height: 1
|
22
|
-
}
|
23
|
-
)
|
24
|
-
search_box.name = "focused-input-box"
|
25
|
-
|
26
|
-
Treefell['shell'].puts "editor.content_box.children: #{editor.content_box.children.inspect}"
|
27
|
-
history_search_env = editor.new_env
|
28
|
-
history_search = Search.new(editor.history,
|
29
|
-
line: ::RawLine::LineEditor.new(::RawLine::Line.new, sync_with: -> { search_box }),
|
30
|
-
keys: history_search_env.keys,
|
31
|
-
search: -> (term:, result:){
|
32
|
-
# when there is no match, result will be nil, #to_s clears out the content
|
33
|
-
editor.input_box.content = result.to_s
|
34
|
-
},
|
35
|
-
done: -> (execute:, result:){
|
36
|
-
editor.content_box.children = []
|
37
|
-
editor.pop_env
|
38
|
-
editor.focus_input_box(editor.input_box)
|
39
|
-
editor.overwrite_line(result.to_s)
|
40
|
-
editor.process_line if execute
|
41
|
-
}
|
42
|
-
)
|
43
|
-
|
44
|
-
editor.push_env history_search_env
|
45
|
-
editor.push_keyboard_input_processor(history_search)
|
46
|
-
|
47
|
-
editor.content_box.children = [search_label, search_box]
|
48
|
-
editor.focus_input_box(search_box)
|
49
|
-
end
|
50
|
-
|
51
|
-
class Search
|
52
|
-
attr_reader :keys
|
53
|
-
|
54
|
-
def initialize(history, keys:, line:, search:, done:)
|
55
|
-
@history = history
|
56
|
-
@keys = keys
|
57
|
-
@line = line
|
58
|
-
@search_proc = search || -> {}
|
59
|
-
@done_proc = done || -> {}
|
60
|
-
|
61
|
-
initialize_key_bindings
|
62
|
-
end
|
63
|
-
|
64
|
-
def initialize_key_bindings
|
65
|
-
@keys.bind(:return){ execute }
|
66
|
-
@keys.bind(:ctrl_j){ accept }
|
67
|
-
@keys.bind(:ctrl_r){ search_again_backward }
|
68
|
-
@keys.bind(:ctrl_n){ search_again_forward }
|
69
|
-
@keys.bind(:ctrl_a){ @line.move_to_beginning_of_input }
|
70
|
-
@keys.bind(:ctrl_e){ @line.move_to_end_of_input }
|
71
|
-
@keys.bind(:backspace) do
|
72
|
-
@line.delete_left_character
|
73
|
-
perform_search(type: @last_search_was)
|
74
|
-
end
|
75
|
-
@keys.bind(:escape){ cancel }
|
76
|
-
@keys.bind(:ctrl_c){ cancel }
|
77
|
-
end
|
78
|
-
|
79
|
-
def read_bytes(bytes)
|
80
|
-
if bytes.any?
|
81
|
-
Treefell['shell'].puts "history search found bytes: #{bytes.inspect}"
|
82
|
-
|
83
|
-
search_bytes = process_bytes(bytes)
|
84
|
-
|
85
|
-
if search_bytes.any?
|
86
|
-
Treefell['shell'].puts "history searching with bytes=#{bytes.inspect}"
|
87
|
-
search_with_bytes(bytes)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
private
|
93
|
-
|
94
|
-
# given [1,2,3] first try [1,2,3]. If no key binding
|
95
|
-
# matches, then try [1,2]. If no keybinding matches try [1].
|
96
|
-
# Execution should flow in left to right, positional order.
|
97
|
-
# This returns an array of left over bytes in the order they
|
98
|
-
# appeared that did not match any keybinding
|
99
|
-
def process_bytes bytes, leftover=[]
|
100
|
-
if bytes.empty?
|
101
|
-
leftover
|
102
|
-
elsif @keys[bytes]
|
103
|
-
@keys[bytes].call
|
104
|
-
leftover
|
105
|
-
else
|
106
|
-
process_bytes(
|
107
|
-
bytes[0..-2],
|
108
|
-
[bytes[-1]].concat(leftover)
|
109
|
-
)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def accept
|
114
|
-
@done_proc.call(execute: false, result: result)
|
115
|
-
end
|
116
|
-
|
117
|
-
def cancel
|
118
|
-
@done_proc.call(execute: false, result: nil)
|
119
|
-
end
|
120
|
-
|
121
|
-
def execute
|
122
|
-
@done_proc.call(execute: true, result: result)
|
123
|
-
end
|
124
|
-
|
125
|
-
def found_match(result:)
|
126
|
-
@search_proc.call(term: @line.text, result: result)
|
127
|
-
end
|
128
|
-
|
129
|
-
def no_match_found
|
130
|
-
@last_match_index = nil
|
131
|
-
@search_proc.call(term: @line.text, result: result)
|
132
|
-
end
|
133
|
-
|
134
|
-
def result
|
135
|
-
@history2search[@last_match_index] if @last_match_index
|
136
|
-
end
|
137
|
-
|
138
|
-
def search_again_backward
|
139
|
-
Treefell['shell'].puts "history searching again backward"
|
140
|
-
if @last_search_was == :forward
|
141
|
-
@last_match_index = @history.length - @last_match_index - 1
|
142
|
-
@history2search = @history.reverse
|
143
|
-
end
|
144
|
-
perform_search(starting_index: @last_match_index, type: :backward)
|
145
|
-
end
|
146
|
-
|
147
|
-
def search_again_forward
|
148
|
-
Treefell['shell'].puts "history searching again forward"
|
149
|
-
if @last_search_was == :backward
|
150
|
-
@last_match_index = @history.length - @last_match_index - 1
|
151
|
-
@history2search = @history
|
152
|
-
end
|
153
|
-
perform_search(starting_index: @last_match_index, type: :forward)
|
154
|
-
end
|
155
|
-
|
156
|
-
def search_with_bytes(bytes)
|
157
|
-
part = bytes.map(&:chr).join
|
158
|
-
@line.write(part.scan(/[[:print:]]/).join)
|
159
|
-
@history2search = @history.reverse
|
160
|
-
perform_search(type: :backward)
|
161
|
-
end
|
162
|
-
|
163
|
-
def perform_search(starting_index: -1, type:)
|
164
|
-
if @line.text.empty?
|
165
|
-
no_match_found
|
166
|
-
return
|
167
|
-
end
|
168
|
-
|
169
|
-
# fuzzy search
|
170
|
-
characters = @line.text.split('').map { |ch| Regexp.escape(ch) }
|
171
|
-
fuzzy_search_regex = /#{characters.join('.*?')}/
|
172
|
-
|
173
|
-
# non-fuzzy-search
|
174
|
-
# fuzzy_search_regex = /#{Regexp.escape(@line.text)}/
|
175
|
-
|
176
|
-
Treefell['shell'].puts "history search matching on regex=#{fuzzy_search_regex.inspect} starting_index=#{starting_index}"
|
177
|
-
@last_search_was = type
|
178
|
-
|
179
|
-
match = @history2search.detect.with_index do |item, i|
|
180
|
-
next if i <= starting_index
|
181
|
-
|
182
|
-
# Treefell['shell'].puts "history search matching #{(item + @line.text).inspect} =~ #{fuzzy_search_regex}"
|
183
|
-
md = (item + @line.text).match(fuzzy_search_regex)
|
184
|
-
if md && md.end(0) <= item.length
|
185
|
-
# Treefell['shell'].puts "history search match #{item} at #{i}"
|
186
|
-
@last_match_index = i
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
if match
|
191
|
-
found_match(result: match)
|
192
|
-
else
|
193
|
-
no_match_found
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
@@ -1,425 +0,0 @@
|
|
1
|
-
class KeyboardMacros < Addon
|
2
|
-
require 'keyboard_macros/cycle'
|
3
|
-
|
4
|
-
module PrettyPrintKey
|
5
|
-
# ppk means "pretty print key". For example, it returns \C-g if the given
|
6
|
-
# byte is 7.
|
7
|
-
def ppk(byte)
|
8
|
-
if byte && byte.ord <= 26
|
9
|
-
'\C-' + ('a'..'z').to_a[byte.ord - 1]
|
10
|
-
else
|
11
|
-
byte.inspect
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
include PrettyPrintKey
|
17
|
-
|
18
|
-
DEFAULT_TRIGGER_KEY = :ctrl_g
|
19
|
-
DEFAULT_CANCEL_KEY = " "
|
20
|
-
DEFAULT_TIMEOUT_IN_MS = 500
|
21
|
-
|
22
|
-
def self.load_addon
|
23
|
-
@instance ||= new
|
24
|
-
end
|
25
|
-
|
26
|
-
attr_reader :world
|
27
|
-
attr_accessor :timeout_in_ms
|
28
|
-
attr_accessor :cancel_key, :trigger_key
|
29
|
-
attr_accessor :cancel_on_unknown_sequences
|
30
|
-
|
31
|
-
def initialize_world(world)
|
32
|
-
@world = world
|
33
|
-
@configurations = []
|
34
|
-
@stack = []
|
35
|
-
@timeout_in_ms = DEFAULT_TIMEOUT_IN_MS
|
36
|
-
@cancel_key = DEFAULT_CANCEL_KEY
|
37
|
-
@trigger_key = DEFAULT_TRIGGER_KEY
|
38
|
-
@cancel_on_unknown_sequences = false
|
39
|
-
end
|
40
|
-
|
41
|
-
def cancel_key=(key)
|
42
|
-
debug_log "setting default cancel_key key=#{ppk(key)}"
|
43
|
-
@cancel_key = key
|
44
|
-
end
|
45
|
-
|
46
|
-
def cancel_on_unknown_sequences=(true_or_false)
|
47
|
-
debug_log "setting default cancel_on_unknown_sequences=#{true_or_false}"
|
48
|
-
@cancel_on_unknown_sequences = true_or_false
|
49
|
-
end
|
50
|
-
|
51
|
-
def timeout_in_ms=(milliseconds)
|
52
|
-
debug_log "setting default timeout_in_ms milliseconds=#{milliseconds.inspect}"
|
53
|
-
@timeout_in_ms = milliseconds
|
54
|
-
end
|
55
|
-
|
56
|
-
def trigger_key=(key)
|
57
|
-
debug_log "setting default trigger_key key=#{ppk(key)}"
|
58
|
-
@trigger_key = key
|
59
|
-
end
|
60
|
-
|
61
|
-
def configure(cancel_key: nil, trigger_key: nil, &blk)
|
62
|
-
debug_log "configure cancel_key=#{ppk(cancel_key)} trigger_key=#{ppk(trigger_key)} block_given?=#{block_given?}"
|
63
|
-
|
64
|
-
cancel_key ||= @cancel_key
|
65
|
-
trigger_key ||= @trigger_key
|
66
|
-
|
67
|
-
cancel_blk = lambda do
|
68
|
-
world.editor.event_loop.clear @event_id
|
69
|
-
cancel_processing
|
70
|
-
nil
|
71
|
-
end
|
72
|
-
|
73
|
-
configuration = Configuration.new(
|
74
|
-
keymap: world.editor.terminal.keys,
|
75
|
-
trigger_key: trigger_key,
|
76
|
-
cancellation: Cancellation.new(cancel_key: cancel_key, &cancel_blk),
|
77
|
-
editor: world.editor,
|
78
|
-
)
|
79
|
-
|
80
|
-
blk.call configuration if blk
|
81
|
-
|
82
|
-
world.unbind(trigger_key)
|
83
|
-
world.bind(trigger_key) do
|
84
|
-
debug_log "macro triggered key=#{ppk(trigger_key)}"
|
85
|
-
|
86
|
-
begin
|
87
|
-
@previous_result = nil
|
88
|
-
@stack << OpenStruct.new(configuration: configuration)
|
89
|
-
configuration.start.call if configuration.start
|
90
|
-
|
91
|
-
debug_log "taking over keyboard input processing from editor"
|
92
|
-
world.editor.push_keyboard_input_processor(self)
|
93
|
-
|
94
|
-
wait_timeout_in_seconds = 0.1
|
95
|
-
world.editor.input.wait_timeout_in_seconds = wait_timeout_in_seconds
|
96
|
-
ensure
|
97
|
-
queue_up_remove_input_processor(&configuration.stop)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
@configurations << configuration
|
102
|
-
end
|
103
|
-
|
104
|
-
def cycle(name, &cycle_thru_blk)
|
105
|
-
debug_log "defining cycle name=#{name.inspect}"
|
106
|
-
|
107
|
-
@cycles ||= {}
|
108
|
-
if block_given?
|
109
|
-
cycle = KeyboardMacros::Cycle.new(
|
110
|
-
cycle_proc: cycle_thru_blk,
|
111
|
-
on_cycle_proc: -> (old_value, new_value) {
|
112
|
-
@world.editor.delete_n_characters(old_value.to_s.length)
|
113
|
-
process_result(new_value)
|
114
|
-
}
|
115
|
-
)
|
116
|
-
@cycles[name] = cycle
|
117
|
-
else
|
118
|
-
@cycles.fetch(name)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
#
|
123
|
-
# InputProcessor Methods
|
124
|
-
#
|
125
|
-
|
126
|
-
def read_bytes(bytes)
|
127
|
-
if @stack.last
|
128
|
-
current_definition = @stack.last
|
129
|
-
configuration = current_definition.configuration
|
130
|
-
end
|
131
|
-
|
132
|
-
bytes.each_with_index do |byte, i|
|
133
|
-
definition = configuration[byte]
|
134
|
-
if !definition
|
135
|
-
cancel_processing if cancel_on_unknown_sequences
|
136
|
-
break
|
137
|
-
end
|
138
|
-
|
139
|
-
configuration = definition.configuration
|
140
|
-
configuration.start.call if configuration.start
|
141
|
-
@stack << definition
|
142
|
-
|
143
|
-
result = definition.process
|
144
|
-
|
145
|
-
if result =~ /\n$/
|
146
|
-
world.editor.write result.chomp, add_to_line_history: false
|
147
|
-
world.editor.event_loop.clear @event_id if @event_id
|
148
|
-
cancel_processing
|
149
|
-
world.editor.newline # add_to_history
|
150
|
-
world.editor.process_line
|
151
|
-
break
|
152
|
-
end
|
153
|
-
|
154
|
-
if i == bytes.length - 1
|
155
|
-
while @stack.last && @stack.last.fragment?
|
156
|
-
@stack.pop
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
if @event_id
|
161
|
-
world.editor.event_loop.clear @event_id
|
162
|
-
@event_id = queue_up_remove_input_processor
|
163
|
-
end
|
164
|
-
|
165
|
-
process_result(result)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
private
|
170
|
-
|
171
|
-
def process_result(result)
|
172
|
-
if result.is_a?(String)
|
173
|
-
@world.editor.write result, add_to_line_history: false
|
174
|
-
@previous_result = result
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def queue_up_remove_input_processor(&blk)
|
179
|
-
return unless @timeout_in_ms
|
180
|
-
|
181
|
-
event_args = {
|
182
|
-
name: 'remove_input_processor',
|
183
|
-
source: self,
|
184
|
-
interval_in_ms: @timeout_in_ms,
|
185
|
-
}
|
186
|
-
@event_id = world.editor.event_loop.once(event_args) do
|
187
|
-
cancel_processing
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def cancel_processing
|
192
|
-
debug_log "cancel_processing"
|
193
|
-
@event_id = nil
|
194
|
-
@stack.reverse.each do |definition|
|
195
|
-
definition.configuration.stop.call if definition.configuration.stop
|
196
|
-
end
|
197
|
-
@stack.clear
|
198
|
-
if world.editor.keyboard_input_processor == self
|
199
|
-
debug_log "giving keyboard input processing control back"
|
200
|
-
world.editor.pop_keyboard_input_processor
|
201
|
-
|
202
|
-
debug_log "restoring default editor input timeout"
|
203
|
-
world.editor.input.restore_default_timeout
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
class Cancellation
|
208
|
-
attr_reader :cancel_key
|
209
|
-
|
210
|
-
def initialize(cancel_key: , &blk)
|
211
|
-
@cancel_key = cancel_key
|
212
|
-
@blk = blk
|
213
|
-
end
|
214
|
-
|
215
|
-
def call
|
216
|
-
@blk.call
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
class Configuration
|
221
|
-
include PrettyPrintKey
|
222
|
-
|
223
|
-
attr_reader :cancellation, :trigger_key, :keymap
|
224
|
-
|
225
|
-
def debug_log(*args)
|
226
|
-
KeyboardMacros.debug_log(*args)
|
227
|
-
end
|
228
|
-
|
229
|
-
def initialize(cancellation: nil, editor:, keymap: {}, trigger_key: nil)
|
230
|
-
@cancellation = cancellation
|
231
|
-
@editor = editor
|
232
|
-
@keymap = keymap
|
233
|
-
@trigger_key = trigger_key
|
234
|
-
@storage = {}
|
235
|
-
@on_start_blk = nil
|
236
|
-
@on_stop_blk = nil
|
237
|
-
@cycles = {}
|
238
|
-
|
239
|
-
debug_log "configuring a macro trigger_key=#{ppk(trigger_key)}"
|
240
|
-
|
241
|
-
if @cancellation
|
242
|
-
define @cancellation.cancel_key, -> { @cancellation.call }
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
def start(&blk)
|
247
|
-
@on_start_blk = blk if blk
|
248
|
-
@on_start_blk
|
249
|
-
end
|
250
|
-
|
251
|
-
def stop(&blk)
|
252
|
-
@on_stop_blk = blk if blk
|
253
|
-
@on_stop_blk
|
254
|
-
end
|
255
|
-
|
256
|
-
def cycle(name, &cycle_thru_blk)
|
257
|
-
debug_log "defining a cycle on macro name=#{name.inspect}"
|
258
|
-
|
259
|
-
if block_given?
|
260
|
-
cycle = KeyboardMacros::Cycle.new(
|
261
|
-
cycle_proc: cycle_thru_blk,
|
262
|
-
on_cycle_proc: -> (old_value, new_value) {
|
263
|
-
@editor.delete_n_characters(old_value.to_s.length)
|
264
|
-
}
|
265
|
-
)
|
266
|
-
@cycles[name] = cycle
|
267
|
-
else
|
268
|
-
@cycles.fetch(name)
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def fragment(sequence, result)
|
273
|
-
define(sequence, result, fragment: true)
|
274
|
-
end
|
275
|
-
|
276
|
-
def define(sequence, result=nil, fragment: false, &blk)
|
277
|
-
debug_log "defining macro sequence=#{sequence.inspect} result=#{result.inspect} fragment=#{fragment.inspect} under macro #{ppk(trigger_key)}"
|
278
|
-
unless result.respond_to?(:call)
|
279
|
-
string_result = result
|
280
|
-
result = -> { string_result }
|
281
|
-
end
|
282
|
-
|
283
|
-
case sequence
|
284
|
-
when String
|
285
|
-
recursively_define_sequence_for_bytes(
|
286
|
-
self,
|
287
|
-
sequence.bytes,
|
288
|
-
result,
|
289
|
-
fragment: fragment,
|
290
|
-
&blk
|
291
|
-
)
|
292
|
-
when Symbol
|
293
|
-
recursively_define_sequence_for_bytes(
|
294
|
-
self,
|
295
|
-
@keymap.fetch(sequence){
|
296
|
-
fail "Cannot bind unknown sequence #{sequence.inspect}"
|
297
|
-
},
|
298
|
-
result,
|
299
|
-
fragment: fragment,
|
300
|
-
&blk
|
301
|
-
)
|
302
|
-
when Regexp
|
303
|
-
define_sequence_for_regex(sequence, result, fragment: fragment, &blk)
|
304
|
-
else
|
305
|
-
raise NotImplementedError, <<-EOT.gsub(/^\s*/, '')
|
306
|
-
Don't know how to define macro for sequence: #{sequence.inspect}
|
307
|
-
EOT
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
def [](byte)
|
312
|
-
@storage.values.detect { |definition| definition.matches?(byte) }
|
313
|
-
end
|
314
|
-
|
315
|
-
def []=(key, definition)
|
316
|
-
@storage[key] = definition
|
317
|
-
end
|
318
|
-
|
319
|
-
def inspect
|
320
|
-
str = @storage.map{ |k,v| "#{k}=#{v.inspect}" }.join("\n ")
|
321
|
-
num_items = @storage.reduce(0) { |s, arr| s + arr.length }
|
322
|
-
"<Configuration num_items=#{num_items} stored_keys=#{str}>"
|
323
|
-
end
|
324
|
-
|
325
|
-
private
|
326
|
-
|
327
|
-
def define_sequence_for_regex(regex, result, fragment: false, &blk)
|
328
|
-
@storage[regex] = Definition.new(
|
329
|
-
configuration: Configuration.new(
|
330
|
-
cancellation: @cancellation,
|
331
|
-
keymap: @keymap,
|
332
|
-
editor: @editor
|
333
|
-
),
|
334
|
-
fragment: fragment,
|
335
|
-
sequence: regex,
|
336
|
-
result: result,
|
337
|
-
&blk
|
338
|
-
)
|
339
|
-
end
|
340
|
-
|
341
|
-
def recursively_define_sequence_for_bytes(configuration, bytes, result, fragment: false, &blk)
|
342
|
-
byte, rest = bytes[0], bytes[1..-1]
|
343
|
-
if rest.any?
|
344
|
-
definition = if configuration[byte]
|
345
|
-
configuration[byte]
|
346
|
-
else
|
347
|
-
Definition.new(
|
348
|
-
configuration: Configuration.new(
|
349
|
-
cancellation: @cancellation,
|
350
|
-
keymap: @keymap,
|
351
|
-
editor: @editor
|
352
|
-
),
|
353
|
-
fragment: fragment,
|
354
|
-
sequence: byte,
|
355
|
-
result: nil
|
356
|
-
)
|
357
|
-
end
|
358
|
-
blk.call(definition.configuration) if blk
|
359
|
-
configuration[byte] = definition
|
360
|
-
recursively_define_sequence_for_bytes(
|
361
|
-
definition.configuration,
|
362
|
-
rest,
|
363
|
-
result,
|
364
|
-
fragment: fragment,
|
365
|
-
&blk
|
366
|
-
)
|
367
|
-
else
|
368
|
-
definition = Definition.new(
|
369
|
-
configuration: Configuration.new(
|
370
|
-
keymap: @keymap,
|
371
|
-
editor: @editor
|
372
|
-
),
|
373
|
-
fragment: fragment,
|
374
|
-
sequence: byte,
|
375
|
-
result: result
|
376
|
-
)
|
377
|
-
configuration[byte] = definition
|
378
|
-
blk.call(definition.configuration) if blk
|
379
|
-
definition
|
380
|
-
end
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
class Definition
|
385
|
-
attr_reader :configuration, :result, :sequence
|
386
|
-
|
387
|
-
def initialize(configuration: nil, fragment: false, sequence:, result: nil)
|
388
|
-
@fragment = fragment
|
389
|
-
@configuration = configuration
|
390
|
-
@sequence = sequence
|
391
|
-
@result = result
|
392
|
-
end
|
393
|
-
|
394
|
-
def inspect
|
395
|
-
"<Definition fragment=#{@fragment.inspect} sequence=#{@sequence.inspect} result=#{@result.inspect} configuration=#{@configuration.inspect}>"
|
396
|
-
end
|
397
|
-
|
398
|
-
def fragment?
|
399
|
-
@fragment
|
400
|
-
end
|
401
|
-
|
402
|
-
def matches?(byte)
|
403
|
-
if @sequence.is_a?(Regexp)
|
404
|
-
@match_data = @sequence.match(byte.chr)
|
405
|
-
else
|
406
|
-
@sequence == byte
|
407
|
-
end
|
408
|
-
end
|
409
|
-
|
410
|
-
def process
|
411
|
-
if @result
|
412
|
-
if @match_data
|
413
|
-
if @match_data.captures.empty?
|
414
|
-
@result.call(@match_data[0])
|
415
|
-
else
|
416
|
-
@result.call(*@match_data.captures)
|
417
|
-
end
|
418
|
-
else
|
419
|
-
@result.call
|
420
|
-
end
|
421
|
-
end
|
422
|
-
end
|
423
|
-
end
|
424
|
-
|
425
|
-
end
|