textbringer 18 → 20

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/exe/txtb +1 -1
  3. data/lib/textbringer/buffer.rb +23 -2
  4. data/lib/textbringer/commands/clipboard.rb +21 -6
  5. data/lib/textbringer/commands/completion.rb +133 -0
  6. data/lib/textbringer/commands/ctags.rb +1 -1
  7. data/lib/textbringer/commands/files.rb +11 -1
  8. data/lib/textbringer/commands/help.rb +1 -1
  9. data/lib/textbringer/commands/ispell.rb +0 -2
  10. data/lib/textbringer/commands/lsp.rb +389 -0
  11. data/lib/textbringer/commands/misc.rb +2 -1
  12. data/lib/textbringer/commands.rb +7 -3
  13. data/lib/textbringer/completion_popup.rb +188 -0
  14. data/lib/textbringer/faces/basic.rb +1 -0
  15. data/lib/textbringer/faces/completion.rb +4 -0
  16. data/lib/textbringer/floating_window.rb +327 -0
  17. data/lib/textbringer/input_methods/skk_input_method.rb +773 -0
  18. data/lib/textbringer/lsp/client.rb +568 -0
  19. data/lib/textbringer/lsp/server_registry.rb +138 -0
  20. data/lib/textbringer/mode.rb +3 -1
  21. data/lib/textbringer/modes/programming_mode.rb +17 -8
  22. data/lib/textbringer/modes/transient_mark_mode.rb +5 -2
  23. data/lib/textbringer/utils.rb +14 -10
  24. data/lib/textbringer/version.rb +1 -1
  25. data/lib/textbringer/window.rb +36 -9
  26. data/lib/textbringer.rb +7 -0
  27. data/sig/lib/textbringer/buffer.rbs +483 -0
  28. data/sig/lib/textbringer/color.rbs +9 -0
  29. data/sig/lib/textbringer/commands/buffers.rbs +93 -0
  30. data/sig/lib/textbringer/commands/clipboard.rbs +17 -0
  31. data/sig/lib/textbringer/commands/completion.rbs +20 -0
  32. data/sig/lib/textbringer/commands/ctags.rbs +11 -0
  33. data/sig/lib/textbringer/commands/dabbrev.rbs +4 -0
  34. data/sig/lib/textbringer/commands/files.rbs +29 -0
  35. data/sig/lib/textbringer/commands/fill.rbs +5 -0
  36. data/sig/lib/textbringer/commands/help.rbs +28 -0
  37. data/sig/lib/textbringer/commands/input_method.rbs +6 -0
  38. data/sig/lib/textbringer/commands/isearch.rbs +38 -0
  39. data/sig/lib/textbringer/commands/ispell.rbs +39 -0
  40. data/sig/lib/textbringer/commands/keyboard_macro.rbs +25 -0
  41. data/sig/lib/textbringer/commands/lsp.rbs +8 -0
  42. data/sig/lib/textbringer/commands/misc.rbs +74 -0
  43. data/sig/lib/textbringer/commands/rectangle.rbs +19 -0
  44. data/sig/lib/textbringer/commands/register.rbs +31 -0
  45. data/sig/lib/textbringer/commands/replace.rbs +17 -0
  46. data/sig/lib/textbringer/commands/server.rbs +31 -0
  47. data/sig/lib/textbringer/commands/ucs_normalize.rbs +9 -0
  48. data/sig/lib/textbringer/commands/windows.rbs +45 -0
  49. data/sig/lib/textbringer/commands.rbs +21 -0
  50. data/sig/lib/textbringer/completion_popup.rbs +40 -0
  51. data/sig/lib/textbringer/controller.rbs +58 -0
  52. data/sig/lib/textbringer/default_output.rbs +7 -0
  53. data/sig/lib/textbringer/errors.rbs +3 -0
  54. data/sig/lib/textbringer/face.rbs +19 -0
  55. data/sig/lib/textbringer/floating_window.rbs +42 -0
  56. data/sig/lib/textbringer/global_minor_mode.rbs +7 -0
  57. data/sig/lib/textbringer/input_method.rbs +28 -0
  58. data/sig/lib/textbringer/input_methods/hangul_input_method.rbs +12 -0
  59. data/sig/lib/textbringer/input_methods/hiragana_input_method.rbs +12 -0
  60. data/sig/lib/textbringer/input_methods/t_code_input_method.rbs +49 -0
  61. data/sig/lib/textbringer/keymap.rbs +33 -0
  62. data/sig/lib/textbringer/lsp/client.rbs +21 -0
  63. data/sig/lib/textbringer/lsp/server_registry.rbs +23 -0
  64. data/sig/lib/textbringer/minor_mode.rbs +12 -0
  65. data/sig/lib/textbringer/mode.rbs +70 -0
  66. data/sig/lib/textbringer/modes/backtrace_mode.rbs +8 -0
  67. data/sig/lib/textbringer/modes/buffer_list_mode.rbs +5 -0
  68. data/sig/lib/textbringer/modes/c_mode.rbs +21 -0
  69. data/sig/lib/textbringer/modes/completion_list_mode.rbs +5 -0
  70. data/sig/lib/textbringer/modes/fundamental_mode.rbs +3 -0
  71. data/sig/lib/textbringer/modes/help_mode.rbs +7 -0
  72. data/sig/lib/textbringer/modes/overwrite_mode.rbs +15 -0
  73. data/sig/lib/textbringer/modes/programming_mode.rbs +14 -0
  74. data/sig/lib/textbringer/modes/ruby_mode.rbs +57 -0
  75. data/sig/lib/textbringer/plugin.rbs +3 -0
  76. data/sig/lib/textbringer/ring.rbs +36 -0
  77. data/sig/lib/textbringer/utils.rbs +95 -0
  78. data/sig/lib/textbringer/window.rbs +183 -0
  79. data/textbringer.gemspec +1 -0
  80. metadata +76 -2
