mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2025-01-12 12:53:37 +00:00
Add edit support for Notepad (#1093)
* WIP file editing * WIP file editing * Add "on_pop" handler to navigation. * WIP Editing * WIP for draft * Fix mock and unit tests, support +newline at end. * Clean up Painter API and use string_view * Fix optional rvalue functions * Fix Result 'take' to be more standard * FileWrapper stack buffer reads * Grasping at straws * Nit * Move set_on_pop impl to cpp * Workaround "Open" when file not dirty. --------- Co-authored-by: kallanreed <kallanreed@outlook.com>
This commit is contained in:
parent
69011754c9
commit
8d7fdeb633
@ -124,6 +124,22 @@ bool TextViewer::on_encoder(EncoderEvent delta) {
|
||||
return updated;
|
||||
}
|
||||
|
||||
void TextViewer::redraw(bool redraw_text) {
|
||||
paint_state_.redraw_text = redraw_text;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
uint32_t TextViewer::offset() const {
|
||||
auto range = file_->line_range(cursor_.line);
|
||||
if (range)
|
||||
return range->start + col();
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t TextViewer::line_length() {
|
||||
return file_->line_length(cursor_.line);
|
||||
}
|
||||
|
||||
bool TextViewer::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
||||
if (!has_file())
|
||||
return false;
|
||||
@ -175,28 +191,24 @@ bool TextViewer::apply_scrolling_constraints(int16_t delta_line, int16_t delta_c
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextViewer::redraw(bool redraw_text) {
|
||||
paint_state_.redraw_text = redraw_text;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
||||
auto r = screen_rect();
|
||||
char buffer[max_col + 1];
|
||||
|
||||
// Draw the lines from the file
|
||||
for (auto i = 0u; i < max_line; ++i) {
|
||||
if (line + i >= file_->line_count())
|
||||
break;
|
||||
|
||||
auto str = file_->get_text(line + i, col, max_col);
|
||||
auto result = file_->get_text(line + i, col, buffer, max_col);
|
||||
|
||||
if (str && str->length() > 0)
|
||||
if (result && *result > 0)
|
||||
painter.draw_string(
|
||||
{0, r.top() + (int)i * char_height},
|
||||
style_text, *str);
|
||||
style_text, {buffer, *result});
|
||||
|
||||
// Clear empty line sections. This is less visually jarring than full clear.
|
||||
int32_t clear_width = max_col - (str ? str->length() : 0);
|
||||
int32_t clear_width = max_col - (result ? *result : 0);
|
||||
if (clear_width > 0)
|
||||
painter.fill_rectangle(
|
||||
{(max_col - clear_width) * char_width,
|
||||
@ -238,10 +250,6 @@ void TextViewer::reset_file(FileWrapper* file) {
|
||||
redraw(true);
|
||||
}
|
||||
|
||||
uint16_t TextViewer::line_length() {
|
||||
return file_->line_length(cursor_.line);
|
||||
}
|
||||
|
||||
/* TextEditorMenu ***************************************************/
|
||||
|
||||
TextEditorMenu::TextEditorMenu()
|
||||
@ -309,25 +317,52 @@ TextEditorView::TextEditorView(NavigationView& nav)
|
||||
menu.on_copy() = [this]() {
|
||||
show_nyi();
|
||||
};
|
||||
|
||||
menu.on_delete_line() = [this]() {
|
||||
show_nyi();
|
||||
prepare_for_write();
|
||||
file_->delete_line(viewer.line());
|
||||
refresh_ui();
|
||||
hide_menu(true);
|
||||
};
|
||||
|
||||
menu.on_edit_line() = [this]() {
|
||||
show_nyi();
|
||||
show_edit_line();
|
||||
};
|
||||
|
||||
menu.on_add_line() = [this]() {
|
||||
show_nyi();
|
||||
prepare_for_write();
|
||||
|
||||
if (viewer.offset() < file_->size() - 1)
|
||||
file_->insert_line(viewer.line());
|
||||
else
|
||||
file_->insert_line(-1); // Add after last line.
|
||||
|
||||
refresh_ui();
|
||||
hide_menu(true);
|
||||
};
|
||||
|
||||
menu.on_open() = [this]() {
|
||||
// TODO: confirm.
|
||||
show_file_picker();
|
||||
/*show_save_prompt([this]() {
|
||||
show_file_picker();
|
||||
});*/
|
||||
// HACK: above should work but it's faulting.
|
||||
if (!file_dirty_) {
|
||||
show_file_picker();
|
||||
} else {
|
||||
show_save_prompt(nullptr);
|
||||
show_file_picker(false);
|
||||
}
|
||||
};
|
||||
|
||||
menu.on_save() = [this]() {
|
||||
show_nyi();
|
||||
save_temp_file();
|
||||
hide_menu(true);
|
||||
};
|
||||
|
||||
menu.on_exit() = [this]() {
|
||||
// TODO: confirm.
|
||||
nav_.pop();
|
||||
show_save_prompt([this]() {
|
||||
nav_.pop();
|
||||
});
|
||||
};
|
||||
|
||||
button_menu.on_select = [this]() {
|
||||
@ -345,6 +380,10 @@ TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
|
||||
open_file(path);
|
||||
}
|
||||
|
||||
TextEditorView::~TextEditorView() {
|
||||
delete_temp_file();
|
||||
}
|
||||
|
||||
void TextEditorView::on_show() {
|
||||
if (file_)
|
||||
viewer.focus();
|
||||
@ -353,14 +392,21 @@ void TextEditorView::on_show() {
|
||||
}
|
||||
|
||||
void TextEditorView::open_file(const fs::path& path) {
|
||||
file_.reset();
|
||||
viewer.clear_file();
|
||||
delete_temp_file();
|
||||
|
||||
path_ = {};
|
||||
file_dirty_ = false;
|
||||
has_temp_file_ = false;
|
||||
auto result = FileWrapper::open(path);
|
||||
|
||||
if (!result) {
|
||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
|
||||
file_.reset();
|
||||
viewer.clear_file();
|
||||
|
||||
} else {
|
||||
file_ = result.take();
|
||||
file_ = *std::move(result);
|
||||
path_ = path;
|
||||
viewer.set_file(*file_);
|
||||
}
|
||||
|
||||
@ -401,16 +447,100 @@ void TextEditorView::hide_menu(bool hidden) {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TextEditorView::show_file_picker() {
|
||||
auto open_view = nav_.push<FileLoadView>("");
|
||||
open_view->on_changed = [this](std::filesystem::path path) {
|
||||
open_file(path);
|
||||
hide_menu();
|
||||
};
|
||||
void TextEditorView::show_file_picker(bool immediate) {
|
||||
// TODO: immediate is a hack until nav_.on_pop is fixed.
|
||||
auto open_view = immediate ? nav_.push<FileLoadView>("") : nav_.push_under_current<FileLoadView>("");
|
||||
|
||||
if (open_view) {
|
||||
open_view->on_changed = [this](std::filesystem::path path) {
|
||||
open_file(path);
|
||||
hide_menu();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::show_edit_line() {
|
||||
auto str = file_->get_text(viewer.line(), 0, viewer.line_length());
|
||||
if (!str) {
|
||||
nav_.display_modal("Error", "Failed to get line text.");
|
||||
return;
|
||||
}
|
||||
|
||||
edit_line_buffer_ = *std::move(str);
|
||||
|
||||
text_prompt(
|
||||
nav_,
|
||||
edit_line_buffer_,
|
||||
viewer.col(),
|
||||
max_edit_length,
|
||||
[this](std::string& buffer) {
|
||||
auto range = file_->line_range(viewer.line());
|
||||
if (!range)
|
||||
return;
|
||||
|
||||
prepare_for_write();
|
||||
file_->replace_range(*range, buffer);
|
||||
});
|
||||
nav_.set_on_pop([this]() {
|
||||
edit_line_buffer_.clear();
|
||||
refresh_ui();
|
||||
hide_menu(true);
|
||||
});
|
||||
}
|
||||
|
||||
void TextEditorView::show_nyi() {
|
||||
nav_.display_modal("Soon...", "Coming soon.");
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
void TextEditorView::show_save_prompt(std::function<void()> continuation) {
|
||||
if (!file_dirty_) {
|
||||
if (continuation)
|
||||
continuation();
|
||||
return;
|
||||
}
|
||||
|
||||
nav_.display_modal(
|
||||
"Save?", "Save changes?", YESNO,
|
||||
[this](bool choice) {
|
||||
if (choice)
|
||||
save_temp_file();
|
||||
});
|
||||
nav_.set_on_pop(continuation);
|
||||
}
|
||||
|
||||
void TextEditorView::prepare_for_write() {
|
||||
file_dirty_ = true;
|
||||
|
||||
if (has_temp_file_)
|
||||
return;
|
||||
|
||||
// Copy to temp file on write.
|
||||
has_temp_file_ = true;
|
||||
delete_temp_file();
|
||||
copy_file(path_, get_temp_path());
|
||||
file_->assume_file(get_temp_path());
|
||||
}
|
||||
|
||||
void TextEditorView::delete_temp_file() const {
|
||||
auto temp_path = get_temp_path();
|
||||
if (!temp_path.empty()) {
|
||||
delete_file(temp_path);
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorView::save_temp_file() {
|
||||
if (file_dirty_) {
|
||||
delete_file(path_);
|
||||
copy_file(get_temp_path(), path_);
|
||||
file_dirty_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
fs::path TextEditorView::get_temp_path() const {
|
||||
if (!path_.empty())
|
||||
return path_ + "~";
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
@ -19,6 +19,10 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/* TODO:
|
||||
* - Busy indicator while reading files.
|
||||
*/
|
||||
|
||||
#ifndef __UI_TEXT_EDITOR_H__
|
||||
#define __UI_TEXT_EDITOR_H__
|
||||
|
||||
@ -66,6 +70,10 @@ class TextViewer : public Widget {
|
||||
|
||||
uint32_t line() const { return cursor_.line; }
|
||||
uint32_t col() const { return cursor_.col; }
|
||||
uint32_t offset() const;
|
||||
|
||||
// Gets the length of the current line.
|
||||
uint16_t line_length();
|
||||
|
||||
private:
|
||||
static constexpr int8_t char_width = 5;
|
||||
@ -89,9 +97,6 @@ class TextViewer : public Widget {
|
||||
|
||||
void reset_file(FileWrapper* file = nullptr);
|
||||
|
||||
// Gets the length of the current line.
|
||||
uint16_t line_length();
|
||||
|
||||
FileWrapper* file_{};
|
||||
|
||||
struct {
|
||||
@ -201,6 +206,7 @@ class TextEditorView : public View {
|
||||
TextEditorView(
|
||||
NavigationView& nav,
|
||||
const std::filesystem::path& path);
|
||||
~TextEditorView();
|
||||
|
||||
std::string title() const override {
|
||||
return "Notepad";
|
||||
@ -209,15 +215,30 @@ class TextEditorView : public View {
|
||||
void on_show() override;
|
||||
|
||||
private:
|
||||
static constexpr size_t max_edit_length = 1024;
|
||||
std::string edit_line_buffer_{};
|
||||
|
||||
void open_file(const std::filesystem::path& path);
|
||||
void save_file();
|
||||
void refresh_ui();
|
||||
void update_position();
|
||||
void hide_menu(bool hidden = true);
|
||||
void show_file_picker();
|
||||
void show_file_picker(bool immediate = true);
|
||||
void show_edit_line();
|
||||
void show_nyi();
|
||||
void show_save_prompt(std::function<void()> continuation);
|
||||
|
||||
void prepare_for_write();
|
||||
void create_temp_file() const;
|
||||
void delete_temp_file() const;
|
||||
void save_temp_file();
|
||||
std::filesystem::path get_temp_path() const;
|
||||
|
||||
NavigationView& nav_;
|
||||
std::unique_ptr<FileWrapper> file_{};
|
||||
std::filesystem::path path_{};
|
||||
bool file_dirty_{false};
|
||||
bool has_temp_file_{false};
|
||||
|
||||
TextViewer viewer{
|
||||
/* 272 = 320 - 16 (top bar) - 32 (bottom controls) */
|
||||
|
@ -44,8 +44,9 @@ Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BY
|
||||
}
|
||||
}
|
||||
|
||||
Optional<File::Error> File::open(const std::filesystem::path& filename) {
|
||||
return open_fatfs(filename, FA_READ);
|
||||
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only) {
|
||||
BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE;
|
||||
return open_fatfs(filename, mode);
|
||||
}
|
||||
|
||||
Optional<File::Error> File::append(const std::filesystem::path& filename) {
|
||||
@ -97,6 +98,15 @@ File::Result<File::Offset> File::seek(Offset new_position) {
|
||||
return {static_cast<File::Offset>(old_position)};
|
||||
}
|
||||
|
||||
File::Result<File::Offset> File::truncate() {
|
||||
const auto position = f_tell(&f);
|
||||
const auto result = f_truncate(&f);
|
||||
if (result != FR_OK) {
|
||||
return {static_cast<Error>(result)};
|
||||
}
|
||||
return {static_cast<File::Offset>(position)};
|
||||
}
|
||||
|
||||
File::Size File::size() const {
|
||||
return f_size(&f);
|
||||
}
|
||||
@ -248,19 +258,19 @@ std::filesystem::filesystem_error copy_file(
|
||||
File dst;
|
||||
|
||||
auto error = src.open(file_path);
|
||||
if (error.is_valid()) return error.value();
|
||||
if (error) return error.value();
|
||||
|
||||
error = dst.create(dest_path);
|
||||
if (error.is_valid()) return error.value();
|
||||
if (error) return error.value();
|
||||
|
||||
while (true) {
|
||||
auto result = src.read(buffer, buffer_size);
|
||||
if (result.is_error()) return result.error();
|
||||
|
||||
result = dst.write(buffer, result.value());
|
||||
result = dst.write(buffer, *result);
|
||||
if (result.is_error()) return result.error();
|
||||
|
||||
if (result.value() < buffer_size)
|
||||
if (*result < buffer_size)
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -320,13 +320,8 @@ class File {
|
||||
return value_;
|
||||
}
|
||||
|
||||
/* Allows value to be moved out of the Result. */
|
||||
T take() {
|
||||
if (is_error())
|
||||
return {};
|
||||
T temp;
|
||||
std::swap(temp, value_);
|
||||
return temp;
|
||||
T&& operator*() && {
|
||||
return std::move(value_);
|
||||
}
|
||||
|
||||
Error error() const {
|
||||
@ -369,7 +364,7 @@ class File {
|
||||
File& operator=(const File&) = delete;
|
||||
|
||||
// TODO: Return Result<>.
|
||||
Optional<Error> open(const std::filesystem::path& filename);
|
||||
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true);
|
||||
Optional<Error> append(const std::filesystem::path& filename);
|
||||
Optional<Error> create(const std::filesystem::path& filename);
|
||||
|
||||
@ -377,6 +372,7 @@ class File {
|
||||
Result<Size> write(const void* data, Size bytes_to_write);
|
||||
|
||||
Result<Offset> seek(uint64_t Offset);
|
||||
Result<Offset> truncate();
|
||||
// Timestamp created_date() const;
|
||||
Size size() const;
|
||||
|
||||
|
@ -27,7 +27,7 @@
|
||||
#include "optional.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
enum class LineEnding : uint8_t {
|
||||
LF,
|
||||
@ -36,12 +36,20 @@ enum class LineEnding : uint8_t {
|
||||
|
||||
/* TODO:
|
||||
* - CRLF handling.
|
||||
* - Avoid full re-read on edits.
|
||||
* - Would need to read old/new text when editing to track newlines.
|
||||
* - How to surface errors? Exceptions?
|
||||
*/
|
||||
|
||||
/* FatFs docs http://elm-chan.org/fsw/ff/00index_e.html */
|
||||
|
||||
/* BufferType requires the following members
|
||||
* Size size()
|
||||
* Result<Size> read(void* data, Size bytes_to_read)
|
||||
* Result<Size> write(const void* data, Size bytes_to_write)
|
||||
* Result<Offset> seek(uint32_t offset)
|
||||
* Result<Offset> truncate()
|
||||
* Optional<Error> sync()
|
||||
*/
|
||||
|
||||
/* Wraps a buffer and provides an API for accessing lines efficiently. */
|
||||
@ -52,10 +60,12 @@ class BufferWrapper {
|
||||
using Line = uint32_t;
|
||||
using Column = uint32_t;
|
||||
using Range = struct {
|
||||
// Offset of the line start.
|
||||
// Offset of the start, inclusive.
|
||||
Offset start;
|
||||
// Offset of one past the line end.
|
||||
// Offset of the end, exclusive.
|
||||
Offset end;
|
||||
|
||||
Offset length() const { return end - start; }
|
||||
};
|
||||
|
||||
BufferWrapper(BufferType* buffer)
|
||||
@ -69,6 +79,18 @@ class BufferWrapper {
|
||||
BufferWrapper& operator=(const BufferWrapper&) = delete;
|
||||
|
||||
Optional<std::string> get_text(Line line, Column col, Offset length) {
|
||||
std::string buffer;
|
||||
buffer.resize(length);
|
||||
|
||||
auto result = get_text(line, col, &buffer[0], length);
|
||||
if (!result)
|
||||
return {};
|
||||
|
||||
buffer.resize(*result);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
Optional<Offset> get_text(Line line, Column col, char* output, Offset length) {
|
||||
auto range = line_range(line);
|
||||
int32_t to_read = length;
|
||||
|
||||
@ -82,7 +104,7 @@ class BufferWrapper {
|
||||
if (to_read <= 0)
|
||||
return {};
|
||||
|
||||
return read(range->start + col, to_read);
|
||||
return read(range->start + col, output, to_read);
|
||||
}
|
||||
|
||||
/* Gets the size of the buffer in bytes. */
|
||||
@ -95,12 +117,12 @@ class BufferWrapper {
|
||||
Optional<Range> line_range(Line line) {
|
||||
ensure_cached(line);
|
||||
|
||||
auto offset = offset_for_line(line);
|
||||
if (!offset)
|
||||
auto index = index_for_line(line);
|
||||
if (!index)
|
||||
return {};
|
||||
|
||||
auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1);
|
||||
auto end = newlines_[*offset] + 1;
|
||||
auto start = *index == 0 ? start_offset_ : (newlines_[*index - 1] + 1);
|
||||
auto end = newlines_[*index] + 1;
|
||||
|
||||
return Range{start, end};
|
||||
}
|
||||
@ -108,17 +130,66 @@ class BufferWrapper {
|
||||
/* Gets the length of the line, or 0 if invalid. */
|
||||
Offset line_length(Line line) {
|
||||
auto range = line_range(line);
|
||||
return range ? range->length() : 0;
|
||||
}
|
||||
|
||||
/* Gets the buffer offset of the line & col if valid. */
|
||||
Optional<Offset> get_offset(Line line, Column col) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
return range->end - range->start;
|
||||
return range->start + col;
|
||||
|
||||
return 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
/* Gets the index of the first line in the cache.
|
||||
* Only really useful for unit testing or diagnostics. */
|
||||
Offset start_line() { return start_line_; };
|
||||
|
||||
/* Inserts a line before the specified line or at the
|
||||
* end of the buffer if line >= line_count. */
|
||||
void insert_line(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
replace_range({range->start, range->start}, "\n");
|
||||
else if (line >= line_count_)
|
||||
replace_range({(Offset)size(), (Offset)size()}, "\n");
|
||||
}
|
||||
|
||||
/* Deletes the specified line. */
|
||||
void delete_line(Line line) {
|
||||
auto range = line_range(line);
|
||||
|
||||
if (range)
|
||||
replace_range(*range, {});
|
||||
}
|
||||
|
||||
/* Replace the specified range with the string contents.
|
||||
* A range with start/end set to the same value will insert.
|
||||
* A range with an empty string will delete. */
|
||||
void replace_range(Range range, std::string_view value) {
|
||||
if (range.start > size() || range.end > size() || range.start > range.end)
|
||||
return;
|
||||
|
||||
/* If delta_length == 0, it's an overwrite. Could still have
|
||||
* added or removed newlines so caches will need to be rebuilt.
|
||||
* If delta_length > 0, the file needs to grow and content needs
|
||||
* to be shifted forward until the end of the range.
|
||||
* If delta_length < 0, the file needs to be truncated and the
|
||||
* content after the value needs to be shifted backward. */
|
||||
int32_t delta_length = value.length() - range.length();
|
||||
if (delta_length > 0)
|
||||
expand(range.end, delta_length);
|
||||
else if (delta_length < 0)
|
||||
shrink(range.end, delta_length);
|
||||
|
||||
write(range.start, value);
|
||||
wrapped_->sync();
|
||||
rebuild_cache();
|
||||
}
|
||||
|
||||
protected:
|
||||
BufferWrapper() {}
|
||||
|
||||
@ -132,12 +203,16 @@ class BufferWrapper {
|
||||
static constexpr Offset max_newlines = CacheSize;
|
||||
|
||||
/* Size of stack buffer used for reading/writing. */
|
||||
static constexpr size_t buffer_size = 512;
|
||||
static constexpr Offset buffer_size = 512;
|
||||
|
||||
void initialize() {
|
||||
start_offset_ = 0;
|
||||
start_line_ = 0;
|
||||
line_count_ = 0;
|
||||
rebuild_cache();
|
||||
}
|
||||
|
||||
void rebuild_cache() {
|
||||
newlines_.clear();
|
||||
|
||||
// Special case for empty files to keep them consistent.
|
||||
@ -147,7 +222,17 @@ class BufferWrapper {
|
||||
return;
|
||||
}
|
||||
|
||||
Offset offset = 0;
|
||||
// TODO: think through this for edit cases.
|
||||
// E.g. don't read to end, maybe could specify
|
||||
// a range to re-read because it should be possible
|
||||
// to tell where the dirty regions are. After the
|
||||
// dirty region, it should be possible to fixup
|
||||
// the line_count data.
|
||||
// TODO: seems like shrink/expand could do this while
|
||||
// they are running.
|
||||
|
||||
line_count_ = start_line_;
|
||||
Offset offset = start_offset_;
|
||||
auto result = next_newline(offset);
|
||||
|
||||
while (result) {
|
||||
@ -159,25 +244,28 @@ class BufferWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
Optional<std::string> read(Offset offset, Offset length) {
|
||||
Optional<Offset> read(Offset offset, char* buffer, Offset length) {
|
||||
if (offset + length > size())
|
||||
return {};
|
||||
|
||||
std::string buffer;
|
||||
buffer.resize(length);
|
||||
wrapped_->seek(offset);
|
||||
|
||||
auto result = wrapped_->read(&buffer[0], length);
|
||||
auto result = wrapped_->read(buffer, length);
|
||||
if (result.is_error())
|
||||
// TODO: better error handling.
|
||||
return std::string{"[Bad Read]"};
|
||||
return {};
|
||||
|
||||
buffer.resize(*result);
|
||||
return buffer;
|
||||
return *result;
|
||||
}
|
||||
|
||||
/* Returns the offset of the line in the newline cache if valid. */
|
||||
Optional<Offset> offset_for_line(Line line) const {
|
||||
bool write(Offset offset, std::string_view value) {
|
||||
wrapped_->seek(offset);
|
||||
auto result = wrapped_->write(value.data(), value.length());
|
||||
|
||||
return result.is_ok();
|
||||
}
|
||||
|
||||
/* Returns the index of the line in the newline cache if valid. */
|
||||
Optional<Offset> index_for_line(Line line) const {
|
||||
if (line >= line_count_)
|
||||
return {};
|
||||
|
||||
@ -193,8 +281,8 @@ class BufferWrapper {
|
||||
if (line >= line_count_)
|
||||
return;
|
||||
|
||||
auto result = offset_for_line(line);
|
||||
if (result)
|
||||
auto index = index_for_line(line);
|
||||
if (index)
|
||||
return;
|
||||
|
||||
if (line < start_line_) {
|
||||
@ -229,7 +317,7 @@ class BufferWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
/* Helpers for finding the prev/next newline. */
|
||||
/* Finding the first newline backward from offset. */
|
||||
Optional<Offset> previous_newline(Offset offset) {
|
||||
char buffer[buffer_size];
|
||||
auto to_read = buffer_size;
|
||||
@ -264,6 +352,7 @@ class BufferWrapper {
|
||||
return {}; // Didn't find one.
|
||||
}
|
||||
|
||||
/* Finding the first newline forward from offset. */
|
||||
Optional<Offset> next_newline(Offset offset) {
|
||||
// EOF, no more newlines to find.
|
||||
if (offset >= size())
|
||||
@ -295,6 +384,65 @@ class BufferWrapper {
|
||||
return size() - 1;
|
||||
}
|
||||
|
||||
/* Grow the file and move file content so that the
|
||||
* content at src is shifted forward by 'delta'. */
|
||||
void expand(Offset src, int32_t delta) {
|
||||
if (delta <= 0) // Not an expand.
|
||||
return;
|
||||
|
||||
char buffer[buffer_size];
|
||||
auto to_read = buffer_size;
|
||||
|
||||
// Number of bytes left to shift.
|
||||
Offset remaining = size() - src;
|
||||
Offset offset = size();
|
||||
|
||||
while (remaining > 0) {
|
||||
offset -= std::min(remaining, buffer_size);
|
||||
to_read = std::min(remaining, buffer_size);
|
||||
|
||||
wrapped_->seek(offset);
|
||||
auto result = wrapped_->read(buffer, to_read);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
wrapped_->seek(offset + delta);
|
||||
result = wrapped_->write(buffer, *result);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
remaining -= *result;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shrink the file and move file content so that the
|
||||
* content at src is shifted backward by 'delta'. */
|
||||
void shrink(Offset src, int32_t delta) {
|
||||
if (delta >= 0) // Not a shrink.
|
||||
return;
|
||||
|
||||
char buffer[buffer_size];
|
||||
auto offset = src;
|
||||
|
||||
while (true) {
|
||||
wrapped_->seek(offset);
|
||||
auto result = wrapped_->read(buffer, buffer_size);
|
||||
if (result.is_error())
|
||||
break;
|
||||
|
||||
wrapped_->seek(offset + delta);
|
||||
result = wrapped_->write(buffer, *result);
|
||||
|
||||
if (result.is_error() || *result < buffer_size)
|
||||
break;
|
||||
|
||||
offset += *result;
|
||||
}
|
||||
|
||||
// Delete the extra bytes at the end of the file.
|
||||
wrapped_->truncate();
|
||||
}
|
||||
|
||||
BufferType* wrapped_{};
|
||||
|
||||
/* Total number of lines in the buffer. */
|
||||
@ -316,7 +464,7 @@ class FileWrapper : public BufferWrapper<File, 64> {
|
||||
using Error = File::Error;
|
||||
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path) {
|
||||
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
|
||||
auto error = fw->file_.open(path);
|
||||
auto error = fw->file_.open(path, /*read_only*/ false);
|
||||
|
||||
if (error)
|
||||
return *error;
|
||||
@ -325,6 +473,23 @@ class FileWrapper : public BufferWrapper<File, 64> {
|
||||
return fw;
|
||||
}
|
||||
|
||||
/* Underlying file. */
|
||||
File& file() { return file_; }
|
||||
|
||||
/* Swaps out the underlying file for the specified file.
|
||||
* The swapped file is expected have the same contents.
|
||||
* For copy-on-write scenario with a temp file. */
|
||||
bool assume_file(const std::filesystem::path& path) {
|
||||
File file;
|
||||
auto error = file.open(path, /*read_only*/ false);
|
||||
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
file_ = std::move(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
FileWrapper() {}
|
||||
void initialize() {
|
||||
|
@ -236,10 +236,8 @@ void SystemStatusView::refresh() {
|
||||
|
||||
if (portapack::clock_manager.get_reference().source == ClockManager::ReferenceSource::External) {
|
||||
button_clock_status.set_bitmap(&bitmap_icon_clk_ext);
|
||||
// button_bias_tee.set_foreground(ui::Color::green()); Typo?
|
||||
} else {
|
||||
button_clock_status.set_bitmap(&bitmap_icon_clk_int);
|
||||
// button_bias_tee.set_foreground(ui::Color::green());
|
||||
}
|
||||
|
||||
if (portapack::persistent_memory::clkout_enabled()) {
|
||||
@ -420,7 +418,7 @@ View* NavigationView::push_view(std::unique_ptr<View> new_view) {
|
||||
free_view();
|
||||
|
||||
const auto p = new_view.get();
|
||||
view_stack.emplace_back(std::move(new_view));
|
||||
view_stack.emplace_back(ViewState{std::move(new_view), {}});
|
||||
|
||||
update_view();
|
||||
|
||||
@ -428,34 +426,14 @@ View* NavigationView::push_view(std::unique_ptr<View> new_view) {
|
||||
}
|
||||
|
||||
void NavigationView::pop() {
|
||||
if (view() == modal_view) {
|
||||
modal_view = nullptr;
|
||||
}
|
||||
|
||||
// Can't pop last item from stack.
|
||||
if (view_stack.size() > 1) {
|
||||
free_view();
|
||||
|
||||
view_stack.pop_back();
|
||||
|
||||
update_view();
|
||||
}
|
||||
pop(true);
|
||||
}
|
||||
|
||||
void NavigationView::pop_modal() {
|
||||
if (view() == modal_view) {
|
||||
modal_view = nullptr;
|
||||
}
|
||||
|
||||
// Pop modal view + underlying app view
|
||||
if (view_stack.size() > 2) {
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
|
||||
update_view();
|
||||
}
|
||||
// Pop modal view + underlying app view.
|
||||
// TODO: this shouldn't be necessary.
|
||||
pop(false);
|
||||
pop(true);
|
||||
}
|
||||
|
||||
void NavigationView::display_modal(
|
||||
@ -475,12 +453,31 @@ void NavigationView::display_modal(
|
||||
}
|
||||
}
|
||||
|
||||
void NavigationView::pop(bool update) {
|
||||
if (view() == modal_view) {
|
||||
modal_view = nullptr;
|
||||
}
|
||||
|
||||
// Can't pop last item from stack.
|
||||
if (view_stack.size() > 1) {
|
||||
auto on_pop = view_stack.back().on_pop;
|
||||
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
|
||||
if (update)
|
||||
update_view();
|
||||
|
||||
if (on_pop) on_pop();
|
||||
}
|
||||
}
|
||||
|
||||
void NavigationView::free_view() {
|
||||
remove_child(view());
|
||||
}
|
||||
|
||||
void NavigationView::update_view() {
|
||||
const auto new_view = view_stack.back().get();
|
||||
const auto new_view = view_stack.back().view.get();
|
||||
|
||||
add_child(new_view);
|
||||
new_view->set_parent_rect({{0, 0}, size()});
|
||||
@ -503,6 +500,18 @@ void NavigationView::focus() {
|
||||
}
|
||||
}
|
||||
|
||||
bool NavigationView::set_on_pop(std::function<void()> on_pop) {
|
||||
if (view_stack.size() <= 1)
|
||||
return false;
|
||||
|
||||
auto& top = view_stack.back();
|
||||
if (top.on_pop)
|
||||
return false;
|
||||
|
||||
top.on_pop = on_pop;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ReceiversMenuView *****************************************************/
|
||||
|
||||
ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
|
||||
|
@ -75,9 +75,11 @@ class NavigationView : public View {
|
||||
|
||||
// Pushes a new view under the current on the stack so the current view returns into this new one.
|
||||
template <class T, class... Args>
|
||||
void push_under_current(Args&&... args) {
|
||||
T* push_under_current(Args&&... args) {
|
||||
auto new_view = std::unique_ptr<View>(new T(*this, std::forward<Args>(args)...));
|
||||
view_stack.insert(view_stack.end() - 1, std::move(new_view));
|
||||
auto new_view_ptr = new_view.get();
|
||||
view_stack.insert(view_stack.end() - 1, ViewState{std::move(new_view), {}});
|
||||
return reinterpret_cast<T*>(new_view_ptr);
|
||||
}
|
||||
|
||||
template <class T, class... Args>
|
||||
@ -97,12 +99,22 @@ class NavigationView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
/* Sets the 'on_pop' handler for the current view.
|
||||
* Returns true if the handler was bound successfully. */
|
||||
bool set_on_pop(std::function<void()> on_pop);
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<View>> view_stack{};
|
||||
struct ViewState {
|
||||
std::unique_ptr<View> view;
|
||||
std::function<void()> on_pop;
|
||||
};
|
||||
|
||||
std::vector<ViewState> view_stack{};
|
||||
Widget* modal_view{nullptr};
|
||||
|
||||
Widget* view() const;
|
||||
|
||||
void pop(bool update);
|
||||
void free_view();
|
||||
void update_view();
|
||||
View* push_view(std::unique_ptr<View> new_view);
|
||||
|
@ -43,10 +43,8 @@ class Optional {
|
||||
const T& value() const& { return value_; }
|
||||
const T& operator*() const& { return value_; }
|
||||
|
||||
T&& value() && { return value_; }
|
||||
T&& operator*() && { return value_; }
|
||||
const T&& value() const&& { return value_; }
|
||||
const T&& operator*() const&& { return value_; }
|
||||
T&& value() && { return std::move(value_); }
|
||||
T&& operator*() && { return std::move(value_); }
|
||||
|
||||
T* operator->() { return &value_; }
|
||||
const T* operator->() const { return &value_; }
|
||||
|
@ -35,18 +35,27 @@ Style Style::invert() const {
|
||||
.foreground = background};
|
||||
}
|
||||
|
||||
int Painter::draw_char(const Point p, const Style& style, const char c) {
|
||||
int Painter::draw_char(Point p, const Style& style, char c) {
|
||||
const auto glyph = style.font.glyph(c);
|
||||
display.draw_glyph(p, glyph, style.foreground, style.background);
|
||||
return glyph.advance().x();
|
||||
}
|
||||
|
||||
int Painter::draw_string(Point p, const Font& font, const Color foreground, const Color background, const std::string& text) {
|
||||
int Painter::draw_string(Point p, const Style& style, std::string_view text) {
|
||||
return draw_string(p, style.font, style.foreground, style.background, text);
|
||||
}
|
||||
|
||||
int Painter::draw_string(
|
||||
Point p,
|
||||
const Font& font,
|
||||
Color foreground,
|
||||
Color background,
|
||||
std::string_view text) {
|
||||
bool escape = false;
|
||||
size_t width = 0;
|
||||
Color pen = foreground;
|
||||
|
||||
for (const auto c : text) {
|
||||
for (auto c : text) {
|
||||
if (escape) {
|
||||
if (c <= 15)
|
||||
pen = term_colors[c & 15];
|
||||
@ -65,48 +74,45 @@ int Painter::draw_string(Point p, const Font& font, const Color foreground, cons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
int Painter::draw_string(Point p, const Style& style, const std::string& text) {
|
||||
return draw_string(p, style.font, style.foreground, style.background, text);
|
||||
}
|
||||
|
||||
void Painter::draw_bitmap(const Point p, const Bitmap& bitmap, const Color foreground, const Color background) {
|
||||
void Painter::draw_bitmap(Point p, const Bitmap& bitmap, Color foreground, Color background) {
|
||||
display.draw_bitmap(p, bitmap.size, bitmap.data, foreground, background);
|
||||
}
|
||||
|
||||
void Painter::draw_hline(Point p, int width, const Color c) {
|
||||
void Painter::draw_hline(Point p, int width, Color c) {
|
||||
display.fill_rectangle({p, {width, 1}}, c);
|
||||
}
|
||||
|
||||
void Painter::draw_vline(Point p, int height, const Color c) {
|
||||
void Painter::draw_vline(Point p, int height, Color c) {
|
||||
display.fill_rectangle({p, {1, height}}, c);
|
||||
}
|
||||
|
||||
void Painter::draw_rectangle(const Rect r, const Color c) {
|
||||
void Painter::draw_rectangle(Rect r, Color c) {
|
||||
draw_hline(r.location(), r.width(), c);
|
||||
draw_vline({r.left(), r.top() + 1}, r.height() - 2, c);
|
||||
draw_vline({r.left() + r.width() - 1, r.top() + 1}, r.height() - 2, c);
|
||||
draw_hline({r.left(), r.top() + r.height() - 1}, r.width(), c);
|
||||
}
|
||||
|
||||
void Painter::fill_rectangle(const Rect r, const Color c) {
|
||||
void Painter::fill_rectangle(Rect r, Color c) {
|
||||
display.fill_rectangle(r, c);
|
||||
}
|
||||
|
||||
void Painter::fill_rectangle_unrolled8(const Rect r, const Color c) {
|
||||
void Painter::fill_rectangle_unrolled8(Rect r, Color c) {
|
||||
display.fill_rectangle_unrolled8(r, c);
|
||||
}
|
||||
|
||||
void Painter::paint_widget_tree(Widget* const w) {
|
||||
void Painter::paint_widget_tree(Widget* w) {
|
||||
if (ui::is_dirty()) {
|
||||
paint_widget(w);
|
||||
ui::dirty_clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Painter::paint_widget(Widget* const w) {
|
||||
void Painter::paint_widget(Widget* w) {
|
||||
if (w->hidden()) {
|
||||
// Mark widget (and all children) as invisible.
|
||||
w->visible(false);
|
||||
|
@ -25,7 +25,7 @@
|
||||
#include "ui.hpp"
|
||||
#include "ui_text.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@ -46,24 +46,24 @@ class Painter {
|
||||
Painter(const Painter&) = delete;
|
||||
Painter(Painter&&) = delete;
|
||||
|
||||
int draw_char(const Point p, const Style& style, const char c);
|
||||
int draw_char(Point p, const Style& style, char c);
|
||||
|
||||
int draw_string(Point p, const Font& font, const Color foreground, const Color background, const std::string& text);
|
||||
int draw_string(Point p, const Style& style, const std::string& text);
|
||||
int draw_string(Point p, const Style& style, std::string_view text);
|
||||
int draw_string(Point p, const Font& font, Color foreground, Color background, std::string_view text);
|
||||
|
||||
void draw_bitmap(const Point p, const Bitmap& bitmap, const Color background, const Color foreground);
|
||||
void draw_bitmap(Point p, const Bitmap& bitmap, Color background, Color foreground);
|
||||
|
||||
void draw_rectangle(const Rect r, const Color c);
|
||||
void fill_rectangle(const Rect r, const Color c);
|
||||
void fill_rectangle_unrolled8(const Rect r, const Color c);
|
||||
void draw_rectangle(Rect r, Color c);
|
||||
void fill_rectangle(Rect r, Color c);
|
||||
void fill_rectangle_unrolled8(Rect r, Color c);
|
||||
|
||||
void paint_widget_tree(Widget* const w);
|
||||
void paint_widget_tree(Widget* w);
|
||||
|
||||
void draw_hline(Point p, int width, const Color c);
|
||||
void draw_vline(Point p, int height, const Color c);
|
||||
void draw_hline(Point p, int width, Color c);
|
||||
void draw_vline(Point p, int height, Color c);
|
||||
|
||||
private:
|
||||
void paint_widget(Widget* const w);
|
||||
void paint_widget(Widget* w);
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@ -42,14 +42,23 @@ class MockFile {
|
||||
Size size() { return data_.size(); }
|
||||
|
||||
Result<Offset> seek(uint32_t offset) {
|
||||
if (offset >= size())
|
||||
if ((int32_t)offset < 0)
|
||||
return {static_cast<Error>(FR_BAD_SEEK)};
|
||||
|
||||
auto previous = offset_;
|
||||
|
||||
if (offset > size())
|
||||
data_.resize(offset);
|
||||
|
||||
offset_ = offset;
|
||||
return previous;
|
||||
}
|
||||
|
||||
Result<Offset> truncate() {
|
||||
data_.resize(offset_);
|
||||
return offset_;
|
||||
}
|
||||
|
||||
Result<Size> read(void* data, Size bytes_to_read) {
|
||||
if (offset_ + bytes_to_read > size())
|
||||
bytes_to_read = size() - offset_;
|
||||
@ -62,6 +71,20 @@ class MockFile {
|
||||
return bytes_to_read;
|
||||
}
|
||||
|
||||
Result<Size> write(const void* data, Size bytes_to_write) {
|
||||
auto new_offset = offset_ + bytes_to_write;
|
||||
if (new_offset >= size())
|
||||
data_.resize(new_offset);
|
||||
|
||||
memcpy(&data_[offset_], data, bytes_to_write);
|
||||
offset_ = new_offset;
|
||||
return bytes_to_write;
|
||||
}
|
||||
|
||||
Optional<Error> sync() {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string data_;
|
||||
uint32_t offset_{0};
|
||||
};
|
||||
@ -89,11 +112,19 @@ TEST_SUITE("Test MockFile") {
|
||||
SCENARIO("File seek") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto init_size = f.size();
|
||||
|
||||
WHEN("seek()") {
|
||||
f.seek(4);
|
||||
THEN("offset_ should be updated.") {
|
||||
CHECK_EQ(f.offset_, 4);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() negative offset") {
|
||||
auto r = f.seek(-1);
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
THEN("Result should be bad_seek.") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
}
|
||||
@ -102,25 +133,25 @@ TEST_SUITE("Test MockFile") {
|
||||
WHEN("seek() offset is size()") {
|
||||
auto r = f.seek(f.size());
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
THEN("File should not grow.") {
|
||||
CHECK(r.is_ok());
|
||||
CHECK_EQ(f.size(), init_size);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset > size()") {
|
||||
auto r = f.seek(f.size() + 1);
|
||||
|
||||
THEN("Result should be bad_seek") {
|
||||
CHECK(r.is_error());
|
||||
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
|
||||
THEN("File should grow.") {
|
||||
CHECK(r.is_ok());
|
||||
CHECK_EQ(f.size(), init_size + 1);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("seek() offset < size()") {
|
||||
auto r = f.seek(1);
|
||||
|
||||
THEN("Result should be ok") {
|
||||
THEN("Result should be ok.") {
|
||||
CHECK(r.is_ok());
|
||||
}
|
||||
|
||||
@ -185,6 +216,72 @@ TEST_SUITE("Test MockFile") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File write") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
|
||||
WHEN("Writing over existing region") {
|
||||
f.write("xyz", 3);
|
||||
|
||||
THEN("It should overwrite") {
|
||||
CHECK_EQ(f.data_, "xyz\ndef");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Writing over past end") {
|
||||
f.seek(f.size());
|
||||
f.write("xyz", 3);
|
||||
|
||||
THEN("It should extend file and write") {
|
||||
CHECK_EQ(f.size(), 10);
|
||||
CHECK_EQ(f.data_, "abc\ndefxyz");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This scenario was tested on device.
|
||||
SCENARIO("File truncate") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"hello world"};
|
||||
|
||||
WHEN("truncating at offset 5") {
|
||||
f.seek(5);
|
||||
f.truncate();
|
||||
|
||||
THEN("resulting file should be 'hello'.") {
|
||||
CHECK_EQ(f.size(), 5);
|
||||
CHECK_EQ(f.data_, "hello");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("File truncate") {
|
||||
GIVEN("Valid file") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto init_size = f.size();
|
||||
|
||||
WHEN("R/W pointer at end") {
|
||||
f.seek(f.size());
|
||||
f.truncate();
|
||||
|
||||
THEN("It should not change size.") {
|
||||
CHECK_EQ(f.size(), init_size);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("R/W pointer in middle") {
|
||||
f.seek(3);
|
||||
f.truncate();
|
||||
|
||||
THEN("It should change size.") {
|
||||
CHECK_EQ(f.size(), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_BEGIN("Test BufferWrapper");
|
||||
@ -343,4 +440,259 @@ SCENARIO("Reading with cache miss.") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Replace range of same size.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range without changing size") {
|
||||
w.replace_range({0, 3}, "xyz");
|
||||
|
||||
CHECK_EQ("xyz\ndef", f.data_);
|
||||
|
||||
THEN("size should not change.") {
|
||||
CHECK_EQ(w.size(), init_size);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "xyz\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Replace range that increases size.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with larger size") {
|
||||
w.replace_range({0, 3}, "wxyz");
|
||||
|
||||
CHECK_EQ(f.data_, "wxyz\ndef");
|
||||
|
||||
THEN("size should be increased.") {
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "wxyz\n");
|
||||
}
|
||||
|
||||
THEN("following text should not be modified.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "def");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("A file larger than internal buffer_size (512)") {
|
||||
std::string content = std::string(599, 'a');
|
||||
content.push_back('x');
|
||||
MockFile f{content};
|
||||
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with larger size") {
|
||||
w.replace_range({0, 2}, "bbb");
|
||||
|
||||
THEN("size should be increased.") {
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "bbbaaaaaaa");
|
||||
}
|
||||
|
||||
THEN("end text should be preserved.") {
|
||||
CHECK_EQ(f.data_.back(), 'x');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Replace range that decreases size.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with smaller size") {
|
||||
w.replace_range({0, 3}, "yz");
|
||||
|
||||
CHECK_EQ(f.data_, "yz\ndef");
|
||||
|
||||
THEN("size should be decreased.") {
|
||||
CHECK_EQ(w.size(), init_size - 1);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "yz\n");
|
||||
}
|
||||
|
||||
THEN("following text should not be modified.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "def");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("A file larger than internal buffer_size (512)") {
|
||||
std::string content = std::string(599, 'a');
|
||||
content.push_back('x');
|
||||
MockFile f{content};
|
||||
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Replacing range with smaller size") {
|
||||
w.replace_range({0, 10}, "b");
|
||||
|
||||
THEN("size should be decreased.") {
|
||||
CHECK_EQ(w.size(), init_size - 9);
|
||||
}
|
||||
|
||||
THEN("text should be replaced.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
REQUIRE(str);
|
||||
CHECK_EQ(*str, "baaaaaaaaa");
|
||||
}
|
||||
|
||||
THEN("end should be moved toward front.") {
|
||||
CHECK_EQ(f.data_.back(), 'x');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Insert line.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef\nghi\n"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Inserting before the first line") {
|
||||
w.insert_line(0);
|
||||
|
||||
THEN("should increment line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count + 1);
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("should insert empty line content.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
CHECK_EQ(*str, "\n");
|
||||
}
|
||||
|
||||
THEN("first line should be pushed down.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
CHECK_EQ(*str, "abc\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Inserting before the last line") {
|
||||
w.insert_line(2);
|
||||
|
||||
THEN("should increment line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count + 1);
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("should insert empty line content.") {
|
||||
auto str = w.get_text(2, 0, 10);
|
||||
CHECK_EQ(*str, "\n");
|
||||
}
|
||||
|
||||
THEN("last line should be pushed down.") {
|
||||
auto str = w.get_text(3, 0, 10);
|
||||
CHECK_EQ(*str, "ghi\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Inserting after the last line") {
|
||||
w.insert_line(w.line_count());
|
||||
|
||||
THEN("should increment line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count + 1);
|
||||
CHECK_EQ(w.size(), init_size + 1);
|
||||
}
|
||||
|
||||
THEN("should insert empty line content.") {
|
||||
auto str = w.get_text(3, 0, 10);
|
||||
CHECK_EQ(*str, "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Delete line.") {
|
||||
GIVEN("A file with lines") {
|
||||
MockFile f{"abc\ndef\nghi"};
|
||||
auto w = wrap_buffer(f);
|
||||
auto init_line_count = w.line_count();
|
||||
auto init_size = w.size();
|
||||
|
||||
WHEN("Deleting the first line") {
|
||||
w.delete_line(0);
|
||||
|
||||
THEN("should decrement line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count - 1);
|
||||
CHECK_EQ(w.size(), init_size - 4);
|
||||
}
|
||||
|
||||
THEN("should remove first line content.") {
|
||||
auto str = w.get_text(0, 0, 10);
|
||||
CHECK_EQ(*str, "def\n");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Deleting the middle line") {
|
||||
w.delete_line(1);
|
||||
|
||||
THEN("should decrement line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count - 1);
|
||||
CHECK_EQ(w.size(), init_size - 4);
|
||||
}
|
||||
|
||||
THEN("should remove middle line content.") {
|
||||
auto str = w.get_text(1, 0, 10);
|
||||
CHECK_EQ(*str, "ghi");
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Deleting the last line") {
|
||||
w.delete_line(2);
|
||||
|
||||
THEN("should decrement line count and size.") {
|
||||
CHECK_EQ(w.line_count(), init_line_count - 1);
|
||||
CHECK_EQ(w.size(), init_size - 3);
|
||||
}
|
||||
|
||||
THEN("should remove last line content.") {
|
||||
auto str = w.get_text(2, 0, 10);
|
||||
CHECK(!str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
Loading…
x
Reference in New Issue
Block a user