vimamsa 0.1.20 → 0.1.22
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/README.md +28 -7
- data/install.sh +4 -0
- data/lib/vimamsa/buffer.rb +26 -1
- data/lib/vimamsa/buffer_changetext.rb +2 -0
- data/lib/vimamsa/buffer_cursor.rb +1 -1
- data/lib/vimamsa/buffer_list.rb +2 -2
- data/lib/vimamsa/editor.rb +8 -20
- data/lib/vimamsa/file_finder.rb +12 -3
- data/lib/vimamsa/file_manager.rb +10 -3
- data/lib/vimamsa/gui.rb +31 -6
- data/lib/vimamsa/gui_sourceview.rb +40 -58
- data/lib/vimamsa/key_actions.rb +29 -4
- data/lib/vimamsa/key_binding_tree.rb +165 -28
- data/lib/vimamsa/key_bindings_vimlike.rb +148 -129
- data/lib/vimamsa/macro.rb +3 -1
- data/lib/vimamsa/tests.rb +81 -2
- data/lib/vimamsa/text_transforms.rb +2 -0
- data/lib/vimamsa/util.rb +44 -2
- data/lib/vimamsa/version.rb +1 -1
- data/vimamsa.gemspec +1 -1
- metadata +7 -6
data/lib/vimamsa/key_actions.rb
CHANGED
@@ -19,12 +19,12 @@ def jump_to_next_edit
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def is_command_mode()
|
22
|
-
return true if
|
22
|
+
return true if vma.kbd.mode_root_state.to_s() == "C"
|
23
23
|
return false
|
24
24
|
end
|
25
25
|
|
26
26
|
def is_visual_mode()
|
27
|
-
return 1 if
|
27
|
+
return 1 if vma.kbd.mode_root_state.to_s() == "V"
|
28
28
|
return 0
|
29
29
|
end
|
30
30
|
|
@@ -33,6 +33,8 @@ reg_act(:command_to_buf, proc { command_to_buf }, "Execute command, output to bu
|
|
33
33
|
reg_act(:lsp_debug, proc { vma.buf.lsp_get_def }, "LSP get definition")
|
34
34
|
reg_act(:lsp_jump_to_definition, proc { vma.buf.lsp_jump_to_def }, "LSP jump to definition")
|
35
35
|
|
36
|
+
reg_act(:eval_buf, proc { vma.buf.eval_whole_buf }, "Eval whole current buffer as ruby code (DANGEROUS)")
|
37
|
+
|
36
38
|
reg_act(:enable_debug, proc { cnf.debug = true }, "Enable debug")
|
37
39
|
reg_act(:disable_debug, proc { cnf.debug = false }, "Disable debug")
|
38
40
|
|
@@ -67,7 +69,6 @@ reg_act(:set_executable, proc { buf.set_executable }, "Set current file permissi
|
|
67
69
|
reg_act(:close_current_buffer, proc { bufs.close_current_buffer(true) }, "Close current buffer")
|
68
70
|
reg_act(:comment_selection, proc { buf.comment_selection }, "")
|
69
71
|
reg_act(:delete_char_forward, proc { buf.delete(CURRENT_CHAR_FORWARD) }, "Delete char forward", { :group => [:edit, :basic] })
|
70
|
-
reg_act(:load_theme, proc { load_theme }, "Load theme")
|
71
72
|
reg_act(:gui_file_finder, proc { vma.FileFinder.start_gui }, "Fuzzy file finder")
|
72
73
|
reg_act(:gui_file_history_finder, proc { vma.FileHistory.start_gui }, "Fuzzy file history finder")
|
73
74
|
reg_act(:gui_search_replace, proc { gui_search_replace }, "Search and replace")
|
@@ -83,7 +84,7 @@ reg_act(:set_line_style_h4, proc { buf.set_line_style(:h4) }, "Set cur line as H
|
|
83
84
|
reg_act(:set_line_style_bold, proc { buf.set_line_style(:bold) }, "Set style of current line as bold")
|
84
85
|
reg_act(:set_line_style_title, proc { buf.set_line_style(:title) }, "Set style of current line as title")
|
85
86
|
reg_act(:clear_line_styles, proc { buf.set_line_style(:clear) }, "Clear styles of current line")
|
86
|
-
reg_act(:gui_select_buffer, proc {
|
87
|
+
reg_act(:gui_select_buffer, proc { vma.kbd.set_mode("S"); gui_select_buffer }, "Select buffer")
|
87
88
|
reg_act :open_file_dialog, "open_file_dialog", "Open file"
|
88
89
|
reg_act :minibuffer_end, proc { minibuffer_end }
|
89
90
|
reg_act(:invoke_replace, "invoke_replace", "")
|
@@ -96,6 +97,30 @@ reg_act :delete_to_word_end, proc { buf.delete2(:to_word_end) }, "Delete to file
|
|
96
97
|
reg_act :delete_to_next_word_start, proc { buf.delete2(:to_next_word) }, "Delete to start of next word", { :group => [:edit, :basic] }
|
97
98
|
reg_act :delete_to_line_start, proc { buf.delete2(:to_line_start) }, "Delete to line start", { :group => [:edit, :basic] }
|
98
99
|
|
100
|
+
|
101
|
+
reg_act(:ack_search, proc { gui_ack }, "")
|
102
|
+
|
103
|
+
reg_act(:copy_cur_line, proc {buf.copy_line}, "Copy the current line")
|
104
|
+
reg_act(:paste_before_cursor, proc {buf.paste(BEFORE)}, "Paste text before the cursor")
|
105
|
+
reg_act(:paste_after_cursor, proc {buf.paste(AFTER)}, "Paste text after the cursor")
|
106
|
+
reg_act(:redo, proc {buf.redo()}, "Redo the last undone action")
|
107
|
+
reg_act(:undo, proc {buf.undo()}, "Undo the last action")
|
108
|
+
reg_act(:jump_end_of_line, proc { buf.jump(END_OF_LINE) }, "Move to the end of the current line")
|
109
|
+
reg_act(:jump_end_of_buffer, proc {buf.jump(END_OF_BUFFER)}, "Move to the end of the buffer")
|
110
|
+
reg_act(:jump_start_of_buffer, proc { buf.jump(START_OF_BUFFER) }, "Move to the start of the buffer")
|
111
|
+
reg_act(:jump_beginning_of_line, proc { buf.jump(BEGINNING_OF_LINE) }, "Move to the beginning of the current line")
|
112
|
+
reg_act(:jump_next_word_end, proc { buf.jump_word(FORWARD,WORD_END) }, "Jump to the end of the next word")
|
113
|
+
reg_act(:jump_prev_word_start, proc { buf.jump_word(BACKWARD,WORD_START) }, "Jump to the start of the previous word")
|
114
|
+
reg_act(:jump_next_word_start, proc { buf.jump_word(FORWARD,WORD_START) }, "Jump to the start of the next word")
|
115
|
+
reg_act(:insert_mode, proc { vma.kbd.set_mode(:insert) }, "Switch to INSERT mode")
|
116
|
+
reg_act(:prev_mode, proc { vma.kbd.to_previous_mode }, "Return to the previous mode")
|
117
|
+
reg_act(:move_prev_line, proc { buf.move(BACKWARD_LINE) }, "Move the cursor to the previous line")
|
118
|
+
reg_act(:move_next_line, proc { buf.move(FORWARD_LINE) }, "Move the cursor to the next line")
|
119
|
+
reg_act(:move_backward_char, proc { buf.move(BACKWARD_CHAR) }, "Move one character backward")
|
120
|
+
reg_act(:start_visual_mode, proc { buf.start_selection;vma.kbd.set_mode(:visual) }, "Enter VISUAL mode (for selections)")
|
121
|
+
reg_act(:jump_last_edit, proc { buf.jump_to_last_edit }, "Jump to the last edit location")
|
122
|
+
|
123
|
+
|
99
124
|
reg_act :start_browse_mode, proc {
|
100
125
|
vma.kbd.set_mode(:browse)
|
101
126
|
bufs.reset_navigation
|
@@ -7,8 +7,8 @@
|
|
7
7
|
#
|
8
8
|
# Change mode from INSERT into COMMAND when ctrl key is released immediately
|
9
9
|
# after it has been pressed (there are no other key events between key press and key release).
|
10
|
-
# 'I ctrl!'=> '
|
11
|
-
# 'C ctrl!'=> '
|
10
|
+
# 'I ctrl!'=> 'vma.kbd.set_mode(:command)',
|
11
|
+
# 'C ctrl!'=> 'vma.kbd.set_mode(:insert)',
|
12
12
|
|
13
13
|
#
|
14
14
|
# In command mode: press keys "," "r" "v" and "b" sequentially.
|
@@ -18,9 +18,8 @@
|
|
18
18
|
# 'I ctrl-a'=> 'vma.buf.jump(BEGINNING_OF_LINE)',
|
19
19
|
#
|
20
20
|
|
21
|
-
|
22
21
|
class State
|
23
|
-
attr_accessor :key_name, :eval_rule, :children, :action, :label, :major_modes, :level, :cursor_type
|
22
|
+
attr_accessor :key_name, :eval_rule, :children, :action, :label, :major_modes, :level, :cursor_type, :keywords
|
24
23
|
attr_reader :cur_mode, :scope
|
25
24
|
|
26
25
|
def initialize(key_name, eval_rule = "", ctype = :command, scope: :buffer)
|
@@ -29,6 +28,7 @@ class State
|
|
29
28
|
@children = []
|
30
29
|
@scope = scope
|
31
30
|
@major_modes = []
|
31
|
+
@keywords = []
|
32
32
|
@action = nil
|
33
33
|
@level = 0
|
34
34
|
@cursor_type = ctype
|
@@ -86,7 +86,7 @@ class KeyBindingTree
|
|
86
86
|
|
87
87
|
def set_mode_stack(ms)
|
88
88
|
debug "set_mode_stack(#{ms})", 2
|
89
|
-
show_caller if cnf.debug? # TODO: remove
|
89
|
+
show_caller if cnf.debug? # TODO: remove
|
90
90
|
@default_mode_stack = ms
|
91
91
|
label = @default_mode_stack[-1]
|
92
92
|
@match_state = [@modes[label]]
|
@@ -110,7 +110,7 @@ class KeyBindingTree
|
|
110
110
|
end
|
111
111
|
|
112
112
|
def to_previous_mode()
|
113
|
-
debug "to_previous_mode",2
|
113
|
+
debug "to_previous_mode", 2
|
114
114
|
debug @default_mode_stack
|
115
115
|
if @default_mode_stack.size > 1
|
116
116
|
@default_mode_stack.pop
|
@@ -135,7 +135,6 @@ class KeyBindingTree
|
|
135
135
|
@modes[label] = mode
|
136
136
|
if @root.nil?
|
137
137
|
show_caller
|
138
|
-
Ripl.start :binding => binding
|
139
138
|
end
|
140
139
|
@root.children << mode
|
141
140
|
mode.major_modes << major_mode_label
|
@@ -165,6 +164,7 @@ class KeyBindingTree
|
|
165
164
|
|
166
165
|
def match(key_name)
|
167
166
|
new_state = []
|
167
|
+
debug = false
|
168
168
|
@match_state.each { |parent|
|
169
169
|
parent.children.each { |c|
|
170
170
|
# printf(" KEY MATCH: ")
|
@@ -174,6 +174,9 @@ class KeyBindingTree
|
|
174
174
|
elsif c.key_name == key_name and c.eval_rule != ""
|
175
175
|
debug "CHECK EVAL: #{c.eval_rule}"
|
176
176
|
if eval(c.eval_rule)
|
177
|
+
# if eval_rule.match(/macro/)
|
178
|
+
debug = true
|
179
|
+
# end
|
177
180
|
new_state << c
|
178
181
|
debug "EVAL TRUE"
|
179
182
|
else
|
@@ -183,6 +186,10 @@ class KeyBindingTree
|
|
183
186
|
}
|
184
187
|
}
|
185
188
|
|
189
|
+
if debug
|
190
|
+
# require "pry"; binding.pry
|
191
|
+
end
|
192
|
+
|
186
193
|
if new_state.any? # Match found
|
187
194
|
@match_state = new_state
|
188
195
|
return new_state
|
@@ -262,14 +269,66 @@ class KeyBindingTree
|
|
262
269
|
@state_trail = [@mode_root_state]
|
263
270
|
end
|
264
271
|
|
272
|
+
def get_by_keywords(modes: [], keywords: [])
|
273
|
+
s = ""
|
274
|
+
stack = [[@root, ""]]
|
275
|
+
lines = []
|
276
|
+
|
277
|
+
# Traverse the tree (class State objects) using a stack
|
278
|
+
while stack.any?
|
279
|
+
t, p = *stack.pop # t = current state, p = current path
|
280
|
+
if t.children.any?
|
281
|
+
t.children.reverse.each { |c|
|
282
|
+
|
283
|
+
# Restrict by modes if specified
|
284
|
+
if c.level == 1 and !modes.empty?
|
285
|
+
next if !modes.include?(c.key_name)
|
286
|
+
end
|
287
|
+
|
288
|
+
if c.eval_rule.size > 0
|
289
|
+
new_p = "#{p} #{c.key_name}(#{c.eval_rule})"
|
290
|
+
else
|
291
|
+
if c.level == 1
|
292
|
+
new_p = "#{p} [#{c.key_name}]"
|
293
|
+
else
|
294
|
+
new_p = "#{p} #{c.key_name}"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
stack << [c, new_p]
|
298
|
+
}
|
299
|
+
# stack.concat[t.children]
|
300
|
+
|
301
|
+
else
|
302
|
+
method_desc = t.action
|
303
|
+
if t.action.class == Symbol
|
304
|
+
if vma.actions.include?(t.action)
|
305
|
+
a = vma.actions[t.action].method_name
|
306
|
+
if !a.nil? and !a.empty?
|
307
|
+
method_desc = a
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
if (keywords - t.keywords).empty? # All keywords are present
|
313
|
+
kw = ""
|
314
|
+
# kw = ", [#{t.keywords.join(",")}]" if !t.keywords.empty?
|
315
|
+
lines << p + " : #{method_desc}#{kw}"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
s = lines.sort.join("\n")
|
320
|
+
return s
|
321
|
+
end
|
322
|
+
|
265
323
|
# Print key bindings to show as documentation or for debugging
|
266
324
|
def to_s()
|
267
|
-
|
325
|
+
return self.class.to_s
|
268
326
|
s = ""
|
269
327
|
# @cur_state = @root
|
270
328
|
stack = [[@root, ""]]
|
271
329
|
lines = []
|
272
330
|
|
331
|
+
# Traverse the tree (class State objects) using a stack
|
273
332
|
while stack.any?
|
274
333
|
t, p = *stack.pop # t = current state, p = current path
|
275
334
|
if t.children.any?
|
@@ -297,7 +356,9 @@ class KeyBindingTree
|
|
297
356
|
end
|
298
357
|
end
|
299
358
|
|
300
|
-
|
359
|
+
kw = ""
|
360
|
+
# kw = ", " + t.keywords.join(",") if !t.keywords.empty?
|
361
|
+
lines << p + " : #{method_desc}#{kw}"
|
301
362
|
end
|
302
363
|
end
|
303
364
|
s = lines.sort.join("\n")
|
@@ -426,11 +487,15 @@ class KeyBindingTree
|
|
426
487
|
else
|
427
488
|
|
428
489
|
# Don't execute action if one of the states has children
|
490
|
+
# TODO: why?
|
429
491
|
state_with_children = new_state.select { |s| s.children.any? }
|
430
492
|
s_act = new_state.select { |s| s.action != nil }
|
431
493
|
|
432
|
-
if s_act.any? and !state_with_children.any?
|
494
|
+
if s_act.any? #and !state_with_children.any?
|
433
495
|
eval_s = s_act.first.action if eval_s == nil
|
496
|
+
# if eval_s.to_s.match(/end_recording/)
|
497
|
+
# require "pry"; binding.pry
|
498
|
+
# end
|
434
499
|
debug "FOUND MATCH:#{eval_s}"
|
435
500
|
debug "CHAR: #{c}"
|
436
501
|
c.gsub!("\\", %q{\\\\} * 4) # Escape \ -chars
|
@@ -449,7 +514,12 @@ class KeyBindingTree
|
|
449
514
|
return true
|
450
515
|
end
|
451
516
|
|
452
|
-
def bindkey(key, action)
|
517
|
+
def bindkey(key, action, keywords: "")
|
518
|
+
# puts "keywords #{keywords}" if !keywords.empty?
|
519
|
+
if keywords.class == String
|
520
|
+
keywords = keywords.split(" ")
|
521
|
+
end
|
522
|
+
# puts "keywords #{keywords}" if !keywords.empty?
|
453
523
|
if key.class != Array
|
454
524
|
# Handle syntax like :
|
455
525
|
# "X esc || X ctrl!" => "vma.kbd.to_previous_mode",
|
@@ -464,10 +534,10 @@ class KeyBindingTree
|
|
464
534
|
msg = action[2]
|
465
535
|
reg_act(label, proc, msg)
|
466
536
|
end
|
467
|
-
key.each { |k| _bindkey(k, a) }
|
537
|
+
key.each { |k| _bindkey(k, a, keywords: keywords) }
|
468
538
|
end
|
469
539
|
|
470
|
-
def _bindkey(key, action)
|
540
|
+
def _bindkey(key, action, keywords: [])
|
471
541
|
key.strip!
|
472
542
|
key.gsub!(/\s+/, " ")
|
473
543
|
|
@@ -502,15 +572,18 @@ class KeyBindingTree
|
|
502
572
|
fatal_error("Error in keydef #{key.inspect}")
|
503
573
|
end
|
504
574
|
|
575
|
+
# puts "keywords #{keywords}"
|
505
576
|
modes.each { |mode_id|
|
506
|
-
mode_bind_key(mode_id, keydef, action)
|
577
|
+
mode_bind_key(mode_id, keydef, action, keywords: keywords)
|
578
|
+
|
579
|
+
# Map froma actions to keybindings (e.g. to show bindings in menu)
|
507
580
|
@act_bindings[mode_id][action] = keydef
|
508
581
|
}
|
509
582
|
end
|
510
583
|
|
511
584
|
# Binds a keyboard key combination to an action,
|
512
585
|
# for a given keyboard mode like insert ("I") or command ("C")
|
513
|
-
def mode_bind_key(mode_id, keydef, action)
|
586
|
+
def mode_bind_key(mode_id, keydef, action, keywords: [])
|
514
587
|
# debug "mode_bind_key #{mode_id.inspect}, #{keydef.inspect}, #{action.inspect}", 2
|
515
588
|
# Example:
|
516
589
|
# bindkey "C , f", :gui_file_finder
|
@@ -520,7 +593,7 @@ class KeyBindingTree
|
|
520
593
|
set_state(mode_id, "") # TODO: check is ok?
|
521
594
|
start_state = @cur_state
|
522
595
|
|
523
|
-
k_arr = keydef.split
|
596
|
+
k_arr = keydef.split #e.g. definition: "C y e" = > ["C", "y", "e"]
|
524
597
|
|
525
598
|
prev_state = nil
|
526
599
|
s1 = start_state
|
@@ -561,6 +634,7 @@ class KeyBindingTree
|
|
561
634
|
else
|
562
635
|
@cur_state.action = action
|
563
636
|
end
|
637
|
+
@cur_state.keywords = keywords
|
564
638
|
@cur_state = @root
|
565
639
|
end
|
566
640
|
|
@@ -614,13 +688,19 @@ class KeyBindingTree
|
|
614
688
|
end
|
615
689
|
end
|
616
690
|
|
617
|
-
def bindkey(key, action)
|
618
|
-
|
691
|
+
def bindkey(key, action, keywords: "")
|
692
|
+
vma.kbd.bindkey(key, action, keywords: keywords)
|
693
|
+
end
|
694
|
+
|
695
|
+
def add_keys(keywords, to_add)
|
696
|
+
to_add.each { |key, value|
|
697
|
+
bindkey(key, value, keywords: keywords)
|
698
|
+
}
|
619
699
|
end
|
620
700
|
|
621
701
|
def exec_action(action)
|
622
|
-
|
623
|
-
|
702
|
+
vma.kbd.last_action = vma.kbd.cur_action
|
703
|
+
vma.kbd.cur_action = action
|
624
704
|
if action.class == Symbol
|
625
705
|
return call_action(action)
|
626
706
|
elsif action.class == Proc
|
@@ -630,13 +710,70 @@ def exec_action(action)
|
|
630
710
|
end
|
631
711
|
end
|
632
712
|
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
713
|
+
def show_key_bindings()
|
714
|
+
kbd_s = "❙Key bindings❙\n"
|
715
|
+
kbd_s << "\n⦁[Mode] <keys> : <action>⦁\n"
|
716
|
+
done = []
|
717
|
+
|
718
|
+
kbd_s << "[B]=Browse, [C]=Command, [I]=Insert, [V]=Visual\n"
|
719
|
+
kbd_s << "<key>!: Press <key> once, release before pressing any other keys\n"
|
720
|
+
kbd_s << "===============================================\n"
|
721
|
+
kbd_s << "◼ Basic\n"
|
722
|
+
kbd_s << "◼◼ Command mode\n"
|
723
|
+
|
724
|
+
x = vma.kbd.get_by_keywords(modes: ["C"], keywords: ["intro"])
|
725
|
+
done.concat(x.lines); kbd_s << x
|
726
|
+
|
727
|
+
kbd_s << "\n"
|
728
|
+
kbd_s << "◼◼ Insert mode\n"
|
729
|
+
x = vma.kbd.get_by_keywords(modes: ["I"], keywords: ["intro"])
|
730
|
+
done.concat(x.lines); kbd_s << x
|
731
|
+
kbd_s << "\n"
|
732
|
+
kbd_s << "◼◼ Visual mode\n"
|
733
|
+
x = vma.kbd.get_by_keywords(modes: ["V"], keywords: ["intro"])
|
734
|
+
done.concat(x.lines); kbd_s << x
|
735
|
+
kbd_s << "\n"
|
736
|
+
|
737
|
+
kbd_s << "◼ Core\n"
|
738
|
+
x = vma.kbd.get_by_keywords(modes: [], keywords: ["core"])
|
739
|
+
x << vma.kbd.get_by_keywords(modes: ["X"], keywords: ["intro"])
|
740
|
+
|
741
|
+
done.concat(x.lines); kbd_s << x
|
742
|
+
kbd_s << "\n"
|
743
|
+
|
744
|
+
kbd_s << "◼ Debug / Experimental\n"
|
745
|
+
x = vma.kbd.get_by_keywords(modes: [], keywords: ["experimental"])
|
746
|
+
done.concat(x.lines); kbd_s << x
|
747
|
+
kbd_s << "\n"
|
748
|
+
|
749
|
+
kbd_s << "◼ Others\n"
|
750
|
+
# x = vma.kbd.get_by_keywords(modes: [], keywords:["experimental"])
|
751
|
+
# x = vma.kbd.to_s
|
752
|
+
x = vma.kbd.get_by_keywords(modes: [], keywords: [])
|
753
|
+
done << x.lines - done
|
754
|
+
kbd_s << (x.lines - done).join
|
755
|
+
kbd_s << "\n"
|
756
|
+
|
757
|
+
kbd_s << "===============================================\n"
|
758
|
+
# x = vma.kbd.to_s
|
759
|
+
# # require "pry"; binding.pry
|
760
|
+
# done << x.lines - done
|
761
|
+
# kbd_s << (x.lines - done).join
|
762
|
+
# kbd_s << "\n"
|
763
|
+
# kbd_s << "===============================================\n"
|
764
|
+
b = create_new_buffer(kbd_s, "key-bindings")
|
765
|
+
gui_set_file_lang(b.id, "hyperplaintext")
|
766
|
+
#
|
638
767
|
end
|
639
768
|
|
640
|
-
|
641
|
-
|
642
|
-
|
769
|
+
# Try to clear modifiers when program loses focus
|
770
|
+
# e.g. after alt-tab
|
771
|
+
# TODO
|
772
|
+
# def focus_out
|
773
|
+
# debug "RB Clear modifiers"
|
774
|
+
# vma.kbd.clear_modifiers()
|
775
|
+
# end
|
776
|
+
|
777
|
+
# def handle_key_event(event)
|
778
|
+
# vma.kbd.handle_key_event(event)
|
779
|
+
# end
|