@@ -0,0 +1,773 @@
1
+ require "open-uri"
2
+ require "fileutils"
3
+
4
+ module Textbringer
5
+ CONFIG[:skk_dictionary_path] = File.expand_path("~/.textbringer/skk/SKK-JISYO.L")
6
+
7
+ class SkkInputMethod < InputMethod
8
+ HIRAGANA_TABLE = {
9
+ "a" => "あ", "i" => "い", "u" => "う", "e" => "え", "o" => "お",
10
+ "ka" => "か", "ki" => "き", "ku" => "く", "ke" => "け", "ko" => "こ",
11
+ "ga" => "が", "gi" => "ぎ", "gu" => "ぐ", "ge" => "げ", "go" => "ご",
12
+ "sa" => "さ", "si" => "し", "su" => "す", "se" => "せ", "so" => "そ",
13
+ "za" => "ざ", "zi" => "じ", "zu" => "ず", "ze" => "ぜ", "zo" => "ぞ",
14
+ "sha" => "しゃ", "shi" => "し", "shu" => "しゅ", "she" => "しぇ", "sho" => "しょ",
15
+ "ja" => "じゃ", "ji" => "じ", "ju" => "じゅ", "je" => "じぇ", "jo" => "じょ",
16
+ "ta" => "た", "ti" => "ち", "tu" => "つ", "te" => "て", "to" => "と",
17
+ "da" => "だ", "di" => "ぢ", "du" => "づ", "de" => "で", "do" => "ど",
18
+ "cha" => "ちゃ", "chi" => "ち", "chu" => "ちゅ", "che" => "ちぇ", "cho" => "ちょ",
19
+ "na" => "な", "ni" => "に", "nu" => "ぬ", "ne" => "ね", "no" => "の",
20
+ "ha" => "は", "hi" => "ひ", "hu" => "ふ", "he" => "へ", "ho" => "ほ",
21
+ "ba" => "ば", "bi" => "び", "bu" => "ぶ", "be" => "べ", "bo" => "ぼ",
22
+ "pa" => "ぱ", "pi" => "ぴ", "pu" => "ぷ", "pe" => "ぺ", "po" => "ぽ",
23
+ "ma" => "ま", "mi" => "み", "mu" => "む", "me" => "め", "mo" => "も",
24
+ "ya" => "や", "yi" => "い", "yu" => "ゆ", "ye" => "いぇ", "yo" => "よ",
25
+ "ra" => "ら", "ri" => "り", "ru" => "る", "re" => "れ", "ro" => "ろ",
26
+ "wa" => "わ", "wi" => "ゐ", "wu" => "う", "we" => "ゑ", "wo" => "を",
27
+ "nn" => "ん",
28
+ "kya" => "きゃ", "kyi" => "きぃ", "kyu" => "きゅ", "kye" => "きぇ", "kyo" => "きょ",
29
+ "gya" => "ぎゃ", "gyi" => "ぎぃ", "gyu" => "ぎゅ", "gye" => "ぎぇ", "gyo" => "ぎょ",
30
+ "sya" => "しゃ", "syi" => "しぃ", "syu" => "しゅ", "sye" => "しぇ", "syo" => "しょ",
31
+ "zya" => "じゃ", "zyi" => "じぃ", "zyu" => "じゅ", "zye" => "じぇ", "zyo" => "じょ",
32
+ "tya" => "ちゃ", "tyi" => "ちぃ", "tyu" => "ちゅ", "tye" => "ちぇ", "tyo" => "ちょ",
33
+ "dya" => "ぢゃ", "dyi" => "ぢぃ", "dyu" => "ぢゅ", "dye" => "ぢぇ", "dyo" => "ぢょ",
34
+ "nya" => "にゃ", "nyi" => "にぃ", "nyu" => "にゅ", "nye" => "にぇ", "nyo" => "にょ",
35
+ "hya" => "ひゃ", "hyi" => "ひぃ", "hyu" => "ひゅ", "hye" => "ひぇ", "hyo" => "ひょ",
36
+ "bya" => "びゃ", "byi" => "びぃ", "byu" => "びゅ", "bye" => "びぇ", "byo" => "びょ",
37
+ "pya" => "ぴゃ", "pyi" => "ぴぃ", "pyu" => "ぴゅ", "pye" => "ぴぇ", "pyo" => "ぴょ",
38
+ "mya" => "みゃ", "myi" => "みぃ", "myu" => "みゅ", "mye" => "みぇ", "myo" => "みょ",
39
+ "rya" => "りゃ", "ryi" => "りぃ", "ryu" => "りゅ", "rye" => "りぇ", "ryo" => "りょ",
40
+ "fa" => "ふぁ", "fi" => "ふぃ", "fu" => "ふ", "fe" => "ふぇ", "fo" => "ふぉ",
41
+ "va" => "ヴぁ", "vi" => "ヴぃ", "vu" => "ヴ", "ve" => "ヴぇ", "vo" => "ヴぉ",
42
+ "tsa" => "つぁ", "tsi" => "つぃ", "tse" => "つぇ", "tso" => "つぉ",
43
+ "la" => "ぁ", "li" => "ぃ", "lu" => "ぅ", "le" => "ぇ", "lo" => "ぉ",
44
+ "lya" => "ゃ", "lyu" => "ゅ", "lyo" => "ょ",
45
+ "lka" => "ヵ", "lke" => "ヶ",
46
+ "xtu" => "っ", "xtsu" => "っ",
47
+ "xya" => "ゃ", "xyu" => "ゅ", "xyo" => "ょ",
48
+ "xa" => "ぁ", "xi" => "ぃ", "xu" => "ぅ", "xe" => "ぇ", "xo" => "ぉ",
49
+ "," => "、", "." => "。",
50
+ }
51
+
52
+ HIRAGANA_PREFIXES = HIRAGANA_TABLE.keys.flat_map { |s|
53
+ (s.size - 1).times.map { |i| s[0, i + 1] }
54
+ }.uniq
55
+
56
+ KATAKANA_TABLE = HIRAGANA_TABLE.transform_values { |v|
57
+ v.chars.map { |c|
58
+ c.ord.between?(0x3041, 0x3096) ? (c.ord + 0x60).chr("UTF-8") : c
59
+ }.join
60
+ }
61
+
62
+ HANKAKU_KATAKANA_TABLE = {
63
+ "a" => "ア", "i" => "イ", "u" => "ウ", "e" => "エ", "o" => "オ",
64
+ "ka" => "カ", "ki" => "キ", "ku" => "ク", "ke" => "ケ", "ko" => "コ",
65
+ "ga" => "ガ", "gi" => "ギ", "gu" => "グ", "ge" => "ゲ", "go" => "ゴ",
66
+ "sa" => "サ", "si" => "シ", "su" => "ス", "se" => "セ", "so" => "ソ",
67
+ "za" => "ザ", "zi" => "ジ", "zu" => "ズ", "ze" => "ゼ", "zo" => "ゾ",
68
+ "sha" => "シャ", "shi" => "シ", "shu" => "シュ", "she" => "シェ", "sho" => "ショ",
69
+ "ja" => "ジャ", "ji" => "ジ", "ju" => "ジュ", "je" => "ジェ", "jo" => "ジョ",
70
+ "ta" => "タ", "ti" => "チ", "tu" => "ツ", "te" => "テ", "to" => "ト",
71
+ "da" => "ダ", "di" => "ヂ", "du" => "ヅ", "de" => "デ", "do" => "ド",
72
+ "cha" => "チャ", "chi" => "チ", "chu" => "チュ", "che" => "チェ", "cho" => "チョ",
73
+ "na" => "ナ", "ni" => "ニ", "nu" => "ヌ", "ne" => "ネ", "no" => "ノ",
74
+ "ha" => "ハ", "hi" => "ヒ", "hu" => "フ", "he" => "ヘ", "ho" => "ホ",
75
+ "ba" => "バ", "bi" => "ビ", "bu" => "ブ", "be" => "ベ", "bo" => "ボ",
76
+ "pa" => "パ", "pi" => "ピ", "pu" => "プ", "pe" => "ペ", "po" => "ポ",
77
+ "ma" => "マ", "mi" => "ミ", "mu" => "ム", "me" => "メ", "mo" => "モ",
78
+ "ya" => "ヤ", "yu" => "ユ", "yo" => "ヨ",
79
+ "ra" => "ラ", "ri" => "リ", "ru" => "ル", "re" => "レ", "ro" => "ロ",
80
+ "wa" => "ワ", "wo" => "ヲ",
81
+ "nn" => "ン",
82
+ "kya" => "キャ", "kyu" => "キュ", "kyo" => "キョ",
83
+ "gya" => "ギャ", "gyu" => "ギュ", "gyo" => "ギョ",
84
+ "sya" => "シャ", "syu" => "シュ", "syo" => "ショ",
85
+ "zya" => "ジャ", "zyu" => "ジュ", "zyo" => "ジョ",
86
+ "tya" => "チャ", "tyu" => "チュ", "tyo" => "チョ",
87
+ "dya" => "ヂャ", "dyu" => "ヂュ", "dyo" => "ヂョ",
88
+ "nya" => "ニャ", "nyu" => "ニュ", "nyo" => "ニョ",
89
+ "hya" => "ヒャ", "hyu" => "ヒュ", "hyo" => "ヒョ",
90
+ "bya" => "ビャ", "byu" => "ビュ", "byo" => "ビョ",
91
+ "pya" => "ピャ", "pyu" => "ピュ", "pyo" => "ピョ",
92
+ "mya" => "ミャ", "myu" => "ミュ", "myo" => "ミョ",
93
+ "rya" => "リャ", "ryu" => "リュ", "ryo" => "リョ",
94
+ "fa" => "ファ", "fi" => "フィ", "fu" => "フ", "fe" => "フェ", "fo" => "フォ",
95
+ "," => "、", "." => "。",
96
+ }
97
+
98
+ HANKAKU_KATAKANA_PREFIXES = HANKAKU_KATAKANA_TABLE.keys.flat_map { |s|
99
+ (s.size - 1).times.map { |i| s[0, i + 1] }
100
+ }.uniq
101
+
102
+ DEFAULT_CURSOR_COLORS = {
103
+ hiragana: "pink",
104
+ katakana: "green",
105
+ hankaku_katakana: "blue",
106
+ zenkaku_ascii: "yellow",
107
+ ascii: nil, # nil = reset to terminal default
108
+ }
109
+
110
+ def initialize
111
+ super
112
+ @mode = :hiragana # :hiragana | :katakana | :hankaku_katakana | :zenkaku_ascii | :ascii
113
+ @phase = :normal # :normal | :converting | :selecting
114
+ @roman_buffer = +""
115
+ @yomi = +""
116
+ @okuri_roman = nil
117
+ @okuri_kana = nil
118
+ @candidates = []
119
+ @candidate_index = 0
120
+ @marker_pos = nil
121
+ @okuriiari = nil
122
+ @okurinasi = nil
123
+ end
124
+
125
+ def toggle
126
+ super
127
+ if @enabled
128
+ update_cursor_color
129
+ else
130
+ reset_cursor_color
131
+ end
132
+ end
133
+
134
+ def disable
135
+ super
136
+ reset_cursor_color
137
+ end
138
+
139
+ def status
140
+ case @phase
141
+ when :converting then "▽"
142
+ when :selecting then "▼"
143
+ else
144
+ { hiragana: "あ", katakana: "ア", hankaku_katakana: "ア",
145
+ zenkaku_ascii: "A", ascii: "A" }[@mode]
146
+ end
147
+ end
148
+
149
+ def handle_event(event)
150
+ case @phase
151
+ when :normal then handle_normal(event)
152
+ when :converting then handle_converting(event)
153
+ when :selecting then handle_selecting(event)
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def handle_normal(event)
160
+ unless event.is_a?(String)
161
+ @roman_buffer = +""
162
+ return event
163
+ end
164
+
165
+ case event
166
+ when "\C-j"
167
+ @roman_buffer = +""
168
+ @mode = :hiragana
169
+ Window.redisplay
170
+ update_cursor_color
171
+ nil
172
+ when "\C-q"
173
+ if [:hiragana, :katakana].include?(@mode)
174
+ @roman_buffer = +""
175
+ @mode = :hankaku_katakana
176
+ Window.redisplay
177
+ update_cursor_color
178
+ elsif @mode == :hankaku_katakana
179
+ @roman_buffer = +""
180
+ @mode = :hiragana
181
+ Window.redisplay
182
+ update_cursor_color
183
+ else
184
+ return process_romaji(event)
185
+ end
186
+ nil
187
+ when "q"
188
+ if @mode == :hiragana
189
+ @roman_buffer = +""
190
+ @mode = :katakana
191
+ Window.redisplay
192
+ update_cursor_color
193
+ nil
194
+ elsif @mode == :katakana || @mode == :hankaku_katakana
195
+ @roman_buffer = +""
196
+ @mode = :hiragana
197
+ Window.redisplay
198
+ update_cursor_color
199
+ nil
200
+ else
201
+ process_romaji(event)
202
+ end
203
+ when "l"
204
+ if [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
205
+ @roman_buffer = +""
206
+ @mode = :ascii
207
+ Window.redisplay
208
+ update_cursor_color
209
+ nil
210
+ else
211
+ process_romaji(event)
212
+ end
213
+ when "L"
214
+ if [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
215
+ @roman_buffer = +""
216
+ @mode = :zenkaku_ascii
217
+ Window.redisplay
218
+ update_cursor_color
219
+ nil
220
+ else
221
+ process_romaji(event)
222
+ end
223
+ when /\A[A-Z]\z/
224
+ if [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
225
+ start_converting(event.downcase)
226
+ else
227
+ process_romaji(event)
228
+ end
229
+ when /\A[\x00-\x09\x0b-\x1f\x7f]\z/
230
+ # Control characters other than C-j and C-q pass through unchanged
231
+ @roman_buffer = +""
232
+ event
233
+ else
234
+ process_romaji(event)
235
+ end
236
+ end
237
+
238
+ def handle_converting(event)
239
+ unless event.is_a?(String)
240
+ commit_converting
241
+ return event
242
+ end
243
+
244
+ # Control characters not handled below: commit conversion and pass through
245
+ if event.bytesize == 1 && (event.ord < 0x20 || event.ord == 0x7f) &&
246
+ event != "\C-g" && event != "\C-j"
247
+ commit_converting
248
+ return event
249
+ end
250
+
251
+ case event
252
+ when "\C-g"
253
+ cancel_converting
254
+ nil
255
+ when "\C-j"
256
+ commit_converting
257
+ nil
258
+ when " "
259
+ start_selecting
260
+ nil
261
+ when /\A[A-Z]\z/
262
+ if @okuri_roman.nil?
263
+ start_okurigana(event.downcase)
264
+ else
265
+ process_converting_romaji(event.downcase)
266
+ end
267
+ nil
268
+ else
269
+ process_converting_romaji(event)
270
+ end
271
+ end
272
+
273
+ def handle_selecting(event)
274
+ unless event.is_a?(String)
275
+ confirm_selecting
276
+ return event
277
+ end
278
+
279
+ case event
280
+ when "\C-g"
281
+ cancel_selecting
282
+ nil
283
+ when " "
284
+ next_candidate
285
+ nil
286
+ when "x"
287
+ prev_candidate
288
+ nil
289
+ when "\C-m", "\r", "\n"
290
+ confirm_selecting
291
+ nil
292
+ else
293
+ confirm_selecting
294
+ handle_event(event)
295
+ end
296
+ end
297
+
298
+ def process_romaji(event)
299
+ case @mode
300
+ when :ascii
301
+ return event
302
+ when :zenkaku_ascii
303
+ if event.ord.between?(0x21, 0x7E)
304
+ return (event.ord + 0xFEE0).chr("UTF-8")
305
+ else
306
+ return event
307
+ end
308
+ end
309
+
310
+ # Special "n" handling: flush "ん" before appending if next char won't extend "n"
311
+ if @roman_buffer == "n" && !%w[n y a i u e o].include?(event)
312
+ kana = kana_for_mode("ん")
313
+ @roman_buffer = +""
314
+ insert_kana(kana)
315
+ end
316
+
317
+ @roman_buffer << event
318
+
319
+ table = current_table
320
+ prefixes = current_prefixes
321
+
322
+ kana = table[@roman_buffer]
323
+ if kana
324
+ @roman_buffer = +""
325
+ insert_kana(kana_for_mode(kana))
326
+ return nil
327
+ end
328
+
329
+ if prefixes.include?(@roman_buffer)
330
+ return nil
331
+ end
332
+
333
+ # Double consonant: e.g. "kk" → っ + keep second "k"
334
+ if @roman_buffer.size >= 2
335
+ first = @roman_buffer[0]
336
+ rest = @roman_buffer[1..]
337
+ if first == rest[0] && first =~ /[bcdfghjklmnpqrstvwxyz]/
338
+ geminate = kana_for_mode("っ")
339
+ @roman_buffer = +rest
340
+ insert_kana(geminate)
341
+ # Now check if rest completes a kana
342
+ kana2 = table[@roman_buffer]
343
+ if kana2
344
+ @roman_buffer = +""
345
+ insert_kana(kana_for_mode(kana2))
346
+ end
347
+ return nil
348
+ end
349
+ end
350
+
351
+ # No match: if single char, return it to let the controller handle it
352
+ # (consistent with HiraganaInputMethod#flush for unrecognized chars)
353
+ if @roman_buffer.size == 1
354
+ char = @roman_buffer
355
+ @roman_buffer = +""
356
+ return char
357
+ end
358
+
359
+ # Multi-char: flush first char as-is, retry with last char
360
+ first_char = @roman_buffer[0]
361
+ last_char = @roman_buffer[-1]
362
+ @roman_buffer = +""
363
+ with_target_buffer { |b| b.insert(first_char) }
364
+ Window.redisplay
365
+ process_romaji(last_char)
366
+ end
367
+
368
+ def insert_kana(kana)
369
+ with_target_buffer do |buffer|
370
+ buffer.insert(kana)
371
+ end
372
+ Window.redisplay
373
+ nil
374
+ end
375
+
376
+ def process_converting_romaji(event)
377
+ # Special "n" handling: flush "ん" before appending if next char won't extend "n"
378
+ if @roman_buffer == "n" && !%w[n y a i u e o].include?(event)
379
+ @roman_buffer = +""
380
+ append_yomi_kana("ん")
381
+ end
382
+
383
+ @roman_buffer << event
384
+
385
+ table = hiragana_table_for_converting
386
+ prefixes = hiragana_prefixes_for_converting
387
+
388
+ kana = table[@roman_buffer]
389
+ if kana
390
+ @roman_buffer = +""
391
+ if @okuri_roman
392
+ # Completing okurigana
393
+ @okuri_kana = kana
394
+ with_target_buffer do |buffer|
395
+ buffer.insert(kana)
396
+ end
397
+ Window.redisplay
398
+ start_selecting
399
+ else
400
+ append_yomi_kana(kana)
401
+ end
402
+ return
403
+ end
404
+
405
+ if prefixes.include?(@roman_buffer)
406
+ return
407
+ end
408
+
409
+ # Double consonant handling
410
+ if @roman_buffer.size >= 2
411
+ first = @roman_buffer[0]
412
+ rest = @roman_buffer[1..]
413
+ if first == rest[0] && first =~ /[bcdfghjklmnpqrstvwxyz]/
414
+ append_yomi_kana("っ")
415
+ @roman_buffer = +rest # Keep the second consonant buffered
416
+ return
417
+ end
418
+ end
419
+
420
+ # No match: if single char, insert as-is
421
+ if @roman_buffer.size == 1
422
+ char = @roman_buffer
423
+ @roman_buffer = +""
424
+ append_yomi_kana(char)
425
+ return
426
+ end
427
+
428
+ # Multi-char: flush first char as-is, retry with last char
429
+ first_char = @roman_buffer[0]
430
+ last_char = @roman_buffer[-1]
431
+ @roman_buffer = +""
432
+ append_yomi_kana(first_char)
433
+ process_converting_romaji(last_char)
434
+ end
435
+
436
+ def append_yomi_kana(kana)
437
+ @yomi << kana
438
+ with_target_buffer do |buffer|
439
+ buffer.insert(kana)
440
+ end
441
+ Window.redisplay
442
+ end
443
+
444
+ def start_converting(first_char)
445
+ @phase = :converting
446
+ @yomi = +""
447
+ @okuri_roman = nil
448
+ @okuri_kana = nil
449
+ @roman_buffer = +""
450
+ with_target_buffer do |buffer|
451
+ @marker_pos = buffer.point
452
+ buffer.insert("▽")
453
+ end
454
+ Window.redisplay
455
+ update_cursor_color
456
+ process_converting_romaji(first_char)
457
+ nil
458
+ end
459
+
460
+ def start_okurigana(consonant)
461
+ @okuri_roman = consonant.dup
462
+ @roman_buffer = consonant.dup
463
+ end
464
+
465
+ def cancel_converting
466
+ with_target_buffer do |buffer|
467
+ buffer.delete_region(@marker_pos, buffer.point)
468
+ end
469
+ @phase = :normal
470
+ @yomi = +""
471
+ @roman_buffer = +""
472
+ @okuri_roman = nil
473
+ @okuri_kana = nil
474
+ @marker_pos = nil
475
+ Window.redisplay
476
+ update_cursor_color
477
+ end
478
+
479
+ def commit_converting
480
+ with_target_buffer do |buffer|
481
+ # Remove the ▽ marker (3 bytes for ▽ in UTF-8)
482
+ marker_end = @marker_pos + "▽".bytesize
483
+ buffer.delete_region(@marker_pos, marker_end)
484
+ end
485
+ @phase = :normal
486
+ @roman_buffer = +""
487
+ @okuri_roman = nil
488
+ @okuri_kana = nil
489
+ @marker_pos = nil
490
+ Window.redisplay
491
+ update_cursor_color
492
+ end
493
+
494
+ def start_selecting
495
+ ensure_dictionary_loaded
496
+
497
+ lookup_key = if @okuri_roman
498
+ @yomi + @okuri_roman
499
+ else
500
+ @yomi
501
+ end
502
+
503
+ dict = @okuri_roman ? @okuriiari : @okurinasi
504
+ candidates = dict[lookup_key]
505
+
506
+ if candidates.nil? || candidates.empty?
507
+ message("No conversion: #{@yomi}")
508
+ return
509
+ end
510
+
511
+ @candidates = candidates
512
+ @candidate_index = 0
513
+ @phase = :selecting
514
+
515
+ with_target_buffer do |buffer|
516
+ buffer.delete_region(@marker_pos, buffer.point)
517
+ buffer.insert("▼" + @candidates[0] + (@okuri_kana || ""))
518
+ end
519
+ Window.redisplay
520
+ update_cursor_color
521
+ end
522
+
523
+ def next_candidate
524
+ @candidate_index = (@candidate_index + 1) % @candidates.size
525
+ replace_candidate
526
+ end
527
+
528
+ def prev_candidate
529
+ @candidate_index = (@candidate_index - 1 + @candidates.size) % @candidates.size
530
+ replace_candidate
531
+ end
532
+
533
+ def replace_candidate
534
+ with_target_buffer do |buffer|
535
+ buffer.delete_region(@marker_pos, buffer.point)
536
+ buffer.insert("▼" + @candidates[@candidate_index] + (@okuri_kana || ""))
537
+ end
538
+ Window.redisplay
539
+ end
540
+
541
+ def confirm_selecting
542
+ candidate = @candidates[@candidate_index]
543
+ with_target_buffer do |buffer|
544
+ buffer.delete_region(@marker_pos, buffer.point)
545
+ buffer.insert(candidate + (@okuri_kana || ""))
546
+ end
547
+ @phase = :normal
548
+ @yomi = +""
549
+ @roman_buffer = +""
550
+ @okuri_roman = nil
551
+ @okuri_kana = nil
552
+ @candidates = []
553
+ @candidate_index = 0
554
+ @marker_pos = nil
555
+ Window.redisplay
556
+ update_cursor_color
557
+ end
558
+
559
+ def cancel_selecting
560
+ with_target_buffer do |buffer|
561
+ buffer.delete_region(@marker_pos, buffer.point)
562
+ buffer.insert("▽" + @yomi + (@okuri_kana || ""))
563
+ end
564
+ @phase = :converting
565
+ @roman_buffer = +""
566
+ @candidates = []
567
+ @candidate_index = 0
568
+ Window.redisplay
569
+ update_cursor_color
570
+ end
571
+
572
+ def ensure_dictionary_loaded
573
+ return if @okuriiari
574
+
575
+ path = CONFIG[:skk_dictionary_path]
576
+ @okuriiari = {}
577
+ @okurinasi = {}
578
+ section = :okuriiari
579
+
580
+ File.foreach(path, encoding: "EUC-JP:UTF-8") do |line|
581
+ line.chomp!
582
+ if line == ";; okuri-nasi entries."
583
+ section = :okurinasi
584
+ next
585
+ end
586
+ next if line.start_with?(";") || line.empty?
587
+
588
+ key, rest = line.split(" /", 2)
589
+ next unless key && rest
590
+
591
+ candidates = rest.split("/").map { |c| c.split(";").first&.strip }.compact.reject(&:empty?)
592
+ next if candidates.empty?
593
+
594
+ if section == :okuriiari
595
+ @okuriiari[key] = candidates
596
+ else
597
+ @okurinasi[key] = candidates
598
+ end
599
+ end
600
+ end
601
+
602
+ def kana_for_mode(hiragana_kana)
603
+ case @mode
604
+ when :hiragana
605
+ hiragana_kana
606
+ when :katakana
607
+ hiragana_to_katakana(hiragana_kana)
608
+ when :hankaku_katakana
609
+ hiragana_to_hankaku_katakana(hiragana_kana)
610
+ else
611
+ hiragana_kana
612
+ end
613
+ end
614
+
615
+ def hiragana_to_katakana(kana)
616
+ kana.chars.map { |c|
617
+ c.ord.between?(0x3041, 0x3096) ? (c.ord + 0x60).chr("UTF-8") : c
618
+ }.join
619
+ end
620
+
621
+ def hiragana_to_hankaku_katakana(kana)
622
+ kana.chars.map { |c|
623
+ case c.ord
624
+ when 0x3041 then "ァ"
625
+ when 0x3042 then "ア"
626
+ when 0x3043 then "ィ"
627
+ when 0x3044 then "イ"
628
+ when 0x3045 then "ゥ"
629
+ when 0x3046 then "ウ"
630
+ when 0x3047 then "ェ"
631
+ when 0x3048 then "エ"
632
+ when 0x3049 then "ォ"
633
+ when 0x304a then "オ"
634
+ when 0x304b then "カ"
635
+ when 0x304c then "ガ"
636
+ when 0x304d then "キ"
637
+ when 0x304e then "ギ"
638
+ when 0x304f then "ク"
639
+ when 0x3050 then "グ"
640
+ when 0x3051 then "ケ"
641
+ when 0x3052 then "ゲ"
642
+ when 0x3053 then "コ"
643
+ when 0x3054 then "ゴ"
644
+ when 0x3055 then "サ"
645
+ when 0x3056 then "ザ"
646
+ when 0x3057 then "シ"
647
+ when 0x3058 then "ジ"
648
+ when 0x3059 then "ス"
649
+ when 0x305a then "ズ"
650
+ when 0x305b then "セ"
651
+ when 0x305c then "ゼ"
652
+ when 0x305d then "ソ"
653
+ when 0x305e then "ゾ"
654
+ when 0x305f then "タ"
655
+ when 0x3060 then "ダ"
656
+ when 0x3061 then "チ"
657
+ when 0x3062 then "ヂ"
658
+ when 0x3063 then "ッ"
659
+ when 0x3064 then "ツ"
660
+ when 0x3065 then "ヅ"
661
+ when 0x3066 then "テ"
662
+ when 0x3067 then "デ"
663
+ when 0x3068 then "ト"
664
+ when 0x3069 then "ド"
665
+ when 0x306a then "ナ"
666
+ when 0x306b then "ニ"
667
+ when 0x306c then "ヌ"
668
+ when 0x306d then "ネ"
669
+ when 0x306e then "ノ"
670
+ when 0x306f then "ハ"
671
+ when 0x3070 then "バ"
672
+ when 0x3071 then "パ"
673
+ when 0x3072 then "ヒ"
674
+ when 0x3073 then "ビ"
675
+ when 0x3074 then "ピ"
676
+ when 0x3075 then "フ"
677
+ when 0x3076 then "ブ"
678
+ when 0x3077 then "プ"
679
+ when 0x3078 then "ヘ"
680
+ when 0x3079 then "ベ"
681
+ when 0x307a then "ペ"
682
+ when 0x307b then "ホ"
683
+ when 0x307c then "ボ"
684
+ when 0x307d then "ポ"
685
+ when 0x307e then "マ"
686
+ when 0x307f then "ミ"
687
+ when 0x3080 then "ム"
688
+ when 0x3081 then "メ"
689
+ when 0x3082 then "モ"
690
+ when 0x3083 then "ャ"
691
+ when 0x3084 then "ヤ"
692
+ when 0x3085 then "ュ"
693
+ when 0x3086 then "ユ"
694
+ when 0x3087 then "ョ"
695
+ when 0x3088 then "ヨ"
696
+ when 0x3089 then "ラ"
697
+ when 0x308a then "リ"
698
+ when 0x308b then "ル"
699
+ when 0x308c then "レ"
700
+ when 0x308d then "ロ"
701
+ when 0x308e then "ワ"
702
+ when 0x308f then "ワ"
703
+ when 0x3090 then "イ"
704
+ when 0x3091 then "エ"
705
+ when 0x3092 then "ヲ"
706
+ when 0x3093 then "ン"
707
+ else c
708
+ end
709
+ }.join
710
+ end
711
+
712
+ def current_table
713
+ case @mode
714
+ when :katakana then KATAKANA_TABLE
715
+ when :hankaku_katakana then HANKAKU_KATAKANA_TABLE
716
+ else HIRAGANA_TABLE
717
+ end
718
+ end
719
+
720
+ def current_prefixes
721
+ case @mode
722
+ when :hankaku_katakana then HANKAKU_KATAKANA_PREFIXES
723
+ else HIRAGANA_PREFIXES
724
+ end
725
+ end
726
+
727
+ # During converting phase, always use hiragana for yomi tracking
728
+ def hiragana_table_for_converting
729
+ HIRAGANA_TABLE
730
+ end
731
+
732
+ def hiragana_prefixes_for_converting
733
+ HIRAGANA_PREFIXES
734
+ end
735
+
736
+ def update_cursor_color
737
+ return unless STDOUT.tty?
738
+ colors = CONFIG[:skk_cursor_colors] || DEFAULT_CURSOR_COLORS
739
+ color = colors[@mode]
740
+ if color
741
+ STDOUT.write("\e]12;#{color}\a")
742
+ else
743
+ reset_cursor_color
744
+ end
745
+ STDOUT.flush
746
+ end
747
+
748
+ def reset_cursor_color
749
+ return unless STDOUT.tty?
750
+ STDOUT.write("\e]112\a")
751
+ STDOUT.flush
752
+ end
753
+ end
754
+
755
+ SKK_DICTIONARY_URL = "https://github.com/skk-dev/dict/raw/090619ac57ef230a0506c191b569fc8c82b1025b/SKK-JISYO.L"
756
+
757
+ module Commands
758
+ define_command(:skk_download_dictionary, doc: "Download SKK dictionary") do
759
+ path = CONFIG[:skk_dictionary_path]
760
+ if File.exist?(path) && !yes_or_no?("#{path} already exists. Overwrite it?")
761
+ return
762
+ end
763
+ background do
764
+ URI.open(SKK_DICTIONARY_URL) do |f|
765
+ File.binwrite(path, f.read)
766
+ end
767
+ foreground do
768
+ message("Downloaded to #{path}")
769
+ end
770
+ end
771
+ end
772
+ end
773
+ end