Refine help text, keybindings, GUI themes, and undo system.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
- Expanded help text and command documentation with detailed keybinding descriptions. - Added theme customization support to GUIConfig (Nord default, light/dark variants). - Adjusted for consistent indentation and debug instrumentation in undo system. - Enhanced test cases for multi-line, UTF-8, and branching scenarios.
This commit is contained in:
28
.idea/workspace.xml
generated
28
.idea/workspace.xml
generated
@@ -7,6 +7,7 @@
|
|||||||
<option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="3" type="long" />
|
<option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="3" type="long" />
|
||||||
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
|
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
|
||||||
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
|
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
|
||||||
|
<option name="/Default/Housekeeping/LiveTemplatesHousekeeping/HotspotSessionHintIsShown/@EntryValue" value="true" type="bool" />
|
||||||
<option name="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue" value="CppFormatterOtherPage" type="string" />
|
<option name="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue" value="CppFormatterOtherPage" type="string" />
|
||||||
<option name="/Default/Housekeeping/RefactoringsMru/RenameRefactoring/DoSearchForTextInStrings/@EntryValue" value="true" type="bool" />
|
<option name="/Default/Housekeeping/RefactoringsMru/RenameRefactoring/DoSearchForTextInStrings/@EntryValue" value="true" type="bool" />
|
||||||
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
|
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
|
||||||
@@ -34,7 +35,29 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add Nord theme for real">
|
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add Nord theme for real">
|
||||||
|
<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$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" 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$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIConfig.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIConfig.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIConfig.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIConfig.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/GUIFrontend.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.h" 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$/GUITheme.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUITheme.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/HelpText.cc" beforeDir="false" afterPath="$PROJECT_DIR$/HelpText.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/HelpText.h" beforeDir="false" afterPath="$PROJECT_DIR$/HelpText.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/UndoSystem.cc" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/UndoSystem.h" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/docs/kge.1" beforeDir="false" afterPath="$PROJECT_DIR$/docs/kge.1" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/docs/kte.1" beforeDir="false" afterPath="$PROJECT_DIR$/docs/kte.1" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/test_undo.cc" beforeDir="false" afterPath="$PROJECT_DIR$/test_undo.cc" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -63,12 +86,13 @@
|
|||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
</component>
|
</component>
|
||||||
<component name="OptimizeOnSaveOptions">
|
<component name="OptimizeOnSaveOptions">
|
||||||
<option name="myRunOnSave" value="true" />
|
<option name="myRunOnSave" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProblemsViewState">
|
<component name="ProblemsViewState">
|
||||||
<option name="selectedTabId" value="AISelfReview" />
|
<option name="selectedTabId" value="CurrentFile" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectApplicationVersion">
|
<component name="ProjectApplicationVersion">
|
||||||
<option name="ide" value="CLion" />
|
<option name="ide" value="CLion" />
|
||||||
@@ -173,7 +197,7 @@
|
|||||||
<workItem from="1764539556448" duration="156000" />
|
<workItem from="1764539556448" duration="156000" />
|
||||||
<workItem from="1764539725338" duration="1075000" />
|
<workItem from="1764539725338" duration="1075000" />
|
||||||
<workItem from="1764542392763" duration="3512000" />
|
<workItem from="1764542392763" duration="3512000" />
|
||||||
<workItem from="1764548345516" duration="39312000" />
|
<workItem from="1764548345516" duration="50201000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
|
|||||||
12
Buffer.cc
12
Buffer.cc
@@ -368,9 +368,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
|||||||
rows_[y].insert(x, seg);
|
rows_[y].insert(x, seg);
|
||||||
x += seg.size();
|
x += seg.size();
|
||||||
// Split line at x
|
// Split line at x
|
||||||
std::string tail = rows_[y].substr(x);
|
std::string tail = rows_[y].substr(x);
|
||||||
rows_[y].erase(x);
|
rows_[y].erase(x);
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
remain.erase(0, pos + 1);
|
remain.erase(0, pos + 1);
|
||||||
@@ -430,8 +430,8 @@ Buffer::split_line(int row, const int col)
|
|||||||
const auto y = static_cast<std::size_t>(row);
|
const auto y = static_cast<std::size_t>(row);
|
||||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
||||||
const auto tail = rows_[y].substr(x);
|
const auto tail = rows_[y].substr(x);
|
||||||
rows_[y].erase(x);
|
rows_[y].erase(x);
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -459,7 +459,7 @@ Buffer::insert_row(int row, const std::string_view text)
|
|||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
if (static_cast<std::size_t>(row) > rows_.size())
|
||||||
row = static_cast<int>(rows_.size());
|
row = static_cast<int>(rows_.size());
|
||||||
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
60
Buffer.h
60
Buffer.h
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
class Buffer {
|
class Buffer {
|
||||||
public:
|
public:
|
||||||
Buffer();
|
Buffer();
|
||||||
|
|
||||||
Buffer(const Buffer &other);
|
Buffer(const Buffer &other);
|
||||||
|
|
||||||
@@ -262,11 +262,12 @@ public:
|
|||||||
return filename_;
|
return filename_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
||||||
// This does not mark the buffer as file-backed.
|
// This does not mark the buffer as file-backed.
|
||||||
void SetVirtualName(const std::string &name)
|
void SetVirtualName(const std::string &name)
|
||||||
{
|
{
|
||||||
filename_ = name;
|
filename_ = name;
|
||||||
is_file_backed_ = false;
|
is_file_backed_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,26 +278,29 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] bool Dirty() const
|
[[nodiscard]] bool Dirty() const
|
||||||
{
|
{
|
||||||
return dirty_;
|
return dirty_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read-only flag
|
|
||||||
[[nodiscard]] bool IsReadOnly() const
|
|
||||||
{
|
|
||||||
return read_only_;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetReadOnly(bool ro)
|
// Read-only flag
|
||||||
{
|
[[nodiscard]] bool IsReadOnly() const
|
||||||
read_only_ = ro;
|
{
|
||||||
}
|
return read_only_;
|
||||||
|
}
|
||||||
|
|
||||||
void ToggleReadOnly()
|
|
||||||
{
|
void SetReadOnly(bool ro)
|
||||||
read_only_ = !read_only_;
|
{
|
||||||
}
|
read_only_ = ro;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ToggleReadOnly()
|
||||||
|
{
|
||||||
|
read_only_ = !read_only_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetCursor(const std::size_t x, const std::size_t y)
|
void SetCursor(const std::size_t x, const std::size_t y)
|
||||||
@@ -380,18 +384,18 @@ public:
|
|||||||
[[nodiscard]] const UndoSystem *Undo() const;
|
[[nodiscard]] const UndoSystem *Undo() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// State mirroring original C struct (without undo_tree)
|
// State mirroring original C struct (without undo_tree)
|
||||||
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
|
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
|
||||||
std::size_t rx_ = 0; // render x (tabs expanded)
|
std::size_t rx_ = 0; // render x (tabs expanded)
|
||||||
std::size_t nrows_ = 0; // number of rows
|
std::size_t nrows_ = 0; // number of rows
|
||||||
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
|
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
|
||||||
std::vector<Line> rows_; // buffer rows (without trailing newlines)
|
std::vector<Line> rows_; // buffer rows (without trailing newlines)
|
||||||
std::string filename_;
|
std::string filename_;
|
||||||
bool is_file_backed_ = false;
|
bool is_file_backed_ = false;
|
||||||
bool dirty_ = false;
|
bool dirty_ = false;
|
||||||
bool read_only_ = false;
|
bool read_only_ = false;
|
||||||
bool mark_set_ = false;
|
bool mark_set_ = false;
|
||||||
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
||||||
|
|
||||||
// Per-buffer undo state
|
// Per-buffer undo state
|
||||||
std::unique_ptr<struct UndoTree> undo_tree_;
|
std::unique_ptr<struct UndoTree> undo_tree_;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ project(kte)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(KTE_VERSION "1.1.0")
|
set(KTE_VERSION "1.1.1")
|
||||||
|
|
||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
|
|||||||
1149
Command.cc
1149
Command.cc
File diff suppressed because it is too large
Load Diff
11
Command.h
11
Command.h
@@ -69,6 +69,9 @@ enum class CommandId {
|
|||||||
Redo,
|
Redo,
|
||||||
// UI/status helpers
|
// UI/status helpers
|
||||||
UArgStatus, // update status line during universal-argument collection
|
UArgStatus, // update status line during universal-argument collection
|
||||||
|
// Themes (GUI)
|
||||||
|
ThemeNext,
|
||||||
|
ThemePrev,
|
||||||
// Region formatting
|
// Region formatting
|
||||||
IndentRegion, // indent region (C-k =)
|
IndentRegion, // indent region (C-k =)
|
||||||
UnindentRegion, // unindent region (C-k -)
|
UnindentRegion, // unindent region (C-k -)
|
||||||
@@ -86,6 +89,12 @@ enum class CommandId {
|
|||||||
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
||||||
// Meta
|
// Meta
|
||||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||||
|
// Generic command prompt
|
||||||
|
CommandPromptStart, // begin generic command prompt (C-k ;)
|
||||||
|
// Theme by name
|
||||||
|
ThemeSetByName,
|
||||||
|
// Background mode (GUI)
|
||||||
|
BackgroundSet,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +118,8 @@ struct Command {
|
|||||||
std::string name; // stable, unique name (e.g., "save", "save-as")
|
std::string name; // stable, unique name (e.g., "save", "save-as")
|
||||||
std::string help; // short help/description
|
std::string help; // short help/description
|
||||||
CommandHandler handler;
|
CommandHandler handler;
|
||||||
|
// Public commands are exposed in the ": " prompt (C-k ;)
|
||||||
|
bool isPublic = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
71
Editor.h
71
Editor.h
@@ -301,22 +301,23 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
||||||
enum class PromptKind {
|
enum class PromptKind {
|
||||||
None = 0,
|
None = 0,
|
||||||
Search,
|
Search,
|
||||||
RegexSearch,
|
RegexSearch,
|
||||||
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
|
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
|
||||||
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
|
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
|
||||||
OpenFile,
|
OpenFile,
|
||||||
SaveAs,
|
SaveAs,
|
||||||
Confirm,
|
Confirm,
|
||||||
BufferSwitch,
|
BufferSwitch,
|
||||||
GotoLine,
|
GotoLine,
|
||||||
Chdir,
|
Chdir,
|
||||||
ReplaceFind, // step 1 of Search & Replace: find what
|
ReplaceFind, // step 1 of Search & Replace: find what
|
||||||
ReplaceWith // step 2 of Search & Replace: replace with
|
ReplaceWith, // step 2 of Search & Replace: replace with
|
||||||
};
|
Command // generic command prompt (": ")
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
||||||
@@ -518,20 +519,38 @@ private:
|
|||||||
std::string prompt_text_;
|
std::string prompt_text_;
|
||||||
std::string pending_overwrite_path_;
|
std::string pending_overwrite_path_;
|
||||||
|
|
||||||
// GUI-only state (safe no-op in terminal builds)
|
// GUI-only state (safe no-op in terminal builds)
|
||||||
bool file_picker_visible_ = false;
|
bool file_picker_visible_ = false;
|
||||||
std::string file_picker_dir_;
|
std::string file_picker_dir_;
|
||||||
|
|
||||||
// Temporary state for Search & Replace flow
|
// Temporary state for Search & Replace flow
|
||||||
public:
|
public:
|
||||||
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; }
|
void SetReplaceFindTmp(const std::string &s)
|
||||||
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; }
|
{
|
||||||
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; }
|
replace_find_tmp_ = s;
|
||||||
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; }
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetReplaceWithTmp(const std::string &s)
|
||||||
|
{
|
||||||
|
replace_with_tmp_ = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string &ReplaceFindTmp() const
|
||||||
|
{
|
||||||
|
return replace_find_tmp_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string &ReplaceWithTmp() const
|
||||||
|
{
|
||||||
|
return replace_with_tmp_;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string replace_find_tmp_;
|
std::string replace_find_tmp_;
|
||||||
std::string replace_with_tmp_;
|
std::string replace_with_tmp_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_EDITOR_H
|
#endif // KTE_EDITOR_H
|
||||||
|
|||||||
@@ -102,6 +102,15 @@ GUIConfig::LoadFromFile(const std::string &path)
|
|||||||
if (v > 0.0f) {
|
if (v > 0.0f) {
|
||||||
font_size = v;
|
font_size = v;
|
||||||
}
|
}
|
||||||
|
} else if (key == "theme") {
|
||||||
|
theme = val;
|
||||||
|
} else if (key == "background" || key == "bg") {
|
||||||
|
std::string v = val;
|
||||||
|
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
if (v == "light" || v == "dark")
|
||||||
|
background = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
GUIConfig.h
12
GUIConfig.h
@@ -12,10 +12,14 @@
|
|||||||
|
|
||||||
class GUIConfig {
|
class GUIConfig {
|
||||||
public:
|
public:
|
||||||
bool fullscreen = false;
|
bool fullscreen = false;
|
||||||
int columns = 80;
|
int columns = 80;
|
||||||
int rows = 42;
|
int rows = 42;
|
||||||
float font_size = (float) KTE_FONT_SIZE;
|
float font_size = (float) KTE_FONT_SIZE;
|
||||||
|
std::string theme = "nord";
|
||||||
|
// Background mode for themes that support light/dark variants
|
||||||
|
// Values: "dark" (default), "light"
|
||||||
|
std::string background = "dark";
|
||||||
|
|
||||||
// Load from default path: $HOME/.config/kte/kge.ini
|
// Load from default path: $HOME/.config/kte/kge.ini
|
||||||
static GUIConfig Load();
|
static GUIConfig Load();
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load GUI configuration (fullscreen, columns/rows, font size)
|
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
||||||
const auto [fullscreen, columns, rows, font_size] = GUIConfig::Load();
|
GUIConfig cfg = GUIConfig::Load();
|
||||||
|
|
||||||
// GL attributes for core profile
|
// GL attributes for core profile
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||||
@@ -47,7 +47,7 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
// Compute desired window size from config
|
// Compute desired window size from config
|
||||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
|
|
||||||
if (fullscreen) {
|
if (cfg.fullscreen) {
|
||||||
// "Fullscreen": fill the usable bounds of the primary display.
|
// "Fullscreen": fill the usable bounds of the primary display.
|
||||||
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
@@ -61,8 +61,8 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
||||||
int w = static_cast<int>(columns * font_size);
|
int w = cfg.columns * static_cast<int>(cfg.font_size);
|
||||||
int h = static_cast<int>((rows * 2) * font_size);
|
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
|
||||||
|
|
||||||
// As a safety, clamp to display usable bounds if retrievable
|
// As a safety, clamp to display usable bounds if retrievable
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
@@ -86,7 +86,7 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
// macOS: when "fullscreen" is requested, position the window at the
|
// macOS: when "fullscreen" is requested, position the window at the
|
||||||
// top-left of the usable display area to mimic fullscreen while keeping
|
// top-left of the usable display area to mimic fullscreen while keeping
|
||||||
// the system menu bar visible.
|
// the system menu bar visible.
|
||||||
if (fullscreen) {
|
if (cfg.fullscreen) {
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
||||||
@@ -105,8 +105,13 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
(void) io;
|
(void) io;
|
||||||
ImGui::StyleColorsDark();
|
ImGui::StyleColorsDark();
|
||||||
// Apply a Nord-inspired theme
|
|
||||||
kte::ApplyNordImGuiTheme();
|
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||||
|
if (cfg.background == "light")
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
|
else
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||||
|
kte::ApplyThemeByName(cfg.theme);
|
||||||
|
|
||||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||||
return false;
|
return false;
|
||||||
@@ -135,7 +140,7 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Initialize GUI font from embedded default (use configured size or compiled default)
|
// Initialize GUI font from embedded default (use configured size or compiled default)
|
||||||
LoadGuiFont_(nullptr, (float) font_size);
|
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -214,7 +219,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
||||||
|
|
||||||
// Visible content rows inside the scroll child
|
// Visible content rows inside the scroll child
|
||||||
std::size_t content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
auto content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
||||||
// Editor::Rows includes the status line; add 1 back for it.
|
// 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 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)));
|
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||||
@@ -264,11 +269,11 @@ GUIFrontend::Shutdown()
|
|||||||
bool
|
bool
|
||||||
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||||
{
|
{
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
const ImGuiIO &io = ImGui::GetIO();
|
||||||
io.Fonts->Clear();
|
io.Fonts->Clear();
|
||||||
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
const ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||||
(void *) DefaultFontBoldCompressedData,
|
DefaultFontBoldCompressedData,
|
||||||
(int) DefaultFontBoldCompressedSize,
|
DefaultFontBoldCompressedSize,
|
||||||
size_px);
|
size_px);
|
||||||
if (!font) {
|
if (!font) {
|
||||||
font = io.Fonts->AddFontDefault();
|
font = io.Fonts->AddFontDefault();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public:
|
|||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool LoadGuiFont_(const char *path, float size_px);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIInputHandler input_{};
|
GUIInputHandler input_{};
|
||||||
GUIRenderer renderer_{};
|
GUIRenderer renderer_{};
|
||||||
|
|||||||
@@ -92,10 +92,14 @@ map_key(const SDL_Keycode key,
|
|||||||
out = {true, CommandId::Backspace, "", 0};
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_TAB:
|
case SDLK_TAB:
|
||||||
// Do not insert text on KEYDOWN; allow SDL_TEXTINPUT to deliver '\t'
|
// Insert a literal tab character when not interpreting a k-prefix suffix.
|
||||||
// as printable input so that all printable characters flow via TEXTINPUT.
|
// If k-prefix is active, let the k-prefix handler below consume the key
|
||||||
out.hasCommand = false;
|
// (so Tab doesn't leave k-prefix stuck).
|
||||||
return true;
|
if (!k_prefix) {
|
||||||
|
out = {true, CommandId::InsertText, std::string("\t"), 0};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break; // fall through so k-prefix handler can process
|
||||||
case SDLK_RETURN:
|
case SDLK_RETURN:
|
||||||
case SDLK_KP_ENTER:
|
case SDLK_KP_ENTER:
|
||||||
out = {true, CommandId::Newline, "", 0};
|
out = {true, CommandId::Newline, "", 0};
|
||||||
@@ -347,6 +351,12 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
uarg_text_,
|
uarg_text_,
|
||||||
mi);
|
mi);
|
||||||
|
|
||||||
|
// If we inserted a TAB on KEYDOWN, suppress any subsequent SDL_TEXTINPUT
|
||||||
|
// for this keystroke to avoid double insertion on platforms that emit it.
|
||||||
|
if (produced && mi.hasCommand && mi.id == CommandId::InsertText && mi.arg == "\t") {
|
||||||
|
suppress_text_input_once_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
||||||
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
||||||
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
||||||
|
|||||||
713
GUIRenderer.cc
713
GUIRenderer.cc
@@ -152,14 +152,14 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||||
// Compute viewport-relative row so (0) is top row of the visible area
|
// 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;
|
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
|
||||||
long vy = static_cast<long>(vy_f);
|
long vy = static_cast<long>(vy_f);
|
||||||
if (vy < 0)
|
if (vy < 0)
|
||||||
vy = 0;
|
vy = 0;
|
||||||
|
|
||||||
// Clamp vy within visible content height to avoid huge jumps
|
// Clamp vy within visible content height to avoid huge jumps
|
||||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||||
@@ -171,163 +171,169 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
if (vy >= vis_rows)
|
if (vy >= vis_rows)
|
||||||
vy = vis_rows - 1;
|
vy = vis_rows - 1;
|
||||||
|
|
||||||
// Translate viewport row to buffer row using Buffer::Rowoffs
|
// Translate viewport row to buffer row using Buffer::Rowoffs
|
||||||
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
|
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
|
||||||
if (by >= lines.size()) {
|
if (by >= lines.size()) {
|
||||||
if (!lines.empty())
|
if (!lines.empty())
|
||||||
by = lines.size() - 1;
|
by = lines.size() - 1;
|
||||||
else
|
else
|
||||||
by = 0;
|
by = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
|
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
|
||||||
float px = (mp.x - list_origin.x - scroll_x);
|
float px = (mp.x - list_origin.x - scroll_x);
|
||||||
if (px < 0.0f)
|
if (px < 0.0f)
|
||||||
px = 0.0f;
|
px = 0.0f;
|
||||||
|
|
||||||
// Empty buffer guard: if there are no lines yet, just move to 0:0
|
// Empty buffer guard: if there are no lines yet, just move to 0:0
|
||||||
if (lines.empty()) {
|
if (lines.empty()) {
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
|
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
|
||||||
} else {
|
} else {
|
||||||
// Convert pixel X to a render-column target including horizontal col offset
|
// 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.
|
// Use our own tab expansion of width 8 to match command layer logic.
|
||||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
||||||
// then translate to viewport-space by subtracting Coloffs.
|
// then translate to viewport-space by subtracting Coloffs.
|
||||||
std::size_t coloffs = buf->Coloffs();
|
std::size_t coloffs = buf->Coloffs();
|
||||||
std::size_t rx_abs = 0; // absolute rendered column
|
std::size_t rx_abs = 0; // absolute rendered column
|
||||||
std::size_t i = 0; // source column iterator
|
std::size_t i = 0; // source column iterator
|
||||||
|
|
||||||
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
|
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
|
||||||
if (!line_clicked.empty() && coloffs > 0) {
|
if (!line_clicked.empty() && coloffs > 0) {
|
||||||
while (i < line_clicked.size() && rx_abs < coloffs) {
|
while (i < line_clicked.size() && rx_abs < coloffs) {
|
||||||
if (line_clicked[i] == '\t') {
|
if (line_clicked[i] == '\t') {
|
||||||
rx_abs += (tabw - (rx_abs % tabw));
|
rx_abs += (tabw - (rx_abs % tabw));
|
||||||
} else {
|
} else {
|
||||||
rx_abs += 1;
|
rx_abs += 1;
|
||||||
}
|
}
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now search for closest source column to clicked px within/after viewport
|
// Now search for closest source column to clicked px within/after viewport
|
||||||
std::size_t best_col = i; // default to first visible column
|
std::size_t best_col = i; // default to first visible column
|
||||||
float best_dist = std::numeric_limits<float>::infinity();
|
float best_dist = std::numeric_limits<float>::infinity();
|
||||||
while (true) {
|
while (true) {
|
||||||
// For i in [current..size], evaluate candidate including the implicit end position
|
// For i in [current..size], evaluate candidate including the implicit end position
|
||||||
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
|
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
|
||||||
float rx_px = static_cast<float>(rx_view) * space_w;
|
float rx_px = static_cast<float>(rx_view) * space_w;
|
||||||
float dist = std::fabs(px - rx_px);
|
float dist = std::fabs(px - rx_px);
|
||||||
if (dist <= best_dist) {
|
if (dist <= best_dist) {
|
||||||
best_dist = dist;
|
best_dist = dist;
|
||||||
best_col = i;
|
best_col = i;
|
||||||
}
|
}
|
||||||
if (i == line_clicked.size())
|
if (i == line_clicked.size())
|
||||||
break;
|
break;
|
||||||
// advance to next source column
|
// advance to next source column
|
||||||
if (line_clicked[i] == '\t') {
|
if (line_clicked[i] == '\t') {
|
||||||
rx_abs += (tabw - (rx_abs % tabw));
|
rx_abs += (tabw - (rx_abs % tabw));
|
||||||
} else {
|
} else {
|
||||||
rx_abs += 1;
|
rx_abs += 1;
|
||||||
}
|
}
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch absolute buffer coordinates (row:col)
|
// Dispatch absolute buffer coordinates (row:col)
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cache current horizontal offset in rendered columns
|
// Cache current horizontal offset in rendered columns
|
||||||
const std::size_t coloffs_now = buf->Coloffs();
|
const std::size_t coloffs_now = buf->Coloffs();
|
||||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||||
// Capture the screen position before drawing the line
|
// Capture the screen position before drawing the line
|
||||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||||
std::string line = static_cast<std::string>(lines[i]);
|
std::string line = static_cast<std::string>(lines[i]);
|
||||||
|
|
||||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
std::string expanded;
|
std::string expanded;
|
||||||
expanded.reserve(line.size() + 16);
|
expanded.reserve(line.size() + 16);
|
||||||
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
||||||
// Compute search highlight ranges for this line in source indices
|
// Compute search highlight ranges for this line in source indices
|
||||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||||
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
|
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||||
if (search_mode) {
|
if (search_mode) {
|
||||||
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
||||||
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
if (ed.PromptActive() && (
|
||||||
try {
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||||
std::regex rx(ed.SearchQuery());
|
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
try {
|
||||||
it != std::sregex_iterator(); ++it) {
|
std::regex rx(ed.SearchQuery());
|
||||||
const auto &m = *it;
|
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||||
std::size_t sx = static_cast<std::size_t>(m.position());
|
it != std::sregex_iterator(); ++it) {
|
||||||
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
const auto &m = *it;
|
||||||
hl_src_ranges.emplace_back(sx, ex);
|
std::size_t sx = static_cast<std::size_t>(m.position());
|
||||||
}
|
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
||||||
} catch (const std::regex_error &) {
|
hl_src_ranges.emplace_back(sx, ex);
|
||||||
// ignore invalid patterns here; status line already shows the error
|
}
|
||||||
}
|
} catch (const std::regex_error &) {
|
||||||
} else {
|
// ignore invalid patterns here; status line already shows the error
|
||||||
const std::string &q = ed.SearchQuery();
|
}
|
||||||
std::size_t pos = 0;
|
} else {
|
||||||
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
const std::string &q = ed.SearchQuery();
|
||||||
hl_src_ranges.emplace_back(pos, pos + q.size());
|
std::size_t pos = 0;
|
||||||
pos += q.size();
|
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
||||||
}
|
hl_src_ranges.emplace_back(pos, pos + q.size());
|
||||||
}
|
pos += q.size();
|
||||||
}
|
}
|
||||||
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
|
}
|
||||||
std::size_t rx = 0;
|
}
|
||||||
std::size_t s = 0;
|
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
|
||||||
while (s < upto_src_exclusive && s < line.size()) {
|
std::size_t rx = 0;
|
||||||
if (line[s] == '\t')
|
std::size_t s = 0;
|
||||||
rx += (tabw - (rx % tabw));
|
while (s < upto_src_exclusive && s < line.size()) {
|
||||||
else
|
if (line[s] == '\t')
|
||||||
rx += 1;
|
rx += (tabw - (rx % tabw));
|
||||||
++s;
|
else
|
||||||
}
|
rx += 1;
|
||||||
return rx;
|
++s;
|
||||||
};
|
}
|
||||||
// Draw background highlights (under text)
|
return rx;
|
||||||
if (search_mode && !hl_src_ranges.empty()) {
|
};
|
||||||
// Current match emphasis
|
// Draw background highlights (under text)
|
||||||
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
|
if (search_mode && !hl_src_ranges.empty()) {
|
||||||
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
|
// Current match emphasis
|
||||||
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
|
||||||
for (const auto &rg : hl_src_ranges) {
|
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
|
||||||
std::size_t sx = rg.first, ex = rg.second;
|
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||||
std::size_t rx_start = src_to_rx(sx);
|
for (const auto &rg: hl_src_ranges) {
|
||||||
std::size_t rx_end = src_to_rx(ex);
|
std::size_t sx = rg.first, ex = rg.second;
|
||||||
// Apply horizontal scroll offset
|
std::size_t rx_start = src_to_rx(sx);
|
||||||
if (rx_end <= coloffs_now) continue; // fully left of view
|
std::size_t rx_end = src_to_rx(ex);
|
||||||
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
// Apply horizontal scroll offset
|
||||||
std::size_t vx1 = rx_end - coloffs_now;
|
if (rx_end <= coloffs_now)
|
||||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
continue; // fully left of view
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w, line_pos.y + line_h);
|
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
||||||
// Choose color: current match stronger
|
std::size_t vx1 = rx_end - coloffs_now;
|
||||||
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||||
ImU32 col = is_current ? IM_COL32(255, 220, 120, 140) : IM_COL32(200, 200, 0, 90);
|
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
line_pos.y + line_h);
|
||||||
}
|
// Choose color: current match stronger
|
||||||
}
|
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
||||||
// Emit entire line (ImGui child scrolling will handle clipping)
|
ImU32 col = is_current
|
||||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
? IM_COL32(255, 220, 120, 140)
|
||||||
char c = line[src];
|
: IM_COL32(200, 200, 0, 90);
|
||||||
if (c == '\t') {
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
}
|
||||||
// Emit spaces for the tab
|
}
|
||||||
expanded.append(adv, ' ');
|
// Emit entire line (ImGui child scrolling will handle clipping)
|
||||||
rx_abs_draw += adv;
|
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||||
} else {
|
char c = line[src];
|
||||||
expanded.push_back(c);
|
if (c == '\t') {
|
||||||
rx_abs_draw += 1;
|
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());
|
ImGui::TextUnformatted(expanded.c_str());
|
||||||
|
|
||||||
// Draw a visible cursor indicator on the current line
|
// Draw a visible cursor indicator on the current line
|
||||||
if (i == cy) {
|
if (i == cy) {
|
||||||
@@ -349,207 +355,220 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
// Status bar spanning full width
|
// Status bar spanning full width
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// Compute full content width and draw a filled background rectangle
|
// Compute full content width and draw a filled background rectangle
|
||||||
ImVec2 win_pos = ImGui::GetWindowPos();
|
ImVec2 win_pos = ImGui::GetWindowPos();
|
||||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||||
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
||||||
float x0 = win_pos.x + cr_min.x;
|
float x0 = win_pos.x + cr_min.x;
|
||||||
float x1 = win_pos.x + cr_max.x;
|
float x1 = win_pos.x + cr_max.x;
|
||||||
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
||||||
float bar_h = ImGui::GetFrameHeight();
|
float bar_h = ImGui::GetFrameHeight();
|
||||||
ImVec2 p0(x0, cursor.y);
|
ImVec2 p0(x0, cursor.y);
|
||||||
ImVec2 p1(x1, cursor.y + bar_h);
|
ImVec2 p1(x1, cursor.y + bar_h);
|
||||||
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
||||||
// If a prompt is active, replace the entire status bar with the prompt text
|
// If a prompt is active, replace the entire status bar with the prompt text
|
||||||
if (ed.PromptActive()) {
|
|
||||||
std::string label = ed.PromptLabel();
|
|
||||||
std::string ptext = ed.PromptText();
|
|
||||||
auto kind = ed.CurrentPromptKind();
|
|
||||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
|
||||||
kind == Editor::PromptKind::Chdir) {
|
|
||||||
const char *home_c = std::getenv("HOME");
|
|
||||||
if (home_c && *home_c) {
|
|
||||||
std::string home(home_c);
|
|
||||||
if (ptext.rfind(home, 0) == 0) {
|
|
||||||
std::string rest = ptext.substr(home.size());
|
|
||||||
if (rest.empty())
|
|
||||||
ptext = "~";
|
|
||||||
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
|
||||||
ptext = std::string("~") + rest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float pad = 6.f;
|
|
||||||
float left_x = p0.x + pad;
|
|
||||||
float right_x = p1.x - pad;
|
|
||||||
float max_px = std::max(0.0f, right_x - left_x);
|
|
||||||
|
|
||||||
std::string prefix;
|
|
||||||
if (!label.empty()) prefix = label + ": ";
|
|
||||||
|
|
||||||
// Compose showing right-end of filename portion when too long for space
|
|
||||||
std::string final_msg;
|
|
||||||
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
|
||||||
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
|
||||||
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
|
||||||
// Trim from left until it fits by pixel width
|
|
||||||
std::string tail = ptext;
|
|
||||||
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
|
||||||
if (tail_sz.x > avail_px) {
|
|
||||||
// Remove leading chars until it fits
|
|
||||||
// Use a simple loop; text lengths are small here
|
|
||||||
size_t start = 0;
|
|
||||||
// To avoid O(n^2) worst-case, remove chunks
|
|
||||||
while (start < tail.size()) {
|
|
||||||
// Estimate how many chars to skip based on ratio
|
|
||||||
float ratio = tail_sz.x / avail_px;
|
|
||||||
size_t skip = ratio > 1.5f ? std::min(tail.size() - start, (size_t)std::max<size_t>(1, (size_t)(tail.size() / 4))) : 1;
|
|
||||||
start += skip;
|
|
||||||
std::string candidate = tail.substr(start);
|
|
||||||
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
|
||||||
if (cand_sz.x <= avail_px) {
|
|
||||||
tail = candidate;
|
|
||||||
tail_sz = cand_sz;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
|
|
||||||
// As a last resort, ensure fit by chopping exactly
|
|
||||||
// binary reduce
|
|
||||||
size_t lo = 0, hi = tail.size();
|
|
||||||
while (lo < hi) {
|
|
||||||
size_t mid = (lo + hi) / 2;
|
|
||||||
std::string cand = tail.substr(mid);
|
|
||||||
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px) hi = mid; else lo = mid + 1;
|
|
||||||
}
|
|
||||||
tail = tail.substr(lo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final_msg = prefix + tail;
|
|
||||||
} else {
|
|
||||||
final_msg = prefix + ptext;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
|
|
||||||
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
|
||||||
ImGui::TextUnformatted(final_msg.c_str());
|
|
||||||
ImGui::PopClipRect();
|
|
||||||
// Advance cursor to after the bar to keep layout consistent
|
|
||||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
|
||||||
} else {
|
|
||||||
// Build left text
|
|
||||||
std::string left;
|
|
||||||
left.reserve(256);
|
|
||||||
left += "kge"; // GUI app name
|
|
||||||
left += " ";
|
|
||||||
left += KTE_VERSION_STR;
|
|
||||||
std::string fname;
|
|
||||||
try {
|
|
||||||
fname = ed.DisplayNameFor(*buf);
|
|
||||||
} catch (...) {
|
|
||||||
fname = buf->Filename();
|
|
||||||
try {
|
|
||||||
fname = std::filesystem::path(fname).filename().string();
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
|
||||||
left += " ";
|
|
||||||
// Insert buffer position prefix "[x/N] " before filename
|
|
||||||
{
|
|
||||||
std::size_t total = ed.BufferCount();
|
|
||||||
if (total > 0) {
|
|
||||||
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
|
||||||
left += "[";
|
|
||||||
left += std::to_string(static_cast<unsigned long long>(idx1));
|
|
||||||
left += "/";
|
|
||||||
left += std::to_string(static_cast<unsigned long long>(total));
|
|
||||||
left += "] ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
left += fname;
|
|
||||||
if (buf->Dirty())
|
|
||||||
left += " *";
|
|
||||||
// Append total line count as "<n>L"
|
|
||||||
{
|
|
||||||
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
|
||||||
left += " ";
|
|
||||||
left += std::to_string(lcount);
|
|
||||||
left += "L";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build right text (cursor/mark)
|
|
||||||
int row1 = static_cast<int>(buf->Cury()) + 1;
|
|
||||||
int col1 = static_cast<int>(buf->Curx()) + 1;
|
|
||||||
bool have_mark = buf->MarkSet();
|
|
||||||
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
|
||||||
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
|
||||||
char rbuf[128];
|
|
||||||
if (have_mark)
|
|
||||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
|
||||||
else
|
|
||||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
|
||||||
std::string right = rbuf;
|
|
||||||
|
|
||||||
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
|
||||||
std::string msg;
|
|
||||||
if (ed.PromptActive()) {
|
if (ed.PromptActive()) {
|
||||||
msg = ed.PromptLabel();
|
std::string label = ed.PromptLabel();
|
||||||
if (!msg.empty())
|
std::string ptext = ed.PromptText();
|
||||||
msg += ": ";
|
auto kind = ed.CurrentPromptKind();
|
||||||
msg += ed.PromptText();
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||||
} else {
|
kind == Editor::PromptKind::Chdir) {
|
||||||
msg = ed.Status();
|
const char *home_c = std::getenv("HOME");
|
||||||
}
|
if (home_c && *home_c) {
|
||||||
|
std::string home(home_c);
|
||||||
|
if (ptext.rfind(home, 0) == 0) {
|
||||||
|
std::string rest = ptext.substr(home.size());
|
||||||
|
if (rest.empty())
|
||||||
|
ptext = "~";
|
||||||
|
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
||||||
|
ptext = std::string("~") + rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Measurements
|
float pad = 6.f;
|
||||||
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
float left_x = p0.x + pad;
|
||||||
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
float right_x = p1.x - pad;
|
||||||
float pad = 6.f;
|
float max_px = std::max(0.0f, right_x - left_x);
|
||||||
float left_x = p0.x + pad;
|
|
||||||
float right_x = p1.x - pad - right_sz.x;
|
std::string prefix;
|
||||||
if (right_x < left_x + left_sz.x + pad) {
|
if (kind == Editor::PromptKind::Command) {
|
||||||
// Not enough room; clip left to fit
|
prefix = ": ";
|
||||||
float max_left = std::max(0.0f, right_x - left_x - pad);
|
} else if (!label.empty()) {
|
||||||
if (max_left < left_sz.x && max_left > 10.0f) {
|
prefix = label + ": ";
|
||||||
// Render a clipped left using a child region
|
}
|
||||||
|
|
||||||
|
// Compose showing right-end of filename portion when too long for space
|
||||||
|
std::string final_msg;
|
||||||
|
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
||||||
|
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
||||||
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||||
|
Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
||||||
|
// Trim from left until it fits by pixel width
|
||||||
|
std::string tail = ptext;
|
||||||
|
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
||||||
|
if (tail_sz.x > avail_px) {
|
||||||
|
// Remove leading chars until it fits
|
||||||
|
// Use a simple loop; text lengths are small here
|
||||||
|
size_t start = 0;
|
||||||
|
// To avoid O(n^2) worst-case, remove chunks
|
||||||
|
while (start < tail.size()) {
|
||||||
|
// Estimate how many chars to skip based on ratio
|
||||||
|
float ratio = tail_sz.x / avail_px;
|
||||||
|
size_t skip = ratio > 1.5f
|
||||||
|
? std::min(tail.size() - start,
|
||||||
|
(size_t) std::max<size_t>(
|
||||||
|
1, (size_t) (tail.size() / 4)))
|
||||||
|
: 1;
|
||||||
|
start += skip;
|
||||||
|
std::string candidate = tail.substr(start);
|
||||||
|
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
||||||
|
if (cand_sz.x <= avail_px) {
|
||||||
|
tail = candidate;
|
||||||
|
tail_sz = cand_sz;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
|
||||||
|
// As a last resort, ensure fit by chopping exactly
|
||||||
|
// binary reduce
|
||||||
|
size_t lo = 0, hi = tail.size();
|
||||||
|
while (lo < hi) {
|
||||||
|
size_t mid = (lo + hi) / 2;
|
||||||
|
std::string cand = tail.substr(mid);
|
||||||
|
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px)
|
||||||
|
hi = mid;
|
||||||
|
else
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
tail = tail.substr(lo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final_msg = prefix + tail;
|
||||||
|
} else {
|
||||||
|
final_msg = prefix + ptext;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
|
||||||
|
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||||
|
ImGui::TextUnformatted(final_msg.c_str());
|
||||||
|
ImGui::PopClipRect();
|
||||||
|
// Advance cursor to after the bar to keep layout consistent
|
||||||
|
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||||
|
} else {
|
||||||
|
// Build left text
|
||||||
|
std::string left;
|
||||||
|
left.reserve(256);
|
||||||
|
left += "kge"; // GUI app name
|
||||||
|
left += " ";
|
||||||
|
left += KTE_VERSION_STR;
|
||||||
|
std::string fname;
|
||||||
|
try {
|
||||||
|
fname = ed.DisplayNameFor(*buf);
|
||||||
|
} catch (...) {
|
||||||
|
fname = buf->Filename();
|
||||||
|
try {
|
||||||
|
fname = std::filesystem::path(fname).filename().string();
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
left += " ";
|
||||||
|
// Insert buffer position prefix "[x/N] " before filename
|
||||||
|
{
|
||||||
|
std::size_t total = ed.BufferCount();
|
||||||
|
if (total > 0) {
|
||||||
|
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
||||||
|
left += "[";
|
||||||
|
left += std::to_string(static_cast<unsigned long long>(idx1));
|
||||||
|
left += "/";
|
||||||
|
left += std::to_string(static_cast<unsigned long long>(total));
|
||||||
|
left += "] ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left += fname;
|
||||||
|
if (buf->Dirty())
|
||||||
|
left += " *";
|
||||||
|
// Append total line count as "<n>L"
|
||||||
|
{
|
||||||
|
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
||||||
|
left += " ";
|
||||||
|
left += std::to_string(lcount);
|
||||||
|
left += "L";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build right text (cursor/mark)
|
||||||
|
int row1 = static_cast<int>(buf->Cury()) + 1;
|
||||||
|
int col1 = static_cast<int>(buf->Curx()) + 1;
|
||||||
|
bool have_mark = buf->MarkSet();
|
||||||
|
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
||||||
|
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
||||||
|
char rbuf[128];
|
||||||
|
if (have_mark)
|
||||||
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
||||||
|
else
|
||||||
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||||
|
std::string right = rbuf;
|
||||||
|
|
||||||
|
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
||||||
|
std::string msg;
|
||||||
|
if (ed.PromptActive()) {
|
||||||
|
msg = ed.PromptLabel();
|
||||||
|
if (!msg.empty())
|
||||||
|
msg += ": ";
|
||||||
|
msg += ed.PromptText();
|
||||||
|
} else {
|
||||||
|
msg = ed.Status();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measurements
|
||||||
|
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
||||||
|
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
||||||
|
float pad = 6.f;
|
||||||
|
float left_x = p0.x + pad;
|
||||||
|
float right_x = p1.x - pad - right_sz.x;
|
||||||
|
if (right_x < left_x + left_sz.x + pad) {
|
||||||
|
// Not enough room; clip left to fit
|
||||||
|
float max_left = std::max(0.0f, right_x - left_x - pad);
|
||||||
|
if (max_left < left_sz.x && max_left > 10.0f) {
|
||||||
|
// Render a clipped left using a child region
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||||
|
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
||||||
|
ImGui::TextUnformatted(left.c_str());
|
||||||
|
ImGui::PopClipRect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw left normally
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||||
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
|
||||||
ImGui::TextUnformatted(left.c_str());
|
ImGui::TextUnformatted(left.c_str());
|
||||||
ImGui::PopClipRect();
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Draw left normally
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
|
||||||
ImGui::TextUnformatted(left.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw right
|
// Draw right
|
||||||
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x),
|
||||||
ImGui::TextUnformatted(right.c_str());
|
p0.y + (bar_h - right_sz.y) * 0.5f));
|
||||||
|
ImGui::TextUnformatted(right.c_str());
|
||||||
|
|
||||||
// Draw middle message centered in remaining space
|
// Draw middle message centered in remaining space
|
||||||
if (!msg.empty()) {
|
if (!msg.empty()) {
|
||||||
float mid_left = left_x + left_sz.x + pad;
|
float mid_left = left_x + left_sz.x + pad;
|
||||||
float mid_right = std::max(right_x - pad, mid_left);
|
float mid_right = std::max(right_x - pad, mid_left);
|
||||||
float mid_w = std::max(0.0f, mid_right - mid_left);
|
float mid_w = std::max(0.0f, mid_right - mid_left);
|
||||||
if (mid_w > 1.0f) {
|
if (mid_w > 1.0f) {
|
||||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
||||||
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
||||||
// Clip to middle region
|
// Clip to middle region
|
||||||
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||||
ImGui::TextUnformatted(msg.c_str());
|
ImGui::TextUnformatted(msg.c_str());
|
||||||
ImGui::PopClipRect();
|
ImGui::PopClipRect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Advance cursor to after the bar to keep layout consistent
|
||||||
|
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||||
}
|
}
|
||||||
// Advance cursor to after the bar to keep layout consistent
|
|
||||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|||||||
1021
GUITheme.h
1021
GUITheme.h
File diff suppressed because it is too large
Load Diff
50
HelpText.cc
50
HelpText.cc
@@ -15,24 +15,26 @@ HelpText::Text()
|
|||||||
return std::string(
|
return std::string(
|
||||||
"KTE - Kyle's Text Editor\n\n"
|
"KTE - Kyle's Text Editor\n\n"
|
||||||
"About:\n"
|
"About:\n"
|
||||||
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n"
|
" kte is Kyle's Text Editor. It keeps a small, fast core and uses a\n"
|
||||||
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n"
|
" WordStar/VDE-style command model with some emacs influences.\n"
|
||||||
" process of writing a text editor from scratch. It has keybindings inspired by\n"
|
|
||||||
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Core keybindings:\n"
|
"K-commands (prefix C-k):\n"
|
||||||
" C-k ' Toggle read-only\n"
|
" C-k ' Toggle read-only\n"
|
||||||
" C-k - Unindent region\n"
|
" C-k - Unindent region (mark required)\n"
|
||||||
" C-k = Indent region\n"
|
" C-k = Indent region (mark required)\n"
|
||||||
|
" C-k ; Command prompt (:\\ )\n"
|
||||||
" C-k C-d Kill entire line\n"
|
" C-k C-d Kill entire line\n"
|
||||||
" C-k C-q Quit now (no confirm)\n"
|
" C-k C-q Quit now (no confirm)\n"
|
||||||
" C-k a Mark all and jump to end\n"
|
" C-k C-x Save and quit\n"
|
||||||
|
" C-k a Mark start of file, jump to end\n"
|
||||||
" C-k b Switch buffer\n"
|
" C-k b Switch buffer\n"
|
||||||
" C-k c Close current buffer\n"
|
" C-k c Close current buffer\n"
|
||||||
" C-k d Kill to end of line\n"
|
" C-k d Kill to end of line\n"
|
||||||
" C-k e Open file (prompt)\n"
|
" C-k e Open file (prompt)\n"
|
||||||
|
" C-k f Flush kill ring\n"
|
||||||
" C-k g Jump to line\n"
|
" C-k g Jump to line\n"
|
||||||
" C-k h Show this help\n"
|
" C-k h Show this help\n"
|
||||||
|
" C-k j Jump to mark\n"
|
||||||
" C-k l Reload buffer from disk\n"
|
" C-k l Reload buffer from disk\n"
|
||||||
" C-k n Previous buffer\n"
|
" C-k n Previous buffer\n"
|
||||||
" C-k o Change working directory (prompt)\n"
|
" C-k o Change working directory (prompt)\n"
|
||||||
@@ -44,12 +46,36 @@ HelpText::Text()
|
|||||||
" C-k v Toggle visual file picker (GUI)\n"
|
" C-k v Toggle visual file picker (GUI)\n"
|
||||||
" C-k w Show working directory\n"
|
" C-k w Show working directory\n"
|
||||||
" C-k x Save and quit\n"
|
" C-k x Save and quit\n"
|
||||||
|
" C-k y Yank\n"
|
||||||
"\n"
|
"\n"
|
||||||
"ESC/Alt commands:\n"
|
"ESC/Alt commands:\n"
|
||||||
|
" ESC < Go to beginning of file\n"
|
||||||
|
" ESC > Go to end of file\n"
|
||||||
|
" ESC m Toggle mark\n"
|
||||||
|
" ESC w Copy region to kill ring (Alt-w)\n"
|
||||||
|
" ESC b Previous word\n"
|
||||||
|
" ESC f Next word\n"
|
||||||
|
" ESC d Delete next word (Alt-d)\n"
|
||||||
|
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
||||||
" ESC q Reflow paragraph\n"
|
" ESC q Reflow paragraph\n"
|
||||||
" ESC BACKSPACE Delete previous word\n"
|
"\n"
|
||||||
" ESC d Delete next word\n"
|
"Control keys:\n"
|
||||||
" Alt-w Copy region to kill ring\n\n"
|
" C-a C-e Line start / end\n"
|
||||||
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\n"
|
" C-b C-f Move left / right\n"
|
||||||
|
" C-n C-p Move down / up\n"
|
||||||
|
" C-d Delete char\n"
|
||||||
|
" C-w / C-y Kill region / Yank\n"
|
||||||
|
" C-s Incremental find\n"
|
||||||
|
" C-r Regex search\n"
|
||||||
|
" C-t Regex search & replace\n"
|
||||||
|
" C-h Search & replace\n"
|
||||||
|
" C-l / C-g Refresh / Cancel\n"
|
||||||
|
" C-u [digits] Universal argument (repeat count)\n"
|
||||||
|
"\n"
|
||||||
|
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||||
|
"\n"
|
||||||
|
"GUI appearance (command prompt):\n"
|
||||||
|
" : theme NAME Set GUI theme (eink, gruvbox, nord, plan9, solarized)\n"
|
||||||
|
" : background MODE Set background: light | dark (affects eink, gruvbox, solarized)\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
class HelpText {
|
class HelpText {
|
||||||
public:
|
public:
|
||||||
// Returns the embedded help text as a single string with newlines.
|
// Returns the embedded help text as a single string with newlines.
|
||||||
// Project maintainers can customize the returned string below
|
// Project maintainers can customize the returned string below
|
||||||
// (in HelpText.cc) without touching the help command logic.
|
// (in HelpText.cc) without touching the help command logic.
|
||||||
static std::string Text();
|
static std::string Text();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_HELPTEXT_H
|
#endif // KTE_HELPTEXT_H
|
||||||
|
|||||||
25
KKeymap.cc
25
KKeymap.cc
@@ -33,10 +33,10 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
out = CommandId::Redo; // C-k r (redo)
|
out = CommandId::Redo; // C-k r (redo)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (ascii_key == '\'') {
|
if (ascii_key == '\'') {
|
||||||
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (k_lower) {
|
switch (k_lower) {
|
||||||
case 'a':
|
case 'a':
|
||||||
@@ -108,6 +108,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case '=':
|
case '=':
|
||||||
out = CommandId::IndentRegion;
|
out = CommandId::IndentRegion;
|
||||||
return true;
|
return true;
|
||||||
|
case ';':
|
||||||
|
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -121,7 +124,7 @@ auto
|
|||||||
KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
||||||
{
|
{
|
||||||
const int k = KLowerAscii(ascii_key);
|
const int k = KLowerAscii(ascii_key);
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case 'w':
|
case 'w':
|
||||||
out = CommandId::KillRegion; // C-w
|
out = CommandId::KillRegion; // C-w
|
||||||
return true;
|
return true;
|
||||||
@@ -152,12 +155,12 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
case 's':
|
case 's':
|
||||||
out = CommandId::FindStart;
|
out = CommandId::FindStart;
|
||||||
return true;
|
return true;
|
||||||
case 'r':
|
case 'r':
|
||||||
out = CommandId::RegexFindStart; // C-r regex search
|
out = CommandId::RegexFindStart; // C-r regex search
|
||||||
return true;
|
return true;
|
||||||
case 't':
|
case 't':
|
||||||
out = CommandId::RegexpReplace; // C-t regex search & replace
|
out = CommandId::RegexpReplace; // C-t regex search & replace
|
||||||
return true;
|
return true;
|
||||||
case 'h':
|
case 'h':
|
||||||
out = CommandId::SearchReplace; // C-h: search & replace
|
out = CommandId::SearchReplace; // C-h: search & replace
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -41,140 +41,175 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
std::size_t coloffs = buf->Coloffs();
|
std::size_t coloffs = buf->Coloffs();
|
||||||
|
|
||||||
const int tabw = 8;
|
const int tabw = 8;
|
||||||
for (int r = 0; r < content_rows; ++r) {
|
for (int r = 0; r < content_rows; ++r) {
|
||||||
move(r, 0);
|
move(r, 0);
|
||||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||||
std::size_t render_col = 0;
|
std::size_t render_col = 0;
|
||||||
std::size_t src_i = 0;
|
std::size_t src_i = 0;
|
||||||
// Compute matches for this line if search highlighting is active
|
// Compute matches for this line if search highlighting is active
|
||||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||||
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
|
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
|
||||||
if (search_mode && li < lines.size()) {
|
if (search_mode && li < lines.size()) {
|
||||||
std::string sline = static_cast<std::string>(lines[li]);
|
std::string sline = static_cast<std::string>(lines[li]);
|
||||||
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
||||||
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
if (ed.PromptActive() && (
|
||||||
try {
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||||
std::regex rx(ed.SearchQuery());
|
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
try {
|
||||||
it != std::sregex_iterator(); ++it) {
|
std::regex rx(ed.SearchQuery());
|
||||||
const auto &m = *it;
|
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
||||||
std::size_t sx = static_cast<std::size_t>(m.position());
|
it != std::sregex_iterator(); ++it) {
|
||||||
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
const auto &m = *it;
|
||||||
ranges.emplace_back(sx, ex);
|
std::size_t sx = static_cast<std::size_t>(m.position());
|
||||||
}
|
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
||||||
} catch (const std::regex_error &) {
|
ranges.emplace_back(sx, ex);
|
||||||
// ignore invalid patterns here; status shows error
|
}
|
||||||
}
|
} catch (const std::regex_error &) {
|
||||||
} else {
|
// ignore invalid patterns here; status shows error
|
||||||
const std::string &q = ed.SearchQuery();
|
}
|
||||||
std::size_t pos = 0;
|
} else {
|
||||||
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
|
const std::string &q = ed.SearchQuery();
|
||||||
ranges.emplace_back(pos, pos + q.size());
|
std::size_t pos = 0;
|
||||||
pos += q.size();
|
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
|
||||||
}
|
ranges.emplace_back(pos, pos + q.size());
|
||||||
}
|
pos += q.size();
|
||||||
}
|
}
|
||||||
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
}
|
||||||
if (ranges.empty()) return false;
|
}
|
||||||
// ranges are non-overlapping and ordered by construction
|
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
||||||
// linear scan is fine for now
|
if (ranges.empty())
|
||||||
for (const auto &rg : ranges) {
|
return false;
|
||||||
if (si < rg.first) break;
|
// ranges are non-overlapping and ordered by construction
|
||||||
if (si >= rg.first && si < rg.second) return true;
|
// linear scan is fine for now
|
||||||
}
|
for (const auto &rg: ranges) {
|
||||||
return false;
|
if (si < rg.first)
|
||||||
};
|
break;
|
||||||
// Track current-match to optionally emphasize
|
if (si >= rg.first && si < rg.second)
|
||||||
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
|
return true;
|
||||||
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
}
|
||||||
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
return false;
|
||||||
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
};
|
||||||
bool hl_on = false;
|
// Track current-match to optionally emphasize
|
||||||
bool cur_on = false;
|
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
|
||||||
int written = 0;
|
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
||||||
if (li < lines.size()) {
|
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
||||||
std::string line = static_cast<std::string>(lines[li]);
|
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||||
src_i = 0;
|
bool hl_on = false;
|
||||||
render_col = 0;
|
bool cur_on = false;
|
||||||
while (written < cols) {
|
int written = 0;
|
||||||
char ch = ' ';
|
if (li < lines.size()) {
|
||||||
bool from_src = false;
|
std::string line = static_cast<std::string>(lines[li]);
|
||||||
if (src_i < line.size()) {
|
src_i = 0;
|
||||||
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
render_col = 0;
|
||||||
if (c == '\t') {
|
while (written < cols) {
|
||||||
std::size_t next_tab = tabw - (render_col % tabw);
|
char ch = ' ';
|
||||||
if (render_col + next_tab <= coloffs) {
|
bool from_src = false;
|
||||||
render_col += next_tab;
|
if (src_i < line.size()) {
|
||||||
++src_i;
|
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
||||||
continue;
|
if (c == '\t') {
|
||||||
}
|
std::size_t next_tab = tabw - (render_col % tabw);
|
||||||
// Emit spaces for tab
|
if (render_col + next_tab <= coloffs) {
|
||||||
if (render_col < coloffs) {
|
render_col += next_tab;
|
||||||
// skip to coloffs
|
++src_i;
|
||||||
std::size_t to_skip = std::min<std::size_t>(
|
continue;
|
||||||
next_tab, coloffs - render_col);
|
}
|
||||||
render_col += to_skip;
|
// Emit spaces for tab
|
||||||
next_tab -= to_skip;
|
if (render_col < coloffs) {
|
||||||
}
|
// skip to coloffs
|
||||||
// Now render visible spaces
|
std::size_t to_skip = std::min<std::size_t>(
|
||||||
while (next_tab > 0 && written < cols) {
|
next_tab, coloffs - render_col);
|
||||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
render_col += to_skip;
|
||||||
bool in_cur = has_current && li == cur_my && src_i >= cur_mx && src_i < cur_mend;
|
next_tab -= to_skip;
|
||||||
// Toggle highlight attributes
|
}
|
||||||
int attr = 0;
|
// Now render visible spaces
|
||||||
if (in_hl) attr |= A_STANDOUT;
|
while (next_tab > 0 && written < cols) {
|
||||||
if (in_cur) attr |= A_BOLD;
|
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||||
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; }
|
bool in_cur =
|
||||||
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; }
|
has_current && li == cur_my && src_i >= cur_mx
|
||||||
if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; }
|
&& src_i < cur_mend;
|
||||||
if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; }
|
// Toggle highlight attributes
|
||||||
addch(' ');
|
int attr = 0;
|
||||||
++written;
|
if (in_hl)
|
||||||
++render_col;
|
attr |= A_STANDOUT;
|
||||||
--next_tab;
|
if (in_cur)
|
||||||
}
|
attr |= A_BOLD;
|
||||||
++src_i;
|
if ((attr & A_STANDOUT) && !hl_on) {
|
||||||
continue;
|
attron(A_STANDOUT);
|
||||||
} else {
|
hl_on = true;
|
||||||
// normal char
|
}
|
||||||
if (render_col < coloffs) {
|
if (!(attr & A_STANDOUT) && hl_on) {
|
||||||
++render_col;
|
attroff(A_STANDOUT);
|
||||||
++src_i;
|
hl_on = false;
|
||||||
continue;
|
}
|
||||||
}
|
if ((attr & A_BOLD) && !cur_on) {
|
||||||
ch = static_cast<char>(c);
|
attron(A_BOLD);
|
||||||
from_src = true;
|
cur_on = true;
|
||||||
}
|
}
|
||||||
} else {
|
if (!(attr & A_BOLD) && cur_on) {
|
||||||
// beyond EOL, fill spaces
|
attroff(A_BOLD);
|
||||||
ch = ' ';
|
cur_on = false;
|
||||||
from_src = false;
|
}
|
||||||
}
|
addch(' ');
|
||||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
++written;
|
||||||
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx && src_i < cur_mend;
|
++render_col;
|
||||||
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; }
|
--next_tab;
|
||||||
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; }
|
}
|
||||||
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; }
|
++src_i;
|
||||||
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; }
|
continue;
|
||||||
addch(static_cast<unsigned char>(ch));
|
} else {
|
||||||
++written;
|
// normal char
|
||||||
++render_col;
|
if (render_col < coloffs) {
|
||||||
if (from_src)
|
++render_col;
|
||||||
++src_i;
|
++src_i;
|
||||||
if (src_i >= line.size() && written >= cols)
|
continue;
|
||||||
break;
|
}
|
||||||
}
|
ch = static_cast<char>(c);
|
||||||
}
|
from_src = true;
|
||||||
if (hl_on) {
|
}
|
||||||
attroff(A_STANDOUT);
|
} else {
|
||||||
hl_on = false;
|
// beyond EOL, fill spaces
|
||||||
}
|
ch = ' ';
|
||||||
if (cur_on) {
|
from_src = false;
|
||||||
attroff(A_BOLD);
|
}
|
||||||
cur_on = false;
|
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||||
}
|
bool in_cur =
|
||||||
clrtoeol();
|
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||||
}
|
cur_mend;
|
||||||
|
if (in_hl && !hl_on) {
|
||||||
|
attron(A_STANDOUT);
|
||||||
|
hl_on = true;
|
||||||
|
}
|
||||||
|
if (!in_hl && hl_on) {
|
||||||
|
attroff(A_STANDOUT);
|
||||||
|
hl_on = false;
|
||||||
|
}
|
||||||
|
if (in_cur && !cur_on) {
|
||||||
|
attron(A_BOLD);
|
||||||
|
cur_on = true;
|
||||||
|
}
|
||||||
|
if (!in_cur && cur_on) {
|
||||||
|
attroff(A_BOLD);
|
||||||
|
cur_on = false;
|
||||||
|
}
|
||||||
|
addch(static_cast<unsigned char>(ch));
|
||||||
|
++written;
|
||||||
|
++render_col;
|
||||||
|
if (from_src)
|
||||||
|
++src_i;
|
||||||
|
if (src_i >= line.size() && written >= cols)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hl_on) {
|
||||||
|
attroff(A_STANDOUT);
|
||||||
|
hl_on = false;
|
||||||
|
}
|
||||||
|
if (cur_on) {
|
||||||
|
attroff(A_BOLD);
|
||||||
|
cur_on = false;
|
||||||
|
}
|
||||||
|
clrtoeol();
|
||||||
|
}
|
||||||
|
|
||||||
// Place terminal cursor at logical position accounting for tabs and coloffs
|
// Place terminal cursor at logical position accounting for tabs and coloffs
|
||||||
std::size_t cy = buf->Cury();
|
std::size_t cy = buf->Cury();
|
||||||
@@ -191,71 +226,74 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
mvaddstr(0, 0, "[no buffer]");
|
mvaddstr(0, 0, "[no buffer]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status line (inverse)
|
// Status line (inverse)
|
||||||
move(rows - 1, 0);
|
move(rows - 1, 0);
|
||||||
attron(A_REVERSE);
|
attron(A_REVERSE);
|
||||||
|
|
||||||
// Fill the status line with spaces first
|
// Fill the status line with spaces first
|
||||||
for (int i = 0; i < cols; ++i)
|
for (int i = 0; i < cols; ++i)
|
||||||
addch(' ');
|
addch(' ');
|
||||||
|
|
||||||
// If a prompt is active, replace the status bar with the full prompt text
|
// If a prompt is active, replace the status bar with the full prompt text
|
||||||
if (ed.PromptActive()) {
|
if (ed.PromptActive()) {
|
||||||
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
||||||
std::string label = ed.PromptLabel();
|
std::string label = ed.PromptLabel();
|
||||||
std::string ptext = ed.PromptText();
|
std::string ptext = ed.PromptText();
|
||||||
auto kind = ed.CurrentPromptKind();
|
auto kind = ed.CurrentPromptKind();
|
||||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||||
kind == Editor::PromptKind::Chdir) {
|
kind == Editor::PromptKind::Chdir) {
|
||||||
const char *home_c = std::getenv("HOME");
|
const char *home_c = std::getenv("HOME");
|
||||||
if (home_c && *home_c) {
|
if (home_c && *home_c) {
|
||||||
std::string home(home_c);
|
std::string home(home_c);
|
||||||
// Ensure we match only at the start
|
// Ensure we match only at the start
|
||||||
if (ptext.rfind(home, 0) == 0) {
|
if (ptext.rfind(home, 0) == 0) {
|
||||||
std::string rest = ptext.substr(home.size());
|
std::string rest = ptext.substr(home.size());
|
||||||
if (rest.empty())
|
if (rest.empty())
|
||||||
ptext = "~";
|
ptext = "~";
|
||||||
else if (rest[0] == '/' || rest[0] == '\\')
|
else if (rest[0] == '/' || rest[0] == '\\')
|
||||||
ptext = std::string("~") + rest;
|
ptext = std::string("~") + rest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Prefer keeping the tail of the filename visible when it exceeds the window
|
// Prefer keeping the tail of the filename visible when it exceeds the window
|
||||||
std::string msg;
|
std::string msg;
|
||||||
if (!label.empty()) {
|
if (kind == Editor::PromptKind::Command) {
|
||||||
msg = label + ": ";
|
msg = ": ";
|
||||||
}
|
} else if (!label.empty()) {
|
||||||
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
msg = label + ": ";
|
||||||
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && cols > 0) {
|
}
|
||||||
int avail = cols - static_cast<int>(msg.size());
|
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
||||||
if (avail <= 0) {
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||||
// No room for label; fall back to showing the rightmost portion of the whole string
|
Editor::PromptKind::Chdir) && cols > 0) {
|
||||||
std::string whole = msg + ptext;
|
int avail = cols - static_cast<int>(msg.size());
|
||||||
if ((int)whole.size() > cols)
|
if (avail <= 0) {
|
||||||
whole = whole.substr(whole.size() - cols);
|
// No room for label; fall back to showing the rightmost portion of the whole string
|
||||||
msg = whole;
|
std::string whole = msg + ptext;
|
||||||
} else {
|
if ((int) whole.size() > cols)
|
||||||
if ((int)ptext.size() > avail) {
|
whole = whole.substr(whole.size() - cols);
|
||||||
ptext = ptext.substr(ptext.size() - avail);
|
msg = whole;
|
||||||
}
|
} else {
|
||||||
msg += ptext;
|
if ((int) ptext.size() > avail) {
|
||||||
}
|
ptext = ptext.substr(ptext.size() - avail);
|
||||||
} else {
|
}
|
||||||
// Non-file prompts: simple concatenation and clip by terminal
|
msg += ptext;
|
||||||
msg += ptext;
|
}
|
||||||
}
|
} else {
|
||||||
|
// Non-file prompts: simple concatenation and clip by terminal
|
||||||
|
msg += ptext;
|
||||||
|
}
|
||||||
|
|
||||||
// Draw left-aligned, clipped to width
|
// Draw left-aligned, clipped to width
|
||||||
if (!msg.empty())
|
if (!msg.empty())
|
||||||
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
|
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
|
||||||
|
|
||||||
// End status rendering for prompt mode
|
// End status rendering for prompt mode
|
||||||
attroff(A_REVERSE);
|
attroff(A_REVERSE);
|
||||||
// Restore logical cursor position in content area
|
// Restore logical cursor position in content area
|
||||||
if (saved_cur_y >= 0 && saved_cur_x >= 0)
|
if (saved_cur_y >= 0 && saved_cur_x >= 0)
|
||||||
move(saved_cur_y, saved_cur_x);
|
move(saved_cur_y, saved_cur_x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build left segment
|
// Build left segment
|
||||||
std::string left;
|
std::string left;
|
||||||
@@ -346,10 +384,10 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
if (llen > 0)
|
if (llen > 0)
|
||||||
mvaddnstr(rows - 1, 0, left.c_str(), llen);
|
mvaddnstr(rows - 1, 0, left.c_str(), llen);
|
||||||
|
|
||||||
// Draw right, flush to end
|
// Draw right, flush to end
|
||||||
int rstart = std::max(0, cols - rlen);
|
int rstart = std::max(0, cols - rlen);
|
||||||
if (rlen > 0)
|
if (rlen > 0)
|
||||||
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
|
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
|
||||||
|
|
||||||
// Middle message
|
// Middle message
|
||||||
const std::string &msg = ed.Status();
|
const std::string &msg = ed.Status();
|
||||||
@@ -365,7 +403,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attroff(A_REVERSE);
|
attroff(A_REVERSE);
|
||||||
|
|
||||||
// Restore terminal cursor to the content position so a visible caret
|
// Restore terminal cursor to the content position so a visible caret
|
||||||
// remains in the editing area (not on the status line).
|
// remains in the editing area (not on the status line).
|
||||||
|
|||||||
303
UndoSystem.cc
303
UndoSystem.cc
@@ -5,79 +5,79 @@
|
|||||||
|
|
||||||
|
|
||||||
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
||||||
: buf_(&owner), tree_(tree) {}
|
: buf_(&owner), tree_(tree) {}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::Begin(UndoType type)
|
UndoSystem::Begin(UndoType type)
|
||||||
{
|
{
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("Begin");
|
debug_log("Begin");
|
||||||
#endif
|
#endif
|
||||||
// Reuse pending if batching conditions are met
|
// Reuse pending if batching conditions are met
|
||||||
const int row = static_cast<int>(buf_->Cury());
|
const int row = static_cast<int>(buf_->Cury());
|
||||||
const int col = static_cast<int>(buf_->Curx());
|
const int col = static_cast<int>(buf_->Curx());
|
||||||
if (tree_.pending && tree_.pending->type == type && tree_.pending->row == row) {
|
if (tree_.pending && tree_.pending->type == type && tree_.pending->row == row) {
|
||||||
if (type == UndoType::Delete) {
|
if (type == UndoType::Delete) {
|
||||||
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
|
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
|
||||||
// Forward delete: cursor stays at anchor col; keep batching when col == anchor
|
// Forward delete: cursor stays at anchor col; keep batching when col == anchor
|
||||||
const auto anchor = static_cast<std::size_t>(tree_.pending->col);
|
const auto anchor = static_cast<std::size_t>(tree_.pending->col);
|
||||||
if (anchor == static_cast<std::size_t>(col)) {
|
if (anchor == static_cast<std::size_t>(col)) {
|
||||||
pending_prepend_ = false;
|
pending_prepend_ = false;
|
||||||
return; // keep batching forward delete
|
return; // keep batching forward delete
|
||||||
}
|
}
|
||||||
// Backspace: cursor moved left by exactly one position relative to current anchor.
|
// Backspace: cursor moved left by exactly one position relative to current anchor.
|
||||||
// Extend batch by shifting anchor left and prepending the deleted byte.
|
// Extend batch by shifting anchor left and prepending the deleted byte.
|
||||||
if (static_cast<std::size_t>(col) + 1 == anchor) {
|
if (static_cast<std::size_t>(col) + 1 == anchor) {
|
||||||
tree_.pending->col = col;
|
tree_.pending->col = col;
|
||||||
pending_prepend_ = true;
|
pending_prepend_ = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
std::size_t expected = static_cast<std::size_t>(tree_.pending->col) + tree_.pending->text.
|
std::size_t expected = static_cast<std::size_t>(tree_.pending->col) + tree_.pending->text.
|
||||||
size();
|
size();
|
||||||
if (expected == static_cast<std::size_t>(col)) {
|
if (expected == static_cast<std::size_t>(col)) {
|
||||||
pending_prepend_ = false;
|
pending_prepend_ = false;
|
||||||
return; // keep batching
|
return; // keep batching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise commit any existing batch and start a new node
|
// Otherwise commit any existing batch and start a new node
|
||||||
commit();
|
commit();
|
||||||
auto *node = new UndoNode();
|
auto *node = new UndoNode();
|
||||||
node->type = type;
|
node->type = type;
|
||||||
node->row = row;
|
node->row = row;
|
||||||
node->col = col;
|
node->col = col;
|
||||||
node->child = nullptr;
|
node->child = nullptr;
|
||||||
node->next = nullptr;
|
node->next = nullptr;
|
||||||
tree_.pending = node;
|
tree_.pending = node;
|
||||||
pending_prepend_ = false;
|
pending_prepend_ = false;
|
||||||
|
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("Begin:new");
|
debug_log("Begin:new");
|
||||||
#endif
|
#endif
|
||||||
// Assert pending is detached from the tree
|
// Assert pending is detached from the tree
|
||||||
assert(tree_.pending && "pending must exist after Begin");
|
assert(tree_.pending && "pending must exist after Begin");
|
||||||
assert(tree_.pending != tree_.root);
|
assert(tree_.pending != tree_.root);
|
||||||
assert(tree_.pending != tree_.current);
|
assert(tree_.pending != tree_.current);
|
||||||
assert(tree_.pending != tree_.saved);
|
assert(tree_.pending != tree_.saved);
|
||||||
assert(!is_descendant(tree_.root, tree_.pending));
|
assert(!is_descendant(tree_.root, tree_.pending));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::Append(char ch)
|
UndoSystem::Append(char ch)
|
||||||
{
|
{
|
||||||
if (!tree_.pending)
|
if (!tree_.pending)
|
||||||
return;
|
return;
|
||||||
if (pending_prepend_ && tree_.pending->type == UndoType::Delete) {
|
if (pending_prepend_ && tree_.pending->type == UndoType::Delete) {
|
||||||
// Prepend for backspace so that text is in increasing column order
|
// Prepend for backspace so that text is in increasing column order
|
||||||
tree_.pending->text.insert(tree_.pending->text.begin(), ch);
|
tree_.pending->text.insert(tree_.pending->text.begin(), ch);
|
||||||
} else {
|
} else {
|
||||||
tree_.pending->text.push_back(ch);
|
tree_.pending->text.push_back(ch);
|
||||||
}
|
}
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("Append:ch");
|
debug_log("Append:ch");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,11 +85,11 @@ UndoSystem::Append(char ch)
|
|||||||
void
|
void
|
||||||
UndoSystem::Append(std::string_view text)
|
UndoSystem::Append(std::string_view text)
|
||||||
{
|
{
|
||||||
if (!tree_.pending)
|
if (!tree_.pending)
|
||||||
return;
|
return;
|
||||||
tree_.pending->text.append(text.data(), text.size());
|
tree_.pending->text.append(text.data(), text.size());
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("Append:sv");
|
debug_log("Append:sv");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +98,10 @@ void
|
|||||||
UndoSystem::commit()
|
UndoSystem::commit()
|
||||||
{
|
{
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("commit:enter");
|
debug_log("commit:enter");
|
||||||
#endif
|
#endif
|
||||||
if (!tree_.pending)
|
if (!tree_.pending)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// If we have redo branches from current, discard them (non-linear behavior)
|
// If we have redo branches from current, discard them (non-linear behavior)
|
||||||
if (tree_.current && tree_.current->child) {
|
if (tree_.current && tree_.current->child) {
|
||||||
@@ -127,31 +127,31 @@ UndoSystem::commit()
|
|||||||
tree_.current->child = tree_.pending;
|
tree_.current->child = tree_.pending;
|
||||||
tree_.current = tree_.pending;
|
tree_.current = tree_.pending;
|
||||||
}
|
}
|
||||||
tree_.pending = nullptr;
|
tree_.pending = nullptr;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("commit:done");
|
debug_log("commit:done");
|
||||||
#endif
|
#endif
|
||||||
// post-conditions
|
// post-conditions
|
||||||
assert(tree_.pending == nullptr && "pending must be cleared after commit");
|
assert(tree_.pending == nullptr && "pending must be cleared after commit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::undo()
|
UndoSystem::undo()
|
||||||
{
|
{
|
||||||
// Close any pending batch
|
// Close any pending batch
|
||||||
commit();
|
commit();
|
||||||
if (!tree_.current)
|
if (!tree_.current)
|
||||||
return;
|
return;
|
||||||
UndoNode *parent = find_parent(tree_.root, tree_.current);
|
UndoNode *parent = find_parent(tree_.root, tree_.current);
|
||||||
UndoNode *node = tree_.current;
|
UndoNode *node = tree_.current;
|
||||||
// Apply inverse of current node
|
// Apply inverse of current node
|
||||||
apply(node, -1);
|
apply(node, -1);
|
||||||
tree_.current = parent;
|
tree_.current = parent;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("undo");
|
debug_log("undo");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,24 +159,24 @@ UndoSystem::undo()
|
|||||||
void
|
void
|
||||||
UndoSystem::redo()
|
UndoSystem::redo()
|
||||||
{
|
{
|
||||||
// Redo next child along current timeline
|
// Redo next child along current timeline
|
||||||
if (tree_.pending) {
|
if (tree_.pending) {
|
||||||
// If app added pending edits, finalize them before redo chain
|
// If app added pending edits, finalize them before redo chain
|
||||||
commit();
|
commit();
|
||||||
}
|
}
|
||||||
UndoNode *next = nullptr;
|
UndoNode *next = nullptr;
|
||||||
if (!tree_.current) {
|
if (!tree_.current) {
|
||||||
next = tree_.root; // if nothing yet, try applying first node
|
next = tree_.root; // if nothing yet, try applying first node
|
||||||
} else {
|
} else {
|
||||||
next = tree_.current->child;
|
next = tree_.current->child;
|
||||||
}
|
}
|
||||||
if (!next)
|
if (!next)
|
||||||
return;
|
return;
|
||||||
apply(next, +1);
|
apply(next, +1);
|
||||||
tree_.current = next;
|
tree_.current = next;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("redo");
|
debug_log("redo");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,10 +184,10 @@ UndoSystem::redo()
|
|||||||
void
|
void
|
||||||
UndoSystem::mark_saved()
|
UndoSystem::mark_saved()
|
||||||
{
|
{
|
||||||
tree_.saved = tree_.current;
|
tree_.saved = tree_.current;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("mark_saved");
|
debug_log("mark_saved");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +195,12 @@ UndoSystem::mark_saved()
|
|||||||
void
|
void
|
||||||
UndoSystem::discard_pending()
|
UndoSystem::discard_pending()
|
||||||
{
|
{
|
||||||
if (tree_.pending) {
|
if (tree_.pending) {
|
||||||
delete tree_.pending;
|
delete tree_.pending;
|
||||||
tree_.pending = nullptr;
|
tree_.pending = nullptr;
|
||||||
}
|
}
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("discard_pending");
|
debug_log("discard_pending");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,16 +208,16 @@ UndoSystem::discard_pending()
|
|||||||
void
|
void
|
||||||
UndoSystem::clear()
|
UndoSystem::clear()
|
||||||
{
|
{
|
||||||
if (tree_.root) {
|
if (tree_.root) {
|
||||||
free_node(tree_.root);
|
free_node(tree_.root);
|
||||||
}
|
}
|
||||||
if (tree_.pending) {
|
if (tree_.pending) {
|
||||||
delete tree_.pending;
|
delete tree_.pending;
|
||||||
}
|
}
|
||||||
tree_.root = tree_.current = tree_.saved = tree_.pending = nullptr;
|
tree_.root = tree_.current = tree_.saved = tree_.pending = nullptr;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
debug_log("clear");
|
debug_log("clear");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,62 +326,73 @@ UndoSystem::find_parent(UndoNode *from, UndoNode *target)
|
|||||||
void
|
void
|
||||||
UndoSystem::update_dirty_flag()
|
UndoSystem::update_dirty_flag()
|
||||||
{
|
{
|
||||||
// dirty if current != saved
|
// dirty if current != saved
|
||||||
bool dirty = (tree_.current != tree_.saved);
|
bool dirty = (tree_.current != tree_.saved);
|
||||||
buf_->SetDirty(dirty);
|
buf_->SetDirty(dirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::UpdateBufferReference(Buffer &new_buf)
|
UndoSystem::UpdateBufferReference(Buffer &new_buf)
|
||||||
{
|
{
|
||||||
buf_ = &new_buf;
|
buf_ = &new_buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---- Debug helpers ----
|
// ---- Debug helpers ----
|
||||||
const char *
|
const char *
|
||||||
UndoSystem::type_str(UndoType t)
|
UndoSystem::type_str(UndoType t)
|
||||||
{
|
{
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case UndoType::Insert: return "Insert";
|
case UndoType::Insert:
|
||||||
case UndoType::Delete: return "Delete";
|
return "Insert";
|
||||||
case UndoType::Paste: return "Paste";
|
case UndoType::Delete:
|
||||||
case UndoType::Newline: return "Newline";
|
return "Delete";
|
||||||
case UndoType::DeleteRow: return "DeleteRow";
|
case UndoType::Paste:
|
||||||
}
|
return "Paste";
|
||||||
return "?";
|
case UndoType::Newline:
|
||||||
|
return "Newline";
|
||||||
|
case UndoType::DeleteRow:
|
||||||
|
return "DeleteRow";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
|
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
|
||||||
{
|
{
|
||||||
if (!root || !target) return false;
|
if (!root || !target)
|
||||||
if (root == target) return true;
|
return false;
|
||||||
for (UndoNode *child = root->child; child != nullptr; child = child->next) {
|
if (root == target)
|
||||||
if (is_descendant(child, target)) return true;
|
return true;
|
||||||
}
|
for (UndoNode *child = root->child; child != nullptr; child = child->next) {
|
||||||
return false;
|
if (is_descendant(child, target))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::debug_log(const char *op) const
|
UndoSystem::debug_log(const char *op) const
|
||||||
{
|
{
|
||||||
#ifdef KTE_UNDO_DEBUG
|
#ifdef KTE_UNDO_DEBUG
|
||||||
int row = static_cast<int>(buf_->Cury());
|
int row = static_cast<int>(buf_->Cury());
|
||||||
int col = static_cast<int>(buf_->Curx());
|
int col = static_cast<int>(buf_->Curx());
|
||||||
const UndoNode *p = tree_.pending;
|
const UndoNode *p = tree_.pending;
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[UNDO] %s cur=(%d,%d) pending=%p t=%s r=%d c=%d nlen=%zu current=%p saved=%p\n",
|
"[UNDO] %s cur=(%d,%d) pending=%p t=%s r=%d c=%d nlen=%zu current=%p saved=%p\n",
|
||||||
op,
|
op,
|
||||||
row, col,
|
row, col,
|
||||||
(const void*)p,
|
(const void *) p,
|
||||||
p ? type_str(p->type) : "-",
|
p ? type_str(p->type) : "-",
|
||||||
p ? p->row : -1,
|
p ? p->row : -1,
|
||||||
p ? p->col : -1,
|
p ? p->col : -1,
|
||||||
p ? p->text.size() : 0,
|
p ? p->text.size() : 0,
|
||||||
(void*)tree_.current,
|
(void *) tree_.current,
|
||||||
(void*)tree_.saved);
|
(void *) tree_.saved);
|
||||||
#else
|
#else
|
||||||
(void)op;
|
(void) op;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
30
UndoSystem.h
30
UndoSystem.h
@@ -12,7 +12,7 @@ class Buffer;
|
|||||||
|
|
||||||
class UndoSystem {
|
class UndoSystem {
|
||||||
public:
|
public:
|
||||||
explicit UndoSystem(Buffer &owner, UndoTree &tree);
|
explicit UndoSystem(Buffer &owner, UndoTree &tree);
|
||||||
|
|
||||||
void Begin(UndoType type);
|
void Begin(UndoType type);
|
||||||
|
|
||||||
@@ -30,28 +30,30 @@ public:
|
|||||||
|
|
||||||
void discard_pending();
|
void discard_pending();
|
||||||
|
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
void UpdateBufferReference(Buffer &new_buf);
|
void UpdateBufferReference(Buffer &new_buf);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void apply(const UndoNode *node, int direction); // +1 redo, -1 undo
|
void apply(const UndoNode *node, int direction); // +1 redo, -1 undo
|
||||||
void free_node(UndoNode *node);
|
void free_node(UndoNode *node);
|
||||||
|
|
||||||
void free_branch(UndoNode *node); // frees redo siblings only
|
void free_branch(UndoNode *node); // frees redo siblings only
|
||||||
UndoNode *find_parent(UndoNode *from, UndoNode *target);
|
UndoNode *find_parent(UndoNode *from, UndoNode *target);
|
||||||
|
|
||||||
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
|
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
|
||||||
void debug_log(const char *op) const;
|
void debug_log(const char *op) const;
|
||||||
static const char *type_str(UndoType t);
|
|
||||||
static bool is_descendant(UndoNode *root, const UndoNode *target);
|
|
||||||
|
|
||||||
void update_dirty_flag();
|
static const char *type_str(UndoType t);
|
||||||
|
|
||||||
|
static bool is_descendant(UndoNode *root, const UndoNode *target);
|
||||||
|
|
||||||
|
void update_dirty_flag();
|
||||||
|
|
||||||
Buffer *buf_;
|
Buffer *buf_;
|
||||||
UndoTree &tree_;
|
UndoTree &tree_;
|
||||||
// Internal hint for Delete batching: whether next Append() should prepend
|
// Internal hint for Delete batching: whether next Append() should prepend
|
||||||
bool pending_prepend_ = false;
|
bool pending_prepend_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_UNDOSYSTEM_H
|
#endif // KTE_UNDOSYSTEM_H
|
||||||
|
|||||||
74
docs/kge.1
74
docs/kge.1
@@ -1,7 +1,7 @@
|
|||||||
.\" kge(1) — Kyle's Graphical Editor (GUI-first)
|
.\" kge(1) — Kyle's Graphical Editor (GUI-first)
|
||||||
.\"
|
.\"
|
||||||
.\" Project homepage: https://github.com/wntrmute/kte
|
.\" Project homepage: https://github.com/wntrmute/kte
|
||||||
.TH KGE 1 "2025-11-30" "kte 0.1.0" "User Commands"
|
.TH KGE 1 "2025-12-01" "kte 0.1.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
kge \- Kyle's Graphical Editor (GUI-first)
|
kge \- Kyle's Graphical Editor (GUI-first)
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -52,11 +52,8 @@ tree for the canonical reference and notes:
|
|||||||
.PP
|
.PP
|
||||||
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
||||||
.TP
|
.TP
|
||||||
.B C-k BACKSPACE
|
.B C-k '
|
||||||
Delete from the cursor to the beginning of the line.
|
Toggle read-only for the current buffer.
|
||||||
.TP
|
|
||||||
.B C-k SPACE
|
|
||||||
Toggle the mark.
|
|
||||||
.TP
|
.TP
|
||||||
.B C-k -
|
.B C-k -
|
||||||
If the mark is set, unindent the region.
|
If the mark is set, unindent the region.
|
||||||
@@ -64,6 +61,9 @@ If the mark is set, unindent the region.
|
|||||||
.B C-k =
|
.B C-k =
|
||||||
If the mark is set, indent the region.
|
If the mark is set, indent the region.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k ;
|
||||||
|
Open the generic command prompt (": ").
|
||||||
|
.TP
|
||||||
.B C-k a
|
.B C-k a
|
||||||
Set the mark at the beginning of the file, then jump to the end of the file.
|
Set the mark at the beginning of the file, then jump to the end of the file.
|
||||||
.TP
|
.TP
|
||||||
@@ -80,7 +80,7 @@ Delete from the cursor to the end of the line.
|
|||||||
Delete the entire line.
|
Delete the entire line.
|
||||||
.TP
|
.TP
|
||||||
.B C-k e
|
.B C-k e
|
||||||
Edit a new file.
|
Edit (open) a new file.
|
||||||
.TP
|
.TP
|
||||||
.B C-k f
|
.B C-k f
|
||||||
Flush the kill ring.
|
Flush the kill ring.
|
||||||
@@ -88,14 +88,20 @@ Flush the kill ring.
|
|||||||
.B C-k g
|
.B C-k g
|
||||||
Go to a specific line.
|
Go to a specific line.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k h
|
||||||
|
Show the built-in help (+HELP+ buffer).
|
||||||
|
.TP
|
||||||
.B C-k j
|
.B C-k j
|
||||||
Jump to the mark.
|
Jump to the mark.
|
||||||
.TP
|
.TP
|
||||||
.B C-k l
|
.B C-k l
|
||||||
Reload the current buffer from disk.
|
Reload the current buffer from disk.
|
||||||
.TP
|
.TP
|
||||||
.B C-k m
|
.B C-k n
|
||||||
Run make(1), reporting success or failure.
|
Switch to the previous buffer.
|
||||||
|
.TP
|
||||||
|
.B C-k o
|
||||||
|
Change working directory (prompt).
|
||||||
.TP
|
.TP
|
||||||
.B C-k p
|
.B C-k p
|
||||||
Switch to the next buffer.
|
Switch to the next buffer.
|
||||||
@@ -106,14 +112,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
|
|||||||
.B C-k C-q
|
.B C-k C-q
|
||||||
Immediately exit the editor.
|
Immediately exit the editor.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k r
|
||||||
|
Redo changes.
|
||||||
|
.TP
|
||||||
.B C-k s
|
.B C-k s
|
||||||
Save the file, prompting for a filename if needed.
|
Save the file, prompting for a filename if needed.
|
||||||
.TP
|
.TP
|
||||||
.B C-k u
|
.B C-k u
|
||||||
Undo.
|
Undo.
|
||||||
.TP
|
.TP
|
||||||
.B C-k r
|
.B C-k v
|
||||||
Redo changes.
|
Toggle visual file picker (GUI).
|
||||||
|
.TP
|
||||||
|
.B C-k w
|
||||||
|
Show the current working directory.
|
||||||
.TP
|
.TP
|
||||||
.B C-k x
|
.B C-k x
|
||||||
Save the file and exit. Also C-k C-x.
|
Save the file and exit. Also C-k C-x.
|
||||||
@@ -121,23 +133,50 @@ Save the file and exit. Also C-k C-x.
|
|||||||
.B C-k y
|
.B C-k y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
.B C-k \e
|
.B C-k C-x
|
||||||
Dump core.
|
Save the file and exit.
|
||||||
|
|
||||||
.SS Other keybindings
|
.SS Other keybindings
|
||||||
.TP
|
.TP
|
||||||
.B C-g
|
.B C-g
|
||||||
Cancel the current operation.
|
Cancel the current operation.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-a
|
||||||
|
Move to the beginning of the line.
|
||||||
|
.TP
|
||||||
|
.B C-e
|
||||||
|
Move to the end of the line.
|
||||||
|
.TP
|
||||||
|
.B C-b
|
||||||
|
Move left.
|
||||||
|
.TP
|
||||||
|
.B C-f
|
||||||
|
Move right.
|
||||||
|
.TP
|
||||||
|
.B C-n
|
||||||
|
Move down.
|
||||||
|
.TP
|
||||||
|
.B C-p
|
||||||
|
Move up.
|
||||||
|
.TP
|
||||||
.B C-l
|
.B C-l
|
||||||
Refresh the display.
|
Refresh the display.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-d
|
||||||
|
Delete the character at the cursor.
|
||||||
|
.TP
|
||||||
.B C-r
|
.B C-r
|
||||||
Regex search.
|
Regex search.
|
||||||
.TP
|
.TP
|
||||||
.B C-s
|
.B C-s
|
||||||
Incremental find.
|
Incremental find.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-t
|
||||||
|
Regex search and replace.
|
||||||
|
.TP
|
||||||
|
.B C-h
|
||||||
|
Search and replace.
|
||||||
|
.TP
|
||||||
.B C-u
|
.B C-u
|
||||||
Universal argument. C-u followed by numbers will repeat an operation n times.
|
Universal argument. C-u followed by numbers will repeat an operation n times.
|
||||||
.TP
|
.TP
|
||||||
@@ -147,6 +186,15 @@ Kill the region if the mark is set.
|
|||||||
.B C-y
|
.B C-y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
|
.B ESC <
|
||||||
|
Move to the beginning of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC >
|
||||||
|
Move to the end of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC m
|
||||||
|
Toggle the mark.
|
||||||
|
.TP
|
||||||
.B ESC BACKSPACE
|
.B ESC BACKSPACE
|
||||||
Delete the previous word.
|
Delete the previous word.
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
100
docs/kte.1
100
docs/kte.1
@@ -1,7 +1,7 @@
|
|||||||
.\" kte(1) — Kyle's Text Editor (terminal-first)
|
.\" kte(1) — Kyle's Text Editor (terminal-first)
|
||||||
.\"
|
.\"
|
||||||
.\" Project homepage: https://github.com/wntrmute/kte
|
.\" Project homepage: https://github.com/wntrmute/kte
|
||||||
.TH KTE 1 "2025-11-30" "kte 0.1.0" "User Commands"
|
.TH KTE 1 "2025-12-01" "kte 0.1.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
kte \- Kyle's Text Editor (terminal-first)
|
kte \- Kyle's Text Editor (terminal-first)
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -57,11 +57,8 @@ in the source tree for the canonical reference and notes.
|
|||||||
.PP
|
.PP
|
||||||
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
|
||||||
.TP
|
.TP
|
||||||
.B C-k BACKSPACE
|
.B C-k '
|
||||||
Delete from the cursor to the beginning of the line.
|
Toggle read-only for the current buffer.
|
||||||
.TP
|
|
||||||
.B C-k SPACE
|
|
||||||
Toggle the mark.
|
|
||||||
.TP
|
.TP
|
||||||
.B C-k -
|
.B C-k -
|
||||||
If the mark is set, unindent the region.
|
If the mark is set, unindent the region.
|
||||||
@@ -69,6 +66,9 @@ If the mark is set, unindent the region.
|
|||||||
.B C-k =
|
.B C-k =
|
||||||
If the mark is set, indent the region.
|
If the mark is set, indent the region.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k ;
|
||||||
|
Open the generic command prompt (": ").
|
||||||
|
.TP
|
||||||
.B C-k a
|
.B C-k a
|
||||||
Set the mark at the beginning of the file, then jump to the end of the file.
|
Set the mark at the beginning of the file, then jump to the end of the file.
|
||||||
.TP
|
.TP
|
||||||
@@ -85,7 +85,7 @@ Delete from the cursor to the end of the line.
|
|||||||
Delete the entire line.
|
Delete the entire line.
|
||||||
.TP
|
.TP
|
||||||
.B C-k e
|
.B C-k e
|
||||||
Edit a new file.
|
Edit (open) a new file.
|
||||||
.TP
|
.TP
|
||||||
.B C-k f
|
.B C-k f
|
||||||
Flush the kill ring.
|
Flush the kill ring.
|
||||||
@@ -93,14 +93,20 @@ Flush the kill ring.
|
|||||||
.B C-k g
|
.B C-k g
|
||||||
Go to a specific line.
|
Go to a specific line.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k h
|
||||||
|
Show the built-in help (+HELP+ buffer).
|
||||||
|
.TP
|
||||||
.B C-k j
|
.B C-k j
|
||||||
Jump to the mark.
|
Jump to the mark.
|
||||||
.TP
|
.TP
|
||||||
.B C-k l
|
.B C-k l
|
||||||
Reload the current buffer from disk.
|
Reload the current buffer from disk.
|
||||||
.TP
|
.TP
|
||||||
.B C-k m
|
.B C-k n
|
||||||
Run make(1), reporting success or failure.
|
Switch to the previous buffer.
|
||||||
|
.TP
|
||||||
|
.B C-k o
|
||||||
|
Change working directory (prompt).
|
||||||
.TP
|
.TP
|
||||||
.B C-k p
|
.B C-k p
|
||||||
Switch to the next buffer.
|
Switch to the next buffer.
|
||||||
@@ -111,14 +117,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
|
|||||||
.B C-k C-q
|
.B C-k C-q
|
||||||
Immediately exit the editor.
|
Immediately exit the editor.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-k r
|
||||||
|
Redo changes.
|
||||||
|
.TP
|
||||||
.B C-k s
|
.B C-k s
|
||||||
Save the file, prompting for a filename if needed.
|
Save the file, prompting for a filename if needed.
|
||||||
.TP
|
.TP
|
||||||
.B C-k u
|
.B C-k u
|
||||||
Undo.
|
Undo.
|
||||||
.TP
|
.TP
|
||||||
.B C-k r
|
.B C-k v
|
||||||
Redo changes.
|
Toggle visual file picker (GUI).
|
||||||
|
.TP
|
||||||
|
.B C-k w
|
||||||
|
Show the current working directory.
|
||||||
.TP
|
.TP
|
||||||
.B C-k x
|
.B C-k x
|
||||||
Save the file and exit. Also C-k C-x.
|
Save the file and exit. Also C-k C-x.
|
||||||
@@ -126,23 +138,76 @@ Save the file and exit. Also C-k C-x.
|
|||||||
.B C-k y
|
.B C-k y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
.B C-k \e
|
.B C-k C-x
|
||||||
Dump core.
|
Save the file and exit.
|
||||||
|
|
||||||
|
.SH GUI APPEARANCE
|
||||||
|
When running the GUI frontend, you can control appearance via the generic
|
||||||
|
command prompt (type "C-k ;" then enter commands):
|
||||||
|
.TP
|
||||||
|
.B : theme NAME
|
||||||
|
Set the GUI theme. Available names: "nord", "gruvbox", "plan9", "solarized", "eink".
|
||||||
|
Compatibility aliases are also accepted: "gruvbox-dark", "gruvbox-light",
|
||||||
|
"solarized-dark", "solarized-light", "eink-dark", "eink-light".
|
||||||
|
.TP
|
||||||
|
.B : background MODE
|
||||||
|
Set background mode for supported themes. MODE is either "light" or "dark".
|
||||||
|
Themes that respond to background: eink, gruvbox, solarized. The
|
||||||
|
"nord" and "plan9" themes do not vary with background.
|
||||||
|
|
||||||
|
.SH CONFIGURATION
|
||||||
|
The GUI reads a simple configuration file at
|
||||||
|
~/.config/kte/kge.ini. Recognized keys include:
|
||||||
|
.IP "fullscreen=on|off"
|
||||||
|
.IP "columns=NUM"
|
||||||
|
.IP "rows=NUM"
|
||||||
|
.IP "font_size=NUM"
|
||||||
|
.IP "theme=NAME"
|
||||||
|
.IP "background=light|dark"
|
||||||
|
The theme name accepts the values listed above. The background key controls
|
||||||
|
light/dark variants when the selected theme supports it.
|
||||||
|
|
||||||
.SS Other keybindings
|
.SS Other keybindings
|
||||||
.TP
|
.TP
|
||||||
.B C-g
|
.B C-g
|
||||||
Cancel the current operation.
|
Cancel the current operation.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-a
|
||||||
|
Move to the beginning of the line.
|
||||||
|
.TP
|
||||||
|
.B C-e
|
||||||
|
Move to the end of the line.
|
||||||
|
.TP
|
||||||
|
.B C-b
|
||||||
|
Move left.
|
||||||
|
.TP
|
||||||
|
.B C-f
|
||||||
|
Move right.
|
||||||
|
.TP
|
||||||
|
.B C-n
|
||||||
|
Move down.
|
||||||
|
.TP
|
||||||
|
.B C-p
|
||||||
|
Move up.
|
||||||
|
.TP
|
||||||
.B C-l
|
.B C-l
|
||||||
Refresh the display.
|
Refresh the display.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-d
|
||||||
|
Delete the character at the cursor.
|
||||||
|
.TP
|
||||||
.B C-r
|
.B C-r
|
||||||
Regex search.
|
Regex search.
|
||||||
.TP
|
.TP
|
||||||
.B C-s
|
.B C-s
|
||||||
Incremental find.
|
Incremental find.
|
||||||
.TP
|
.TP
|
||||||
|
.B C-t
|
||||||
|
Regex search and replace.
|
||||||
|
.TP
|
||||||
|
.B C-h
|
||||||
|
Search and replace.
|
||||||
|
.TP
|
||||||
.B C-u
|
.B C-u
|
||||||
Universal argument. C-u followed by numbers will repeat an operation n times.
|
Universal argument. C-u followed by numbers will repeat an operation n times.
|
||||||
.TP
|
.TP
|
||||||
@@ -152,6 +217,15 @@ Kill the region if the mark is set.
|
|||||||
.B C-y
|
.B C-y
|
||||||
Yank the kill ring.
|
Yank the kill ring.
|
||||||
.TP
|
.TP
|
||||||
|
.B ESC <
|
||||||
|
Move to the beginning of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC >
|
||||||
|
Move to the end of the file.
|
||||||
|
.TP
|
||||||
|
.B ESC m
|
||||||
|
Toggle the mark.
|
||||||
|
.TP
|
||||||
.B ESC BACKSPACE
|
.B ESC BACKSPACE
|
||||||
Delete the previous word.
|
Delete the previous word.
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
102
docs/syntax on.md
Normal file
102
docs/syntax on.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
### Objective
|
||||||
|
Introduce fast, minimal‑dependency syntax highlighting to kte, consistent with current architecture (Editor/Buffer + GUI/Terminal renderers), preserving ke UX and performance.
|
||||||
|
|
||||||
|
### Guiding principles
|
||||||
|
- Keep core small and fast; no heavy deps (C++17 only).
|
||||||
|
- Start simple (stateless line regex), evolve incrementally (stateful, caching).
|
||||||
|
- Work in both Terminal (ncurses) and GUI (ImGui) with consistent token classes and theme mapping.
|
||||||
|
- Integrate without disrupting existing search highlight, selection, or cursor rendering.
|
||||||
|
|
||||||
|
### Scope of v1
|
||||||
|
- Languages: plain text (off), C/C++ minimal set (keywords, types, strings, chars, comments, numbers, preprocessor).
|
||||||
|
- Stateless per‑line highlighting; handle single‑line comments and strings; defer multi‑line state to v2.
|
||||||
|
- Toggle: `:syntax on|off` and per‑buffer filetype selection.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
1. Core types (new):
|
||||||
|
- `enum class TokenKind { Default, Keyword, Type, String, Char, Comment, Number, Preproc, Constant, Function, Operator, Punctuation, Identifier, Whitespace, Error };`
|
||||||
|
- `struct HighlightSpan { int col_start; int col_end; TokenKind kind; };` // 0‑based columns in buffer indices per rendered line
|
||||||
|
- `struct LineHighlight { std::vector<HighlightSpan> spans; uint64_t version; };`
|
||||||
|
|
||||||
|
2. Interfaces (new):
|
||||||
|
- `class LanguageHighlighter { public: virtual ~LanguageHighlighter() = default; virtual void HighlightLine(const Buffer& buf, int row, std::vector<HighlightSpan>& out) const = 0; virtual bool Stateful() const { return false; } };`
|
||||||
|
- `class HighlighterEngine { public: void SetHighlighter(std::unique_ptr<LanguageHighlighter>); const LineHighlight& GetLine(const Buffer&, int row, uint64_t buf_version); void InvalidateFrom(int row); };`
|
||||||
|
- `class HighlighterRegistry { public: static const LanguageHighlighter& ForFiletype(std::string_view ft); static std::string DetectForPath(std::string_view path, std::string_view first_line); };`
|
||||||
|
|
||||||
|
3. Editor/Buffer integration:
|
||||||
|
- Per‑Buffer settings: `bool syntax_enabled; std::string filetype; std::unique_ptr<HighlighterEngine> highlighter;`
|
||||||
|
- Buffer emits a monotonically increasing `version` on edit; renderers request line highlights by `(row, version)`.
|
||||||
|
- Invalidate cache minimally on edits (v1: current line only; v2: from current line down when stateful constructs present).
|
||||||
|
|
||||||
|
### Rendering integration
|
||||||
|
- TerminalRenderer/GUIRenderer changes:
|
||||||
|
- During line rendering, query `Editor.CurrentBuffer()->highlighter->GetLine(buf, row, buf_version)` to obtain spans.
|
||||||
|
- Apply token styles while drawing glyph runs.
|
||||||
|
- Z‑order and blending:
|
||||||
|
1) Backgrounds (e.g., selection, search highlight rectangles)
|
||||||
|
2) Text with syntax colors
|
||||||
|
3) Cursor/IME decorations
|
||||||
|
- Search highlights must remain visible over syntax colors:
|
||||||
|
- Terminal: combine color/attr with reverse/bold for search; if color conflicts, prefer search.
|
||||||
|
- GUI: draw semi‑transparent rects behind text (already present); keep syntax color for text.
|
||||||
|
|
||||||
|
### Theme and color mapping
|
||||||
|
- Extend `GUITheme.h` with a `SyntaxPalette` mapping `TokenKind -> ImVec4 ink` (and optional background tint for comments/strings disabled by default). Provide default Light/Dark palettes.
|
||||||
|
- Terminal: map `TokenKind` to ncurses color pairs where available; degrade gracefully on 8/16‑color terminals (e.g., comments=dim, keywords=bold, strings=yellow/green if available).
|
||||||
|
|
||||||
|
### Language detection
|
||||||
|
- v1: by file extension; allow manual `:set filetype=<lang>`.
|
||||||
|
- v2: add shebang detection for scripts, simple modelines (optional).
|
||||||
|
|
||||||
|
### Commands/UX
|
||||||
|
- `:syntax on|off` — global default; buffer inherits on open.
|
||||||
|
- `:set filetype=<lang>` — per‑buffer override.
|
||||||
|
- `:syntax reload` — rebuild patterns/themes.
|
||||||
|
- Status line shows filetype and syntax state when changed.
|
||||||
|
|
||||||
|
### Implementation plan (phased)
|
||||||
|
1. Phase 1 — Minimal regex highlighter for C/C++
|
||||||
|
- Implement `CppRegexHighlighter : LanguageHighlighter` with precompiled `std::regex` (or hand‑rolled simple scanners to avoid regex backtracking). Classes: line comment `//…`, block comment start `/*` (no state), string `"…"`, char `'…'` (no multiline), numbers, keywords/types, preprocessor `^\s*#\w+`.
|
||||||
|
- Add `HighlighterEngine` with a simple per‑row cache keyed by `(row, buf_version)`; no background worker.
|
||||||
|
- Integrate into both renderers; add palette to `GUITheme.h`; add terminal color selection.
|
||||||
|
- Add commands.
|
||||||
|
|
||||||
|
2. Phase 2 — Stateful constructs and more languages
|
||||||
|
- Add state machine for multiline comments `/*…*/` and multiline strings (C++11 raw strings), with invalidation from edit line downward until state stabilizes.
|
||||||
|
- Add simple highlighters: JSON (strings, numbers, booleans, null, punctuation), Markdown (headers/emphasis/code fences), Shell (comments, strings, keywords), Go (types, constants, keywords), Python (strings, comments, keywords), Rust (strings, comments, keywords), Lisp (comments, strings, keywords),.
|
||||||
|
- Filetype detection by extension + shebang.
|
||||||
|
|
||||||
|
3. Phase 3 — Performance and caching
|
||||||
|
- Viewport‑first highlighting: compute only visible rows each frame; background task warms cache around viewport.
|
||||||
|
- Reuse span buffers, avoid allocations; small‑vector optimization if needed.
|
||||||
|
- Bench with large files; ensure O(n_visible) cost per frame.
|
||||||
|
|
||||||
|
4. Phase 4 — Extensibility
|
||||||
|
- Public registration API for external highlighters.
|
||||||
|
- Optional Tree‑sitter adapter behind a compile flag (off by default) to keep dependencies minimal.
|
||||||
|
|
||||||
|
### Data flow (per frame)
|
||||||
|
- Renderer asks Editor for Buffer and viewport rows.
|
||||||
|
- For each row: `engine.GetLine(buf, row, buf.version)` → spans.
|
||||||
|
- Renderer emits runs with style from `SyntaxPalette[kind]`.
|
||||||
|
- Search highlights are applied as separate background rectangles (GUI) or attribute toggles (Terminal), not overriding text color.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Unit tests for tokenization per language: golden inputs → spans.
|
||||||
|
- Fuzz/edge cases: escaped quotes, numeric literals, preprocessor lines.
|
||||||
|
- Renderer tests with `TestRenderer` asserting the sequence of style changes for a line.
|
||||||
|
- Performance tests: highlight 1k visible lines repeatedly; assert time under threshold.
|
||||||
|
|
||||||
|
### Risks and mitigations
|
||||||
|
- Regex backtracking/perf: prefer linear scans; precompute keyword tables; avoid nested regex.
|
||||||
|
- Terminal color limitations: feature‑detect colors; provide bold/dim fallbacks.
|
||||||
|
- Stateful correctness: invalidate conservatively (from edit line downward) and cap work per frame.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- New files: `Highlight.h/.cc`, `HighlighterEngine.h/.cc`, `LanguageHighlighter.h`, `CppHighlighter.h/.cc`, optional `HighlighterRegistry.h/.cc`.
|
||||||
|
- Renderer updates: `GUIRenderer.cc`, `TerminalRenderer.cc` to consume spans.
|
||||||
|
- Theming: `GUITheme.h` additions for syntax colors.
|
||||||
|
- Editor/Buffer: per‑buffer syntax settings and highlighter handle.
|
||||||
|
- Commands in `Command.cc` and help text updates.
|
||||||
|
- Docs: README/ROADMAP update and a brief `docs/syntax.md`.
|
||||||
|
- Tests: unit and renderer golden tests.
|
||||||
460
test_undo.cc
460
test_undo.cc
@@ -47,14 +47,14 @@ main()
|
|||||||
// Initialize cursor to (0,0) explicitly
|
// Initialize cursor to (0,0) explicitly
|
||||||
buf->SetCursor(0, 0);
|
buf->SetCursor(0, 0);
|
||||||
|
|
||||||
std::cout << "test_undo: Testing undo/redo system\n";
|
std::cout << "test_undo: Testing undo/redo system\n";
|
||||||
std::cout << "====================================\n\n";
|
std::cout << "====================================\n\n";
|
||||||
|
|
||||||
bool running = true;
|
bool running = true;
|
||||||
|
|
||||||
// Test 1: Insert text and verify buffer contains expected text
|
// Test 1: Insert text and verify buffer contains expected text
|
||||||
std::cout << "Test 1: Insert text 'Hello'\n";
|
std::cout << "Test 1: Insert text 'Hello'\n";
|
||||||
frontend.Input().QueueText("Hello");
|
frontend.Input().QueueText("Hello");
|
||||||
|
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
@@ -66,9 +66,9 @@ main()
|
|||||||
std::cout << " Buffer content: '" << line_after_insert << "'\n";
|
std::cout << " Buffer content: '" << line_after_insert << "'\n";
|
||||||
std::cout << " ✓ Text insertion verified\n\n";
|
std::cout << " ✓ Text insertion verified\n\n";
|
||||||
|
|
||||||
// Test 2: Undo insertion - text should be removed
|
// Test 2: Undo insertion - text should be removed
|
||||||
std::cout << "Test 2: Undo insertion\n";
|
std::cout << "Test 2: Undo insertion\n";
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
@@ -80,9 +80,9 @@ main()
|
|||||||
std::cout << " Buffer content: '" << line_after_undo << "'\n";
|
std::cout << " Buffer content: '" << line_after_undo << "'\n";
|
||||||
std::cout << " ✓ Undo successful - text removed\n\n";
|
std::cout << " ✓ Undo successful - text removed\n\n";
|
||||||
|
|
||||||
// Test 3: Redo insertion - text should be restored
|
// Test 3: Redo insertion - text should be restored
|
||||||
std::cout << "Test 3: Redo insertion\n";
|
std::cout << "Test 3: Redo insertion\n";
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
@@ -94,242 +94,242 @@ main()
|
|||||||
std::cout << " Buffer content: '" << line_after_redo << "'\n";
|
std::cout << " Buffer content: '" << line_after_redo << "'\n";
|
||||||
std::cout << " ✓ Redo successful - text restored\n\n";
|
std::cout << " ✓ Redo successful - text restored\n\n";
|
||||||
|
|
||||||
// Test 4: Branching behavior – redo is discarded after new edits
|
// Test 4: Branching behavior – redo is discarded after new edits
|
||||||
std::cout << "Test 4: Branching behavior (redo discarded after new edits)\n";
|
std::cout << "Test 4: Branching behavior (redo discarded after new edits)\n";
|
||||||
// Reset to empty by undoing the last redo and the original insert, then reinsert 'abc'
|
// Reset to empty by undoing the last redo and the original insert, then reinsert 'abc'
|
||||||
// Ensure buffer is empty before starting this scenario
|
// Ensure buffer is empty before starting this scenario
|
||||||
frontend.Input().QueueCommand(CommandId::Undo); // undo Hello
|
frontend.Input().QueueCommand(CommandId::Undo); // undo Hello
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "");
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
// Type a contiguous word 'abc' (single batch)
|
// Type a contiguous word 'abc' (single batch)
|
||||||
frontend.Input().QueueText("abc");
|
frontend.Input().QueueText("abc");
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "abc");
|
assert(std::string(buf->Rows()[0]) == "abc");
|
||||||
|
|
||||||
// Undo once – should remove the whole batch and leave empty
|
// Undo once – should remove the whole batch and leave empty
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "");
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
// Now type new text 'X' – this should create a new branch and discard old redo chain
|
// Now type new text 'X' – this should create a new branch and discard old redo chain
|
||||||
frontend.Input().QueueText("X");
|
frontend.Input().QueueText("X");
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "X");
|
assert(std::string(buf->Rows()[0]) == "X");
|
||||||
|
|
||||||
// Attempt Redo – should be a no-op (redo branch was discarded by new edit)
|
// Attempt Redo – should be a no-op (redo branch was discarded by new edit)
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "X");
|
assert(std::string(buf->Rows()[0]) == "X");
|
||||||
// Undo and Redo along the new branch should still work
|
// Undo and Redo along the new branch should still work
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "X");
|
assert(std::string(buf->Rows()[0]) == "X");
|
||||||
std::cout << " ✓ Redo discarded after new edit; new branch undo/redo works\n\n";
|
std::cout << " ✓ Redo discarded after new edit; new branch undo/redo works\n\n";
|
||||||
|
|
||||||
// Clear buffer state for next tests: undo to empty if needed
|
// Clear buffer state for next tests: undo to empty if needed
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "");
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
// Test 5: UTF-8 insertion and undo/redo round-trip
|
// Test 5: UTF-8 insertion and undo/redo round-trip
|
||||||
std::cout << "Test 5: UTF-8 insertion 'é漢' and undo/redo\n";
|
std::cout << "Test 5: UTF-8 insertion 'é漢' and undo/redo\n";
|
||||||
const std::string utf8_text = "é漢"; // multi-byte UTF-8 (2 bytes + 3 bytes)
|
const std::string utf8_text = "é漢"; // multi-byte UTF-8 (2 bytes + 3 bytes)
|
||||||
frontend.Input().QueueText(utf8_text);
|
frontend.Input().QueueText(utf8_text);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == utf8_text);
|
assert(std::string(buf->Rows()[0]) == utf8_text);
|
||||||
// Undo should remove the entire contiguous insertion batch
|
// Undo should remove the entire contiguous insertion batch
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "");
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
// Redo restores it
|
// Redo restores it
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == utf8_text);
|
assert(std::string(buf->Rows()[0]) == utf8_text);
|
||||||
std::cout << " ✓ UTF-8 insert round-trips with undo/redo\n\n";
|
std::cout << " ✓ UTF-8 insert round-trips with undo/redo\n\n";
|
||||||
|
|
||||||
// Clear for next test
|
// Clear for next test
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "");
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
// Test 6: Multi-line operations (newline split and join via backspace at BOL)
|
// Test 6: Multi-line operations (newline split and join via backspace at BOL)
|
||||||
std::cout << "Test 6: Newline split and join via backspace at BOL\n";
|
std::cout << "Test 6: Newline split and join via backspace at BOL\n";
|
||||||
// Insert "ab" then newline then "cd" → expect two lines
|
// Insert "ab" then newline then "cd" → expect two lines
|
||||||
frontend.Input().QueueText("ab");
|
frontend.Input().QueueText("ab");
|
||||||
frontend.Input().QueueCommand(CommandId::Newline);
|
frontend.Input().QueueCommand(CommandId::Newline);
|
||||||
frontend.Input().QueueText("cd");
|
frontend.Input().QueueText("cd");
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(buf->Rows().size() >= 2);
|
assert(buf->Rows().size() >= 2);
|
||||||
assert(std::string(buf->Rows()[0]) == "ab");
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
assert(std::string(buf->Rows()[1]) == "cd");
|
assert(std::string(buf->Rows()[1]) == "cd");
|
||||||
std::cout << " ✓ Split into two lines\n";
|
std::cout << " ✓ Split into two lines\n";
|
||||||
|
|
||||||
// Undo once – should remove "cd" insertion leaving two lines ["ab", ""] or join depending on commit
|
// Undo once – should remove "cd" insertion leaving two lines ["ab", ""] or join depending on commit
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
// Current design batches typing on the second line; after undo, the second line should exist but be empty
|
// Current design batches typing on the second line; after undo, the second line should exist but be empty
|
||||||
assert(buf->Rows().size() >= 2);
|
assert(buf->Rows().size() >= 2);
|
||||||
assert(std::string(buf->Rows()[0]) == "ab");
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
assert(std::string(buf->Rows()[1]) == "");
|
assert(std::string(buf->Rows()[1]) == "");
|
||||||
|
|
||||||
// Undo the newline – should rejoin to a single line "ab"
|
// Undo the newline – should rejoin to a single line "ab"
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(buf->Rows().size() >= 1);
|
assert(buf->Rows().size() >= 1);
|
||||||
assert(std::string(buf->Rows()[0]) == "ab");
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
|
|
||||||
// Redo twice to get back to ["ab","cd"]
|
// Redo twice to get back to ["ab","cd"]
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "ab");
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
assert(std::string(buf->Rows()[1]) == "cd");
|
assert(std::string(buf->Rows()[1]) == "cd");
|
||||||
std::cout << " ✓ Newline undo/redo round-trip\n";
|
std::cout << " ✓ Newline undo/redo round-trip\n";
|
||||||
|
|
||||||
// Now join via Backspace at beginning of second line
|
// Now join via Backspace at beginning of second line
|
||||||
frontend.Input().QueueCommand(CommandId::MoveDown); // ensure we're on the second line
|
frontend.Input().QueueCommand(CommandId::MoveDown); // ensure we're on the second line
|
||||||
frontend.Input().QueueCommand(CommandId::MoveHome); // go to BOL on second line
|
frontend.Input().QueueCommand(CommandId::MoveHome); // go to BOL on second line
|
||||||
frontend.Input().QueueCommand(CommandId::Backspace); // join with previous line
|
frontend.Input().QueueCommand(CommandId::Backspace); // join with previous line
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(buf->Rows().size() >= 1);
|
assert(buf->Rows().size() >= 1);
|
||||||
assert(std::string(buf->Rows()[0]) == "abcd");
|
assert(std::string(buf->Rows()[0]) == "abcd");
|
||||||
std::cout << " ✓ Backspace at BOL joins lines\n";
|
std::cout << " ✓ Backspace at BOL joins lines\n";
|
||||||
|
|
||||||
// Undo/Redo the join
|
// Undo/Redo the join
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(buf->Rows().size() >= 1);
|
assert(buf->Rows().size() >= 1);
|
||||||
assert(std::string(buf->Rows()[0]) == "abcd");
|
assert(std::string(buf->Rows()[0]) == "abcd");
|
||||||
std::cout << " ✓ Join undo/redo round-trip\n\n";
|
std::cout << " ✓ Join undo/redo round-trip\n\n";
|
||||||
|
|
||||||
// Test 7: Typing batching – a contiguous word undone in one step
|
// Test 7: Typing batching – a contiguous word undone in one step
|
||||||
std::cout << "Test 7: Typing batching (single undo removes whole word)\n";
|
std::cout << "Test 7: Typing batching (single undo removes whole word)\n";
|
||||||
// Clear current line first
|
// Clear current line first
|
||||||
frontend.Input().QueueCommand(CommandId::MoveHome);
|
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||||
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]).empty());
|
assert(std::string(buf->Rows()[0]).empty());
|
||||||
// Type a word and verify one undo clears it
|
// Type a word and verify one undo clears it
|
||||||
frontend.Input().QueueText("hello");
|
frontend.Input().QueueText("hello");
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "hello");
|
assert(std::string(buf->Rows()[0]) == "hello");
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]).empty());
|
assert(std::string(buf->Rows()[0]).empty());
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "hello");
|
assert(std::string(buf->Rows()[0]) == "hello");
|
||||||
std::cout << " ✓ Contiguous typing batched into single undo step\n\n";
|
std::cout << " ✓ Contiguous typing batched into single undo step\n\n";
|
||||||
|
|
||||||
// Test 8: Forward delete batching at a fixed anchor column
|
// Test 8: Forward delete batching at a fixed anchor column
|
||||||
std::cout << "Test 8: Forward delete batching at fixed anchor (DeleteChar)\n";
|
std::cout << "Test 8: Forward delete batching at fixed anchor (DeleteChar)\n";
|
||||||
// Prepare line content
|
// Prepare line content
|
||||||
frontend.Input().QueueCommand(CommandId::MoveHome);
|
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||||
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
frontend.Input().QueueText("abcdef");
|
frontend.Input().QueueText("abcdef");
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
// Ensure cursor at anchor column 0
|
// Ensure cursor at anchor column 0
|
||||||
frontend.Input().QueueCommand(CommandId::MoveHome);
|
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||||
// Delete three chars at cursor; should batch into one Delete node
|
// Delete three chars at cursor; should batch into one Delete node
|
||||||
frontend.Input().QueueCommand(CommandId::DeleteChar, "", 3);
|
frontend.Input().QueueCommand(CommandId::DeleteChar, "", 3);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "def");
|
assert(std::string(buf->Rows()[0]) == "def");
|
||||||
// Single undo should restore the entire deleted run
|
// Single undo should restore the entire deleted run
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "abcdef");
|
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||||
// Redo should remove the same run again
|
// Redo should remove the same run again
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "def");
|
assert(std::string(buf->Rows()[0]) == "def");
|
||||||
std::cout << " ✓ Forward delete batched and undo/redo round-trips\n\n";
|
std::cout << " ✓ Forward delete batched and undo/redo round-trips\n\n";
|
||||||
|
|
||||||
// Test 9: Backspace batching with prepend rule (cursor moves left)
|
// Test 9: Backspace batching with prepend rule (cursor moves left)
|
||||||
std::cout << "Test 9: Backspace batching with prepend rule\n";
|
std::cout << "Test 9: Backspace batching with prepend rule\n";
|
||||||
// Restore to full string then backspace a run
|
// Restore to full string then backspace a run
|
||||||
frontend.Input().QueueCommand(CommandId::Undo); // bring back to "abcdef"
|
frontend.Input().QueueCommand(CommandId::Undo); // bring back to "abcdef"
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "abcdef");
|
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||||
// Move to end and backspace three characters; should batch into one Delete node
|
// Move to end and backspace three characters; should batch into one Delete node
|
||||||
frontend.Input().QueueCommand(CommandId::MoveEnd);
|
frontend.Input().QueueCommand(CommandId::MoveEnd);
|
||||||
frontend.Input().QueueCommand(CommandId::Backspace, "", 3);
|
frontend.Input().QueueCommand(CommandId::Backspace, "", 3);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "abc");
|
assert(std::string(buf->Rows()[0]) == "abc");
|
||||||
// Single undo restores the deleted run
|
// Single undo restores the deleted run
|
||||||
frontend.Input().QueueCommand(CommandId::Undo);
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "abcdef");
|
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||||
// Redo removes it again
|
// Redo removes it again
|
||||||
frontend.Input().QueueCommand(CommandId::Redo);
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
while (!frontend.Input().IsEmpty() && running) {
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
frontend.Step(editor, running);
|
frontend.Step(editor, running);
|
||||||
}
|
}
|
||||||
assert(std::string(buf->Rows()[0]) == "abc");
|
assert(std::string(buf->Rows()[0]) == "abc");
|
||||||
std::cout << " ✓ Backspace run batched and undo/redo round-trips\n\n";
|
std::cout << " ✓ Backspace run batched and undo/redo round-trips\n\n";
|
||||||
|
|
||||||
frontend.Shutdown();
|
frontend.Shutdown();
|
||||||
|
|
||||||
std::cout << "====================================\n";
|
std::cout << "====================================\n";
|
||||||
std::cout << "All tests passed!\n";
|
std::cout << "All tests passed!\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user