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
4 changes: 2 additions & 2 deletions experiments/core/selection_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ static void empty_stale_and_capacity_boundaries() {
}

static std::vector<std::string> observed_discards;
static void observe_discard(const char* reason) { observed_discards.push_back(reason ? reason : ""); }
static void observe_discard(const char* reason, size_t) { observed_discards.push_back(reason ? reason : ""); }
static void discard_reasons_are_reported() {
Parser p(2, 6, 2);
p.feed("ONETWO\r\nTHREE");
Expand Down Expand Up @@ -222,4 +222,4 @@ int main() {
discard_reasons_are_reported();
std::cout << "selection contracts passed (8 independent groups)\n";
} catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; }
}
}
16 changes: 8 additions & 8 deletions experiments/core/text_selection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class TextSelection {
// Diagnostics only: never changes behaviour. A consumer may install a hook to
// learn why a reconciled selection was discarded; the hook runs synchronously
// on the caller's thread and must not reenter selection or the document.
using DiscardObserver = void (*)(const char* reason);
using DiscardObserver = void (*)(const char* reason, size_t row);
static void set_discard_observer(DiscardObserver observer) { discard_observer_ = observer; }
private:
static inline DiscardObserver discard_observer_ = nullptr;
Expand All @@ -40,8 +40,8 @@ class TextSelection {
return c >= 0x80 || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-' ? 0 : 2;
}
bool invalidate(const char* reason) {
if (discard_observer_) discard_observer_(reason);
bool invalidate(const char* reason, size_t row = 0) {
if (discard_observer_) discard_observer_(reason, row);
clear(); invalidated_ = true; return false;
}
public:
Expand Down Expand Up @@ -77,28 +77,28 @@ class TextSelection {
bool reconcile(const TextDocument& document) {
if (!range_) return !invalidated_;
if (range_->begin.epoch != document.epoch() || range_->end.epoch != document.epoch())
return invalidate("epoch-mismatch");
return invalidate("epoch-mismatch", 0);
const auto begin = document.index_of(range_->begin.line_id);
if (!begin || saved_.size() > document.size() - *begin) return invalidate("anchor-missing-or-shrunk");
if (!begin || saved_.size() > document.size() - *begin) return invalidate("anchor-missing-or-shrunk", 0);
for (size_t i = 0; i < saved_.size(); ++i) {
const auto meta = document.meta(*begin + i);
auto& saved = saved_[i];
if (meta.id != saved.projection.id &&
(i + 1 != saved_.size() || range_->end.column != 0 ||
!saved.projection.text.empty() || !saved.projection.spans.empty())) return invalidate("row-identity-replaced");
!saved.projection.text.empty() || !saved.projection.spans.empty())) return invalidate("row-identity-replaced", i);
if (meta.id != saved.projection.id || meta.version != saved.projection.version ||
meta.joins_next != saved.joins_next) {
auto current = document.line(*begin + i);
// The last row at column zero is an excluded half-open
// endpoint. Its own following break is outside the copy.
const bool compare_boundary = i + 1 != saved_.size() || range_->end.column != 0;
if (!same_text(saved, current, compare_boundary)) return invalidate("row-text-changed");
if (!same_text(saved, current, compare_boundary)) return invalidate("row-text-changed", i);
saved = std::move(current);
}
}
const auto first = document.position(*begin, range_->begin.column);
const auto last = document.position(*begin + saved_.size() - 1, range_->end.column);
if (!first || !last) return invalidate("endpoint-unavailable");
if (!first || !last) return invalidate("endpoint-unavailable", 0);
range_ = TextRange{*first, *last}; return true;
}
bool select_word(const TextDocument& document, size_t index, size_t column) {
Expand Down
2 changes: 1 addition & 1 deletion experiments/shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ R1 的 [#12–#17 限定合同](../../docs/milestones/R1-local-text.md)和 [#18

## 选区丢弃诊断

几何/DPI/缩放变化后若选区被 `TextSelection::reconcile` 丢弃,可用环境变量 `TERMFORGE_SELECTION_DIAG=<jsonl 路径>` 打开一次性诊断:每次丢弃写入一行 `{"schema":"termforge-selection-discard-v1","reason":"..."}`,`reason` 取 `epoch-mismatch`、`anchor-missing-or-shrunk`、`row-identity-replaced`、`row-text-changed`、`endpoint-unavailable` 之一。未设置该变量时不写任何文件、行为不变;`experiments/shell/run.py` 的 `execute()` 会自动把该变量指向本次场景产物目录下的 `selection-discard.jsonl`,因此失败时该文件随 `build/test-artifacts/` 一起保留。`selection_core` 里新增的分组只验证观察器能收到分支名。
几何/DPI/缩放变化后若选区被 `TextSelection::reconcile` 丢弃,可用环境变量 `TERMFORGE_SELECTION_DIAG=<jsonl 路径>` 打开一次性诊断:每次丢弃写入一行 `{"schema":"termforge-selection-discard-v1","reason":"...","row":N}`(`row` 是发生不一致的选区行序号,`0` 表示与具体行无关的分支),`reason` 取 `epoch-mismatch`、`anchor-missing-or-shrunk`、`row-identity-replaced`、`row-text-changed`、`endpoint-unavailable` 之一。未设置该变量时不写任何文件、行为不变;`experiments/shell/run.py` 的 `execute()` 会自动把该变量指向本次场景产物目录下的 `selection-discard.jsonl`,因此失败时该文件随 `build/test-artifacts/` 一起保留。`selection_core` 里新增的分组只验证观察器能收到分支名。

## 所有权与调度

Expand Down
5 changes: 3 additions & 2 deletions experiments/shell/shell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ std::string selection_diag_path() {
return value ? std::string(value) : std::string{};
#endif
}
void selection_discard_diagnostic(const char* reason) {
void selection_discard_diagnostic(const char* reason, size_t row) {
static const std::string path = selection_diag_path();
if (path.empty()) return;
std::ofstream stream(path, std::ios::app);
if (!stream) return;
stream << "{\"schema\":\"termforge-selection-discard-v1\",\"reason\":\"" << reason << "\"}\n";
stream << "{\"schema\":\"termforge-selection-discard-v1\",\"reason\":\"" << reason
<< "\",\"row\":" << row << "}\n";
}
void install_selection_diagnostic_once() {
static std::once_flag once;
Expand Down