From ebb81725514e6dfa089c245264eacdb6c1f26419 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 23 Apr 2019 18:28:32 -0700 Subject: [PATCH] From scratch forth text editor. --- from_scratch/editor.rb | 203 +++++++++++++++++++++++++++++++++++++++++ from_scratch/test.fs | 1 + 2 files changed, 204 insertions(+) create mode 100644 from_scratch/editor.rb create mode 100644 from_scratch/test.fs diff --git a/from_scratch/editor.rb b/from_scratch/editor.rb new file mode 100644 index 0000000..5ac417c --- /dev/null +++ b/from_scratch/editor.rb @@ -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 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 diff --git a/from_scratch/test.fs b/from_scratch/test.fs new file mode 100644 index 0000000..1abff7d --- /dev/null +++ b/from_scratch/test.fs @@ -0,0 +1 @@ +2 3 + . bye \ No newline at end of file