textbringer 1.4.1 → 2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c32799602faad1b9a13a0d298bcf922aa4fcac7430fc9f17258e457ef0463bd8
4
- data.tar.gz: 853b1b04ed778c8f04117ca23bfe22e0989d79028869d54fbcca7fff65203795
3
+ metadata.gz: a7815997c04d50a71d83ccb13cc2b2ec378679b26e74d6a2add40635d033c7ad
4
+ data.tar.gz: 82193a90e99ba1d56df8c1f6cfde23ae6560585273e3c25777d55f3f7fa3c78a
5
5
  SHA512:
6
- metadata.gz: 2d2cc2d14d52bdfe261a5a1499db09a76917e20040b142d5c266131fa255d90cb5823059b719f40cfb608453a1090cc4affa0abf6f741aa3bd587be47b092c87
7
- data.tar.gz: c068f9f253ea113ecf2bd082600a89c5dd483a4240a1546f06e08e10e76bb22a9e0ee31fcbb753755687f43b19d73b492f3d3cb207657bd170f7a67ef382b8d9
6
+ metadata.gz: 6bc44799c79165fee0acf71efedd019692432da5a3b6f6c005a485587e7aecd44e2a82ba380272f42fa51a4519e1d461c9f1cdcb5f2f25f76a62029dff0358ca
7
+ data.tar.gz: 21ae07326bfbb38398950efcf5d0f0e9abea6195aafa26430691d2879d9e92ff3f4d50a4be5f77ed1d5082fbe776724c737ec7103d7ccd2d977638c3e160be81
@@ -24,21 +24,21 @@ jobs:
24
24
  steps:
25
25
  # Set up
26
26
  - name: Harden Runner
27
- uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
27
+ uses: step-security/harden-runner@v2
28
28
  with:
29
29
  egress-policy: audit
30
30
 
31
- - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
31
+ - uses: actions/checkout@v4
32
32
 
33
33
  - name: Set up Ruby
34
- uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49 # v1.176.0
34
+ uses: ruby/setup-ruby@v1
35
35
  with:
36
36
  bundler-cache: true
37
37
  ruby-version: ruby
38
38
 
39
39
  # Release
40
40
  - name: Publish to RubyGems
41
- uses: rubygems/release-gem@612653d273a73bdae1df8453e090060bb4db5f31 # v1
41
+ uses: rubygems/release-gem@v1
42
42
 
43
43
  - name: Create GitHub release
44
44
  run: |
@@ -71,6 +71,8 @@ module Textbringer
71
71
  false
72
72
  end
73
73
 
74
+ WORD_COMPONENT_REGEXP = /\p{Letter}|\p{Number}|\p{M}/
75
+
74
76
  if !defined?(@@detect_encoding_proc)
75
77
  @@detect_encoding_proc = DEFAULT_DETECT_ENCODING
76
78
 
@@ -683,7 +685,8 @@ module Textbringer
683
685
  forward_char(-n)
684
686
  end
685
687
 
686
- def forward_word(n = 1, regexp: /\p{Letter}|\p{Number}/)
688
+ def forward_word(n = 1, regexp: WORD_COMPONENT_REGEXP)
689
+ return backward_word(-n) if n < 0
687
690
  n.times do
688
691
  while !end_of_buffer? && regexp !~ char_after
689
692
  forward_char
@@ -694,7 +697,8 @@ module Textbringer
694
697
  end
695
698
  end
696
699
 
697
- def backward_word(n = 1, regexp: /\p{Letter}|\p{Number}/)
700
+ def backward_word(n = 1, regexp: WORD_COMPONENT_REGEXP)
701
+ return forward_word(-n) if n < 0
698
702
  n.times do
699
703
  break if beginning_of_buffer?
700
704
  backward_char
@@ -820,6 +824,10 @@ module Textbringer
820
824
  @point > mark.location
821
825
  end
822
826
 
827
+ def point_compare_mark(mark)
828
+ @point - mark.location
829
+ end
830
+
823
831
  def exchange_point_and_mark(mark = @mark)
824
832
  if mark.nil?
825
833
  raise EditorError, "The mark is not set"
@@ -989,14 +997,23 @@ module Textbringer
989
997
  end
990
998
  end
991
999
 
992
- def replace(str, start: point_min, end: point_max)
1000
+ def replace(str = nil, start: point_min, end: point_max, &block)
1001
+ end_pos = binding.local_variable_get(:end)
1002
+ if str.nil?
1003
+ str = block.call(substring(start, end_pos))
1004
+ end
993
1005
  composite_edit do
994
- delete_region(start, binding.local_variable_get(:end))
1006
+ delete_region(start, end_pos)
995
1007
  goto_char(start)
996
1008
  insert(str)
997
1009
  end
998
1010
  end
999
1011
 
1012
+ def replace_region(str = nil, &block)
1013
+ s, e = Buffer.region_boundaries(@point, mark)
1014
+ replace(str, start: s, end: e, &block)
1015
+ end
1016
+
1000
1017
  def clear
1001
1018
  check_read_only_flag
1002
1019
  @contents = String.new
@@ -1040,6 +1057,27 @@ module Textbringer
1040
1057
  end
1041
1058
  end
1042
1059
 
1060
+ def convert_word(n = 1, &block)
1061
+ s = point
1062
+ forward_word(n)
1063
+ e = point
1064
+ s, e = Buffer.region_boundaries(s, e)
1065
+ str = substring(s, e).gsub(/[\p{Letter}\p{Number}]+/, &block)
1066
+ replace(str, start: s, end: e)
1067
+ end
1068
+
1069
+ def downcase_word(n = 1)
1070
+ convert_word(n, &:downcase)
1071
+ end
1072
+
1073
+ def upcase_word(n = 1)
1074
+ convert_word(n, &:upcase)
1075
+ end
1076
+
1077
+ def capitalize_word(n = 1)
1078
+ convert_word(n, &:capitalize)
1079
+ end
1080
+
1043
1081
  def insert_for_yank(s)
