emacs-style movememnts and page up / down.
This commit is contained in:
9
.idea/workspace.xml
generated
9
.idea/workspace.xml
generated
@@ -35,10 +35,15 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalFrontend.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/main.cc" beforeDir="false" afterPath="$PROJECT_DIR$/main.cc" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -143,7 +148,7 @@
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1764457173148</updated>
|
||||
<workItem from="1764457174208" duration="18589000" />
|
||||
<workItem from="1764457174208" duration="19741000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
140
Command.cc
140
Command.cc
@@ -365,9 +365,23 @@ cmd_refresh(CommandContext &ctx)
|
||||
static bool
|
||||
cmd_kprefix(CommandContext &ctx)
|
||||
{
|
||||
// Show k-command mode hint in status
|
||||
ctx.editor.SetStatus("C-k _");
|
||||
return true;
|
||||
// Show k-command mode hint in status
|
||||
ctx.editor.SetStatus("C-k _");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_unknown_kcommand(CommandContext &ctx)
|
||||
{
|
||||
char ch = '?';
|
||||
if (!ctx.arg.empty()) {
|
||||
ch = ctx.arg[0];
|
||||
}
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf), "unknown k-command %c", ch);
|
||||
ctx.editor.SetStatus(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -887,57 +901,75 @@ cmd_move_end(CommandContext &ctx)
|
||||
static bool
|
||||
cmd_page_up(CommandContext &ctx)
|
||||
{
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
std::size_t x = buf->Curx();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0;
|
||||
if (content_rows == 0)
|
||||
content_rows = 1;
|
||||
while (repeat-- > 0) {
|
||||
if (y > content_rows)
|
||||
y -= content_rows;
|
||||
else
|
||||
y = 0;
|
||||
if (x > rows[y].size())
|
||||
x = rows[y].size();
|
||||
}
|
||||
buf->SetCursor(x, y);
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
return true;
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0;
|
||||
if (content_rows == 0)
|
||||
content_rows = 1;
|
||||
|
||||
// Base on current top-of-screen (row offset)
|
||||
std::size_t rowoffs = buf->Rowoffs();
|
||||
while (repeat-- > 0) {
|
||||
if (rowoffs >= content_rows)
|
||||
rowoffs -= content_rows;
|
||||
else
|
||||
rowoffs = 0;
|
||||
}
|
||||
// Clamp to valid range
|
||||
if (rows.size() > content_rows) {
|
||||
std::size_t max_top = rows.size() - content_rows;
|
||||
if (rowoffs > max_top) rowoffs = max_top;
|
||||
} else {
|
||||
rowoffs = 0;
|
||||
}
|
||||
// Move cursor to first visible line, column 0
|
||||
std::size_t y = rowoffs;
|
||||
if (y >= rows.size()) y = rows.empty() ? 0 : rows.size() - 1;
|
||||
buf->SetOffsets(rowoffs, 0);
|
||||
buf->SetCursor(0, y);
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_page_down(CommandContext &ctx)
|
||||
{
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
std::size_t y = buf->Cury();
|
||||
std::size_t x = buf->Curx();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0;
|
||||
if (content_rows == 0)
|
||||
content_rows = 1;
|
||||
while (repeat-- > 0) {
|
||||
std::size_t max_down = rows.empty() ? 0 : (rows.size() - 1 - y);
|
||||
if (content_rows < max_down)
|
||||
y += content_rows;
|
||||
else
|
||||
y += max_down;
|
||||
if (x > rows[y].size())
|
||||
x = rows[y].size();
|
||||
}
|
||||
buf->SetCursor(x, y);
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
return true;
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
ensure_at_least_one_line(*buf);
|
||||
auto &rows = buf->Rows();
|
||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0;
|
||||
if (content_rows == 0)
|
||||
content_rows = 1;
|
||||
|
||||
std::size_t rowoffs = buf->Rowoffs();
|
||||
// Compute maximum top offset
|
||||
std::size_t max_top = 0;
|
||||
if (!rows.empty()) {
|
||||
if (rows.size() > content_rows)
|
||||
max_top = rows.size() - content_rows;
|
||||
else
|
||||
max_top = 0;
|
||||
}
|
||||
while (repeat-- > 0) {
|
||||
if (rowoffs + content_rows <= max_top)
|
||||
rowoffs += content_rows;
|
||||
else
|
||||
rowoffs = max_top;
|
||||
}
|
||||
// Move cursor to first visible line, column 0
|
||||
std::size_t y = std::min<std::size_t>(rowoffs, rows.empty() ? 0 : rows.size() - 1);
|
||||
buf->SetOffsets(rowoffs, 0);
|
||||
buf->SetCursor(0, y);
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1115,11 +1147,13 @@ InstallDefaultCommands()
|
||||
CommandRegistry::Register({CommandId::Save, "save", "Save current buffer", cmd_save});
|
||||
CommandRegistry::Register({CommandId::SaveAs, "save-as", "Save current buffer as...", cmd_save_as});
|
||||
CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit});
|
||||
CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
|
||||
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
|
||||
CommandRegistry::Register(
|
||||
{CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix});
|
||||
CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
|
||||
CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
|
||||
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
|
||||
CommandRegistry::Register(
|
||||
{CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix});
|
||||
CommandRegistry::Register({CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)",
|
||||
cmd_unknown_kcommand});
|
||||
CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
|
||||
CommandRegistry::Register({
|
||||
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
|
||||
});
|
||||
|
||||
@@ -41,6 +41,8 @@ enum class CommandId {
|
||||
WordNext,
|
||||
// Direct cursor placement
|
||||
MoveCursorTo, // arg: "y:x" (zero-based row:col)
|
||||
// Meta
|
||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ static const char *kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatib
|
||||
bool
|
||||
GUIFrontend::Init(Editor &ed)
|
||||
{
|
||||
(void)ed; // editor dimensions will be initialized during the first Step() frame
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -57,12 +58,11 @@ GUIFrontend::Init(Editor &ed)
|
||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
||||
return false;
|
||||
|
||||
// Initialize editor reported dimensions to pixels for now
|
||||
int w, h;
|
||||
SDL_GetWindowSize(window_, &w, &h);
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
ed.SetDimensions(static_cast<std::size_t>(height_), static_cast<std::size_t>(width_));
|
||||
// Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists
|
||||
int w, h;
|
||||
SDL_GetWindowSize(window_, &w, &h);
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
|
||||
// Initialize GUI font from embedded default
|
||||
LoadGuiFont_(nullptr, 16.f);
|
||||
@@ -81,14 +81,12 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
case SDL_QUIT:
|
||||
running = false;
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
width_ = e.window.data1;
|
||||
height_ = e.window.data2;
|
||||
ed.SetDimensions(static_cast<std::size_t>(height_),
|
||||
static_cast<std::size_t>(width_));
|
||||
}
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
width_ = e.window.data1;
|
||||
height_ = e.window.data2;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -96,7 +94,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
input_.ProcessSDLEvent(e);
|
||||
}
|
||||
|
||||
// Execute pending mapped inputs (drain queue)
|
||||
// Execute pending mapped inputs (drain queue)
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
if (!input_.Poll(mi))
|
||||
@@ -109,10 +107,43 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
}
|
||||
}
|
||||
|
||||
// Start a new ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window_);
|
||||
ImGui::NewFrame();
|
||||
// Start a new ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window_);
|
||||
ImGui::NewFrame();
|
||||
|
||||
// Update editor logical rows/cols using current ImGui metrics and display size
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float line_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
float ch_w = ImGui::CalcTextSize("M").x;
|
||||
if (line_h <= 0.0f) line_h = 16.0f;
|
||||
if (ch_w <= 0.0f) ch_w = 8.0f;
|
||||
// Prefer ImGui IO display size; fall back to cached SDL window size
|
||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
|
||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
|
||||
|
||||
// Account for the GUI window padding and the status bar height used in GUIRenderer
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
float pad_x = style.WindowPadding.x;
|
||||
float pad_y = style.WindowPadding.y;
|
||||
// Status bar reserves one frame height (with spacing) inside the window
|
||||
float status_h = ImGui::GetFrameHeightWithSpacing();
|
||||
|
||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
||||
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
||||
|
||||
// Visible content rows inside the scroll child
|
||||
std::size_t content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
||||
// Editor::Rows includes the status line; add 1 back for it.
|
||||
std::size_t rows = std::max<std::size_t>(1, content_rows + 1);
|
||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||
|
||||
// Only update if changed to avoid churn
|
||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
||||
ed.SetDimensions(rows, cols);
|
||||
}
|
||||
}
|
||||
|
||||
// No runtime font UI; always use embedded font.
|
||||
|
||||
|
||||
@@ -62,6 +62,18 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
||||
k_prefix = true;
|
||||
out = {true, CommandId::KPrefix, "", 0};
|
||||
return true;
|
||||
case SDLK_n: // C-n: down
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
case SDLK_p: // C-p: up
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
case SDLK_f: // C-f: right
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
return true;
|
||||
case SDLK_b: // C-b: left
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
return true;
|
||||
case SDLK_a:
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
return true;
|
||||
@@ -103,25 +115,30 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
||||
}
|
||||
}
|
||||
|
||||
if (k_prefix) {
|
||||
k_prefix = false;
|
||||
// Normalize SDL key to ASCII where possible
|
||||
int ascii_key = 0;
|
||||
if (key >= SDLK_SPACE && key <= SDLK_z) {
|
||||
ascii_key = static_cast<int>(key);
|
||||
}
|
||||
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
||||
if (ascii_key != 0) {
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, ctrl2, id)) {
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
if (k_prefix) {
|
||||
k_prefix = false;
|
||||
// Normalize SDL key to ASCII where possible
|
||||
int ascii_key = 0;
|
||||
if (key >= SDLK_SPACE && key <= SDLK_z) {
|
||||
ascii_key = static_cast<int>(key);
|
||||
}
|
||||
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
||||
if (ascii_key != 0) {
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, ctrl2, id)) {
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Unknown k-command: report the typed character
|
||||
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
||||
std::string arg(1, c);
|
||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
return true;
|
||||
}
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
105
GUIRenderer.cc
105
GUIRenderer.cc
@@ -56,40 +56,77 @@ GUIRenderer::Draw(Editor &ed)
|
||||
const float line_h = ImGui::GetTextLineHeight();
|
||||
const float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
const float space_w = ImGui::CalcTextSize(" ").x;
|
||||
// When the user scrolls and the cursor is off-screen, move it to the nearest visible row
|
||||
{
|
||||
static float prev_scroll_y = -1.0f;
|
||||
float child_h = ImGui::GetWindowHeight(); // child window height
|
||||
long first_row = static_cast<long>(scroll_y / row_h);
|
||||
long vis_rows = static_cast<long>(child_h / row_h);
|
||||
if (vis_rows < 1)
|
||||
vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
long new_row = (cyr < first_row) ? first_row : last_row;
|
||||
if (new_row < 0)
|
||||
new_row = 0;
|
||||
if (new_row >= static_cast<long>(lines.size())) {
|
||||
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
|
||||
}
|
||||
// Clamp column to line length
|
||||
std::size_t new_col = 0;
|
||||
if (!lines.empty()) {
|
||||
const std::string &l = lines[static_cast<std::size_t>(new_row)];
|
||||
new_col = std::min<std::size_t>(cx, l.size());
|
||||
}
|
||||
char tmp2[64];
|
||||
std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col);
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp2));
|
||||
// refresh local variables after move
|
||||
cy = buf->Cury();
|
||||
cx = buf->Curx();
|
||||
}
|
||||
}
|
||||
prev_scroll_y = scroll_y;
|
||||
}
|
||||
// If the command layer requested a specific top-of-screen (via Buffer::Rowoffs),
|
||||
// force the ImGui scroll to match so paging aligns the first visible row.
|
||||
bool forced_scroll = false;
|
||||
{
|
||||
std::size_t desired_top = buf->Rowoffs();
|
||||
long current_top = static_cast<long>(scroll_y / row_h);
|
||||
if (static_cast<long>(desired_top) != current_top) {
|
||||
ImGui::SetScrollY(static_cast<float>(desired_top) * row_h);
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
forced_scroll = true;
|
||||
}
|
||||
}
|
||||
// Synchronize cursor and scrolling.
|
||||
// A) When the user scrolls and the cursor goes off-screen, move the cursor to the nearest visible row.
|
||||
// B) When the cursor moves (via keyboard commands), scroll it back into view.
|
||||
{
|
||||
static float prev_scroll_y = -1.0f;
|
||||
static long prev_cursor_y = -1;
|
||||
// Compute visible row range using the child window height
|
||||
float child_h = ImGui::GetWindowHeight();
|
||||
long first_row = static_cast<long>(scroll_y / row_h);
|
||||
long vis_rows = static_cast<long>(child_h / row_h);
|
||||
if (vis_rows < 1) vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
|
||||
// A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
long new_row = (cyr < first_row) ? first_row : last_row;
|
||||
if (new_row < 0) new_row = 0;
|
||||
if (new_row >= static_cast<long>(lines.size()))
|
||||
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
|
||||
// Clamp column to line length
|
||||
std::size_t new_col = 0;
|
||||
if (!lines.empty()) {
|
||||
const std::string &l = lines[static_cast<std::size_t>(new_row)];
|
||||
new_col = std::min<std::size_t>(cx, l.size());
|
||||
}
|
||||
char tmp2[64];
|
||||
std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col);
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp2));
|
||||
cy = buf->Cury();
|
||||
cx = buf->Curx();
|
||||
cyr = static_cast<long>(cy);
|
||||
// Update visible range again in case content changed
|
||||
first_row = static_cast<long>(ImGui::GetScrollY() / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// B) If cursor moved since last frame and is outside the visible region, scroll to reveal it
|
||||
// Skip this when we just forced a top-of-screen alignment this frame.
|
||||
if (!forced_scroll && prev_cursor_y >= 0 && static_cast<long>(cy) != prev_cursor_y) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f) target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y) target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
// refresh local variables
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev_scroll_y = ImGui::GetScrollY();
|
||||
prev_cursor_y = static_cast<long>(cy);
|
||||
}
|
||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
|
||||
@@ -9,15 +9,21 @@
|
||||
bool
|
||||
TerminalFrontend::Init(Editor &ed)
|
||||
{
|
||||
initscr();
|
||||
cbreak();
|
||||
noecho();
|
||||
keypad(stdscr, TRUE);
|
||||
nodelay(stdscr, TRUE);
|
||||
curs_set(1);
|
||||
// Enable mouse support if available
|
||||
mouseinterval(0);
|
||||
mousemask(ALL_MOUSE_EVENTS, nullptr);
|
||||
initscr();
|
||||
cbreak();
|
||||
noecho();
|
||||
keypad(stdscr, TRUE);
|
||||
// Enable 8-bit meta key sequences (Alt/ESC-prefix handling in terminals)
|
||||
meta(stdscr, TRUE);
|
||||
// Make ESC key sequences resolve quickly so ESC+<key> works as meta
|
||||
#ifdef set_escdelay
|
||||
set_escdelay(50);
|
||||
#endif
|
||||
nodelay(stdscr, TRUE);
|
||||
curs_set(1);
|
||||
// Enable mouse support if available
|
||||
mouseinterval(0);
|
||||
mousemask(ALL_MOUSE_EVENTS, nullptr);
|
||||
|
||||
int r = 0, c = 0;
|
||||
getmaxyx(stdscr, r, c);
|
||||
|
||||
@@ -103,6 +103,23 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
out = {true, CommandId::FindStart, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Emacs-style movement aliases
|
||||
if (ch == CTRL('N')) { // C-n: down
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('P')) { // C-p: up
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('F')) { // C-f: right/forward
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('B')) { // C-b: left/back
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('A')) {
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
return true;
|
||||
@@ -144,26 +161,29 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
return true;
|
||||
}
|
||||
|
||||
if (k_prefix) {
|
||||
k_prefix = false; // single next key only
|
||||
// Determine if this is a control chord (e.g., C-x) and normalize
|
||||
bool ctrl = false;
|
||||
int ascii_key = ch;
|
||||
if (ch >= 1 && ch <= 26) {
|
||||
ctrl = true;
|
||||
ascii_key = 'a' + (ch - 1);
|
||||
}
|
||||
// For letters, normalize to lowercase ASCII
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
if (k_prefix) {
|
||||
k_prefix = false; // single next key only
|
||||
// Determine if this is a control chord (e.g., C-x) and normalize
|
||||
bool ctrl = false;
|
||||
int ascii_key = ch;
|
||||
if (ch >= 1 && ch <= 26) {
|
||||
ctrl = true;
|
||||
ascii_key = 'a' + (ch - 1);
|
||||
}
|
||||
// For letters, normalize to lowercase ASCII
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, ctrl, id)) {
|
||||
out = {true, id, "", 0};
|
||||
} else {
|
||||
out.hasCommand = false; // unknown chord after C-k
|
||||
}
|
||||
return true;
|
||||
}
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, ctrl, id)) {
|
||||
out = {true, id, "", 0};
|
||||
} else {
|
||||
// Show unknown k-command message with the typed character
|
||||
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
||||
std::string arg(1, c);
|
||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Printable ASCII
|
||||
if (ch >= 0x20 && ch <= 0x7E) {
|
||||
|
||||
@@ -32,7 +32,8 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
const Buffer *buf = ed.CurrentBuffer();
|
||||
int content_rows = rows - 1; // last line is status
|
||||
|
||||
if (buf) {
|
||||
int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
|
||||
if (buf) {
|
||||
const auto &lines = buf->Rows();
|
||||
std::size_t rowoffs = buf->Rowoffs();
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
@@ -138,12 +139,15 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
std::size_t rx = buf->Rx(); // render x computed by command layer
|
||||
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
||||
int cur_x = static_cast<int>(rx) - static_cast<int>(buf->Coloffs());
|
||||
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
||||
move(cur_y, cur_x);
|
||||
}
|
||||
} else {
|
||||
mvaddstr(0, 0, "[no buffer]");
|
||||
}
|
||||
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
||||
// remember where to leave the terminal cursor after status is drawn
|
||||
saved_cur_y = cur_y;
|
||||
saved_cur_x = cur_x;
|
||||
move(cur_y, cur_x);
|
||||
}
|
||||
} else {
|
||||
mvaddstr(0, 0, "[no buffer]");
|
||||
}
|
||||
|
||||
// Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark
|
||||
move(rows - 1, 0);
|
||||
@@ -237,5 +241,11 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
|
||||
attroff(A_REVERSE);
|
||||
|
||||
// Restore terminal cursor to the content position so a visible caret
|
||||
// remains in the editing area (not on the status line).
|
||||
if (saved_cur_y >= 0 && saved_cur_x >= 0) {
|
||||
move(saved_cur_y, saved_cur_x);
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user