diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 0cdb6dc..5f75a5e 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -33,18 +33,12 @@
-
-
+
-
-
-
-
-
-
+
@@ -145,7 +139,7 @@
-
+
@@ -176,7 +170,7 @@
-
+
@@ -250,7 +244,15 @@
1764505723411
-
+
+
+ 1764550164829
+
+
+
+ 1764550164829
+
+
@@ -273,7 +275,8 @@
-
+
+
diff --git a/Buffer.cc b/Buffer.cc
index f171f4b..8603cee 100644
--- a/Buffer.cc
+++ b/Buffer.cc
@@ -347,8 +347,8 @@ Buffer::insert_text(int row, int col, std::string_view text)
if (static_cast(row) >= rows_.size())
rows_.emplace_back("");
- std::size_t y = static_cast(row);
- std::size_t x = static_cast(col);
+ auto y = static_cast(row);
+ auto x = static_cast(col);
if (x > rows_[y].size())
x = rows_[y].size();
@@ -384,13 +384,13 @@ Buffer::delete_text(int row, int col, std::size_t len)
row = 0;
if (static_cast(row) >= rows_.size())
return;
- std::size_t y = static_cast(row);
- std::size_t x = std::min(static_cast(col), rows_[y].size());
+ const auto y = static_cast(row);
+ const auto x = std::min(static_cast(col), rows_[y].size());
std::size_t remaining = len;
while (remaining > 0 && y < rows_.size()) {
- auto &line = rows_[y];
- std::size_t in_line = std::min(remaining, line.size() - std::min(x, line.size()));
+ auto &line = rows_[y];
+ const std::size_t in_line = std::min(remaining, line.size() - std::min(x, line.size()));
if (x < line.size() && in_line > 0) {
line.erase(x, in_line);
remaining -= in_line;
@@ -414,15 +414,18 @@ Buffer::delete_text(int row, int col, std::size_t len)
void
-Buffer::split_line(int row, int col)
+Buffer::split_line(int row, const int col)
{
- if (row < 0)
+ if (row < 0) {
row = 0;
- if (static_cast(row) >= rows_.size())
+ }
+
+ if (static_cast(row) >= rows_.size()) {
rows_.resize(static_cast(row) + 1);
- std::size_t y = static_cast(row);
- std::size_t x = std::min(static_cast(col), rows_[y].size());
- std::string tail = rows_[y].substr(x);
+ }
+ const auto y = static_cast(row);
+ const auto x = std::min(static_cast(col), rows_[y].size());
+ const auto tail = rows_[y].substr(x);
rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast(y + 1), tail);
}
@@ -431,24 +434,28 @@ Buffer::split_line(int row, int col)
void
Buffer::join_lines(int row)
{
- if (row < 0)
+ if (row < 0) {
row = 0;
- std::size_t y = static_cast(row);
- if (y + 1 >= rows_.size())
+ }
+
+ const auto y = static_cast(row);
+ if (y + 1 >= rows_.size()) {
return;
+ }
+
rows_[y] += rows_[y + 1];
rows_.erase(rows_.begin() + static_cast(y + 1));
}
void
-Buffer::insert_row(int row, std::string_view text)
+Buffer::insert_row(int row, const std::string_view text)
{
if (row < 0)
row = 0;
if (static_cast(row) > rows_.size())
row = static_cast(rows_.size());
- rows_.insert(rows_.begin() + static_cast(row), std::string(text));
+ rows_.insert(rows_.begin() + row, std::string(text));
}
@@ -459,7 +466,7 @@ Buffer::delete_row(int row)
row = 0;
if (static_cast(row) >= rows_.size())
return;
- rows_.erase(rows_.begin() + static_cast(row));
+ rows_.erase(rows_.begin() + row);
}
diff --git a/GUIRenderer.cc b/GUIRenderer.cc
index 236490f..db763a3 100644
--- a/GUIRenderer.cc
+++ b/GUIRenderer.cc
@@ -8,6 +8,8 @@
#include
#include
#include
+#include
+#include
// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
@@ -63,30 +65,45 @@ GUIRenderer::Draw(Editor &ed)
bool forced_scroll = false;
{
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
+ static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
+ static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
const long buf_rowoffs = static_cast(buf->Rowoffs());
+ const long buf_coloffs = static_cast(buf->Coloffs());
const long scroll_top = static_cast(scroll_y / row_h);
+ const long scroll_left = static_cast(scroll_x / space_w);
// Detect programmatic change (e.g., keyboard navigation ensured visibility)
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
ImGui::SetScrollY(static_cast(buf_rowoffs) * row_h);
scroll_y = ImGui::GetScrollY();
forced_scroll = true;
- } else {
- // If user scrolled (scroll_y changed), update buffer row offset accordingly
- if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
- if (Buffer *mbuf = const_cast(buf)) {
- // Keep horizontal offset owned by GUI; only update vertical offset here
- mbuf->SetOffsets(static_cast(std::max(0L, scroll_top)),
- mbuf->Coloffs());
- }
+ }
+ if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
+ ImGui::SetScrollX(static_cast(buf_coloffs) * space_w);
+ scroll_x = ImGui::GetScrollX();
+ forced_scroll = true;
+ }
+ // If user scrolled, update buffer offsets accordingly
+ if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
+ if (Buffer *mbuf = const_cast(buf)) {
+ mbuf->SetOffsets(static_cast(std::max(0L, scroll_top)),
+ mbuf->Coloffs());
+ }
+ }
+ if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
+ if (Buffer *mbuf = const_cast(buf)) {
+ mbuf->SetOffsets(mbuf->Rowoffs(),
+ static_cast(std::max(0L, scroll_left)));
}
}
// Update trackers for next frame
prev_buf_rowoffs = static_cast(buf->Rowoffs());
+ prev_buf_coloffs = static_cast(buf->Coloffs());
prev_scroll_y = ImGui::GetScrollY();
+ prev_scroll_x = ImGui::GetScrollX();
}
// Synchronize cursor and scrolling.
// Ensure the cursor is visible even on the first frame or when it didn't move,
@@ -120,61 +137,127 @@ GUIRenderer::Draw(Editor &ed)
// Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos;
- // Map Y to row
- float rel_y = scroll_y + (mp.y - list_origin.y);
- long row = static_cast(rel_y / row_h);
- if (row < 0)
- row = 0;
- if (row >= static_cast(lines.size()))
- row = static_cast(lines.empty() ? 0 : (lines.size() - 1));
- // Map X to column by measuring text width
- std::size_t col = 0;
- if (!lines.empty()) {
- const std::string &line = lines[static_cast(row)];
- float rel_x = scroll_x + (mp.x - list_origin.x);
- if (rel_x <= 0.0f) {
- col = 0;
- } else {
- float prev_w = 0.0f;
- for (std::size_t i = 1; i <= line.size(); ++i) {
- ImVec2 sz = ImGui::CalcTextSize(
- line.c_str(), line.c_str() + static_cast(i));
- if (sz.x >= rel_x) {
- // Pick closer between i-1 and i
- float d_prev = rel_x - prev_w;
- float d_curr = sz.x - rel_x;
- col = (d_prev <= d_curr) ? (i - 1) : i;
- break;
- }
- prev_w = sz.x;
- if (i == line.size()) {
- // clicked beyond EOL
- float eol_w = sz.x;
- col = (rel_x > eol_w + space_w * 0.5f)
- ? line.size()
- : line.size();
- }
+ // Compute viewport-relative row so (0) is top row of the visible area
+ float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
+ long vy = static_cast(vy_f);
+ if (vy < 0)
+ vy = 0;
+
+ // Clamp vy within visible content height to avoid huge jumps
+ ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
+ ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
+ float child_h = (cr_max.y - cr_min.y);
+ long vis_rows = static_cast(child_h / row_h);
+ if (vis_rows < 1)
+ vis_rows = 1;
+ if (vy >= vis_rows)
+ vy = vis_rows - 1;
+
+ // Translate viewport row to buffer row using Buffer::Rowoffs
+ std::size_t by = buf->Rowoffs() + static_cast(vy);
+ if (by >= lines.size()) {
+ if (!lines.empty())
+ by = lines.size() - 1;
+ else
+ by = 0;
+ }
+
+ // Compute desired pixel X inside the viewport content (subtract horizontal scroll)
+ float px = (mp.x - list_origin.x - scroll_x);
+ if (px < 0.0f)
+ px = 0.0f;
+
+ // Convert pixel X to a render-column target including horizontal col offset
+ // Use our own tab expansion of width 8 to match command layer logic.
+ const std::string &line_clicked = lines[by];
+ const std::size_t tabw = 8;
+ // We iterate source columns computing absolute rendered column (rx_abs) from 0,
+ // then translate to viewport-space by subtracting Coloffs.
+ std::size_t coloffs = buf->Coloffs();
+ std::size_t rx_abs = 0; // absolute rendered column
+ std::size_t i = 0; // source column iterator
+
+ // Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
+ if (!line_clicked.empty() && coloffs > 0) {
+ while (i < line_clicked.size() && rx_abs < coloffs) {
+ if (line_clicked[i] == '\t') {
+ rx_abs += (tabw - (rx_abs % tabw));
+ } else {
+ rx_abs += 1;
}
+ ++i;
}
}
- // Dispatch command to move cursor
+
+ // Now search for closest source column to clicked px within/after viewport
+ std::size_t best_col = i; // default to first visible column
+ float best_dist = std::numeric_limits::infinity();
+ while (true) {
+ // For i in [current..size], evaluate candidate including the implicit end position
+ std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
+ float rx_px = static_cast(rx_view) * space_w;
+ float dist = std::fabs(px - rx_px);
+ if (dist <= best_dist) {
+ best_dist = dist;
+ best_col = i;
+ }
+ if (i == line_clicked.size())
+ break;
+ // advance to next source column
+ if (line_clicked[i] == '\t') {
+ rx_abs += (tabw - (rx_abs % tabw));
+ } else {
+ rx_abs += 1;
+ }
+ ++i;
+ }
+
+ // Dispatch absolute buffer coordinates (row:col)
char tmp[64];
- std::snprintf(tmp, sizeof(tmp), "%ld:%zu", row, col);
+ std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
}
+ // Cache current horizontal offset in rendered columns
+ const std::size_t coloffs_now = buf->Coloffs();
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
// Capture the screen position before drawing the line
ImVec2 line_pos = ImGui::GetCursorScreenPos();
const std::string &line = lines[i];
- ImGui::TextUnformatted(line.c_str());
+
+ // Expand tabs to spaces with width=8 and apply horizontal scroll offset
+ const std::size_t tabw = 8;
+ std::string expanded;
+ expanded.reserve(line.size() + 16);
+ std::size_t rx_abs_draw = 0; // rendered column for drawing
+ // Emit entire line (ImGui child scrolling will handle clipping)
+ for (std::size_t src = 0; src < line.size(); ++src) {
+ char c = line[src];
+ if (c == '\t') {
+ std::size_t adv = (tabw - (rx_abs_draw % tabw));
+ // Emit spaces for the tab
+ expanded.append(adv, ' ');
+ rx_abs_draw += adv;
+ } else {
+ expanded.push_back(c);
+ rx_abs_draw += 1;
+ }
+ }
+
+ ImGui::TextUnformatted(expanded.c_str());
// Draw a visible cursor indicator on the current line
if (i == cy) {
- // Compute X offset by measuring text width up to cursor column
- std::size_t px_count = std::min(cx, line.size());
- ImVec2 pre_sz = ImGui::CalcTextSize(line.c_str(),
- line.c_str() + static_cast(px_count));
- ImVec2 p0 = ImVec2(line_pos.x + pre_sz.x, line_pos.y);
+ // Compute rendered X (rx) from source column with tab expansion
+ std::size_t rx_abs = 0;
+ for (std::size_t k = 0; k < std::min(cx, line.size()); ++k) {
+ if (line[k] == '\t')
+ rx_abs += (tabw - (rx_abs % tabw));
+ else
+ rx_abs += 1;
+ }
+ // Convert to viewport x by subtracting horizontal col offset
+ std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0;
+ ImVec2 p0 = ImVec2(line_pos.x + static_cast(rx_viewport) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
diff --git a/KKeymap.cc b/KKeymap.cc
index a1ba404..a938717 100644
--- a/KKeymap.cc
+++ b/KKeymap.cc
@@ -1,5 +1,8 @@
#include "KKeymap.h"
+
+#include
#include
+#include
auto
@@ -21,9 +24,7 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
out = CommandId::SaveAndQuit;
return true;
default:
- // Important: do not return here — fall through to non-ctrl table
- // so that C-k u/U still work even if Ctrl is (incorrectly) held
- break;
+ return false;
}
}
@@ -49,6 +50,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'e':
out = CommandId::OpenFileStart;
return true;
+ case 'E':
+ std::cerr << "E is not a valid command" << std::endl;
+ return false;
case 'f':
out = CommandId::FlushKillRing;
return true;
diff --git a/main.cc b/main.cc
index d3ca77a..cfcd609 100644
--- a/main.cc
+++ b/main.cc
@@ -189,6 +189,15 @@ main(int argc, const char *argv[])
fe = std::make_unique();
}
+#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
+ if (use_gui) {
+ /* likely using the .app, so need to cd */
+ if (chdir(getenv("HOME")) != 0) {
+ std::cerr << "kge.app: failed to chdir to HOME" << std::endl;
+ }
+ }
+#endif
+
if (!fe->Init(editor)) {
std::cerr << "kte: failed to initialize frontend" << std::endl;
return 1;