1044
1082
  if @mark.nil? || !point_at_mark?(@mark)
1045
1083
  push_mark
@@ -1441,7 +1479,7 @@ module Textbringer
1441
1479
  def set_contents(s, enc)
1442
1480
  case s.encoding
1443
1481
  when Encoding::UTF_8, Encoding::ASCII_8BIT
1444
- @contents = s.frozen? ? s.dup : s
1482
+ @contents = +s
1445
1483
  else
1446
1484
  @contents = s.encode(Encoding::UTF_8)
1447
1485
  end
@@ -264,5 +264,38 @@ module Textbringer
264
264
  e = buffer.re_search_forward(Regexp.quote(char), count: count)
265
265
  buffer.kill_region(s, e)
266
266
  end
267
+
268
+ define_command(:downcase_word,
269
+ doc: <<~EOD) do
270
+ Convert to lower case from point to end of word, moving over.
271
+ EOD
272
+ |n = number_prefix_arg|
273
+ Buffer.current.downcase_word(n)
274
+ end
275
+
276
+ define_command(:upcase_word,
277
+ doc: <<~EOD) do
278
+ Convert to upper case from point to end of word, moving over.
279
+ EOD
280
+ |n = number_prefix_arg|
281
+ Buffer.current.upcase_word(n)
282
+ end
283
+
284
+ define_command(:capitalize_word,
285
+ doc: <<~EOD) do
286
+ Capitalize from point to the end of word, moving over.
287
+ EOD
288
+ |n = number_prefix_arg|
289
+ Buffer.current.capitalize_word(n)
290
+ end
291
+
292
+ define_command(:insert_char,
293
+ doc: <<~EOD) do
294
+ Insert character.
295
+ EOD
296
+ |c = read_from_minibuffer("Insert character (hex): "),
297
+ n = number_prefix_arg|
298
+ Buffer.current.insert(c.hex.chr(Encoding::UTF_8) * n)
299
+ end
267
300
  end
268
301
  end
@@ -128,7 +128,7 @@ module Textbringer
128
128
  |dir_name = read_file_name("Change directory: ",
129
129
  default: Buffer.current.file_name &&
130
130
  File.dirname(Buffer.current.file_name))|
131
- Dir.chdir(dir_name)
131
+ Dir.chdir(File.expand_path(dir_name))
132
132
  end
133
133
 
134
134
  define_command(:find_alternate_file, doc: "Find an alternate file.") do
@@ -18,9 +18,14 @@ module Textbringer
18
18
  help.clear
19
19
  yield(help)
20
20
  help.beginning_of_buffer
21
- switch_to_buffer(help)
22
- help_mode
21
+ help.apply_mode(HelpMode)
23
22
  end
23
+ if Window.list.size == 1
24
+ split_window
25
+ end
26
+ windows = Window.list
27
+ i = (windows.index(Window.current) + 1) % windows.size
28
+ windows[i].buffer = help
24
29
  end
25
30
  private :show_help
26
31
 
@@ -143,6 +148,43 @@ module Textbringer
143
148
  push_help_command([:describe_method, name])
144
149
  end
145
150
 
151
+ define_command(:describe_char,
152
+ doc: "Describe the char after point") do
153
+ require "unicode/name"
154
+ require "unicode/categories"
155
+ require "unicode/blocks"
156
+ require "unicode/scripts"
157
+ require "unicode/types"
158
+
159
+ show_help do |help|
160
+ buffer = Buffer.current
161
+ c = buffer.char_after
162
+ if c.nil?
163
+ raise "No character follows specified position"
164
+ end
165
+ percent = (100.0 * buffer.point / buffer.bytesize).to_i
166
+ char = /[\0-\x20\x7f]/.match?(c) ? Keymap.key_name(c) : c
167
+ codepoint = "U+%04X" % c.ord
168
+ name = Unicode::Name.readable(c)
169
+ category = Unicode::Categories.category(c)
170
+ category_long = Unicode::Categories.category(c, format: :long)
171
+ script = Unicode::Scripts.script(c)
172
+ block = Unicode::Blocks.block(c)
173
+ type = Unicode::Types.type(c)
174
+ help.insert(<<EOF)
175
+ position: #{buffer.point} of #{buffer.bytesize} (#{percent}%), column: #{buffer.current_column}
176
+ character: #{char}
177
+ codepoint: #{codepoint}
178
+ name: #{name}
179
+ category: #{category} (#{category_long})
180
+ script: #{script}
181
+ block: #{block}
182
+ type: #{type}
183
+ EOF
184
+ end
185
+ push_help_command([:describe_char])
186
+ end
187
+
146
188
  define_command(:help_go_back, doc: "Go back to the previous help.") do
147
189
  if !HELP_RING.empty?
148
190
  HELP_RING.rotate(1)
@@ -353,5 +353,30 @@ module Textbringer
353
353
  define_command(:jit_resume) do
354
354
  RubyVM::MJIT.resume
355
355
  end
