yap-shell 0.6.0 → 0.7.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/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
|