textbringer 0.1.0

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