356
+
357
+ define_command(:what_cursor_position,
358
+ doc: "Print info on cursor position.") do
359
+ |arg = current_prefix_arg|
360
+
361
+ buffer = Buffer.current
362
+ c = buffer.char_after
363
+ if c
364
+ char = format("Char: %s (U+%04X) ",
365
+ /[\0-\x20\x7f]/.match?(c) ? Keymap.key_name(c) : c,
366
+ c.ord)
367
+ else
368
+ char = ""
369
+ end
370
+ if buffer.bytesize == 0
371
+ percent = "EOB"
372
+ else
373
+ percent = (100.0 * buffer.point / buffer.bytesize).to_i
374
+ end
375
+ column = buffer.current_column
376
+ message("#{char}point=#{buffer.point} of #{buffer.bytesize} (#{percent}%) column=#{column}")
377
+ if arg && c
378
+ describe_char
379
+ end
380
+ end
356
381
  end
357
382
  end
@@ -88,7 +88,10 @@ module Textbringer
88
88
 
89
89
  define_command(:insert_register) do
90
90
  |register = read_register("Insert register:"),
91
- arg = current_prefix_arg|
91
+ arg = nil|
92
+ if arg.nil? && Controller.current.this_command == :insert_register
93
+ arg = !current_prefix_arg
94
+ end
92
95
  buffer = Buffer.current
93
96
  str = REGISTERS[register]
94
97
  if arg
@@ -0,0 +1,39 @@
1
+ module Textbringer
2
+ module Commands
3
+ define_command(:ucs_normalize_nfc_region,
4
+ doc: <<~EOD) do
5
+ Compose the region according to the Unicode NFC.
6
+ EOD
7
+ Buffer.current.replace_region do |s|
8
+ s.unicode_normalize(:nfc)
9
+ end
10
+ end
11
+
12
+ define_command(:ucs_normalize_nfd_region,
13
+ doc: <<~EOD) do
14
+ Decompose the region according to the Unicode NFD.
15
+ EOD
16
+ Buffer.current.replace_region do |s|
17
+ s.unicode_normalize(:nfd)
18
+ end
19
+ end
20
+
21
+ define_command(:ucs_normalize_nfkc_region,
22
+ doc: <<~EOD) do
23
+ Compose the region according to the Unicode NFKC.
24
+ EOD
25
+ Buffer.current.replace_region do |s|
26
+ s.unicode_normalize(:nfkc)
27
+ end
28
+ end
29
+
30
+ define_command(:ucs_normalize_nfkd_region,
31
+ doc: <<~EOD) do
32
+ Decompose the region according to the Unicode NFKD.
33
+ EOD
34
+ Buffer.current.replace_region do |s|
35
+ s.unicode_normalize(:nfkd)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -9,10 +9,18 @@ module Textbringer
9
9
  def flush
10
10
  end
11
11
 
12
- def method_missing(mid, ...)
13
- buffer = StringIO.new
14
- buffer.send(mid, ...)
15
- write(buffer.string)
12
+ [
13
+ :print,
14
+ :printf,
15
+ :putc,
16
+ :puts,
17
+ :"<<"
18
+ ].each do |mid|
19
+ define_method(mid) do |*args|
20
+ buffer = StringIO.new
21
+ buffer.send(mid, *args)
22
+ write(buffer.string)
23
+ end
16
24
  end
17
25
  end
18
26
  end
@@ -59,5 +59,27 @@ module Textbringer
59
59
  def handle_event(event)
60
60
  raise EditorError, "subclass must override InputMethod#handle_event"
61
61
  end
62
+
63
+ def with_target_buffer(&block)
64
+ if isearch_mode?
65
+ @isearch_buffer ||= Buffer.new
66
+ if @isearch_buffer.to_s != ISEARCH_STATUS[:string]
67
+ @isearch_buffer.replace(ISEARCH_STATUS[:string])
68
+ end
69
+ @isearch_buffer.modified = false
70
+ begin
71
+ block.call(@isearch_buffer)
72
+ ensure
73
+ ISEARCH_STATUS[:string] = @isearch_buffer.to_s
74
+ isearch_search if @isearch_buffer.modified?
75
+ if Buffer.current != Buffer.minibuffer
76
+ message(isearch_prompt + ISEARCH_STATUS[:string], log: false)
77
+ end
78
+ Window.redisplay
79
+ end
80
+ else
81
+ block.call(Buffer.current)
82
+ end
83
+ end
62
84
  end
63
85
  end
