diff --git a/CMakeLists.txt b/CMakeLists.txt index c4d65ab..a12cc26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -336,6 +336,7 @@ if (BUILD_TESTS) tests/test_visual_line_mode.cc tests/test_benchmarks.cc tests/test_migration_coverage.cc + tests/test_smart_newline.cc # minimal engine sources required by Buffer PieceTable.cc diff --git a/Command.cc b/Command.cc index 7746740..782101e 100644 --- a/Command.cc +++ b/Command.cc @@ -1109,34 +1109,33 @@ cmd_theme_set_by_name(const CommandContext &ctx) static bool cmd_theme_set_by_name(CommandContext &ctx) { - # if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT) -// Qt GUI build: schedule theme change for frontend -std::string name = ctx.arg; -// trim spaces -auto ltrim = [](std::string &s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { - return !std::isspace(ch); - })); -}; -auto rtrim = [](std::string &s) { - s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { - return !std::isspace(ch); - }).base(), s.end()); -}; -ltrim (name); -rtrim (name); + // Qt GUI build: schedule theme change for frontend + std::string name = ctx.arg; + // trim spaces + auto ltrim = [](std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + }; + auto rtrim = [](std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), s.end()); + }; + ltrim(name); + rtrim(name); if (name.empty()) { ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)"); return true; } -kte::gThemeChangeRequest= name; -kte::gThemeChangePending=true; -ctx.editor.SetStatus (std::string("Theme requested: ") + name); + kte::gThemeChangeRequest = name; + kte::gThemeChangePending = true; + ctx.editor.SetStatus(std::string("Theme requested: ") + name); return true; # else -(void) ctx; -// No-op in terminal build + (void) ctx; + // No-op in terminal build return true; # endif } @@ -2949,6 +2948,58 @@ cmd_newline(CommandContext &ctx) } +static bool +cmd_smart_newline(CommandContext &ctx) +{ + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) { + ctx.editor.SetStatus("No buffer to edit"); + return false; + } + + if (buf->IsReadOnly()) { + ctx.editor.SetStatus("Read-only buffer"); + return true; + } + + // Smart newline behavior: add a newline with the same indentation as the current line. + // Find indentation of current line + std::size_t y = buf->Cury(); + std::string line = buf->GetLineString(y); + std::string indent; + for (char c: line) { + if (c == ' ' || c == '\t') { + indent += c; + } else { + break; + } + } + + // Perform standard newline first + if (!cmd_newline(ctx)) { + return false; + } + + // Now insert the indentation at the new cursor position + if (!indent.empty()) { + std::size_t new_y = buf->Cury(); + std::size_t new_x = buf->Curx(); + buf->insert_text(static_cast(new_y), static_cast(new_x), indent); + buf->SetCursor(new_x + indent.size(), new_y); + buf->SetDirty(true); + + if (auto *u = buf->Undo()) { + u->Begin(UndoType::Insert); + u->Append(indent); + u->commit(); + } + } + + ensure_cursor_visible(ctx.editor, *buf); + return true; +} + + static bool cmd_backspace(CommandContext &ctx) { @@ -4806,6 +4857,9 @@ InstallDefaultCommands() CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text, false, true }); CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline}); + CommandRegistry::Register({ + CommandId::SmartNewline, "smart-newline", "Insert newline with auto-indent", cmd_smart_newline + }); CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace}); CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char}); CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol}); @@ -4991,4 +5045,4 @@ Execute(Editor &ed, const std::string &name, const std::string &arg, int count) return false; CommandContext ctx{ed, arg, count}; return cmd->handler ? cmd->handler(ctx) : false; -} +} \ No newline at end of file diff --git a/Command.h b/Command.h index a2be16d..1a8d1e5 100644 --- a/Command.h +++ b/Command.h @@ -38,6 +38,7 @@ enum class CommandId { // Editing InsertText, // arg: text to insert at cursor (UTF-8, no newlines) Newline, // insert a newline at cursor + SmartNewline, // insert a newline with auto-indent (Shift-Enter) Backspace, // delete char before cursor (may join lines) DeleteChar, // delete char at cursor (may join lines) KillToEOL, // delete from cursor to end of line; if at EOL, delete newline @@ -164,4 +165,4 @@ void InstallDefaultCommands(); // Returns true if the command executed successfully. bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0); -bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0); +bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0); \ No newline at end of file diff --git a/ImGuiInputHandler.cc b/ImGuiInputHandler.cc index 8fa57ea..956c90e 100644 --- a/ImGuiInputHandler.cc +++ b/ImGuiInputHandler.cc @@ -125,7 +125,11 @@ map_key(const SDL_Keycode key, case SDLK_KP_ENTER: k_prefix = false; k_ctrl_pending = false; - out = {true, CommandId::Newline, "", 0}; + if (mod & KMOD_SHIFT) { + out = {true, CommandId::SmartNewline, "", 0}; + } else { + out = {true, CommandId::Newline, "", 0}; + } return true; case SDLK_ESCAPE: k_prefix = false; @@ -439,12 +443,14 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e) } // If editor universal argument is active, consume digit TEXTINPUT - if (ed_ && ed_ + if (ed_ &&ed_ - -> - UArg() != 0 - ) { + + -> + UArg() != 0 + ) + { const char *txt = e.text.text; if (txt && *txt) { unsigned char c0 = static_cast(txt[0]); @@ -606,4 +612,4 @@ ImGuiInputHandler::Poll(MappedInput &out) out = q_.front(); q_.pop(); return true; -} +} \ No newline at end of file diff --git a/ImGuiRenderer.cc b/ImGuiRenderer.cc index 8b100c7..3995f30 100644 --- a/ImGuiRenderer.cc +++ b/ImGuiRenderer.cc @@ -209,19 +209,35 @@ ImGuiRenderer::Draw(Editor &ed) return {by, best_col}; }; - // Mouse-driven selection: set mark on press, update cursor on drag + // Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { mouse_selecting = true; auto [by, bx] = mouse_pos_to_buf(); char tmp[64]; std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx); Execute(ed, CommandId::MoveCursorTo, std::string(tmp)); - if (Buffer *mbuf = const_cast(buf)) { - mbuf->SetMark(bx, by); + + // Only set mark on double click. + // Dragging will also set the mark if not already set (handled below). + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + if (Buffer *mbuf = const_cast(buf)) { + mbuf->SetMark(bx, by); + } } } if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { auto [by, bx] = mouse_pos_to_buf(); + // If we are dragging (mouse moved while down), ensure mark is set to start selection + if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) { + if (Buffer *mbuf = const_cast(buf)) { + if (!mbuf->MarkSet()) { + // We'd need to convert click_pos to buf coords, but it's complex here. + // Setting it to where the cursor was *before* we started moving it + // in this frame is a good approximation, or just using current. + mbuf->SetMark(mbuf->Curx(), mbuf->Cury()); + } + } + } char tmp[64]; std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx); Execute(ed, CommandId::MoveCursorTo, std::string(tmp)); @@ -927,4 +943,4 @@ ImGuiRenderer::Draw(Editor &ed) ed.SetFilePickerVisible(false); } } -} +} \ No newline at end of file diff --git a/KKeymap.cc b/KKeymap.cc index 59bbafb..cd1e83d 100644 --- a/KKeymap.cc +++ b/KKeymap.cc @@ -226,8 +226,12 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool case 'q': out = CommandId::ReflowParagraph; // Esc q (reflow paragraph) return true; + case '\n': + case '\r': + out = CommandId::SmartNewline; // Shift+Enter (some terminals send this as Alt+Enter sequences) + return true; default: break; } return false; -} +} \ No newline at end of file diff --git a/TerminalInputHandler.cc b/TerminalInputHandler.cc index 8539754..198d517 100644 --- a/TerminalInputHandler.cc +++ b/TerminalInputHandler.cc @@ -67,13 +67,20 @@ map_key_to_command(const int ch, if (pressed) { mouse_selecting = true; Execute(*ed, CommandId::MoveCursorTo, std::string(buf)); - if (Buffer *b = ed->CurrentBuffer()) { - b->SetMark(b->Curx(), b->Cury()); - } + // We don't set the mark on simple click anymore in ncurses either, + // to be consistent. ncurses doesn't easily support double-click + // or drag-threshold in a platform-independent way here, + // but we can at least only set mark on MOVED. out.hasCommand = false; return true; } if (mouse_selecting && moved) { + if (Buffer *b = ed->CurrentBuffer()) { + if (!b->MarkSet()) { + // Set mark at CURRENT cursor position (which is where we were before this move) + b->SetMark(b->Curx(), b->Cury()); + } + } Execute(*ed, CommandId::MoveCursorTo, std::string(buf)); out.hasCommand = false; return true; @@ -329,4 +336,4 @@ TerminalInputHandler::Poll(MappedInput &out) { out = {}; return decode_(out) && out.hasCommand; -} +} \ No newline at end of file diff --git a/tests/test_smart_newline.cc b/tests/test_smart_newline.cc new file mode 100644 index 0000000..8c5fc8b --- /dev/null +++ b/tests/test_smart_newline.cc @@ -0,0 +1,79 @@ +#include "Test.h" +#include "Buffer.h" +#include "Editor.h" +#include "Command.h" +#include + + +TEST (SmartNewline_AutoIndent) +{ + Editor editor; + InstallDefaultCommands(); + Buffer &buf = editor.Buffers().emplace_back(); + + // Set up initial state: " line1" + buf.insert_text(0, 0, " line1"); + buf.SetCursor(7, 0); // At end of line + + // Execute SmartNewline + bool ok = Execute(editor, CommandId::SmartNewline); + ASSERT_TRUE(ok); + + // Should have two lines now + ASSERT_EQ(buf.Nrows(), 2); + // Line 0 remains " line1" + ASSERT_EQ(buf.GetLineString(0), " line1"); + // Line 1 should have " " (two spaces) + ASSERT_EQ(buf.GetLineString(1), " "); + // Cursor should be at (2, 1) + ASSERT_EQ(buf.Curx(), 2); + ASSERT_EQ(buf.Cury(), 1); +} + + +TEST (SmartNewline_TabIndent) +{ + Editor editor; + InstallDefaultCommands(); + Buffer &buf = editor.Buffers().emplace_back(); + + // Set up initial state: "\tline1" + buf.insert_text(0, 0, "\tline1"); + buf.SetCursor(6, 0); // At end of line + + // Execute SmartNewline + bool ok = Execute(editor, CommandId::SmartNewline); + ASSERT_TRUE(ok); + + // Should have two lines now + ASSERT_EQ(buf.Nrows(), 2); + // Line 1 should have "\t" + ASSERT_EQ(buf.GetLineString(1), "\t"); + // Cursor should be at (1, 1) + ASSERT_EQ(buf.Curx(), 1); + ASSERT_EQ(buf.Cury(), 1); +} + + +TEST (SmartNewline_NoIndent) +{ + Editor editor; + InstallDefaultCommands(); + Buffer &buf = editor.Buffers().emplace_back(); + + // Set up initial state: "line1" + buf.insert_text(0, 0, "line1"); + buf.SetCursor(5, 0); // At end of line + + // Execute SmartNewline + bool ok = Execute(editor, CommandId::SmartNewline); + ASSERT_TRUE(ok); + + // Should have two lines now + ASSERT_EQ(buf.Nrows(), 2); + // Line 1 should be empty + ASSERT_EQ(buf.GetLineString(1), ""); + // Cursor should be at (0, 1) + ASSERT_EQ(buf.Curx(), 0); + ASSERT_EQ(buf.Cury(), 1); +} \ No newline at end of file