Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ add_executable(decawm_mode_test experiments/core/decawm_mode_test.cpp)
target_link_libraries(decawm_mode_test PRIVATE termforge_core)
termforge_warnings(decawm_mode_test)

add_executable(view_mapping_test experiments/core/view_mapping_test.cpp)
target_link_libraries(view_mapping_test PRIVATE termforge_core)
termforge_warnings(view_mapping_test)

add_executable(reply_test experiments/core/reply_test.cpp)
target_link_libraries(reply_test PRIVATE termforge_core)
termforge_warnings(reply_test)
Expand Down Expand Up @@ -325,6 +329,7 @@ if(BUILD_TESTING)
add_test(NAME search_reorder_core COMMAND search_reorder_test)
set_tests_properties(search_reorder_core PROPERTIES TIMEOUT 10)
add_test(NAME buffer_state_core COMMAND buffer_state_test)
add_test(NAME view_mapping_core COMMAND view_mapping_test)
add_test(NAME alternate_mode_core COMMAND alternate_mode_test)
add_test(NAME decawm_mode_core COMMAND decawm_mode_test)
add_test(NAME reply_core COMMAND reply_test)
Expand Down
91 changes: 91 additions & 0 deletions experiments/core/view_mapping.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Value-only source coordinates published with one immutable display page.
#pragma once
#include "text_document.hpp"

namespace termforge {
enum class ViewAffinity { leading, trailing };
struct ViewCellSpan {
size_t row = 0, column_begin = 0, column_end = 0;
};
struct ViewSourceSpan {
size_t row = 0, column_begin = 0, column_end = 0;
TextRange source;
};

class ViewMapping {
size_t columns_ = 0;
std::vector<ViewSourceSpan> sources_;

// Identities do not order the document: retained sequence does. Document
// order therefore comes from TextDocument::compare, which stays correct for
// reordered rows and rejects positions captured at an older buffer epoch.
static bool before(const TextDocument& document, TextPosition a, TextPosition b) {
const auto order = document.compare(a, b);
return order.has_value() && *order < 0;
}
bool valid(const TextDocument& document, const ViewSourceSpan& span) const {
return span.column_begin <= span.column_end && span.column_end <= columns_ &&
span.source.begin.line_id == span.source.end.line_id &&
span.source.begin.version == span.source.end.version &&
span.source.begin.column <= span.source.end.column &&
document.valid(span.source.begin) && document.valid(span.source.end);
}
public:
ViewMapping() = default;
ViewMapping(size_t columns, std::vector<ViewSourceSpan> sources)
: columns_(columns), sources_(std::move(sources)) {}

std::optional<TextPosition> hit(const TextDocument& document, size_t row, size_t column,
ViewAffinity affinity) const {
if (column > columns_) return std::nullopt;
const ViewSourceSpan* first = nullptr;
const ViewSourceSpan* previous = nullptr;
for (const auto& span : sources_) {
if (span.row != row) continue;
if (!valid(document, span)) return std::nullopt;
if (!first) first = &span;
}
if (!first) return std::nullopt;
for (const auto& span : sources_) {
if (span.row != row) continue;
if (column <= span.column_begin) {
if (previous && affinity == ViewAffinity::trailing) return previous->source.end;
return span.source.begin;
}
if (column < span.column_end) {
auto result = span.source.begin;
const auto source_width = span.source.end.column - result.column;
const auto display_width = span.column_end - span.column_begin;
// Ordinary wide-cell interiors keep their physical boundary.
// TextDocument normalizes a complete drag range afterward.
if (source_width == display_width) {
result.column += column - span.column_begin;
return result;
}
return affinity == ViewAffinity::leading ? span.source.begin : span.source.end;
}
if (column == span.column_end && affinity == ViewAffinity::trailing) return span.source.end;
previous = &span;
}
// Synthetic trailing padding has no independent source coordinates.
return previous ? std::optional<TextPosition>{previous->source.end} : std::nullopt;
}

std::vector<ViewCellSpan> project(const TextDocument& document, TextRange range) const {
const auto normalized = document.normalize(range);
if (!normalized || normalized->begin == normalized->end) return {};
std::vector<ViewCellSpan> result;
for (const auto& span : sources_) {
if (!valid(document, span)) return {};
if (!before(document, span.source.begin, normalized->end) ||
!before(document, normalized->begin, span.source.end))
continue;
if (span.column_begin == span.column_end) continue;
if (!result.empty() && result.back().row == span.row &&
result.back().column_end == span.column_begin) result.back().column_end = span.column_end;
else result.push_back({span.row, span.column_begin, span.column_end});
}
return result;
}
};
} // namespace termforge
85 changes: 85 additions & 0 deletions experiments/core/view_mapping_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Display-page coordinates published beside one immutable view: a display row
// is composed from source ranges, so hit-testing and range projection must map
// back to document coordinates instead of assuming they are the same thing.
#include "terminal.hpp"
#include "text_document.hpp"
#include "view_mapping.hpp"
#include <iostream>
#include <stdexcept>