@@ -0,0 +1,73 @@
1
+ module Textbringer
2
+ class HangulInputMethod < InputMethod
3
+ KEY_TO_COMPATIBILITY_JAMO = {
4
+ "q" => "ㅂ", "w" => "ㅈ", "e" => "ㄷ", "r" => "ㄱ", "t" => "ㅅ",
5
+ "y" => "ㅛ", "u" => "ㅕ", "i" => "ㅑ", "o" => "ㅐ", "p" => "ㅔ",
6
+ "a" => "ㅁ", "s" => "ㄴ", "d" => "ㅇ", "f" => "ㄹ", "g" => "ㅎ",
7
+ "h" => "ㅗ", "j" => "ㅓ", "k" => "ㅏ", "l" => "ㅣ",
8
+ "z" => "ㅋ", "x" => "ㅌ", "c" => "ㅊ", "v" => "ㅍ", "b" => "ㅠ",
9
+ "n" => "ㅜ", "m" => "ㅡ",
10
+ "Q" => "ㅃ", "W" => "ㅉ", "E" => "ㄸ", "R" => "ㄲ", "T" => "ㅆ",
11
+ "O" => "ㅒ", "P" => "ㅖ"
12
+ }
13
+
14
+ COMPATIBILITY_JAMO_TO_FINAL = {
15
+ "ㄱ" => "ᆨ", "ㄲ" => "ᆩ", "ㄳ" => "ᆪ", "ㄴ" => "ᆫ",
16
+ "ㄵ" => "ᆬ", "ㄶ" => "ᆭ", "ㄷ" => "ᆮ", "ㄹ" => "ᆯ",
17
+ "ㄺ" => "ᆰ", "ㄻ" => "ᆱ", "ㄼ" => "ᆲ", "ㄽ" => "ᆳ",
18
+ "ㄾ" => "ᆴ", "ㄿ" => "ᆵ", "ㅀ" => "ᆶ", "ㅁ" => "ᆷ",
19
+ "ㅂ" => "ᆸ", "ㅄ" => "ᆹ", "ㅅ" => "ᆺ", "ㅆ" => "ᆻ",
20
+ "ㅇ" => "ᆼ", "ㅈ" => "ᆽ", "ㅊ" => "ᆾ", "ㅋ" => "ᆿ",
21
+ "ㅌ" => "ᇀ", "ㅍ" => "ᇁ", "ㅎ" => "ᇂ"
22
+ }
23
+
24
+ FINAL_TO_INITIAL = {
25
+ "ᆨ" => "ᄀ", "ᆩ" => "ᄁ", "ᆫ" => "ᄂ", "ᆮ" => "ᄃ",
26
+ "ᆯ" => "ᄅ", "ᆷ" => "ᄆ", "ᆸ" => "ᄇ", "ᆺ" => "ᄉ",
27
+ "ᆻ" => "ᄊ", "ᆼ" => "ᄋ", "ᆽ" => "ᄌ", "ᆾ" => "ᄎ",
28
+ "ᆿ" => "ᄏ", "ᇀ" => "ᄐ", "ᇁ" => "ᄑ", "ᇂ" => "ᄒ"
29
+ }
30
+
31
+ def status
32
+ "한"
33
+ end
34
+
35
+ def handle_event(event)
36
+ return event if !event.is_a?(String)
37
+ jamo = KEY_TO_COMPATIBILITY_JAMO[event]
38
+ return event if jamo.nil?
39
+ with_target_buffer do |buffer|
40
+ prev = buffer.char_before
41
+ if /[\u{3131}-\u{3183}\u{ac00}-\u{d7a3}]/.match?(prev) # jamo or syllables
42
+ decomposed_prev, prev = decompose_prev(prev, jamo)
43
+ if c = compose_hangul(prev, jamo)
44
+ buffer.backward_delete_char
45
+ c = decomposed_prev + c if decomposed_prev
46
+ buffer.insert(c)
47
+ Window.redisplay
48
+ return nil
49
+ end
50
+ end
51
+ end
52
+ jamo
53
+ end
54
+
55
+ def decompose_prev(prev, jamo)
56
+ if /[\u{ac00}-\u{d7a3}]/.match?(prev) && # syllables
57
+ /[\u{314f}-\u{3163}]/.match?(jamo) # vowels
58
+ s = prev.unicode_normalize(:nfd)
59
+ if s.size == 3 && (initial = FINAL_TO_INITIAL[s[2]])
60
+ return s[0, 2].unicode_normalize(:nfc), initial
61
+ end
62
+ end
63
+ return nil, prev
64
+ end
65
+
66
+ def compose_hangul(prev, jamo)
67
+ # Use NFKC for compatibility jamo
68
+ c = (prev + (COMPATIBILITY_JAMO_TO_FINAL[jamo] || jamo)).
69
+ unicode_normalize(:nfkc)
70
+ c.size == 1 ? c : nil
71
+ end
72
+ end
73
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Textbringer
4
2
  class HiraganaInputMethod < InputMethod
