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
30 changes: 30 additions & 0 deletions .github/workflows/conpty-compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,33 @@ jobs:
retention-days: 14
if-no-files-found: warn
path: node-contract-timing.json

windows-2025-actions-repeat:
# Bounded reproduction for #156 / #99 / #73: repeats the clipboard and
# geometry scenarios on the hosted image so the selection-discard
# diagnostics can be ordered against the scenario phase timeline.
runs-on: windows-2025
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.12'
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --config Release --parallel 2
- name: Prepare hosted Windows shell prerequisites
run: python -m tools.prepare_hosted_shells --out local/hosted-shells
- name: Repeat the actions scenarios
env:
TERMFORGE_REQUIRE_SYSTEM_CLIPBOARD: '1'
run: ctest --test-dir build -C Release -R "text_actions|selection_context" --repeat until-fail:20 --output-on-failure
- name: Preserve repeat evidence
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: actions-repeat-evidence
retention-days: 14
if-no-files-found: warn
path: build/test-artifacts/
2 changes: 1 addition & 1 deletion 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, size_t) { observed_discards.push_back(reason ? reason : ""); }
static void observe_discard(const TextSelection::DiscardReport& report) { observed_discards.push_back(report.reason ? report.reason : ""); }
static void discard_reasons_are_reported() {
Parser p(2, 6, 2);
p.feed("ONETWO\r\nTHREE");
Expand Down
24 changes: 20 additions & 4 deletions experiments/core/text_selection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ 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, size_t row);
// Diagnostics only: the report names the branch and, where it helps, the
// row plus the saved/current projection shape that differed.
struct DiscardReport {
const char* reason = "";
size_t row = 0;
size_t saved_bytes = 0, current_bytes = 0;
size_t saved_columns = 0, current_columns = 0;
};
using DiscardObserver = void (*)(const DiscardReport&);
static void set_discard_observer(DiscardObserver observer) { discard_observer_ = observer; }
private:
static inline DiscardObserver discard_observer_ = nullptr;
Expand All @@ -40,10 +48,13 @@ 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, size_t row = 0) {
if (discard_observer_) discard_observer_(reason, row);
bool invalidate(const DiscardReport& report) {
if (discard_observer_) discard_observer_(report);
clear(); invalidated_ = true; return false;
}
bool invalidate(const char* reason, size_t row = 0) {
DiscardReport report; report.reason = reason; report.row = row; return invalidate(report);
}
public:
static constexpr size_t maximum_snapshot_bytes = 8 * 1024 * 1024;
void clear() { range_.reset(); saved_.clear(); invalidated_ = false; }
Expand Down Expand Up @@ -92,7 +103,12 @@ class TextSelection {
// 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", i);
if (!same_text(saved, current, compare_boundary)) {
DiscardReport report; report.reason = "row-text-changed"; report.row = i;
report.saved_bytes = saved.projection.text.size(); report.current_bytes = current.projection.text.size();
report.saved_columns = saved.columns; report.current_columns = current.columns;
return invalidate(report);
}
saved = std::move(current);
}
}
Expand Down
8 changes: 5 additions & 3 deletions experiments/shell/shell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ std::string selection_diag_path() {
return value ? std::string(value) : std::string{};
#endif
}
void selection_discard_diagnostic(const char* reason, size_t row) {
void selection_discard_diagnostic(const termforge::TextSelection::DiscardReport& report) {
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
<< "\",\"row\":" << row << ",\"t_ms\":" << GetTickCount64() << "}\n";
stream << "{\"schema\":\"termforge-selection-discard-v1\",\"reason\":\"" << report.reason
<< "\",\"row\":" << report.row << ",\"saved_bytes\":" << report.saved_bytes
<< ",\"current_bytes\":" << report.current_bytes << ",\"saved_columns\":" << report.saved_columns
<< ",\"current_columns\":" << report.current_columns << ",\"t_ms\":" << GetTickCount64() << "}\n";
}
void install_selection_diagnostic_once() {
static std::once_flag once;
Expand Down
Loading