textbringer 0.1.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.
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ TOP_LEVEL_TAG = Object.new
5
+ RECURSIVE_EDIT_TAG = Object.new
6
+
7
+ class Controller
8
+ attr_accessor :this_command, :last_command, :overriding_map
9
+ attr_accessor :prefix_arg, :current_prefix_arg
10
+ attr_reader :last_key
11
+
12
+ @@current = nil
13
+
14
+ def self.current
15
+ @@current
16
+ end
17
+
18
+ def self.current=(controller)
19
+ @@current = controller
20
+ end
21
+
22
+ def initialize
23
+ @key_sequence = []
24
+ @last_key = nil
25
+ @recursive_edit_level = 0
26
+ @this_command = nil
27
+ @last_command = nil
28
+ @overriding_map = nil
29
+ @prefix_arg = nil
30
+ @current_prefix_arg = nil
31
+ end
32
+
33
+ def command_loop(tag)
34
+ catch(tag) do
35
+ loop do
36
+ begin
37
+ c = Window.current.getch
38
+ Window.echo_area.clear_message
39
+ @last_key = c
40
+ @key_sequence << @last_key
41
+ cmd = key_binding(@key_sequence)
42
+ if cmd.is_a?(Symbol) || cmd.respond_to?(:call)
43
+ @key_sequence.clear
44
+ @this_command = cmd
45
+ @current_prefix_arg = @prefix_arg
46
+ @prefix_arg = nil
47
+ begin
48
+ run_hooks(:pre_command_hook, remove_on_error: true)
49
+ if cmd.is_a?(Symbol)
50
+ send(cmd)
51
+ else
52
+ cmd.call
53
+ end
54
+ ensure
55
+ run_hooks(:post_command_hook, remove_on_error: true)
56
+ @last_command = @this_command
57
+ @this_command = nil
58
+ end
59
+ else
60
+ if cmd.nil?
61
+ keys = @key_sequence.map { |c| key_name(c) }.join(" ")
62
+ @key_sequence.clear
63
+ Window.echo_area.show("#{keys} is undefined")
64
+ end
65
+ end
66
+ rescue Exception => e
67
+ handle_exception(e)
68
+ end
69
+ Window.redisplay
70
+ end
71
+ end
72
+ end
73
+
74
+ def wait_input(msecs)
75
+ Window.current.wait_input(msecs)
76
+ end
77
+
78
+ def read_char
79
+ Window.current.getch
80
+ end
81
+
82
+ def received_keyboard_quit?
83
+ while (key = Window.current.getch_nonblock) && key >= 0
84
+ if GLOBAL_MAP.lookup([key]) == :keyboard_quit
85
+ return true
86
+ end
87
+ end
88
+ false
89
+ end
90
+
91
+ def recursive_edit
92
+ @recursive_edit_level += 1
93
+ begin
94
+ if command_loop(RECURSIVE_EDIT_TAG)
95
+ raise Quit
96
+ end
97
+ ensure
98
+ @recursive_edit_level -= 1
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def key_name(key)
105
+ case key
106
+ when Integer
107
+ if key < 0x80
108
+ s = Ncurses.keyname(key)
109
+ case s
110
+ when /\AKEY_(.*)/
111
+ "<#{$1.downcase}>"
112
+ else
113
+ s
114
+ end
115
+ else
116
+ key.chr(Encoding::UTF_8)
117
+ end
118
+ else
119
+ key.to_s
120
+ end
121
+ end
122
+
123
+ def key_binding(key_sequence)
124
+ @overriding_map&.lookup(key_sequence) ||
125
+ Buffer.current&.keymap&.lookup(key_sequence) ||
126
+ GLOBAL_MAP.lookup(key_sequence)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ class EditorError < StandardError
5
+ end
6
+
7
+ class SearchError < EditorError
8
+ end
9
+
10
+ class ReadOnlyError < EditorError
11
+ end
12
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ncursesw"
4
+
5
+ module Textbringer
6
+ class Keymap
7
+ def initialize
8
+ @map = {}
9
+ end
10
+
11
+ def define_key(key, command)
12
+ key_sequence = kbd(key)
13
+
14
+ case key_sequence.size
15
+ when 0
16
+ raise ArgumentError, "Empty key"
17
+ when 1
18
+ @map[key_sequence.first] = command
19
+ else
20
+ k, *ks = key_sequence
21
+ (@map[k] ||= Keymap.new).define_key(ks, command)
22
+ end
23
+ end
24
+ alias [] define_key
25
+
26
+ def lookup(key_sequence)
27
+ case key_sequence.size
28
+ when 0
29
+ raise ArgumentError, "Empty key"
30
+ when 1
31
+ @map[key_sequence.first]
32
+ else
33
+ k, *ks = key_sequence
34
+ @map[k]&.lookup(ks)
35
+ end
36
+ end
37
+
38
+ def handle_undefined_key
39
+ @map.default_proc = Proc.new { |h, k| yield(k) }
40
+ end
41
+
42
+ private
43
+
44
+ def kbd(key)
45
+ case key
46
+ when Integer, Symbol
47
+ [key]
48
+ when String
49
+ key.unpack("C*")
50
+ when Array
51
+ key
52
+ else
53
+ raise TypeError, "invalid key type #{key.class}"
54
+ end
55
+ end
56
+ end
57
+
58
+ GLOBAL_MAP = Keymap.new
59
+ GLOBAL_MAP.define_key(:resize, :resize_window)
60
+ GLOBAL_MAP.define_key(:right, :forward_char)
61
+ GLOBAL_MAP.define_key(?\C-f, :forward_char)
62
+ GLOBAL_MAP.define_key(:left, :backward_char)
63
+ GLOBAL_MAP.define_key(?\C-b, :backward_char)
64
+ GLOBAL_MAP.define_key("\ef", :forward_word)
65
+ GLOBAL_MAP.define_key("\eb", :backward_word)
66
+ GLOBAL_MAP.define_key("\egc", :goto_char)
67
+ GLOBAL_MAP.define_key("\egg", :goto_line)
68
+ GLOBAL_MAP.define_key("\eg\eg", :goto_line)
69
+ GLOBAL_MAP.define_key(:down, :next_line)
70
+ GLOBAL_MAP.define_key(?\C-n, :next_line)
71
+ GLOBAL_MAP.define_key(:up, :previous_line)
72
+ GLOBAL_MAP.define_key(?\C-p, :previous_line)
73
+ GLOBAL_MAP.define_key(:dc, :delete_char)
74
+ GLOBAL_MAP.define_key(?\C-d, :delete_char)
75
+ GLOBAL_MAP.define_key(:backspace, :backward_delete_char)
76
+ GLOBAL_MAP.define_key(?\C-h, :backward_delete_char)
77
+ GLOBAL_MAP.define_key(?\C-a, :beginning_of_line)
78
+ GLOBAL_MAP.define_key(:home, :beginning_of_line)
79
+ GLOBAL_MAP.define_key(?\C-e, :end_of_line)
80
+ GLOBAL_MAP.define_key(:end, :end_of_line)
81
+ GLOBAL_MAP.define_key("\e<", :beginning_of_buffer)
82
+ GLOBAL_MAP.define_key("\e>", :end_of_buffer)
83
+ (0x20..0x7e).each do |c|
84
+ GLOBAL_MAP.define_key(c, :self_insert)
85
+ end
86
+ GLOBAL_MAP.define_key(?\t, :self_insert)
87
+ GLOBAL_MAP.define_key(?\C-q, :quoted_insert)
88
+ GLOBAL_MAP.define_key("\C- ", :set_mark)
89
+ GLOBAL_MAP.define_key("\C-x\C-x", :exchange_point_and_mark)
90
+ GLOBAL_MAP.define_key("\ew", :copy_region)
91
+ GLOBAL_MAP.define_key(?\C-w, :kill_region)
92
+ GLOBAL_MAP.define_key(?\C-k, :kill_line)
93
+ GLOBAL_MAP.define_key("\ed", :kill_word)
94
+ GLOBAL_MAP.define_key(?\C-y, :yank)
95
+ GLOBAL_MAP.define_key("\ey", :yank_pop)
96
+ GLOBAL_MAP.define_key(?\C-_, :undo)
97
+ GLOBAL_MAP.define_key("\C-x\C-_", :redo)
98
+ GLOBAL_MAP.define_key("\C-t", :transpose_chars)
99
+ GLOBAL_MAP.define_key(?\n, :newline)
100
+ GLOBAL_MAP.define_key("\C-l", :recenter)
101
+ GLOBAL_MAP.define_key("\C-v", :scroll_up)
102
+ GLOBAL_MAP.define_key(:npage, :scroll_up)
103
+ GLOBAL_MAP.define_key("\ev", :scroll_down)
104
+ GLOBAL_MAP.define_key(:ppage, :scroll_down)
105
+ GLOBAL_MAP.define_key("\C-x0", :delete_window)
106
+ GLOBAL_MAP.define_key("\C-x1", :delete_other_windows)
107
+ GLOBAL_MAP.define_key("\C-x2", :split_window)
108
+ GLOBAL_MAP.define_key("\C-xo", :other_window)
109
+ GLOBAL_MAP.define_key("\C-x\C-c", :exit_textbringer)
110
+ GLOBAL_MAP.define_key("\C-z", :suspend_textbringer)
111
+ GLOBAL_MAP.define_key("\C-x\C-f", :find_file)
112
+ GLOBAL_MAP.define_key("\C-xb", :switch_to_buffer)
113
+ GLOBAL_MAP.define_key("\C-x\C-s", :save_buffer)
114
+ GLOBAL_MAP.define_key("\C-x\C-w", :write_file)
115
+ GLOBAL_MAP.define_key("\C-xk", :kill_buffer)
116
+ GLOBAL_MAP.define_key("\C-x\nf", :set_buffer_file_encoding)
117
+ GLOBAL_MAP.define_key("\C-x\nn", :set_buffer_file_format)
118
+ GLOBAL_MAP.define_key("\ex", :execute_command)
119
+ GLOBAL_MAP.define_key("\e:", :eval_expression)
120
+ GLOBAL_MAP.define_key(?\C-u, :universal_argument)
121
+ GLOBAL_MAP.define_key(?\C-g, :keyboard_quit)
122
+ GLOBAL_MAP.define_key(?\C-s, :isearch_forward)
123
+ GLOBAL_MAP.define_key(?\C-r, :isearch_backward)
124
+ GLOBAL_MAP.handle_undefined_key do |key|
125
+ if key.is_a?(Integer) && key > 0x80
126
+ begin
127
+ key.chr(Encoding::UTF_8)
128
+ :self_insert
129
+ rescue RangeError
130
+ nil
131
+ end
132
+ else
133
+ nil
134
+ end
135
+ end
136
+
137
+ MINIBUFFER_LOCAL_MAP = Keymap.new
138
+ MINIBUFFER_LOCAL_MAP.define_key(?\n, :exit_recursive_edit)
139
+ MINIBUFFER_LOCAL_MAP.define_key(?\t, :complete_minibuffer)
140
+ MINIBUFFER_LOCAL_MAP.define_key(?\C-g, :abort_recursive_edit)
141
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ class Mode
5
+ extend Commands
6
+ include Commands
7
+
8
+ @@mode_list = []
9
+
10
+ def self.list
11
+ @@mode_list
12
+ end
13
+
14
+ class << self
15
+ attr_accessor :mode_name
16
+ attr_accessor :command_name
17
+ attr_accessor :hook_name
18
+ attr_accessor :file_name_pattern
19
+ end
20
+
21
+ def self.define_generic_command(name)
22
+ command_name = (name.to_s + "_command").intern
23
+ define_command(command_name) do |*args|
24
+ begin
25
+ Buffer.current.mode.send(name, *args)
26
+ rescue NoMethodError => e
27
+ if e.receiver == Buffer.current.mode && e.name == name
28
+ raise EditorError,
29
+ "#{command_name} is not supported in the current mode"
30
+ else
31
+ raise
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.inherited(child)
38
+ base_name = child.name.slice(/[^:]*\z/)
39
+ child.mode_name = base_name.sub(/Mode\z/, "")
40
+ command_name = base_name.sub(/\A[A-Z]/) { |s| s.downcase }.
41
+ gsub(/(?<=[a-z])([A-Z])/) {
42
+ "_" + $1.downcase
43
+ }
44
+ command = command_name.intern
45
+ hook = (command_name + "_hook").intern
46
+ child.command_name = command
47
+ child.hook_name = hook
48
+ define_command(command) do
49
+ Buffer.current.apply_mode(child)
50
+ end
51
+ @@mode_list.push(child)
52
+ end
53
+
54
+ attr_reader :buffer
55
+
56
+ def initialize(buffer)
57
+ @buffer = buffer
58
+ end
59
+
60
+ def name
61
+ self.class.mode_name
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ class BacktraceMode < Mode
5
+ define_generic_command :jump_to_source_location
6
+
7
+ BACKTRACE_MODE_MAP = Keymap.new
8
+ BACKTRACE_MODE_MAP.define_key("\n", :jump_to_source_location_command)
9
+
10
+ def initialize(buffer)
11
+ super(buffer)
12
+ buffer.keymap = BACKTRACE_MODE_MAP
13
+ end
14
+
15
+ def jump_to_source_location
16
+ file_name, line_number = get_source_location
17
+ if file_name
18
+ find_file(file_name)
19
+ goto_line(line_number)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def get_source_location
26
+ @buffer.save_excursion do
27
+ @buffer.beginning_of_line
28
+ if @buffer.looking_at?(/^(\S*?):(\d+):/)
29
+ file_name = @buffer.match_string(1)
30
+ line_number = @buffer.match_string(2).to_i
31
+ [file_name, line_number]
32
+ else
33
+ nil
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ class FundamentalMode < Mode
5
+ end
6
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ class ProgrammingMode < Mode
5
+ # abstract mode
6
+ undefine_command(:programming_mode)
7
+
8
+ define_generic_command :indent_line
9
+ define_generic_command :newline_and_reindent
10
+ define_generic_command :forward_definition
11
+ define_generic_command :backward_definition
12
+ define_generic_command :compile
13
+
14
+ PROGRAMMING_MODE_MAP = Keymap.new
15
+ PROGRAMMING_MODE_MAP.define_key("\t", :indent_line_command)
16
+ PROGRAMMING_MODE_MAP.define_key("\n", :newline_and_reindent_command)
17
+ PROGRAMMING_MODE_MAP.define_key("\C-c\C-n", :forward_definition_command)
18
+ PROGRAMMING_MODE_MAP.define_key("\C-c\C-p", :backward_definition_command)
19
+ PROGRAMMING_MODE_MAP.define_key("\C-c\C-c", :compile_command)
20
+
21
+ def initialize(buffer)
22
+ super(buffer)
23
+ buffer.keymap = PROGRAMMING_MODE_MAP
24
+ end
25
+
26
+ def newline_and_reindent
27
+ n = 1
28
+ @buffer.save_excursion do
29
+ pos = @buffer.point
30
+ @buffer.beginning_of_line
31
+ if /\A\s+\z/ =~ @buffer.substring(@buffer.point, pos)
32
+ @buffer.delete_region(@buffer.point, pos)
33
+ n += 1
34
+ end
35
+ end
36
+ @buffer.insert("\n")
37
+ if indent_line
38
+ n += 1
39
+ end
40
+ @buffer.merge_undo(n) if n > 1
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module Textbringer
6
+ CONFIG[:ruby_indent_level] = 2
7
+ CONFIG[:ruby_indent_tabs_mode] = false
8
+
9
+ class RubyMode < ProgrammingMode
10
+ self.file_name_pattern = /\A(?:.*\.(?:rb|ru|rake|thor)|
11
+ (?:Gem|Rake|Cap|Thor|Vagrant|Guard|Pod)file)\z/x
12
+
13
+ def initialize(buffer)
14
+ super(buffer)
15
+ @buffer[:indent_tabs_mode] = CONFIG[:ruby_indent_tabs_mode]
16
+ end
17
+
18
+ # Return true if modified.
19
+ def indent_line
20
+ result = false
21
+ level = calculate_indentation
22
+ @buffer.save_excursion do
23
+ @buffer.beginning_of_line
24
+ has_space = @buffer.looking_at?(/[ \t]+/)
25
+ if has_space
26
+ s = @buffer.match_string(0)
27
+ break if /\t/ !~ s && s.size == level
28
+ @buffer.delete_region(@buffer.match_beginning(0),
29
+ @buffer.match_end(0))
30
+ else
31
+ break if level == 0
32
+ end
33
+ @buffer.indent_to(level)
34
+ if has_space
35
+ @buffer.merge_undo(2)
36
+ end
37
+ result = true
38
+ end
39
+ pos = @buffer.point
40
+ @buffer.beginning_of_line
41
+ @buffer.forward_char while /[ \t]/ =~ @buffer.char_after
42
+ if @buffer.point < pos
43
+ @buffer.goto_char(pos)
44
+ end
45
+ result
46
+ end
47
+
48
+ def forward_definition(n = number_prefix_arg || 1)
49
+ tokens = Ripper.lex(@buffer.to_s)
50
+ @buffer.forward_line
51
+ n.times do |i|
52
+ tokens = tokens.drop_while { |(l, c), e, t|
53
+ l < @buffer.current_line ||
54
+ e != :on_kw || /\A(?:class|module|def)\z/ !~ t
55
+ }
56
+ (line, column), event, text = tokens.first
57
+ if line.nil?
58
+ @buffer.end_of_buffer
59
+ break
60
+ end
61
+ @buffer.goto_line(line)
62
+ tokens = tokens.drop(1)
63
+ end
64
+ while /\s/ =~ @buffer.char_after
65
+ @buffer.forward_char
66
+ end
67
+ end
68
+
69
+ def backward_definition(n = number_prefix_arg || 1)
70
+ tokens = Ripper.lex(@buffer.to_s).reverse
71
+ @buffer.beginning_of_line
72
+ n.times do |i|
73
+ tokens = tokens.drop_while { |(l, c), e, t|
74
+ l >= @buffer.current_line ||
75
+ e != :on_kw || /\A(?:class|module|def)\z/ !~ t
76
+ }
77
+ (line, column), event, text = tokens.first
78
+ if line.nil?
79
+ @buffer.beginning_of_buffer
80
+ break
81
+ end
82
+ @buffer.goto_line(line)
83
+ tokens = tokens.drop(1)
84
+ end
85
+ while /\s/ =~ @buffer.char_after
86
+ @buffer.forward_char
87
+ end
88
+ end
89
+
90
+ def compile
91
+ cmd = read_from_minibuffer("Compile: ", default: default_compile_command)
92
+ shell_execute(cmd, "*Ruby compile result*")
93
+ backtrace_mode
94
+ end
95
+
96
+ private
97
+
98
+ def calculate_indentation
99
+ if @buffer.current_line == 1
100
+ return 0
101
+ end
102
+ @buffer.save_excursion do
103
+ @buffer.beginning_of_line
104
+ bol_pos = @buffer.point
105
+ tokens = Ripper.lex(@buffer.substring(@buffer.point_min,
106
+ @buffer.point))
107
+ line, column, event, text = find_nearest_beginning_token(tokens)
108
+ if event == :on_lparen
109
+ return column + 1
110
+ end
111
+ if line
112
+ @buffer.goto_line(line)
113
+ else
114
+ @buffer.backward_line
115
+ end
116
+ @buffer.looking_at?(/[ \t]*/)
117
+ base_indentation = @buffer.match_string(0).
118
+ gsub(/\t/, " " * @buffer[:tab_width]).size
119
+ @buffer.goto_char(bol_pos)
120
+ if line.nil? ||
121
+ @buffer.looking_at?(/[ \t]*([}\])]|(end|else|elsif|when|rescue|ensure)\b)/)
122
+ indentation = base_indentation
123
+ else
124
+ indentation = base_indentation + @buffer[:ruby_indent_level]
125
+ end
126
+ _, last_event, last_text = tokens.reverse_each.find { |_, e, _|
127
+ e != :on_sp && e != :on_nl && e != :on_ignored_nl
128
+ }
129
+ if (last_event == :on_op && last_text != "|") ||
130
+ last_event == :on_period
131
+ indentation += @buffer[:ruby_indent_level]
132
+ end
133
+ indentation
134
+ end
135
+ end
136
+
137
+ BLOCK_END = {
138
+ "{" => "}",
139
+ "(" => ")",
140
+ "[" => "]"
141
+ }
142
+
143
+ def find_nearest_beginning_token(tokens)
144
+ stack = []
145
+ (tokens.size - 1).downto(0) do |i|
146
+ (line, column), event, text = tokens[i]
147
+ case event
148
+ when :on_kw
149
+ case text
150
+ when "class", "module", "def", "if", "unless", "case",
151
+ "do", "for", "while", "until", "begin"
152
+ if /\A(if|unless|while|until)\z/ =~ text
153
+ ts = tokens[0...i].reverse_each.take_while { |(l,_),| l == line }
154
+ t = ts.find { |_, e| e != :on_sp }
155
+ next if t && !(t[1] == :on_op && t[2] == "=")
156
+ end
157
+ if stack.empty?
158
+ return line, column, event, text
159
+ end
160
+ if stack.last != "end"
161
+ raise EditorError, "#{@buffer.name}:#{line}: Unmatched #{text}"
162
+ end
163
+ stack.pop
164
+ when "end"
165
+ stack.push(text)
166
+ end
167
+ when :on_rbrace, :on_rparen, :on_rbracket
168
+ stack.push(text)
169
+ when :on_lbrace, :on_lparen, :on_lbracket, :on_tlambeg
170
+ if stack.empty?
171
+ return line, column, event, text
172
+ end
173
+ if stack.last != BLOCK_END[text]
174
+ raise EditorError, "#{@buffer.name}:#{line}: Unmatched #{text}"
175
+ end
176
+ stack.pop
177
+ end
178
+ end
179
+ return nil
180
+ end
181
+
182
+ def default_compile_command
183
+ @buffer[:ruby_compile_command] ||
184
+ if File.exist?("Rakefile")
185
+ prefix = File.exist?("Gemfile") ? "bundle exec " : ""
186
+ prefix + "rake"
187
+ else
188
+ "ruby " + @buffer.file_name
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mode"
4
+ require_relative "modes/fundamental_mode"
5
+ require_relative "modes/programming_mode"
6
+ require_relative "modes/ruby_mode"
7
+ require_relative "modes/backtrace_mode"