5
3
  HIRAGANA_TABLE = {
@@ -86,23 +86,6 @@ module Textbringer
86
86
  end
87
87
  end
88
88
 
89
- def with_target_buffer(&block)
90
- if isearch_mode?
91
- @isearch_buffer ||= Buffer.new
92
- if @isearch_buffer.to_s != ISEARCH_STATUS[:string]
93
- @isearch_buffer.replace(ISEARCH_STATUS[:string])
94
- end
95
- block.call(@isearch_buffer)
96
- ISEARCH_STATUS[:string] = @isearch_buffer.to_s
97
- if Buffer.current != Buffer.minibuffer
98
- message(isearch_prompt + ISEARCH_STATUS[:string], log: false)
99
- Window.redisplay
100
- end
101
- else
102
- block.call(Buffer.current)
103
- end
104
- end
105
-
106
89
  def bushu_compose
107
90
  with_target_buffer do |buffer|
108
91
  pos = buffer.point
@@ -117,7 +100,6 @@ module Textbringer
117
100
  buffer.goto_char(pos)
118
101
  end
119
102
  end
120
- isearch_search if isearch_mode?
121
103
  Window.redisplay
122
104
  nil
123
105
  end
@@ -276,7 +258,6 @@ module Textbringer
276
258
  buffer.insert(s + @mazegaki_suffix)
277
259
  end
278
260
  end
279
- isearch_search if isearch_mode?
280
261
  end
281
262
 
282
263
  def hide_help_window
@@ -215,6 +215,11 @@ module Textbringer
215
215
  GLOBAL_MAP.define_key([:f1, "m"], :describe_method)
216
216
  GLOBAL_MAP.define_key("\C-x#", :server_edit_done)
217
217
  GLOBAL_MAP.define_key("\C-\\", :toggle_input_method)
218
+ GLOBAL_MAP.define_key("\M-l", :downcase_word)
219
+ GLOBAL_MAP.define_key("\M-u", :upcase_word)
220
+ GLOBAL_MAP.define_key("\M-c", :capitalize_word)
221
+ GLOBAL_MAP.define_key("\C-x8\C-m", :insert_char)
222
+ GLOBAL_MAP.define_key("\C-x=", :what_cursor_position)
218
223
  GLOBAL_MAP.handle_undefined_key do |key|
219
224
  if key.is_a?(String) && /[\0-\x7f]/ !~ key
220
225
  :self_insert
@@ -6,7 +6,7 @@ module Textbringer
6
6
  @@mode_list = []
7
7
 
8
8
  DEFAULT_SYNTAX_TABLE = {
9
- control: /[\0-\t\v-\x1f\x7f\u{3000}]+/
9
+ control: /\p{C}+/
10
10
  }
11
11
 
12
12
  def self.list
@@ -1,3 +1,3 @@
1
1
  module Textbringer
2
- VERSION = "1.4.1"
2
+ VERSION = "2"
3
3
  end
@@ -0,0 +1,81 @@
1
+ module Textbringer
2
+ class Window
3
+ FALLBACK_CHARACTERS = {
4
+ # Hiragana
5
+ "゙" => "゛",
6
+ "゚" => "゜",
7
+
8
+ # Hangul jamo
9
+ ## initial
10
+ "ᄀ" => "ㄱ",
11
+ "ᄁ" => "ㄲ",
12
+ "ᄂ" => "ㄴ",
13
+ "ᄃ" => "ㄷ",
14
+ "ᄄ" => "ㄸ",
15
+ "ᄅ" => "ㄹ",
16
+ "ᄆ" => "ㅁ",
17
+ "ᄇ" => "ㅂ",
18
+ "ᄈ" => "ㅃ",
19
+ "ᄉ" => "ㅅ",
20
+ "ᄊ" => "ㅆ",
21
+ "ᄋ" => "ㅇ",
22
+ "ᄌ" => "ㅈ",
23
+ "ᄍ" => "ㅉ",
24
+ "ᄎ" => "ㅊ",
25
+ "ᄏ" => "ㅋ",
26
+ "ᄐ" => "ㅌ",
27
+ "ᄑ" => "ㅍ",
28
+ "ᄒ" => "ㅎ",
29
+ ## medial
30
+ "ᅡ" => "ㅏ",
31
+ "ᅢ" => "ㅐ",
32
+ "ᅣ" => "ㅑ",
33
+ "ᅤ" => "ㅒ",
34
+ "ᅥ" => "ㅓ",
35
+ "ᅦ" => "ㅔ",
36
+ "ᅧ" => "ㅕ",
37
+ "ᅨ" => "ㅖ",
38
+ "ᅩ" => "ㅗ",
39
+ "ᅪ" => "ㅘ",
40
+ "ᅫ" => "ㅙ",
41
+ "ᅬ" => "ㅚ",
42
+ "ᅭ" => "ㅛ",
43
+ "ᅮ" => "ㅜ",
44
+ "ᅯ" => "ㅝ",
45
+ "ᅰ" => "ㅞ",
46
+ "ᅱ" => "ㅟ",
47
+ "ᅲ" => "ㅠ",
48
+ "ᅳ" => "ㅡ",
49
+ "ᅴ" => "ㅢ",
50
+ "ᅵ" => "ㅣ",
51
+ ## final
52
+ "ᆨ" => "ㄱ",
53
+ "ᆩ" => "ㄲ",
54
+ "ᆪ" => "ㄳ",
55
+ "ᆫ" => "ㄴ",
56
+ "ᆬ" => "ㄵ",
57
+ "ᆭ" => "ㄶ",
58
+ "ᆮ" => "ㄷ",
59
+ "ᆯ" => "ㄹ",
60
+ "ᆰ" => "ㄺ",
61
+ "ᆱ" => "ㄻ",
62
+ "ᆲ" => "ㄼ",
63
+ "ᆳ" => "ㄽ",
64
+ "ᆴ" => "ㄾ",
65
+ "ᆵ" => "ㄿ",
66
+ "ᆶ" => "ㅀ",
67
+ "ᆷ" => "ㅁ",
68
+ "ᆸ" => "ㅂ",
69
+ "ᆹ" => "ㅄ",
70
+ "ᆺ" => "ㅅ",
71
+ "ᆻ" => "ㅆ",
72
+ "ᆼ" => "ㅇ",
73
+ "ᆽ" => "ㅈ",
74
+ "ᆾ" => "ㅊ",
75
+ "ᆿ" => "ㅋ",
76
+ "ᇀ" => "ㅌ",
77
+ "ᇁ" => "ㅍ",
78
+ "ᇂ" => "ㅎ"
79
+ }
80
+ end
81
+ end
@@ -1,7 +1,10 @@
1
1
  require "curses"
2
+ require_relative "window/fallback_characters"
2
3
 
3
4
  module Textbringer
4
5
  class Window
6
+ Cursor = Struct.new(:y, :x)
7
+
5
8
  KEY_NAMES = {}
6
9
  Curses.constants.grep(/\AKEY_/).each do |name|
7
10
  KEY_NAMES[Curses.const_get(name)] =
@@ -232,6 +235,7 @@ module Textbringer
232
235
  @deleted = false
233
236
  @raw_key_buffer = []
234
237
  @key_buffer = []
238
+ @cursor = Cursor.new(0, 0)
235
239
  end
236
240
 
237
241
  def echo_area?
@@ -395,7 +399,7 @@ module Textbringer
395
399
  @buffer.point_to_mark(@point_mark)
396
400
  end
397
401
  framer
398
- y = x = 0
402
+ @cursor.y = @cursor.x = 0
399
403
  @buffer.point_to_mark(@top_of_window)
400
404
  highlight
401
405
  @window.erase
@@ -406,25 +410,9 @@ module Textbringer
406
410
  @window.attron(Curses::A_REVERSE)
407
411
  end
408
412
  while !@buffer.end_of_buffer?
409
- cury, curx = @window.cury, @window.curx
410
- if @buffer.point_at_mark?(point)
411
- y, x = cury, curx
412
- if current? && @buffer.visible_mark
413
- if @buffer.point_after_mark?(@buffer.visible_mark)
414
- @window.attroff(Curses::A_REVERSE)
415
- elsif @buffer.point_before_mark?(@buffer.visible_mark)
416
- @window.attron(Curses::A_REVERSE)
417
- end
418
- end
419
- end
420
- if current? && @buffer.visible_mark &&
421
- @buffer.point_at_mark?(@buffer.visible_mark)
422
- if @buffer.point_after_mark?(point)
423
- @window.attroff(Curses::A_REVERSE)
424
- elsif @buffer.point_before_mark?(point)
425
- @window.attron(Curses::A_REVERSE)
426
- end
427
- end
413
+ cury = @window.cury
414
+ curx = @window.curx
415
+ update_cursor_and_attr(point, cury, curx)
428
416
  if attrs = @highlight_off[@buffer.point]
429
417
  @window.attroff(attrs)
430
418
  end
@@ -442,12 +430,13 @@ module Textbringer
442
430
  n = calc_tab_width(curx)
443
431
  c = " " * n
444
432
  else
445
- c = escape(c)
433
+ c = compose_character(point, cury, curx, c)
446
434
  end
435
+ s = escape(c)
447
436
  if curx < columns - 4
448
437
  newx = nil
449
438
  else
450
- newx = curx + Buffer.display_width(c)
439
+ newx = curx + Buffer.display_width(s)
451
440
  if newx > columns
452
441
  if cury == lines - 2
453
442
  break
@@ -457,12 +446,7 @@ module Textbringer
457
446
  end
458
447
  end
459
448
  end
460
- if Buffer.display_width(c) == 0
461
- # ncurses on macOS prints U+FEFF, U+FE0F etc. as space,
462
- # so ignore it
463
- else
464
- @window.addstr(c)
465
- end
449
+ @window.addstr(s)
466
450
  break if newx == columns && cury == lines - 2
467
451
  @buffer.forward_char
468
452
  end
@@ -471,16 +455,17 @@ module Textbringer
471
455
  end
472
456
  @buffer.mark_to_point(@bottom_of_window)
473
457
  if @buffer.point_at_mark?(point)
474
- y, x = @window.cury, @window.curx
458
+ @cursor.y = @window.cury
459
+ @cursor.x = @window.curx
475
460
  end
476
- if x == columns - 1
461
+ if @cursor.x == columns - 1
477
462
  c = @buffer.char_after(point.location)
478
463
  if c && Buffer.display_width(c) > 1
479
- y += 1
480
- x = 0
464
+ @cursor.y += 1
465
+ @cursor.x = 0
481
466
  end
482
467
  end
483
- @window.setpos(y, x)
468
+ @window.setpos(@cursor.y, @cursor.x)
484
469
  @window.noutrefresh
485
470
  end
486
471
  end
@@ -690,6 +675,86 @@ module Textbringer
690
675
  "0x" + c.unpack("H*")[0]
691
676
  end
692
677
 
678
+ def update_cursor_and_attr(point, cury, curx)
679
+ if @buffer.point_at_mark?(point)
680
+ @cursor.y = cury
681
+ @cursor.x = curx
682
+ if current? && @buffer.visible_mark
683
+ if @buffer.point_after_mark?(@buffer.visible_mark)
684
+ @window.attroff(Curses::A_REVERSE)
685
+ elsif @buffer.point_before_mark?(@buffer.visible_mark)
686
+ @window.attron(Curses::A_REVERSE)
687
+ end
688
+ end
689
+ end
690
+ if current? && @buffer.visible_mark &&
691
+ @buffer.point_at_mark?(@buffer.visible_mark)
692
+ if @buffer.point_after_mark?(point)
693
+ @window.attroff(Curses::A_REVERSE)
694
+ elsif @buffer.point_before_mark?(point)
695
+ @window.attron(Curses::A_REVERSE)
696
+ end
697
+ end
698
+ end
699
+
700
+ def compose_character(point, cury, curx, c)
701
+ return c if @buffer.binary? || c.nil? || c.match?(/\p{M}/)
702
+ if c.match?(/[\u{1100}-\u{115f}]/) # hangul initial consonants
703
+ return compose_hangul_character(point, cury, curx, c)
704
+ end
705
+ pos = @buffer.point + c.bytesize
706
+ while nextc = @buffer.char_after(pos)
707
+ case nextc
708
+ when /[\u{fe00}-\u{fe0f}\u{e0100}-\u{e01ef}]/ # variation selectors
709
+ c += nextc
710
+ when /[\p{Mn}\p{Me}]/ # nonspacing & enclosing marks
711
+ # Normalize パ (U+30CF + U+309A) to パ (U+30D1) so that curses can
712
+ # caluculate display width correctly.
713
+ # Display combining marks by codepoint when characters cannot be
714
+ # combined by NFC.
715
+ newc = (c + nextc).unicode_normalize(:nfc)
716
+ return c if newc.size != c.size
717
+ c = newc
718
+ else
719
+ return c
720
+ end
721
+ @buffer.forward_char
722
+ update_cursor_and_attr(point, cury, curx)
723
+ pos += nextc.bytesize
724
+ end
725
+ c
726
+ end
727
+
728
+ def compose_hangul_character(point, cury, curx, initial)
729
+ pos = @buffer.point + initial.bytesize
730
+ medial = @buffer.char_after(pos)
731
+ if !medial&.match?(/[\u{1160}-\u{11a7}]/)
732
+ return initial
733
+ end
734
+ final = @buffer.char_after(pos + medial.bytesize)
735
+ if final&.match?(/[\u{11a8}-\u{11ff}]/)
736
+ newc = (initial + medial + final).unicode_normalize(:nfc)
737
+ if newc.size == 1
738
+ 2.times do
739
+ @buffer.forward_char
740
+ update_cursor_and_attr(point, cury, curx)
741
+ end
742
+ newc
743
+ else
744
+ c
745
+ end
746
+ else
747
+ newc = (initial + medial).unicode_normalize(:nfc)
748
+ if newc.size == 1
749
+ @buffer.forward_char
750
+ update_cursor_and_attr(point, cury, curx)
751
+ newc
752
+ else
753
+ c
754
+ end
755
+ end
756
+ end
757
+
693
758
  def escape(s)
694
759
  if !s.valid_encoding?
695
760
  s = s.b
@@ -701,8 +766,23 @@ module Textbringer
701
766
  "<%02X>" % c.ord
702
767
  }
