Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2b255901e |
+2
-1
@@ -4,7 +4,7 @@ project(kte)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(KTE_VERSION "1.11.2")
|
||||
set(KTE_VERSION "1.11.3")
|
||||
|
||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||
@@ -344,6 +344,7 @@ if (BUILD_TESTS)
|
||||
tests/test_migration_coverage.cc
|
||||
tests/test_smart_newline.cc
|
||||
tests/test_reflow_undo.cc
|
||||
tests/test_paste_split.cc
|
||||
|
||||
# minimal engine sources required by Buffer
|
||||
PieceTable.cc
|
||||
|
||||
+6
-25
@@ -8,6 +8,7 @@
|
||||
#include "ImGuiInputHandler.h"
|
||||
#include "KKeymap.h"
|
||||
#include "Editor.h"
|
||||
#include "PasteSplit.h"
|
||||
|
||||
|
||||
static bool
|
||||
@@ -376,32 +377,12 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
if (clip) {
|
||||
std::string text(clip);
|
||||
SDL_free(clip);
|
||||
// Split on '\n' and enqueue as InsertText/Newline commands
|
||||
// Turn line breaks (\n, \r\n, or bare \r) into Newline
|
||||
// commands and the rest into InsertText; InsertText itself
|
||||
// rejects any embedded '\r'/'\n'.
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
std::size_t start = 0;
|
||||
while (start <= text.size()) {
|
||||
std::size_t pos = text.find('\n', start);
|
||||
std::string_view segment;
|
||||
bool has_nl = (pos != std::string::npos);
|
||||
if (has_nl) {
|
||||
segment = std::string_view(text).substr(start, pos - start);
|
||||
} else {
|
||||
segment = std::string_view(text).substr(start);
|
||||
}
|
||||
if (!segment.empty()) {
|
||||
MappedInput ins{
|
||||
true, CommandId::InsertText, std::string(segment), 0
|
||||
};
|
||||
q_.push(ins);
|
||||
}
|
||||
if (has_nl) {
|
||||
MappedInput nl{true, CommandId::Newline, std::string(), 0};
|
||||
q_.push(nl);
|
||||
start = pos + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const auto &cmd : SplitPasteIntoCommands(text))
|
||||
q_.push(cmd);
|
||||
// Suppress the corresponding TEXTINPUT that may follow
|
||||
suppress_text_input_once_ = true;
|
||||
return true; // consumed
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* PasteSplit.h - split pasted/clipboard text into editor commands
|
||||
*
|
||||
* Pure logic (no SDL/GUI dependencies) so it can be unit tested and shared
|
||||
* across frontends. Line breaks in the pasted text must become Newline
|
||||
* commands; InsertText itself rejects embedded '\r'/'\n' (see Command.cc).
|
||||
*/
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "InputHandler.h"
|
||||
|
||||
|
||||
// Translate a block of pasted text into a sequence of editor commands.
|
||||
//
|
||||
// Any of "\n", "\r\n", or a bare "\r" is treated as a single line break and
|
||||
// emitted as a Newline command; the text between breaks becomes InsertText
|
||||
// commands. Empty text segments are skipped, but line breaks are always
|
||||
// emitted so blank lines round-trip correctly.
|
||||
inline std::vector<MappedInput> SplitPasteIntoCommands(const std::string &text)
|
||||
{
|
||||
std::vector<MappedInput> out;
|
||||
std::string segment;
|
||||
|
||||
auto flush_segment = [&]() {
|
||||
if (!segment.empty()) {
|
||||
out.push_back(MappedInput{true, CommandId::InsertText, segment, 0});
|
||||
segment.clear();
|
||||
}
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < text.size(); ++i) {
|
||||
const char c = text[i];
|
||||
if (c == '\n' || c == '\r') {
|
||||
flush_segment();
|
||||
out.push_back(MappedInput{true, CommandId::Newline, std::string(), 0});
|
||||
// Collapse a "\r\n" pair into one line break.
|
||||
if (c == '\r' && i + 1 < text.size() && text[i + 1] == '\n')
|
||||
++i;
|
||||
} else {
|
||||
segment.push_back(c);
|
||||
}
|
||||
}
|
||||
flush_segment();
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Tests for SplitPasteIntoCommands: pasted text must turn embedded line
|
||||
// breaks into Newline commands so InsertText never receives a '\r' or '\n'.
|
||||
#include "Test.h"
|
||||
|
||||
#include "PasteSplit.h"
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
// Assert that no InsertText command carries an embedded newline/carriage
|
||||
// return, mirroring the rejection in Command.cc's InsertText handler.
|
||||
void
|
||||
assert_no_newlines_in_inserts(const std::vector<MappedInput> &cmds)
|
||||
{
|
||||
for (const auto &c : cmds) {
|
||||
if (c.id == CommandId::InsertText) {
|
||||
ASSERT_TRUE(c.arg.find('\n') == std::string::npos);
|
||||
ASSERT_TRUE(c.arg.find('\r') == std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(PasteSplit_PlainTextNoNewline)
|
||||
{
|
||||
auto cmds = SplitPasteIntoCommands("hello world");
|
||||
ASSERT_EQ(cmds.size(), 1u);
|
||||
ASSERT_TRUE(cmds[0].id == CommandId::InsertText);
|
||||
ASSERT_TRUE(cmds[0].arg == "hello world");
|
||||
}
|
||||
|
||||
|
||||
TEST(PasteSplit_UnixNewlines)
|
||||
{
|
||||
auto cmds = SplitPasteIntoCommands("a\nb");
|
||||
assert_no_newlines_in_inserts(cmds);
|
||||
ASSERT_EQ(cmds.size(), 3u);
|
||||
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
|
||||
}
|
||||
|
||||
|
||||
// Regression: CRLF clipboards left a '\r' in the InsertText segment, which
|
||||
// InsertText rejected with "InsertText arg must not contain newlines".
|
||||
TEST(PasteSplit_WindowsCRLF)
|
||||
{
|
||||
auto cmds = SplitPasteIntoCommands("a\r\nb");
|
||||
assert_no_newlines_in_inserts(cmds);
|
||||
ASSERT_EQ(cmds.size(), 3u);
|
||||
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
|
||||
}
|
||||
|
||||
|
||||
// Regression: a bare '\r' (classic Mac / some macOS apps) must also break lines.
|
||||
TEST(PasteSplit_BareCR)
|
||||
{
|
||||
auto cmds = SplitPasteIntoCommands("a\rb");
|
||||
assert_no_newlines_in_inserts(cmds);
|
||||
ASSERT_EQ(cmds.size(), 3u);
|
||||
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
|
||||
}
|
||||
|
||||
|
||||
TEST(PasteSplit_BlankLinesPreserved)
|
||||
{
|
||||
// Two line breaks between "a" and "b" => one empty line.
|
||||
auto cmds = SplitPasteIntoCommands("a\n\nb");
|
||||
assert_no_newlines_in_inserts(cmds);
|
||||
ASSERT_EQ(cmds.size(), 4u);
|
||||
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[2].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[3].id == CommandId::InsertText && cmds[3].arg == "b");
|
||||
}
|
||||
|
||||
|
||||
TEST(PasteSplit_TrailingNewline)
|
||||
{
|
||||
auto cmds = SplitPasteIntoCommands("abc\n");
|
||||
assert_no_newlines_in_inserts(cmds);
|
||||
ASSERT_EQ(cmds.size(), 2u);
|
||||
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "abc");
|
||||
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||
}
|
||||
|
||||
|
||||
TEST(PasteSplit_MultilineCRLF)
|
||||
{
|
||||
auto cmds = SplitPasteIntoCommands("line1\r\nline2\r\nline3");
|
||||
assert_no_newlines_in_inserts(cmds);
|
||||
ASSERT_EQ(cmds.size(), 5u);
|
||||
ASSERT_TRUE(cmds[0].arg == "line1");
|
||||
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[2].arg == "line2");
|
||||
ASSERT_TRUE(cmds[3].id == CommandId::Newline);
|
||||
ASSERT_TRUE(cmds[4].arg == "line3");
|
||||
}
|
||||
Reference in New Issue
Block a user