using namespace termforge;

static void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}

static void interior_columns_map_by_offset() {
Parser parser(3, 4, 8);
parser.feed("ABCD\r\nEF");
const TextDocument document(parser.screen);
const auto begin = document.position(0, 0);
const auto end = document.position(0, 4);
check(begin.has_value() && end.has_value(), "source positions for the first row");

ViewMapping mapping(4, {ViewSourceSpan{0, 0, 4, TextRange{*begin, *end}}});
const auto hit = mapping.hit(document, 0, 2, ViewAffinity::leading);
check(hit.has_value() && document.valid(*hit) && hit->column == 2,
"an equal-width span maps a display column onto the same source column");

const auto projected = mapping.project(document, TextRange{*begin, *end});
check(projected.size() == 1, "one composed row yields one display span");
check(projected[0].row == 0 && projected[0].column_begin == 0 && projected[0].column_end == 4,
"projection returns the mapped display range");
}

static void trailing_padding_has_no_source_coordinates() {
Parser parser(3, 4, 8);
parser.feed("ABCD");
const TextDocument document(parser.screen);
const auto begin = document.position(0, 0);
const auto end = document.position(0, 4);
check(begin.has_value() && end.has_value(), "source positions for the padded row");

// Six display columns, but only the first four carry source coordinates.
ViewMapping mapping(6, {ViewSourceSpan{0, 0, 4, TextRange{*begin, *end}}});
const auto padded = mapping.hit(document, 0, 5, ViewAffinity::leading);
check(padded.has_value() && *padded == *end,
"synthetic padding reports the end of the last mapped source span");
const auto beyond = mapping.hit(document, 0, 7, ViewAffinity::leading);
check(!beyond.has_value(), "columns past the page width have no mapping");
}

static void stale_epoch_positions_do_not_hit() {
Parser parser(3, 4, 8);
parser.feed("ABCD");
const TextDocument current(parser.screen);
const auto begin = current.position(0, 0);
const auto end = current.position(0, 4);
check(begin.has_value() && end.has_value(), "source positions before the epoch change");

ViewMapping mapping(4, {ViewSourceSpan{0, 0, 4, TextRange{*begin, *end}}});

// Alternate-buffer activation advances the buffer epoch, so positions
// captured against the old page are stale even though the ids look usable.
parser.screen.activate_buffer(ScreenBuffer::alternate);
parser.feed("ALT");
const TextDocument refreshed(parser.screen);

const auto hit = mapping.hit(refreshed, 0, 2, ViewAffinity::leading);
check(!hit.has_value(), "a mapping captured at an older epoch must not resolve");
const auto projected = mapping.project(refreshed, TextRange{*begin, *end});
check(projected.empty(), "projection over stale coordinates returns nothing");
}

int main() {
try {
interior_columns_map_by_offset();
trailing_padding_has_no_source_coordinates();
stale_epoch_positions_do_not_hit();
std::cout << "view mapping checks passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}