703
768
  else
704
- s.gsub(/[\0-\b\v-\x1f\x7f]/) { |c|
705
- "^" + (c.ord ^ 0x40).chr
769
+ s.gsub(/
770
+ (?<ascii_control>[\0-\b\v-\x1f\x7f])
771
+ | (?<nonascii_control>\p{C})
772
+ | (?<combining_diacritical_mark>[\u{0300}-\u{036f}])
773
+ | (?<other_special_char>[\p{M}\u{1100}-\u{11ff}])
774
+ /x) { |c|
775
+ if $~[:ascii_control]
776
+ "^" + (c.ord ^ 0x40).chr
777
+ elsif $~[:nonascii_control]
778
+ "<%04x>" % c.ord
779
+ elsif $~[:combining_diacritical_mark]
780
+ # Use U+00A0 as the base character, following the convention
781
+ # described in section 2.11.4 of Unicode Standard 16.0.0
782
+ "\u{00a0}#{c}"
783
+ elsif $~[:other_special_char]
784
+ FALLBACK_CHARACTERS[c] || c
785
+ end
706
786
  }
707
787
  end
708
788
  end
@@ -836,8 +916,12 @@ module Textbringer
836
916
 
837
917
  def redisplay
838
918
  return if @buffer.nil?
839
- @buffer.save_point do |saved|
919
+ @buffer.save_point do |point|
840
920
  @window.erase
921
+ if @buffer.input_method_status != "--"
922
+ @window.setpos(@window.cury, editable_columns + 1)
923
+ @window.addstr(@buffer.input_method_status)
924
+ end
841
925
  @window.setpos(0, 0)
842
926
  if @message
843
927
  @window.addstr(escape(@message))
@@ -846,29 +930,30 @@ module Textbringer
846
930
  @window.addstr(prompt)
847
931
  framer
848
932
  @buffer.point_to_mark(@top_of_window)
849
- y = x = 0
933
+ @cursor.y = @cursor.x = 0
850
934
  while !@buffer.end_of_buffer?
851
- cury, curx = @window.cury, @window.curx
852
- if @buffer.point_at_mark?(saved)
853
- y, x = cury, curx
854
- end
935
+ cury = @window.cury
936
+ curx = @window.curx
937
+ update_cursor_and_attr(point, cury, curx)
855
938
  c = @buffer.char_after
856
939
  if c == "\n"
857
940
  break
858
941
  end
942
+ c = compose_character(point, cury, curx, c)
859
943
  s = escape(c)
860
944
  newx = curx + Buffer.display_width(s)
861
- if newx > @columns
945
+ if newx > editable_columns
862
946
  break
863
947
  end
864
948
  @window.addstr(s)
865
- break if newx >= @columns
866
949
  @buffer.forward_char
950
+ break if newx >= editable_columns
867
951
  end
868
- if @buffer.point_at_mark?(saved)
869
- y, x = @window.cury, @window.curx
952
+ if @buffer.point_at_mark?(point)
953
+ @cursor.y = @window.cury
954
+ @cursor.x = @window.curx
870
955
  end
871
- @window.setpos(y, x)
956
+ @window.setpos(@cursor.y, @cursor.x)
872
957
  end
873
958
  @window.noutrefresh
874
959
  end
@@ -890,6 +975,14 @@ module Textbringer
890
975
  @window.resize(lines, columns)
891
976
  end
892
977
 
978
+ def editable_columns
979
+ if @buffer.input_method_status == "--"
980
+ @columns
981
+ else
982
+ @columns - Buffer.display_width(@buffer.input_method_status) - 1
983
+ end
984
+ end
985
+
893
986
  private
894
987
 
895
988
  def initialize_window(num_lines, num_columns, y, x)
@@ -902,7 +995,7 @@ module Textbringer
902
995
 
903
996
  def framer
904
997
  @buffer.save_point do |saved|
905
- max_width = @columns - @window.curx
998
+ max_width = editable_columns - @window.curx
906
999
  width = 0
907
1000
  loop do
908
1001
  c = @buffer.char_after
data/lib/textbringer.rb CHANGED
@@ -23,6 +23,7 @@ require_relative "textbringer/commands/keyboard_macro"
23
23
  require_relative "textbringer/commands/fill"
24
24
  require_relative "textbringer/commands/server"
25
25
  require_relative "textbringer/commands/input_method"
26
+ require_relative "textbringer/commands/ucs_normalize"
26
27
  require_relative "textbringer/commands/help"
27
28
  require_relative "textbringer/mode"
28
29
  require_relative "textbringer/modes/fundamental_mode"
@@ -36,6 +37,7 @@ require_relative "textbringer/modes/help_mode"
36
37
  require_relative "textbringer/input_method"
37
38
  require_relative "textbringer/input_methods/t_code_input_method"
38
39
  require_relative "textbringer/input_methods/hiragana_input_method"
40
+ require_relative "textbringer/input_methods/hangul_input_method"
39
41
  require_relative "textbringer/plugin"
40
42
  require_relative "textbringer/controller"
41
43
  require_relative "textbringer/default_output"
data/textbringer.gemspec CHANGED
@@ -21,14 +21,23 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.required_ruby_version = '>= 3.1'
23
23
 
24
+ spec.add_runtime_dependency "rdoc"
25
+ spec.add_runtime_dependency "ostruct"
26
+ spec.add_runtime_dependency "irb"
24
27
  spec.add_runtime_dependency "nkf"
25
28
  spec.add_runtime_dependency "drb"
26
29
  spec.add_runtime_dependency "curses", ">= 1.2.7"
27
30
  spec.add_runtime_dependency "unicode-display_width", ">= 1.1"
28
31
  spec.add_runtime_dependency "clipboard", ">= 1.1"
32
+ spec.add_runtime_dependency "fiddle"
29
33
  spec.add_runtime_dependency "fiddley", ">= 0.0.5"
30
34
  spec.add_runtime_dependency "editorconfig"
31
35
  spec.add_runtime_dependency "warning"
36
+ spec.add_runtime_dependency "unicode-name"
37
+ spec.add_runtime_dependency "unicode-categories"
38
+ spec.add_runtime_dependency "unicode-blocks"
39
+ spec.add_runtime_dependency "unicode-scripts"
40
+ spec.add_runtime_dependency "unicode-types"
32
41
 
33
42
  spec.add_development_dependency "bundler"
34
43
  spec.add_development_dependency "rake", ">= 12.0"
metadata CHANGED
@@ -1,15 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textbringer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: '2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-06-05 00:00:00.000000000 Z
10
+ date: 2025-04-04 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rdoc
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ostruct
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: irb
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
13
54
  - !ruby/object:Gem::Dependency
14
55
  name: nkf
15
56
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +121,20 @@ dependencies:
80
121
  - - ">="
81
122
  - !ruby/object:Gem::Version
82
123
  version: '1.1'
124
+ - !ruby/object:Gem::Dependency
125
+ name: fiddle
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
83
138
  - !ruby/object:Gem::Dependency
84
139
  name: fiddley
85
140
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +177,76 @@ dependencies:
122
177
  - - ">="
123
178
  - !ruby/object:Gem::Version
124
179
  version: '0'
180
+ - !ruby/object:Gem::Dependency
181
+ name: unicode-name
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ - !ruby/object:Gem::Dependency
195
+ name: unicode-categories
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ type: :runtime
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ - !ruby/object:Gem::Dependency
209
+ name: unicode-blocks
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ type: :runtime
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ - !ruby/object:Gem::Dependency
223
+ name: unicode-scripts
224
+ requirement: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ type: :runtime
230
+ prerelease: false
231
+ version_requirements: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ - !ruby/object:Gem::Dependency
237
+ name: unicode-types
238
+ requirement: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0'
243
+ type: :runtime
244
+ prerelease: false
245
+ version_requirements: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
125
250
  - !ruby/object:Gem::Dependency
126
251
  name: bundler
127
252
  requirement: !ruby/object:Gem::Requirement
@@ -239,6 +364,7 @@ files:
239
364
  - lib/textbringer/commands/register.rb
240
365
  - lib/textbringer/commands/replace.rb
241
366
  - lib/textbringer/commands/server.rb
367
+ - lib/textbringer/commands/ucs_normalize.rb
242
368
  - lib/textbringer/commands/windows.rb
243
369
  - lib/textbringer/config.rb
244
370
  - lib/textbringer/controller.rb
@@ -248,6 +374,7 @@ files:
248
374
  - lib/textbringer/faces/basic.rb
249
375
  - lib/textbringer/faces/programming.rb
250
376
  - lib/textbringer/input_method.rb
377
+ - lib/textbringer/input_methods/hangul_input_method.rb
251
378
  - lib/textbringer/input_methods/hiragana_input_method.rb
252
379
  - lib/textbringer/input_methods/t_code_input_method.rb
253
380
  - lib/textbringer/input_methods/t_code_input_method/tables.rb
@@ -266,6 +393,7 @@ files:
266
393
  - lib/textbringer/utils.rb
267
394
  - lib/textbringer/version.rb
268
395
  - lib/textbringer/window.rb
396
+ - lib/textbringer/window/fallback_characters.rb
269
397
  - logo/logo.jpg
270
398
  - logo/logo.png
271
399
  - screenshot.png
@@ -274,7 +402,6 @@ homepage: https://github.com/shugo/textbringer
274
402
  licenses:
275
403
  - MIT
276
404
  metadata: {}
277
- post_install_message:
278
405
  rdoc_options: []
279
406
  require_paths:
280
407
  - lib
@@ -289,8 +416,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
289
416
  - !ruby/object:Gem::Version
290
417
  version: '0'
291
418
  requirements: []
292
- rubygems_version: 3.5.9
293
- signing_key:
419
+ rubygems_version: 3.6.2
294
420
  specification_version: 4
295
421
  summary: An Emacs-like text editor
296
422
  test_files: []