exfuz 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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +44 -0
- data/README.md +88 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/exfuz +4 -0
- data/exfuz.gemspec +24 -0
- data/lib/exfuz/body.rb +105 -0
- data/lib/exfuz/book_name.rb +36 -0
- data/lib/exfuz/candidate.rb +48 -0
- data/lib/exfuz/candidates.rb +53 -0
- data/lib/exfuz/cell.rb +82 -0
- data/lib/exfuz/command.rb +11 -0
- data/lib/exfuz/configuration.rb +86 -0
- data/lib/exfuz/event.rb +17 -0
- data/lib/exfuz/fuzzy_finder_command.rb +37 -0
- data/lib/exfuz/group.rb +34 -0
- data/lib/exfuz/jump.rb +29 -0
- data/lib/exfuz/key_map.rb +116 -0
- data/lib/exfuz/main.rb +59 -0
- data/lib/exfuz/operator.ps1 +62 -0
- data/lib/exfuz/parser.rb +67 -0
- data/lib/exfuz/position.rb +63 -0
- data/lib/exfuz/query.rb +103 -0
- data/lib/exfuz/screen.rb +228 -0
- data/lib/exfuz/sheet_name.rb +31 -0
- data/lib/exfuz/status.rb +25 -0
- data/lib/exfuz/util.rb +26 -0
- data/lib/exfuz/version.rb +5 -0
- data/lib/exfuz.rb +27 -0
- metadata +76 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exfuz
|
|
4
|
+
class FuzzyFinderCommand
|
|
5
|
+
attr_reader :selected
|
|
6
|
+
|
|
7
|
+
CMDS = { fzf: %w[fzf -m], peco: 'peco', percol: 'percol', sk: %w[sk -m] }.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(command_type: :fzf)
|
|
10
|
+
@selected = ''
|
|
11
|
+
@cmd = CMDS[command_type] || CMDS[:fzf]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
stdio = IO.popen(@cmd, 'r+')
|
|
16
|
+
fiber = Fiber.new do |init_line|
|
|
17
|
+
puts_line(stdio, init_line)
|
|
18
|
+
loop do
|
|
19
|
+
line = Fiber.yield
|
|
20
|
+
puts_line(stdio, line)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
yield fiber
|
|
25
|
+
ensure
|
|
26
|
+
stdio.close_write
|
|
27
|
+
@selected = stdio.each_line(chomp: true).to_a
|
|
28
|
+
stdio.close_read
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def puts_line(stdio, line)
|
|
34
|
+
stdio.puts line
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/exfuz/group.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exfuz
|
|
4
|
+
class Group
|
|
5
|
+
def initialize(positions, keys)
|
|
6
|
+
@data = {}
|
|
7
|
+
@raw_positions = positions
|
|
8
|
+
create(positions, keys)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create(positions, keys)
|
|
12
|
+
indices = (0...positions.size).to_a
|
|
13
|
+
keys.each do |key|
|
|
14
|
+
@data[key] = indices.group_by { |idx| positions[idx].slice(key) }
|
|
15
|
+
.values
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def key_positions(key)
|
|
20
|
+
@data[key].map do |grouping_indices|
|
|
21
|
+
@raw_positions[grouping_indices.first].slice(key)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def positions(key)
|
|
26
|
+
@data[key].each_with_object({}) do |grouping_indices, result|
|
|
27
|
+
key_position = @raw_positions[grouping_indices.first].slice(key)
|
|
28
|
+
result[key_position] = grouping_indices.map do |grouping_idx|
|
|
29
|
+
@raw_positions[grouping_idx]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/exfuz/jump.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Exfuz
|
|
6
|
+
class Jump
|
|
7
|
+
OPERATOR_PATH = Exfuz::Util.wsl_to_windows(File.join(__dir__, './operator.ps1'))
|
|
8
|
+
# powershellのホストから見て、wslがリモート扱いのためBypassを付与
|
|
9
|
+
OPERATOR_CMD = "PowerShell.exe -ExecutionPolicy Bypass '$Input | #{OPERATOR_PATH}'"
|
|
10
|
+
|
|
11
|
+
def initialize(positions)
|
|
12
|
+
@positions = positions
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
data = @positions.map do |p|
|
|
17
|
+
{ to: p.bottom_name, info: p.jump_info }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
result = nil
|
|
21
|
+
IO.popen(OPERATOR_CMD, 'r+') do |io|
|
|
22
|
+
io.puts JSON.unparse(data)
|
|
23
|
+
io.close_write
|
|
24
|
+
result = io.read
|
|
25
|
+
end
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'curses'
|
|
4
|
+
|
|
5
|
+
module Exfuz
|
|
6
|
+
class KeyMap
|
|
7
|
+
def initialize
|
|
8
|
+
@kmap = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_event_handler(key, obj, func: :update)
|
|
12
|
+
@kmap[key] ||= Exfuz::Event.new
|
|
13
|
+
@kmap[key].add_event_handler(obj, func)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pressed(key, *args)
|
|
17
|
+
return unless @kmap.key?(key)
|
|
18
|
+
|
|
19
|
+
@kmap[key].fired(*args)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Key
|
|
24
|
+
private_constant
|
|
25
|
+
|
|
26
|
+
# input_to_name_and_charで、文字と区別するためにシンボルで定義
|
|
27
|
+
CTRL_A = :ctrl_a
|
|
28
|
+
CTRL_E = :ctrl_e
|
|
29
|
+
CTRL_R = :ctrl_r
|
|
30
|
+
CTRL_H = :ctrl_h
|
|
31
|
+
CTRL_L = :ctrl_l
|
|
32
|
+
ESC = :esc
|
|
33
|
+
F1 = :f1
|
|
34
|
+
F2 = :f2
|
|
35
|
+
F3 = :f3
|
|
36
|
+
F4 = :f4
|
|
37
|
+
F5 = :f5
|
|
38
|
+
F6 = :f6
|
|
39
|
+
F7 = :f7
|
|
40
|
+
F8 = :f8
|
|
41
|
+
F9 = :f9
|
|
42
|
+
F10 = :f10
|
|
43
|
+
F11 = :f11
|
|
44
|
+
F12 = :f12
|
|
45
|
+
UP = :down
|
|
46
|
+
DOWN = :up
|
|
47
|
+
RIGHT = :right
|
|
48
|
+
LEFT = :left
|
|
49
|
+
BACKSPACE = :backspace
|
|
50
|
+
ENTER = :enter
|
|
51
|
+
CHAR = :char
|
|
52
|
+
|
|
53
|
+
# メモ化してもカーソル移動が若干遅い
|
|
54
|
+
INPUT_TO_SPECIAL_KEY_NAME = {
|
|
55
|
+
'OP' => F1,
|
|
56
|
+
'OQ' => F2,
|
|
57
|
+
'OR' => F3,
|
|
58
|
+
'OS' => F4,
|
|
59
|
+
'[15' => F5,
|
|
60
|
+
'[17' => F6,
|
|
61
|
+
'[18' => F7,
|
|
62
|
+
'[19' => F8,
|
|
63
|
+
'[20' => F9,
|
|
64
|
+
'[21' => F10,
|
|
65
|
+
'[23' => F11,
|
|
66
|
+
'[24' => F12,
|
|
67
|
+
'[A' => UP,
|
|
68
|
+
'[B' => DOWN,
|
|
69
|
+
'[C' => RIGHT,
|
|
70
|
+
'[D' => LEFT,
|
|
71
|
+
Curses::Key::ENTER => ENTER,
|
|
72
|
+
10 => ENTER,
|
|
73
|
+
27 => ESC,
|
|
74
|
+
Curses::KEY_CTRL_E => CTRL_E,
|
|
75
|
+
Curses::KEY_CTRL_R => CTRL_R,
|
|
76
|
+
Curses::KEY_CTRL_H => CTRL_H,
|
|
77
|
+
Curses::KEY_CTRL_L => CTRL_L,
|
|
78
|
+
127 => BACKSPACE # 現状は開発環境の値に合わせる (127 or 263)
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
81
|
+
module_function
|
|
82
|
+
|
|
83
|
+
def input_to_name_and_char(input)
|
|
84
|
+
case input
|
|
85
|
+
when Array
|
|
86
|
+
first = input.first
|
|
87
|
+
if special_key?(first)
|
|
88
|
+
input_to_special_key_name(input.slice(1, input.size - 1).join)
|
|
89
|
+
else
|
|
90
|
+
[CHAR, multibytes_to_char(input)]
|
|
91
|
+
end
|
|
92
|
+
when Integer
|
|
93
|
+
input_to_special_key_name(input)
|
|
94
|
+
when String
|
|
95
|
+
[CHAR, input]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def input_to_special_key_name(str_or_number)
|
|
100
|
+
INPUT_TO_SPECIAL_KEY_NAME[str_or_number]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def can_convert_to_name_and_char?(input)
|
|
104
|
+
name = input_to_name_and_char(input)
|
|
105
|
+
!name.nil? && name != ESC
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def special_key?(number)
|
|
109
|
+
number == 27
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def multibytes_to_char(multi_bytes)
|
|
113
|
+
multi_bytes.pack('C*').force_encoding(Encoding::UTF_8)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/exfuz/main.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def read_data(xlsxs, candidates = [], status)
|
|
4
|
+
xlsxs.each_with_index do |xlsx, _idx|
|
|
5
|
+
p = Exfuz::Parser.new(xlsx)
|
|
6
|
+
p.parse
|
|
7
|
+
p.each_cell_with_all do |cell|
|
|
8
|
+
b = Exfuz::BookName.new(cell[:book_name])
|
|
9
|
+
s = Exfuz::SheetName.new(cell[:sheet_name])
|
|
10
|
+
c = Exfuz::Cell.new(address: cell[:cell], value: cell[:value])
|
|
11
|
+
candidates.push(Exfuz::Position.new([{ book_name: b }, { sheet_name: s }, { textable: c }]))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
status.update(1)
|
|
15
|
+
sleep 1
|
|
16
|
+
end
|
|
17
|
+
candidates.close_push
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def candidate_by(candidates, line, sep: ':')
|
|
21
|
+
lnum = line.split(sep)[0]
|
|
22
|
+
candidates[lnum.to_i - 1]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def main
|
|
26
|
+
xlsxs = Dir.glob('**/[^~$]*.xlsx')
|
|
27
|
+
|
|
28
|
+
status = Exfuz::Status.new(xlsxs.size)
|
|
29
|
+
candidates = Exfuz::Candidates.new
|
|
30
|
+
key_map = Exfuz::KeyMap.new
|
|
31
|
+
screen = Exfuz::Screen.new(status, key_map, candidates)
|
|
32
|
+
|
|
33
|
+
screen.init
|
|
34
|
+
Curses.close_screen
|
|
35
|
+
screen.init
|
|
36
|
+
|
|
37
|
+
Thread.new do
|
|
38
|
+
sleep 0.01
|
|
39
|
+
read_data(xlsxs, candidates, status)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
loop do
|
|
43
|
+
unless Thread.list.find { |t| t.name == 'wating_for_input' }
|
|
44
|
+
in_t = Thread.new do
|
|
45
|
+
screen.rerender if screen.changed_state?
|
|
46
|
+
end
|
|
47
|
+
in_t.name = 'wating_for_input'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
screen.wait_input
|
|
51
|
+
break if screen.closed?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if $PROGRAM_NAME == __FILE__
|
|
56
|
+
$LOAD_PATH.unshift(File.expand_path('..', __dir__))
|
|
57
|
+
require_relative '../exfuz'
|
|
58
|
+
main
|
|
59
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(ValueFromPipeline=$true)][string]$in
|
|
3
|
+
)
|
|
4
|
+
$excel = New-Object -ComObject Excel.Application
|
|
5
|
+
|
|
6
|
+
$excel.Visible = $true
|
|
7
|
+
Function Jump($to, [ref]$info) {
|
|
8
|
+
switch ($to) {
|
|
9
|
+
'book_name' { $book = JumpBook $info.Value.book_name }
|
|
10
|
+
'sheet_name' {
|
|
11
|
+
$book = JumpBook $info.Value.book_name
|
|
12
|
+
if ($book) {
|
|
13
|
+
$sheet = JumpSheet ([ref]$book) $info.Value.sheet_name
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
'textable' {
|
|
17
|
+
$book = JumpBook $info.Value.book_name
|
|
18
|
+
if ($book) {
|
|
19
|
+
$sheet = JumpSheet ([ref]$book) $info.Value.sheet_name
|
|
20
|
+
$cell = JumpCell ([ref]$sheet) ([ref]$info.Value.textable)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
Default {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Function IsOpen($path) {
|
|
28
|
+
$openedPaths = @($excel.Workbooks | % {$_.Path})
|
|
29
|
+
if ($null -eq $openedPaths) {
|
|
30
|
+
return $false
|
|
31
|
+
}
|
|
32
|
+
return $openedPaths.Contains($path)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Function JumpBook($path) {
|
|
36
|
+
if(IsOpen $path) {
|
|
37
|
+
return $null
|
|
38
|
+
}
|
|
39
|
+
$book = $excel.Workbooks.Open($path)
|
|
40
|
+
return $book
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Function JumpSheet([ref]$book, $sheetName) {
|
|
44
|
+
$sheet = $book.Value.Worksheets($sheetName)
|
|
45
|
+
$sheet.Activate()
|
|
46
|
+
return $sheet
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Function JumpCell([ref]$sheet, [ref]$cellPosition) {
|
|
50
|
+
$cell = $sheet.Value.Cells($cellPosition.Value.row, $cellPosition.Value.col)
|
|
51
|
+
$cell.Activate()
|
|
52
|
+
return $cell
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$targets = ConvertFrom-Json $in
|
|
56
|
+
try {
|
|
57
|
+
foreach ($target in $targets) {
|
|
58
|
+
$result = Jump $target.to ([ref]$target.info)
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
#$excel.Quit()
|
|
62
|
+
}
|
data/lib/exfuz/parser.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'xsv'
|
|
2
|
+
|
|
3
|
+
module Exfuz
|
|
4
|
+
class Parser
|
|
5
|
+
EXTENSIONS = ['.xlsx', '.xls', '.xlsm'].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :sheet_names
|
|
8
|
+
|
|
9
|
+
def initialize(path)
|
|
10
|
+
raise 'not exsits file' unless File.exist?(path)
|
|
11
|
+
|
|
12
|
+
extname = File.extname(path)
|
|
13
|
+
raise 'no match extension name' unless EXTENSIONS.include?(extname)
|
|
14
|
+
|
|
15
|
+
@absolute_path = File.absolute_path(path)
|
|
16
|
+
@book = nil
|
|
17
|
+
@sheet_names = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse
|
|
21
|
+
@book = Xsv.open(@absolute_path)
|
|
22
|
+
@sheet_names = @book.sheets.map { |s| s.name.force_encoding(Encoding::UTF_8) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def book_name
|
|
26
|
+
@absolute_path
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def each_cell_with_all
|
|
30
|
+
@book.sheets.each do |sheet|
|
|
31
|
+
sheet_name = sheet.name
|
|
32
|
+
sheet.each_with_index do |row, r_i|
|
|
33
|
+
row.each_with_index do |val, c_i|
|
|
34
|
+
next if val.nil?
|
|
35
|
+
|
|
36
|
+
cell = {
|
|
37
|
+
book_name: book_name,
|
|
38
|
+
sheet_name: sheet_name,
|
|
39
|
+
cell: to_address(r_i + 1, c_i + 1),
|
|
40
|
+
value: val
|
|
41
|
+
}
|
|
42
|
+
yield cell
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def to_address(r_idx, c_idx)
|
|
51
|
+
"$#{to_alphabet(c_idx)}$#{r_idx}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_alphabet(idx)
|
|
55
|
+
large_a_z = ('A'..'Z').to_a
|
|
56
|
+
alphabet = ''
|
|
57
|
+
q = idx
|
|
58
|
+
|
|
59
|
+
until q.zero?
|
|
60
|
+
q, r = (q - 1).divmod(26)
|
|
61
|
+
alphabet.concat(large_a_z[r])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alphabet
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exfuz
|
|
4
|
+
class Position
|
|
5
|
+
def initialize(hierarchy)
|
|
6
|
+
@key_to_obj = {}
|
|
7
|
+
@hierarchy = hierarchy
|
|
8
|
+
hierarchy.each do |h|
|
|
9
|
+
k = h.keys[0]
|
|
10
|
+
@key_to_obj[k] = h[k]
|
|
11
|
+
|
|
12
|
+
instance_eval "@#{k} = h[k]", __FILE__, __LINE__
|
|
13
|
+
self.class.send(:attr_reader, k)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def bottom_name
|
|
18
|
+
@key_to_obj.keys.last
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def slice(key)
|
|
22
|
+
keys = @key_to_obj.keys
|
|
23
|
+
remaining = keys[0, (keys.find_index { |k| k == key }) + 1]
|
|
24
|
+
args = @hierarchy.filter do |h|
|
|
25
|
+
remaining.include?(h.keys[0])
|
|
26
|
+
end
|
|
27
|
+
Exfuz::Position.new(args)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def slice_keys(key)
|
|
31
|
+
keys = @key_to_obj.keys
|
|
32
|
+
keys[0, (keys.find_index { |k| k == key }) + 1]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def match?(conditions)
|
|
36
|
+
conditions.each do |key, value|
|
|
37
|
+
unless obj = @key_to_obj[key]
|
|
38
|
+
raise 'not exist key'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return false unless obj.match?(value)
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def jump_info
|
|
47
|
+
keys = @key_to_obj.keys
|
|
48
|
+
keys.each_with_object({}) do |key, result|
|
|
49
|
+
obj = send(key)
|
|
50
|
+
result.merge!(obj.jump_info)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ==(other)
|
|
55
|
+
other.class === self && other.hash == hash
|
|
56
|
+
end
|
|
57
|
+
alias eql? ==
|
|
58
|
+
|
|
59
|
+
def hash
|
|
60
|
+
@key_to_obj.values.hash
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/exfuz/query.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'unicode/display_width'
|
|
4
|
+
require 'unicode/display_width/string_ext'
|
|
5
|
+
|
|
6
|
+
module Exfuz
|
|
7
|
+
class Query
|
|
8
|
+
attr_reader :line, :caret
|
|
9
|
+
|
|
10
|
+
MAX_UTF_8_BYTES = 3
|
|
11
|
+
|
|
12
|
+
def initialize(caret)
|
|
13
|
+
@chars = []
|
|
14
|
+
@line = ''
|
|
15
|
+
@caret = caret
|
|
16
|
+
@offset = caret[1]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def text
|
|
20
|
+
@chars.join
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add(ch_or_chs)
|
|
24
|
+
if ch_or_chs.instance_of?(Array)
|
|
25
|
+
ch_or_chs.each do |ch|
|
|
26
|
+
insert_at_caret(ch)
|
|
27
|
+
right
|
|
28
|
+
end
|
|
29
|
+
elsif ch_or_chs.instance_of?(String)
|
|
30
|
+
insert_at_caret(ch_or_chs)
|
|
31
|
+
right
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete
|
|
36
|
+
return if left_end?(@caret[1])
|
|
37
|
+
|
|
38
|
+
left
|
|
39
|
+
remove_at_caret
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def right(direction = 1)
|
|
43
|
+
col = 0
|
|
44
|
+
|
|
45
|
+
col += 1 while direction > col && next_caret
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def left(direction = 1)
|
|
49
|
+
col = 0
|
|
50
|
+
|
|
51
|
+
col += 1 while direction > col && prev_caret
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# ['あ', nil]
|
|
57
|
+
# caret 0 -> 2 (right end)
|
|
58
|
+
def next_caret
|
|
59
|
+
return if right_end?(@caret[1])
|
|
60
|
+
|
|
61
|
+
idx = current - @offset
|
|
62
|
+
idx += 1 while idx < @chars.size - 1 && @chars[idx + 1].nil?
|
|
63
|
+
@caret[1] = @offset + idx + 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# ['あ', nil]
|
|
67
|
+
# caret 2 -> 0 (left end)
|
|
68
|
+
def prev_caret
|
|
69
|
+
return if left_end?(@caret[1])
|
|
70
|
+
|
|
71
|
+
idx = current - @offset
|
|
72
|
+
idx -= 1 while !idx.zero? && @chars[idx - 1].nil?
|
|
73
|
+
@caret[1] = @offset + idx - 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def insert_at_caret(ch)
|
|
77
|
+
r_arr = @chars.slice!((current - @offset)..-1)
|
|
78
|
+
@chars.concat([ch] + [nil] * (ch.display_width - 1) + r_arr)
|
|
79
|
+
@line = @chars * ''
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# [a, あ, nil, い, nil] -> [a, い, nil]
|
|
83
|
+
# caret 1 -> 0
|
|
84
|
+
def remove_at_caret
|
|
85
|
+
r_arr = @chars.slice!((current - @offset)..-1)
|
|
86
|
+
deleted = r_arr.delete_at(0)
|
|
87
|
+
@chars.concat(r_arr.drop_while(&:nil?))
|
|
88
|
+
@line = @chars.clone.push(' ' * deleted.display_width) * ''
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def current
|
|
92
|
+
@caret[1]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def left_end?(col)
|
|
96
|
+
@offset == col
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def right_end?(col)
|
|
100
|
+
@chars.size + @offset == col
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|