-
Notifications
You must be signed in to change notification settings - Fork 0
/
editor.rb
executable file
·165 lines (139 loc) · 3 KB
/
editor.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env ruby
require 'io/console'
class Editor
def initialize
@lines = File.readlines("foo.txt").map do |line|
line.sub(/\n$/, '')
end
@buffer = Buffer.new(@lines)
@cursor = Cursor.new
@history = []
end
def run
IO.console.raw do
loop do
render
handle_input
end
end
rescue
50.times { puts }
raise
end
def render
ANSI.clear_screen
ANSI.move_cursor(0, 0)
@buffer.render
ANSI.move_cursor(@cursor.row, @cursor.col)
end
def handle_input
char = $stdin.getc
case char
when "\C-q" then exit(0)
when "\C-p" then @cursor = @cursor.up(@buffer)
when "\C-n" then @cursor = @cursor.down(@buffer)
when "\C-b" then @cursor = @cursor.left(@buffer)
when "\C-f" then @cursor = @cursor.right(@buffer)
when "\C-u" then restore_snapshot
when "\r"
save_snapshot
@buffer = @buffer.split_line(@cursor.row, @cursor.col)
@cursor = @cursor.down(@buffer).move_to_col(0)
when 127.chr
save_snapshot
if @cursor.col > 0
@buffer = @buffer.delete(@cursor.row, @cursor.col - 1)
@cursor = @cursor.left(@buffer)
end
else
save_snapshot
@buffer = @buffer.insert(char, @cursor.row, @cursor.col)
@cursor = @cursor.right(@buffer)
end
end
def save_snapshot
@history << [@buffer, @cursor]
end
def restore_snapshot
if @history.length > 0
@buffer, @cursor = @history.pop
end
end
end
class Buffer
def initialize(lines)
@lines = lines
end
def render
@lines.each do |line|
$stdout.write(line + "\r\n")
end
end
def insert(char, row, col)
lines = @lines.map(&:dup)
lines.fetch(row).insert(col, char)
Buffer.new(lines)
end
def delete(row, col)
lines = @lines.map(&:dup)
lines.fetch(row).slice!(col)
Buffer.new(lines)
end
def split_line(row, col)
lines = @lines.map(&:dup)
line = lines.fetch(row)
lines[row..row] = [line[0...col], line[col..-1]]
Buffer.new(lines)
end
def line_count
@lines.count
end
def line_length(row)
@lines.fetch(row).length
end
end
class Cursor
attr_reader :row, :col
def initialize(row=0, col=0)
@row = row
@col = col
end
def up(buffer)
Cursor.new(row - 1, col).clamp(buffer)
end
def down(buffer)
Cursor.new(row + 1, col).clamp(buffer)
end
def left(buffer)
Cursor.new(row, col - 1).clamp(buffer)
end
def right(buffer)
Cursor.new(row, col + 1).clamp(buffer)
end
def move_to_col(col)
Cursor.new(row, col)
end
def clamp(buffer)
row = _clamp(@row, 0, buffer.line_count - 1)
col = _clamp(@col, 0, buffer.line_length(row))
Cursor.new(row, col)
end
def _clamp(val, low, high)
if val < low
low
elsif val > high
high
else
val
end
end
end
class ANSI
def self.clear_screen
$stdout.write("\e[2J")
end
def self.move_cursor(row, col)
$stdout.write("\e[#{row + 1};#{col + 1}H")
end
end
Editor.new.run