From scratch forth text editor.
This commit is contained in:
parent
1d483e23ca
commit
ebb8172551
|
@ -0,0 +1,203 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
# This is a simple forth editor written in Ruby, following along with
|
||||||
|
# the DAS "from scratch" text editor tutorial.
|
||||||
|
|
||||||
|
require "io/console"
|
||||||
|
|
||||||
|
class Editor
|
||||||
|
|
||||||
|
def initialize(filename="")
|
||||||
|
if filename.length > 0
|
||||||
|
if File.exists? filename
|
||||||
|
lines = File.readlines(filename).map do |line|
|
||||||
|
line.sub(/\n$/, "")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
lines = ['']
|
||||||
|
end
|
||||||
|
else
|
||||||
|
filename = "untitled.txt"
|
||||||
|
lines = ['']
|
||||||
|
end
|
||||||
|
@filename = filename
|
||||||
|
@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)
|
||||||
|
# operating on the cursor makes undo easier
|
||||||
|
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-s" then @buffer.write(@filename)
|
||||||
|
when "\C-u" then restore_snapshot
|
||||||
|
when "\C-e" then
|
||||||
|
@cursor = @cursor.move_to_col(@buffer.line_length(@cursor.row))
|
||||||
|
when "\C-a" then @cursor = @cursor.move_to_col(0)
|
||||||
|
when "\C-r" then forth
|
||||||
|
when 127.chr
|
||||||
|
if @cursor.col > 0
|
||||||
|
save_snapshot
|
||||||
|
@buffer = @buffer.delete(@cursor.row, @cursor.col-1)
|
||||||
|
@cursor = @cursor.left(@buffer)
|
||||||
|
end
|
||||||
|
when "\r"
|
||||||
|
# in raw mode, enter sends \r
|
||||||
|
save_snapshot
|
||||||
|
@buffer = @buffer.split_line(@cursor.row, @cursor.col)
|
||||||
|
@cursor = @cursor.down(@buffer).move_to_col(0)
|
||||||
|
else
|
||||||
|
save_snapshot
|
||||||
|
# keep buffer immutable so we can do undo
|
||||||
|
@buffer = @buffer.insert(char, @cursor.row, @cursor.col)
|
||||||
|
# lot of the implemenation of basic actions is ensuring
|
||||||
|
# cursor consistency
|
||||||
|
@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
|
||||||
|
|
||||||
|
def forth
|
||||||
|
ANSI.clear_screen
|
||||||
|
puts "gforth {@filename}\r\n"
|
||||||
|
puts "------\r\n"
|
||||||
|
output = `gforth #{@filename}`.split("\n")
|
||||||
|
Buffer.new(output).render
|
||||||
|
puts "-----\r\n"
|
||||||
|
puts "Press <enter> to exit...\r\n"
|
||||||
|
|
||||||
|
loop do
|
||||||
|
char = $stdin.getc
|
||||||
|
case char
|
||||||
|
when "\r" then break
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
def write(filename)
|
||||||
|
File.write(filename, @lines.join("\n"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Cursor
|
||||||
|
attr_reader :row, :col
|
||||||
|
def initialize(row=0, col=0)
|
||||||
|
@row = row
|
||||||
|
@col = col
|
||||||
|
end
|
||||||
|
|
||||||
|
def left(buffer)
|
||||||
|
Cursor.new(@row, @col-1).clamp(buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def right(buffer)
|
||||||
|
Cursor.new(@row, @col+1).clamp(buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def up(buffer)
|
||||||
|
Cursor.new(@row-1, @col).clamp(buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down(buffer)
|
||||||
|
Cursor.new(@row+1, @col).clamp(buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def clamp(buffer)
|
||||||
|
row = @row.clamp(0, buffer.line_count - 1)
|
||||||
|
col = @col.clamp(0, buffer.line_length(row))
|
||||||
|
Cursor.new(row, col)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_to_col(col)
|
||||||
|
Cursor.new(row, col)
|
||||||
|
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
|
||||||
|
|
||||||
|
filename = ""
|
||||||
|
if ARGV.length > 0
|
||||||
|
filename = ARGV[0]
|
||||||
|
end
|
||||||
|
Editor.new(filename).run
|
|
@ -0,0 +1 @@
|
||||||
|
2 3 + . bye
|
Loading…
Reference in New Issue