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,690 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "io/wait"
5
+
6
+ module Textbringer
7
+ module Commands
8
+ include Utils
9
+
10
+ @@command_list = []
11
+
12
+ def self.list
13
+ @@command_list
14
+ end
15
+
16
+ def define_command(name, &block)
17
+ Commands.send(:define_method, name, &block)
18
+ @@command_list << name if !@@command_list.include?(name)
19
+ end
20
+ module_function :define_command
21
+
22
+ def undefine_command(name)
23
+ if @@command_list.include?(name)
24
+ Commands.send(:undef_method, name)
25
+ @@command_list.delete(name)
26
+ end
27
+ end
28
+ module_function :undefine_command
29
+
30
+ define_command(:version) do
31
+ message("Textbringer #{Textbringer::VERSION} "\
32
+ "(ruby #{RUBY_VERSION} [#{RUBY_PLATFORM}])")
33
+ end
34
+
35
+ [
36
+ :forward_char,
37
+ :backward_char,
38
+ :forward_word,
39
+ :backward_word,
40
+ :next_line,
41
+ :previous_line,
42
+ :delete_char,
43
+ :backward_delete_char,
44
+ ].each do |name|
45
+ define_command(name) do |n = number_prefix_arg|
46
+ Buffer.current.send(name, n)
47
+ end
48
+ end
49
+
50
+ [
51
+ :beginning_of_line,
52
+ :end_of_line,
53
+ :beginning_of_buffer,
54
+ :end_of_buffer,
55
+ :set_mark,
56
+ :exchange_point_and_mark,
57
+ :copy_region,
58
+ :kill_region,
59
+ :yank,
60
+ :newline,
61
+ :delete_region,
62
+ :transpose_chars
63
+ ].each do |name|
64
+ define_command(name) do
65
+ Buffer.current.send(name)
66
+ end
67
+ end
68
+
69
+ define_command(:goto_char) do
70
+ |n = read_from_minibuffer("Go to char: ")|
71
+ Buffer.current.goto_char(n.to_i)
72
+ Window.current.recenter_if_needed
73
+ end
74
+
75
+ define_command(:goto_line) do
76
+ |n = read_from_minibuffer("Go to line: ")|
77
+ Buffer.current.goto_line(n.to_i)
78
+ Window.current.recenter_if_needed
79
+ end
80
+
81
+ define_command(:self_insert) do |n = number_prefix_arg|
82
+ c = Controller.current.last_key.chr(Encoding::UTF_8)
83
+ merge_undo = Controller.current.last_command == :self_insert
84
+ n.times do
85
+ Buffer.current.insert(c, merge_undo)
86
+ end
87
+ end
88
+
89
+ define_command(:quoted_insert) do |n = number_prefix_arg|
90
+ c = Controller.current.read_char
91
+ if !c.is_a?(Integer)
92
+ raise "Invalid key"
93
+ end
94
+ ch = c.chr(Encoding::UTF_8)
95
+ n.times do
96
+ Buffer.current.insert(ch)
97
+ end
98
+ end
99
+
100
+ define_command(:kill_line) do
101
+ Buffer.current.kill_line(Controller.current.last_command == :kill_region)
102
+ Controller.current.this_command = :kill_region
103
+ end
104
+
105
+ define_command(:kill_word) do
106
+ Buffer.current.kill_word(Controller.current.last_command == :kill_region)
107
+ Controller.current.this_command = :kill_region
108
+ end
109
+
110
+ define_command(:yank_pop) do
111
+ if Controller.current.last_command != :yank
112
+ raise EditorError, "Previous command was not a yank"
113
+ end
114
+ Buffer.current.yank_pop
115
+ Controller.current.this_command = :yank
116
+ end
117
+
118
+ RE_SEARCH_STATUS = {
119
+ last_regexp: nil
120
+ }
121
+
122
+ define_command(:re_search_forward) do
123
+ |s = read_from_minibuffer("RE search: ",
124
+ default: RE_SEARCH_STATUS[:last_regexp])|
125
+ RE_SEARCH_STATUS[:last_regexp] = s
126
+ Buffer.current.re_search_forward(s)
127
+ end
128
+
129
+ def match_beginning(n)
130
+ Buffer.current.match_beginning(n)
131
+ end
132
+
133
+ def match_end(n)
134
+ Buffer.current.match_end(n)
135
+ end
136
+
137
+ def match_string(n)
138
+ Buffer.current.match_string(n)
139
+ end
140
+
141
+ def replace_match(s)
142
+ Buffer.current.replace_match(s)
143
+ end
144
+
145
+ define_command(:query_replace_regexp) do
146
+ |regexp = read_from_minibuffer("Query replace regexp: "),
147
+ to_str = read_from_minibuffer("Query replace regexp #{regexp} with: ")|
148
+ n = 0
149
+ begin
150
+ loop do
151
+ re_search_forward(regexp)
152
+ Window.current.recenter_if_needed
153
+ Buffer.current.set_visible_mark(match_beginning(0))
154
+ begin
155
+ Window.redisplay
156
+ c = read_single_char("Replace?", [?y, ?n, ?!, ?q, ?.])
157
+ case c
158
+ when ?y
159
+ replace_match(to_str)
160
+ n += 1
161
+ when ?n
162
+ # do nothing
163
+ when ?!
164
+ replace_match(to_str)
165
+ n += 1 + Buffer.current.replace_regexp_forward(regexp, to_str)
166
+ Buffer.current.merge_undo(2)
167
+ break
168
+ when ?q
169
+ break
170
+ when ?.
171
+ replace_match(to_str)
172
+ n += 1
173
+ break
174
+ end
175
+ ensure
176
+ Buffer.current.delete_visible_mark
177
+ end
178
+ end
179
+ rescue SearchError
180
+ end
181
+ if n == 1
182
+ message("Replaced 1 occurrence")
183
+ else
184
+ message("Replaced #{n} occurrences")
185
+ end
186
+ end
187
+
188
+ define_command(:undo) do
189
+ Buffer.current.undo
190
+ message("Undo!")
191
+ end
192
+
193
+ define_command(:redo) do
194
+ Buffer.current.redo
195
+ message("Redo!")
196
+ end
197
+
198
+ define_command(:resize_window) do
199
+ Window.resize
200
+ end
201
+
202
+ define_command(:recenter) do
203
+ Window.current.recenter
204
+ Window.redraw
205
+ end
206
+
207
+ define_command(:scroll_up) do
208
+ Window.current.scroll_up
209
+ end
210
+
211
+ define_command(:scroll_down) do
212
+ Window.current.scroll_down
213
+ end
214
+
215
+ define_command(:delete_window) do
216
+ Window.delete_window
217
+ end
218
+
219
+ define_command(:delete_other_windows) do
220
+ Window.delete_other_windows
221
+ end
222
+
223
+ define_command(:split_window) do
224
+ Window.current.split
225
+ end
226
+
227
+ define_command(:other_window) do
228
+ Window.other_window
229
+ end
230
+
231
+ define_command(:exit_textbringer) do |status = 0|
232
+ if Buffer.any? { |buffer| /\A\*/ !~ buffer.name && buffer.modified? }
233
+ return unless yes_or_no?("Unsaved buffers exist; exit anyway?")
234
+ end
235
+ exit(status)
236
+ end
237
+
238
+ define_command(:suspend_textbringer) do
239
+ Ncurses.endwin
240
+ Process.kill(:STOP, $$)
241
+ end
242
+
243
+ define_command(:pwd) do
244
+ message(Dir.pwd)
245
+ end
246
+
247
+ define_command(:chdir) do
248
+ |dir_name = read_file_name("Change directory: ")|
249
+ Dir.chdir(dir_name)
250
+ end
251
+
252
+ define_command(:find_file) do
253
+ |file_name = read_file_name("Find file: ")|
254
+ buffer = Buffer.find_file(file_name)
255
+ if buffer.new_file?
256
+ message("New file")
257
+ end
258
+ switch_to_buffer(buffer)
259
+ mode = Mode.list.find { |mode|
260
+ mode.file_name_pattern &&
261
+ mode.file_name_pattern =~ File.basename(buffer.file_name)
262
+ } || FundamentalMode
263
+ send(mode.command_name)
264
+ end
265
+
266
+ define_command(:switch_to_buffer) do
267
+ |buffer_name = read_buffer("Switch to buffer: ")|
268
+ if buffer_name.is_a?(Buffer)
269
+ buffer = buffer_name
270
+ else
271
+ buffer = Buffer[buffer_name]
272
+ end
273
+ if buffer
274
+ Window.current.buffer = Buffer.current = buffer
275
+ else
276
+ message("No such buffer: #{buffer_name}")
277
+ end
278
+ end
279
+
280
+ define_command(:save_buffer) do
281
+ if Buffer.current.file_name.nil?
282
+ Buffer.current.file_name = read_file_name("File to save in: ")
283
+ next if Buffer.current.file_name.nil?
284
+ end
285
+ if Buffer.current.file_modified?
286
+ unless yes_or_no?("File changed on disk. Save anyway?")
287
+ message("Cancelled")
288
+ next
289
+ end
290
+ end
291
+ Buffer.current.save
292
+ message("Wrote #{Buffer.current.file_name}")
293
+ end
294
+
295
+ define_command(:write_file) do
296
+ |file_name = read_file_name("Write file: ")|
297
+ if File.directory?(file_name)
298
+ file_name = File.expand_path(Buffer.current.name, file_name)
299
+ end
300
+ if File.exist?(file_name)
301
+ unless y_or_n?("File `#{file_name}' exists; overwrite?")
302
+ message("Cancelled")
303
+ next
304
+ end
305
+ end
306
+ Buffer.current.save(file_name)
307
+ message("Wrote #{Buffer.current.file_name}")
308
+ end
309
+
310
+ define_command(:kill_buffer) do
311
+ |name = read_buffer("Kill buffer: ", default: Buffer.current.name)|
312
+ if name.is_a?(Buffer)
313
+ buffer = name
314
+ else
315
+ buffer = Buffer[name]
316
+ end
317
+ if buffer.modified?
318
+ next unless yes_or_no?("The last change is not saved; kill anyway?")
319
+ message("Arioch! Arioch! Blood and souls for my Lord Arioch!")
320
+ end
321
+ buffer.kill
322
+ if Buffer.count == 0
323
+ buffer = Buffer.new_buffer("*scratch*")
324
+ switch_to_buffer(buffer)
325
+ elsif Buffer.current.nil?
326
+ switch_to_buffer(Buffer.last)
327
+ end
328
+ end
329
+
330
+ define_command(:set_buffer_file_encoding) do
331
+ |enc = read_from_minibuffer("File encoding: ",
332
+ default: Buffer.current.file_encoding.name)|
333
+ Buffer.current.file_encoding = Encoding.find(enc)
334
+ end
335
+
336
+ define_command(:set_buffer_file_format) do
337
+ |format = read_from_minibuffer("File format: ",
338
+ default: Buffer.current.file_format.to_s)|
339
+ Buffer.current.file_format = format
340
+ end
341
+
342
+ define_command(:execute_command) do
343
+ |cmd = read_command_name("M-x ").strip.intern|
344
+ unless Commands.list.include?(cmd)
345
+ raise EditorError, "Undefined command: #{cmd}"
346
+ end
347
+ Controller.current.this_command = cmd
348
+ send(cmd)
349
+ end
350
+
351
+ define_command(:eval_expression) do
352
+ |s = read_from_minibuffer("Eval: ")|
353
+ message(eval(s, TOPLEVEL_BINDING, "(eval_expression)", 1).inspect)
354
+ end
355
+
356
+ define_command(:eval_buffer) do
357
+ buffer = Buffer.current
358
+ result = eval(buffer.to_s, TOPLEVEL_BINDING,
359
+ buffer.file_name || buffer.name, 1)
360
+ message(result.inspect)
361
+ end
362
+
363
+ define_command(:eval_region) do
364
+ buffer = Buffer.current
365
+ b, e = buffer.point, buffer.mark
366
+ if e < b
367
+ b, e = e, b
368
+ end
369
+ result = eval(buffer.substring(b, e), TOPLEVEL_BINDING,
370
+ "(eval_region)", 1)
371
+ message(result.inspect)
372
+ end
373
+
374
+ define_command(:exit_recursive_edit) do
375
+ if @recursive_edit_level == 0
376
+ raise EditorError, "No recursive edit is in progress"
377
+ end
378
+ throw RECURSIVE_EDIT_TAG, false
379
+ end
380
+
381
+ define_command(:abort_recursive_edit) do
382
+ if @recursive_edit_level == 0
383
+ raise EditorError, "No recursive edit is in progress"
384
+ end
385
+ throw RECURSIVE_EDIT_TAG, true
386
+ end
387
+
388
+ define_command(:top_level) do
389
+ throw TOP_LEVEL_TAG
390
+ end
391
+
392
+ define_command(:complete_minibuffer) do
393
+ minibuffer = Buffer.minibuffer
394
+ completion_proc = minibuffer[:completion_proc]
395
+ if completion_proc
396
+ s = completion_proc.call(minibuffer.to_s)
397
+ if s
398
+ minibuffer.delete_region(minibuffer.point_min,
399
+ minibuffer.point_max)
400
+ minibuffer.insert(s)
401
+ end
402
+ end
403
+ end
404
+
405
+ UNIVERSAL_ARGUMENT_MAP = Keymap.new
406
+ (?0..?9).each do |c|
407
+ UNIVERSAL_ARGUMENT_MAP.define_key(c, :digit_argument)
408
+ GLOBAL_MAP.define_key("\e#{c}", :digit_argument)
409
+ end
410
+ UNIVERSAL_ARGUMENT_MAP.define_key(?-, :negative_argument)
411
+ UNIVERSAL_ARGUMENT_MAP.define_key(?\C-u, :universal_argument_more)
412
+
413
+ def universal_argument_mode
414
+ set_transient_map(UNIVERSAL_ARGUMENT_MAP)
415
+ end
416
+
417
+ define_command(:universal_argument) do
418
+ Controller.current.prefix_arg = [4]
419
+ universal_argument_mode
420
+ end
421
+
422
+ def current_prefix_arg
423
+ Controller.current.current_prefix_arg
424
+ end
425
+
426
+ def number_prefix_arg
427
+ arg = current_prefix_arg
428
+ case arg
429
+ when Integer
430
+ arg
431
+ when Array
432
+ arg.first
433
+ when :-
434
+ -1
435
+ else
436
+ 1
437
+ end
438
+ end
439
+
440
+ define_command(:digit_argument) do
441
+ |arg = current_prefix_arg|
442
+ n = last_key.chr.to_i
443
+ Controller.current.prefix_arg =
444
+ case arg
445
+ when Integer
446
+ arg * 10 + (arg < 0 ? -n : n)
447
+ when :-
448
+ -n
449
+ else
450
+ n
451
+ end
452
+ universal_argument_mode
453
+ end
454
+
455
+ define_command(:negative_argument) do
456
+ |arg = current_prefix_arg|
457
+ Controller.current.prefix_arg =
458
+ case arg
459
+ when Integer
460
+ -arg
461
+ when :-
462
+ nil
463
+ else
464
+ :-
465
+ end
466
+ universal_argument_mode
467
+ end
468
+
469
+ define_command(:universal_argument_more) do
470
+ |arg = current_prefix_arg|
471
+ Controller.current.prefix_arg =
472
+ case arg
473
+ when Array
474
+ [4 * arg.first]
475
+ when :-
476
+ [-4]
477
+ else
478
+ nil
479
+ end
480
+ if Controller.current.prefix_arg
481
+ universal_argument_mode
482
+ end
483
+ end
484
+
485
+ define_command(:keyboard_quit) do
486
+ raise Quit
487
+ end
488
+
489
+ define_command(:recursive_edit) do
490
+ Controller.current.recursive_edit
491
+ end
492
+
493
+ ISEARCH_MODE_MAP = Keymap.new
494
+ (0x20..0x7e).each do |c|
495
+ ISEARCH_MODE_MAP.define_key(c, :isearch_printing_char)
496
+ end
497
+ ISEARCH_MODE_MAP.define_key(?\t, :isearch_printing_char)
498
+ ISEARCH_MODE_MAP.handle_undefined_key do |key|
499
+ if key.is_a?(Integer) && key > 0x80
500
+ begin
501
+ key.chr(Encoding::UTF_8)
502
+ :isearch_printing_char
503
+ rescue RangeError
504
+ nil
505
+ end
506
+ else
507
+ nil
508
+ end
509
+ end
510
+ ISEARCH_MODE_MAP.define_key(:backspace, :isearch_delete_char)
511
+ ISEARCH_MODE_MAP.define_key(?\C-h, :isearch_delete_char)
512
+ ISEARCH_MODE_MAP.define_key(?\C-s, :isearch_repeat_forward)
513
+ ISEARCH_MODE_MAP.define_key(?\C-r, :isearch_repeat_backward)
514
+ ISEARCH_MODE_MAP.define_key(?\n, :isearch_exit)
515
+ ISEARCH_MODE_MAP.define_key(?\C-g, :isearch_abort)
516
+
517
+ ISEARCH_STATUS = {
518
+ forward: true,
519
+ string: "",
520
+ last_string: "",
521
+ start: 0,
522
+ last_pos: 0
523
+ }
524
+
525
+ define_command(:isearch_forward) do
526
+ isearch_mode(true)
527
+ end
528
+
529
+ define_command(:isearch_backward) do
530
+ isearch_mode(false)
531
+ end
532
+
533
+ def isearch_mode(forward)
534
+ ISEARCH_STATUS[:forward] = forward
535
+ ISEARCH_STATUS[:string] = String.new
536
+ Controller.current.overriding_map = ISEARCH_MODE_MAP
537
+ run_hooks(:isearch_mode_hook)
538
+ add_hook(:pre_command_hook, :isearch_pre_command_hook)
539
+ ISEARCH_STATUS[:start] = ISEARCH_STATUS[:last_pos] = Buffer.current.point
540
+ if Buffer.current != Buffer.minibuffer
541
+ message(isearch_prompt, log: false)
542
+ end
543
+ end
544
+
545
+ def isearch_prompt
546
+ if ISEARCH_STATUS[:forward]
547
+ "I-search: "
548
+ else
549
+ "I-search backward: "
550
+ end
551
+ end
552
+
553
+ def isearch_pre_command_hook
554
+ if /\Aisearch_/ !~ Controller.current.this_command
555
+ isearch_done
556
+ end
557
+ end
558
+
559
+ def isearch_done
560
+ Buffer.current.delete_visible_mark
561
+ Controller.current.overriding_map = nil
562
+ remove_hook(:pre_command_hook, :isearch_pre_command_hook)
563
+ ISEARCH_STATUS[:last_string] = ISEARCH_STATUS[:string]
564
+ end
565
+
566
+ define_command(:isearch_exit) do
567
+ isearch_done
568
+ end
569
+
570
+ define_command(:isearch_abort) do
571
+ goto_char(Buffer.current[:isearch_start])
572
+ isearch_done
573
+ raise Quit
574
+ end
575
+
576
+ define_command(:isearch_printing_char) do
577
+ c = Controller.current.last_key.chr(Encoding::UTF_8)
578
+ ISEARCH_STATUS[:string].concat(c)
579
+ isearch_search
580
+ end
581
+
582
+ define_command(:isearch_delete_char) do
583
+ ISEARCH_STATUS[:string].chop!
584
+ isearch_search
585
+ end
586
+
587
+ def isearch_search
588
+ forward = ISEARCH_STATUS[:forward]
589
+ options = if /\A[A-Z]/ =~ ISEARCH_STATUS[:string]
590
+ nil
591
+ else
592
+ Regexp::IGNORECASE
593
+ end
594
+ re = Regexp.new(Regexp.quote(ISEARCH_STATUS[:string]), options)
595
+ last_pos = ISEARCH_STATUS[:last_pos]
596
+ offset = forward ? last_pos : last_pos - ISEARCH_STATUS[:string].bytesize
597
+ if Buffer.current.byteindex(forward, re, offset)
598
+ if Buffer.current != Buffer.minibuffer
599
+ message(isearch_prompt + ISEARCH_STATUS[:string], log: false)
600
+ end
601
+ Buffer.current.set_visible_mark(forward ? match_beginning(0) :
602
+ match_end(0))
603
+ goto_char(forward ? match_end(0) : match_beginning(0))
604
+ else
605
+ if Buffer.current != Buffer.minibuffer
606
+ message("Falling " + isearch_prompt + ISEARCH_STATUS[:string],
607
+ log: false)
608
+ end
609
+ end
610
+ end
611
+
612
+ def isearch_repeat_forward
613
+ isearch_repeat(true)
614
+ end
615
+
616
+ def isearch_repeat_backward
617
+ isearch_repeat(false)
618
+ end
619
+
620
+ def isearch_repeat(forward)
621
+ ISEARCH_STATUS[:forward] = forward
622
+ ISEARCH_STATUS[:last_pos] = Buffer.current.point
623
+ if ISEARCH_STATUS[:string].empty?
624
+ ISEARCH_STATUS[:string] = ISEARCH_STATUS[:last_string]
625
+ end
626
+ isearch_search
627
+ end
628
+
629
+ define_command(:shell_execute) do
630
+ |cmd = read_from_minibuffer("Shell execute: "),
631
+ buffer_name = "*Shell output*"|
632
+ buffer = Buffer.find_or_new(buffer_name)
633
+ switch_to_buffer(buffer)
634
+ buffer.read_only = false
635
+ buffer.clear
636
+ Window.redisplay
637
+ signals = [:INT, :TERM, :KILL]
638
+ begin
639
+ Open3.popen2e(cmd) do |input, output, wait_thread|
640
+ input.close
641
+ loop do
642
+ status = output.wait_readable(0.5)
643
+ if status == false
644
+ break # EOF
645
+ end
646
+ if status
647
+ begin
648
+ s = output.read_nonblock(1024)
649
+ buffer.insert(s)
650
+ Window.redisplay
651
+ rescue EOFError
652
+ break
653
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
654
+ next
655
+ end
656
+ end
657
+ if received_keyboard_quit?
658
+ if signals.empty?
659
+ keyboard_quit
660
+ else
661
+ sig = signals.shift
662
+ message("Send #{sig} to #{wait_thread.pid}")
663
+ Process.kill(sig, wait_thread.pid)
664
+ end
665
+ end
666
+ end
667
+ status = wait_thread.value
668
+ pid = status.pid
669
+ if status.exited?
670
+ code = status.exitstatus
671
+ message("Process #{pid} exited with status code #{code}")
672
+ elsif status.signaled?
673
+ signame = Signal.signame(status.termsig)
674
+ message("Process #{pid} was killed by #{signame}")
675
+ else
676
+ message("Process #{pid} exited")
677
+ end
678
+ end
679
+ ensure
680
+ buffer.read_only = true
681
+ end
682
+ end
683
+ end
684
+
685
+ class Quit < StandardError
686
+ def initialize
687
+ super("Quit")
688
+ end
689
+ end
690
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textbringer
4
+ CONFIG = {
5
+ tab_width: 8,
6
+ indent_tabs_mode: false,
7
+ case_fold_search: true
8
+ }
9
+ end