summaryrefslogtreecommitdiff
path: root/Editor
diff options
context:
space:
mode:
authorAndreas Kling <awesomekling@gmail.com>2018-12-04 00:27:16 +0100
committerAndreas Kling <awesomekling@gmail.com>2018-12-04 00:27:16 +0100
commitca6847b5bb6d9b86d5f2123533ebfe510c01d880 (patch)
tree8515f9360e8ade5478edd0acd4d994fe33d8e520 /Editor
parent405383fd2f9757cebd175c5be9cfa65c48fe15f0 (diff)
downloadserenity-ca6847b5bb6d9b86d5f2123533ebfe510c01d880.zip
Import a simple text editor I started working on.
Diffstat (limited to 'Editor')
-rw-r--r--Editor/Document.cpp46
-rw-r--r--Editor/Document.h27
-rw-r--r--Editor/Editor.cpp336
-rw-r--r--Editor/Editor.h74
-rw-r--r--Editor/FileReader.cpp40
-rw-r--r--Editor/FileReader.h17
-rw-r--r--Editor/InsertOperation.cpp26
-rw-r--r--Editor/InsertOperation.h17
-rw-r--r--Editor/Line.cpp95
-rw-r--r--Editor/Line.h40
-rw-r--r--Editor/Makefile25
-rw-r--r--Editor/Operation.cpp9
-rw-r--r--Editor/Operation.h14
-rw-r--r--Editor/Position.h27
-rw-r--r--Editor/UndoStack.cpp14
-rw-r--r--Editor/UndoStack.h18
-rw-r--r--Editor/cuki.h6
-rw-r--r--Editor/main.cpp19
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();
+}