diff options
author | Andreas Kling <awesomekling@gmail.com> | 2018-12-04 00:27:16 +0100 |
---|---|---|
committer | Andreas Kling <awesomekling@gmail.com> | 2018-12-04 00:27:16 +0100 |
commit | ca6847b5bb6d9b86d5f2123533ebfe510c01d880 (patch) | |
tree | 8515f9360e8ade5478edd0acd4d994fe33d8e520 /Editor | |
parent | 405383fd2f9757cebd175c5be9cfa65c48fe15f0 (diff) | |
download | serenity-ca6847b5bb6d9b86d5f2123533ebfe510c01d880.zip |
Import a simple text editor I started working on.
Diffstat (limited to 'Editor')
-rw-r--r-- | Editor/Document.cpp | 46 | ||||
-rw-r--r-- | Editor/Document.h | 27 | ||||
-rw-r--r-- | Editor/Editor.cpp | 336 | ||||
-rw-r--r-- | Editor/Editor.h | 74 | ||||
-rw-r--r-- | Editor/FileReader.cpp | 40 | ||||
-rw-r--r-- | Editor/FileReader.h | 17 | ||||
-rw-r--r-- | Editor/InsertOperation.cpp | 26 | ||||
-rw-r--r-- | Editor/InsertOperation.h | 17 | ||||
-rw-r--r-- | Editor/Line.cpp | 95 | ||||
-rw-r--r-- | Editor/Line.h | 40 | ||||
-rw-r--r-- | Editor/Makefile | 25 | ||||
-rw-r--r-- | Editor/Operation.cpp | 9 | ||||
-rw-r--r-- | Editor/Operation.h | 14 | ||||
-rw-r--r-- | Editor/Position.h | 27 | ||||
-rw-r--r-- | Editor/UndoStack.cpp | 14 | ||||
-rw-r--r-- | Editor/UndoStack.h | 18 | ||||
-rw-r--r-- | Editor/cuki.h | 6 | ||||
-rw-r--r-- | Editor/main.cpp | 19 |
18 files changed, 850 insertions, 0 deletions
diff --git a/Editor/Document.cpp b/Editor/Document.cpp new file mode 100644 index 0000000000..26d3b24f54 --- /dev/null +++ b/Editor/Document.cpp @@ -0,0 +1,46 @@ +#include "Document.h" +#include "FileReader.h" + +OwnPtr<Document> Document::create_from_file(const std::string& path) +{ + auto document = make<Document>(); + + FileReader reader(path); + while (reader.can_read()) { + auto line = reader.read_line(); + document->m_lines.push_back(Line(line)); + } + + return document; +} + +void Document::dump() +{ + fprintf(stderr, "Document{%p}\n", this); + for (size_t i = 0; i < m_lines.size(); ++i) { + fprintf(stderr, "[%02zu] %s\n", i, m_lines[i].data().c_str()); + } +} + +bool Document::backspace_at(Position position) +{ + return false; +} + +bool Document::insert_at(Position position, const std::string& text) +{ + static FILE* f = fopen("log", "a"); + fprintf(f, "@%zu,%zu: +%s\n", position.line(), position.column(), text.c_str()); + fflush(f); + ASSERT(position.is_valid()); + if (!position.is_valid()) + return false; + ASSERT(position.line() < line_count()); + if (position.line() >= line_count()) + return false; + Line& line = m_lines[position.line()]; + if (position.column() > line.length()) + return false; + line.insert(position.column(), text); + return true; +} diff --git a/Editor/Document.h b/Editor/Document.h new file mode 100644 index 0000000000..9243038180 --- /dev/null +++ b/Editor/Document.h @@ -0,0 +1,27 @@ +#pragma once + +#include "cuki.h" +#include "Line.h" +#include "Position.h" +#include "OwnPtr.h" +#include <string> + +class Document { +public: + Document() { } + ~Document() { } + + const std::deque<Line>& lines() const { return m_lines; } + std::deque<Line>& lines() { return m_lines; } + size_t line_count() const { return m_lines.size(); } + + static OwnPtr<Document> create_from_file(const std::string& path); + + bool insert_at(Position, const std::string&); + bool backspace_at(Position); + + void dump(); + +private: + std::deque<Line> m_lines; +}; diff --git a/Editor/Editor.cpp b/Editor/Editor.cpp new file mode 100644 index 0000000000..0ad3b3edd0 --- /dev/null +++ b/Editor/Editor.cpp @@ -0,0 +1,336 @@ +#include "Editor.h" +#include "Document.h" +#include "InsertOperation.h" + +#define _XOPEN_SOURCE_EXTENDED +#include <locale.h> +#include <ncurses.h> + +static int statusbar_attributes; +static int ruler_attributes; + +Editor::Editor() +{ + setlocale(LC_ALL, ""); + initscr(); + start_color(); + use_default_colors(); + + init_pair(1, COLOR_WHITE, COLOR_BLUE); + init_pair(2, COLOR_BLUE, -1); + + statusbar_attributes = COLOR_PAIR(1); + ruler_attributes = COLOR_PAIR(2); + + raw(); + keypad(stdscr, true); + timeout(10); + noecho(); + refresh(); +} + +Editor::~Editor() +{ + //move(2, 2); + //printw("*** Press any key to exit! ***"); + //getch(); + endwin(); +} + +void Editor::set_document(OwnPtr<Document>&& document) +{ + m_document = std::move(document); + m_cursor.move_to(0, 0); + m_scroll_position.move_to(0, 0); +} + +void Editor::redraw() +{ + clear(); + if (!m_document) + return; + + size_t window_height = getmaxy(stdscr); + size_t window_width = getmaxx(stdscr); + + for (size_t row = 0; row < window_height - 1; ++row) { + size_t current_document_line = m_scroll_position.line() + row; + size_t current_document_column = m_scroll_position.column(); + + move(row, 0); + + if (current_document_line >= m_document->line_count()) { + printw("~"); + } else { + attron(ruler_attributes); + printw("%3d ", current_document_line); + attroff(ruler_attributes); + m_ruler_width = 4; + size_t line_length = m_document->lines()[current_document_line].data().size(); + const char* line_data = m_document->lines()[current_document_line].data().c_str(); + if (m_scroll_position.column() < line_length) + addnstr(line_data + m_scroll_position.column(), window_width - m_ruler_width); + } + } + + draw_status_bar(); + + draw_cursor(); + refresh(); +} + +void Editor::draw_cursor() +{ + ssize_t cursor_row_on_screen = m_cursor.line() - m_scroll_position.line(); + ssize_t cursor_column_on_screen = m_cursor.column() - m_scroll_position.column(); + + move(cursor_row_on_screen, cursor_column_on_screen + m_ruler_width); + +} + +void Editor::draw_status_bar() +{ + int old_background = getbkgd(stdscr); + bkgdset(' ' | statusbar_attributes); + + size_t window_height = getmaxy(stdscr); + size_t window_width = getmaxx(stdscr); + + move(window_height - 1, 0); + clrtoeol(); + + if (is_editing_document()) { + attron(A_STANDOUT); + printw("* Editing *"); + attroff(A_STANDOUT); + } else if (is_editing_command()) { + printw("\\%s", m_command.c_str()); + } else { + attron(A_BOLD); + addstr("~(^_^)~ "); + if (m_status_text.size() > 0) { + addstr(m_status_text.c_str()); + } + attroff(A_BOLD); + } + + move(window_height - 1, window_width - 20); + printw("%zu, %zu", m_scroll_position.line(), m_scroll_position.column()); + + move(window_height - 1, window_width - 8); + printw("%zu, %zu", m_cursor.line(), m_cursor.column()); + attroff(statusbar_attributes); + + bkgdset(old_background); +} + +int Editor::exec() +{ + while (!m_should_quit) { + redraw(); + int ch = getch(); + if (ch == ERR) { + continue; + fprintf(stderr, "getch() returned ERR\n"); + break; + } + + if (is_editing_document() || is_editing_command()) { + if (ch == 27) + set_mode(Idle); + else { + if (is_editing_document()) + handle_document_key_press(ch); + else + handle_command_key_press(ch); + } + } else { + switch (ch) { + case 'h': move_left(); break; + case 'j': move_down(); break; + case 'k': move_up(); break; + case 'l': move_right(); break; + case 'i': set_mode(EditingDocument); break; + case 'q': m_should_quit = true; break; + case '\\': set_mode(EditingCommand); break; + } + } + } + return 0; +} + +void Editor::move_left() +{ + if (m_cursor.column() == 0) + return; + m_cursor.move_by(0, -1); + update_scroll_position_if_needed(); +} + +void Editor::move_down() +{ + if (m_cursor.line() >= max_line()) + return; + m_cursor.move_by(1, 0); + if (m_cursor.column() > max_column()) + m_cursor.set_column(max_column()); + + update_scroll_position_if_needed(); +} + +void Editor::move_up() +{ + if (m_cursor.line() == 0) + return; + m_cursor.move_by(-1, 0); + if (m_cursor.column() > max_column()) + m_cursor.set_column(max_column()); + + update_scroll_position_if_needed(); +} + +void Editor::move_right() +{ + if (m_cursor.column() >= max_column()) + return; + m_cursor.move_by(0, 1); + update_scroll_position_if_needed(); +} + +size_t Editor::max_line() const +{ + return m_document->line_count() - 1; +} + +size_t Editor::max_column() const +{ + return m_document->lines()[m_cursor.line()].data().size(); +} + +void Editor::update_scroll_position_if_needed() +{ + ssize_t max_row = getmaxy(stdscr) - 2; + ssize_t max_column = getmaxx(stdscr) - 1 - m_ruler_width; + + ssize_t cursor_row_on_screen = m_cursor.line() - m_scroll_position.line(); + ssize_t cursor_column_on_screen = m_cursor.column() - m_scroll_position.column(); + + // FIXME: Need to move by more than just 1 step sometimes! + + if (cursor_row_on_screen < 0) { + m_scroll_position.move_by(-1, 0); + } + + if (cursor_row_on_screen > max_row) { + m_scroll_position.move_by(1, 0); + } + + if (cursor_column_on_screen < 0) { + m_scroll_position.move_by(0, -1); + } + + if (cursor_column_on_screen > max_column) { + m_scroll_position.move_by(0, 1); + } +} + +void Editor::set_mode(Mode m) +{ + if (m_mode == m) + return; + m_mode = m; + m_command = ""; +} + +static bool is_backspace(int ch) +{ + return ch == 8 || ch == 127; +} + +static bool is_newline(int ch) +{ + return ch == 10 || ch == 13; +} + +void Editor::handle_command_key_press(int ch) +{ + if (is_backspace(ch)) { + if (m_command.size() > 0) + m_command.pop_back(); + else + set_mode(Idle); + return; + } + if (is_newline(ch)) { + if (m_command.size() > 0) + exec_command(); + set_mode(Idle); + return; + } + m_command.push_back(ch); +} + +void Editor::handle_document_key_press(int ch) +{ + if (is_backspace(ch)) { + //auto op = make<EraseOperation>(1); + m_document->backspace_at(m_cursor); + } else { + auto op = make<InsertOperation>(ch); + run(std::move(op)); + } +} + +void Editor::run(OwnPtr<Operation>&& op) +{ + ASSERT(op); + op->apply(*this); + m_undo_stack.push(std::move(op)); +} + +void Editor::insert_at_cursor(int ch) +{ + std::string s; + s += ch; + m_document->insert_at(m_cursor, s); + m_cursor.move_by(0, 1); +} + +bool Editor::insert_text_at_cursor(const std::string& text) +{ + ASSERT(text.size() == 1); + m_document->insert_at(m_cursor, text); + m_cursor.move_by(0, text.size()); + return true; +} + +bool Editor::remove_text_at_cursor(const std::string& text) +{ + // FIXME: Implement + ASSERT(false); + return false; +} + +void Editor::set_status_text(const std::string& text) +{ + m_status_text = text; +} + +void Editor::exec_command() +{ + if (m_command == "q") { + m_should_quit = true; + return; + } + + if (m_command == "about") { + set_status_text("cuki editor!"); + return; + } + + std::string s; + s = "Invalid command: '"; + s += m_command; + s += "'"; + set_status_text(s); +} diff --git a/Editor/Editor.h b/Editor/Editor.h new file mode 100644 index 0000000000..ec1f53ff11 --- /dev/null +++ b/Editor/Editor.h @@ -0,0 +1,74 @@ +#pragma once + +#include "OwnPtr.h" +#include "Position.h" +#include "UndoStack.h" +#include <string> + +class Document; + +class Editor { +public: + Editor(); + ~Editor(); + + void set_document(OwnPtr<Document>&&); + void redraw(); + + int exec(); + + enum Mode { + Idle, + EditingCommand, + EditingDocument, + }; + + void set_mode(Mode); + Mode mode() const { return m_mode; } + bool is_editing_document() const { return m_mode == EditingDocument; } + bool is_editing_command() const { return m_mode == EditingCommand; } + bool is_idle() const { return m_mode == Idle; } + + void set_status_text(const std::string&); + + bool insert_text_at_cursor(const std::string&); + bool remove_text_at_cursor(const std::string&); + + void run(OwnPtr<Operation>&&); + +private: + void move_left(); + void move_down(); + void move_up(); + void move_right(); + + size_t max_line() const; + size_t max_column() const; + + void update_scroll_position_if_needed(); + + void draw_status_bar(); + void draw_cursor(); + + void handle_document_key_press(int ch); + void handle_command_key_press(int ch); + + void insert_at_cursor(int ch); + + void exec_command(); + + OwnPtr<Document> m_document; + + UndoStack m_undo_stack; + + // Document relative + Position m_scroll_position; + Position m_cursor; + + std::string m_command; + std::string m_status_text; + + bool m_should_quit { false }; + size_t m_ruler_width { 0 }; + Mode m_mode { Idle }; +}; diff --git a/Editor/FileReader.cpp b/Editor/FileReader.cpp new file mode 100644 index 0000000000..0788d06b02 --- /dev/null +++ b/Editor/FileReader.cpp @@ -0,0 +1,40 @@ +#include "FileReader.h" + +FileReader::FileReader(const std::string& path) + : m_path(path) +{ + m_file = fopen(path.c_str(), "r"); +} + +FileReader::~FileReader() +{ + if (m_file) + fclose(m_file); + m_file = nullptr; +} + +bool FileReader::can_read() const +{ + return m_file && !feof(m_file); +} + +std::string FileReader::read_line() +{ + if (!m_file) { + fprintf(stderr, "Error: FileReader::readLine() called on invalid FileReader for '%s'\n", m_path.c_str()); + return std::string(); + } + + std::string line; + + while (can_read()) { + int ch = fgetc(m_file); + if (ch == EOF) + break; + if (ch == '\n') + break; + line += ch; + } + + return line; +} diff --git a/Editor/FileReader.h b/Editor/FileReader.h new file mode 100644 index 0000000000..54fde0780e --- /dev/null +++ b/Editor/FileReader.h @@ -0,0 +1,17 @@ +#pragma once + +#include <string> +#include <stdio.h> + +class FileReader { +public: + explicit FileReader(const std::string& path); + ~FileReader(); + + bool can_read() const; + std::string read_line(); + +private: + std::string m_path; + FILE* m_file { nullptr }; +}; diff --git a/Editor/InsertOperation.cpp b/Editor/InsertOperation.cpp new file mode 100644 index 0000000000..cd7de9956b --- /dev/null +++ b/Editor/InsertOperation.cpp @@ -0,0 +1,26 @@ +#include "InsertOperation.h" +#include "Editor.h" + +InsertOperation::InsertOperation(const std::string& text) + : m_text(text) +{ +} + +InsertOperation::InsertOperation(char ch) + : m_text(&ch, 1) +{ +} + +InsertOperation::~InsertOperation() +{ +} + +bool InsertOperation::apply(Editor& editor) +{ + return editor.insert_text_at_cursor(m_text); +} + +bool InsertOperation::unapply(Editor& editor) +{ + return editor.remove_text_at_cursor(m_text); +} diff --git a/Editor/InsertOperation.h b/Editor/InsertOperation.h new file mode 100644 index 0000000000..97096ed312 --- /dev/null +++ b/Editor/InsertOperation.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Operation.h" +#include <string> + +class InsertOperation final : public Operation { +public: + explicit InsertOperation(const std::string&); + explicit InsertOperation(char); + ~InsertOperation(); + + virtual bool apply(Editor&) override; + virtual bool unapply(Editor&) override; + +private: + std::string m_text; +}; diff --git a/Editor/Line.cpp b/Editor/Line.cpp new file mode 100644 index 0000000000..431bf1d04f --- /dev/null +++ b/Editor/Line.cpp @@ -0,0 +1,95 @@ +#include "Line.h" + +Chunk::Chunk(const std::string& str) + : m_data(str) +{ +} + +Chunk::~Chunk() +{ +} + +Line::Line(const std::string& str) +{ + m_chunks.push_back(Chunk(str)); +} + +Line::Line(Line&& other) + : m_chunks(std::move(other.m_chunks)) +{ +} + +Line::~Line() +{ +} + +std::string Line::data() const +{ + std::string str; + for (auto& chunk : m_chunks) + str += chunk.data(); + return str; +} + +void Line::append(const std::string& text) +{ + m_chunks.push_back(Chunk(text)); +} + +void Line::prepend(const std::string& text) +{ + m_chunks.push_front(Chunk(text)); +} + +void Line::insert(size_t index, const std::string& text) +{ + if (index == 0) { + prepend(text); + return; + } + + if (index == length()) { + append(text); + return; + } + + auto chunkAddress = chunkIndexForPosition(index); + auto chunkIndex = std::get<0>(chunkAddress); + auto& chunk = m_chunks[chunkIndex]; + auto indexInChunk = std::get<1>(chunkAddress); + + static FILE* f = fopen("log", "a"); + fprintf(f, "#Column:%zu, Chunk:%zu, Index:%zu\n", index, chunkIndex, indexInChunk); + + auto leftString = chunk.data().substr(0, indexInChunk); + auto rightString = chunk.data().substr(indexInChunk, chunk.length() - indexInChunk); + + fprintf(f, "#{\"%s\", \"%s\", \"%s\"}\n", leftString.c_str(), text.c_str(), rightString.c_str()); + fflush(f); + + Chunk leftChunk { leftString }; + Chunk midChunk { text }; + Chunk rightChunk { rightString }; + + auto iterator = m_chunks.begin() + chunkIndex; + m_chunks.erase(iterator); + iterator = m_chunks.begin() + chunkIndex; + + // Note reverse insertion order! + m_chunks.insert(iterator, rightChunk); + m_chunks.insert(iterator, midChunk); + m_chunks.insert(iterator, leftChunk); +} + +std::tuple<size_t, size_t> Line::chunkIndexForPosition(size_t position) +{ + ASSERT(position < length()); + size_t seen { 0 }; + for (size_t i = 0; i < m_chunks.size(); ++i) { + if (position < seen + m_chunks[i].length()) + return std::make_tuple(i, position - seen); + seen += m_chunks[i].length(); + } + ASSERT(false); + return std::make_tuple(0, 0); +} diff --git a/Editor/Line.h b/Editor/Line.h new file mode 100644 index 0000000000..d8e65fc243 --- /dev/null +++ b/Editor/Line.h @@ -0,0 +1,40 @@ +#pragma once + +#include "cuki.h" +#include <deque> +#include <string> +#include <tuple> + +class Chunk { +public: + explicit Chunk(const std::string&); + ~Chunk(); + + const std::string& data() const { return m_data; } + size_t length() const { return m_data.size(); } + +private: + std::string m_data; +}; + +class Line { + AK_MAKE_NONCOPYABLE(Line); +public: + Line() { } + Line(const std::string&); + Line(Line&&); + ~Line(); + + std::string data() const; + size_t length() const { return data().size(); } + + void insert(size_t index, const std::string&); + +private: + void append(const std::string&); + void prepend(const std::string&); + + std::tuple<size_t, size_t> chunkIndexForPosition(size_t); + + std::deque<Chunk> m_chunks; +}; diff --git a/Editor/Makefile b/Editor/Makefile new file mode 100644 index 0000000000..b67c24746c --- /dev/null +++ b/Editor/Makefile @@ -0,0 +1,25 @@ +BINARY = cuki + +OBJS = \ + Document.o \ + Line.o \ + FileReader.o \ + Editor.o \ + main.o \ + InsertOperation.o \ + Operation.o \ + UndoStack.o + +CXXFLAGS = -Os -std=c++1z -W -Wall -g -I../AK +LDFLAGS = -lncurses + +$(BINARY): $(OBJS) + $(CXX) $(LDFLAGS) -o $@ $(OBJS) + +all: $(BINARY) + +clean: + rm -f $(BINARY) $(OBJS) + +%.o: %.cpp + $(CXX) $(CXXFLAGS) -c $< -o $@ diff --git a/Editor/Operation.cpp b/Editor/Operation.cpp new file mode 100644 index 0000000000..0363ee1ef4 --- /dev/null +++ b/Editor/Operation.cpp @@ -0,0 +1,9 @@ +#include "Operation.h" + +Operation::Operation() +{ +} + +Operation::~Operation() +{ +} diff --git a/Editor/Operation.h b/Editor/Operation.h new file mode 100644 index 0000000000..03fdf0aac5 --- /dev/null +++ b/Editor/Operation.h @@ -0,0 +1,14 @@ +#pragma once + +class Editor; + +class Operation { +public: + virtual ~Operation(); + + virtual bool apply(Editor&) = 0; + virtual bool unapply(Editor&) = 0; + +protected: + Operation(); +}; diff --git a/Editor/Position.h b/Editor/Position.h new file mode 100644 index 0000000000..6037e5e822 --- /dev/null +++ b/Editor/Position.h @@ -0,0 +1,27 @@ +#pragma once + +#include <unistd.h> + +class Position { +public: + Position() { } + Position(size_t line, size_t column) : m_line(line), m_column(column) { } + + size_t line() const { return m_line; } + size_t column() const { return m_column; } + + void set_line(size_t l) { m_line = l; } + void set_column(size_t c) { m_column = c; } + + void move_to(size_t l, size_t c) { m_line = l; m_column = c; } + + void move_by(ssize_t l, ssize_t c) { m_line += l; m_column += c; } + + bool is_valid() const { return m_line != InvalidValue && m_column != InvalidValue; } + +private: + static const size_t InvalidValue = 0xFFFFFFFF; + + size_t m_line { InvalidValue }; + size_t m_column { InvalidValue }; +}; diff --git a/Editor/UndoStack.cpp b/Editor/UndoStack.cpp new file mode 100644 index 0000000000..45867d2c79 --- /dev/null +++ b/Editor/UndoStack.cpp @@ -0,0 +1,14 @@ +#include "cuki.h" +#include "UndoStack.h" + +void UndoStack::push(OwnPtr<Operation>&& op) +{ + m_stack.push(std::move(op)); +} + +OwnPtr<Operation> UndoStack::pop() +{ + OwnPtr<Operation> op = std::move(m_stack.top()); + m_stack.pop(); + return op; +} diff --git a/Editor/UndoStack.h b/Editor/UndoStack.h new file mode 100644 index 0000000000..5938dca216 --- /dev/null +++ b/Editor/UndoStack.h @@ -0,0 +1,18 @@ +#pragma once + +#include <stack> +#include "Operation.h" +#include "OwnPtr.h" + +class UndoStack { +public: + UndoStack() { } + + void push(OwnPtr<Operation>&&); + OwnPtr<Operation> pop(); + + bool is_empty() const { return m_stack.empty(); } + +private: + std::stack<OwnPtr<Operation>> m_stack; +}; diff --git a/Editor/cuki.h b/Editor/cuki.h new file mode 100644 index 0000000000..80b218bfa0 --- /dev/null +++ b/Editor/cuki.h @@ -0,0 +1,6 @@ +// cuki text editor + +#include "Assertions.h" +#include "Noncopyable.h" + +// eof diff --git a/Editor/main.cpp b/Editor/main.cpp new file mode 100644 index 0000000000..1e9f5a8d41 --- /dev/null +++ b/Editor/main.cpp @@ -0,0 +1,19 @@ +#include "Document.h" +#include "Editor.h" +#include <stdio.h> + +int main(int c, char** v) +{ + std::string file_to_open = "cuki.h"; + if (c > 1) { + file_to_open = v[1]; + } + auto document = Document::create_from_file(file_to_open); + if (!document) { + fprintf(stderr, "Failed to open file.\n"); + return 1; + } + Editor editor; + editor.set_document(std::move(document)); + return editor.exec(); +} |