textbringer 0.1.0

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