|
@@ -32,44 +32,11 @@ public:
|
|
|
void set_core(EditorCore* core) override;
|
|
void set_core(EditorCore* core) override;
|
|
|
|
|
|
|
|
private:
|
|
private:
|
|
|
- enum class Mode {
|
|
|
|
|
- Normal,
|
|
|
|
|
- Command, // Minibuffer entry
|
|
|
|
|
- FindFile, // Find file prompt
|
|
|
|
|
- BufferSwitch, // Buffer switching with completion
|
|
|
|
|
- KillBuffer, // Kill buffer with completion
|
|
|
|
|
- ConfirmKill, // Confirm killing modified buffer
|
|
|
|
|
- ISearch // Incremental search
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned
|
|
EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned
|
|
|
bool should_quit_ = false;
|
|
bool should_quit_ = false;
|
|
|
std::string message_line_;
|
|
std::string message_line_;
|
|
|
int height_ = 0, width_ = 0;
|
|
int height_ = 0, width_ = 0;
|
|
|
|
|
|
|
|
- // Input state
|
|
|
|
|
- Mode mode_ = Mode::Normal;
|
|
|
|
|
- std::string command_buffer_;
|
|
|
|
|
-
|
|
|
|
|
- // ISearch state
|
|
|
|
|
- std::string isearch_query_;
|
|
|
|
|
- bool isearch_forward_ = true;
|
|
|
|
|
- Position isearch_start_pos_;
|
|
|
|
|
- std::optional<Range> isearch_match_;
|
|
|
|
|
- bool isearch_failed_ = false;
|
|
|
|
|
-
|
|
|
|
|
- // Completion state
|
|
|
|
|
- std::vector<std::string> completion_candidates_;
|
|
|
|
|
- size_t completion_index_ = 0;
|
|
|
|
|
- std::string completion_prefix_;
|
|
|
|
|
-
|
|
|
|
|
- // Minibuffer history
|
|
|
|
|
- std::vector<std::string> command_history_;
|
|
|
|
|
- std::vector<std::string> buffer_switch_history_;
|
|
|
|
|
- std::vector<std::string> kill_buffer_history_;
|
|
|
|
|
- std::vector<std::string> isearch_history_;
|
|
|
|
|
- size_t history_index_ = 0;
|
|
|
|
|
-
|
|
|
|
|
// Meta key handling
|
|
// Meta key handling
|
|
|
bool waiting_for_meta_ = false;
|
|
bool waiting_for_meta_ = false;
|
|
|
std::chrono::steady_clock::time_point meta_time_;
|
|
std::chrono::steady_clock::time_point meta_time_;
|
|
@@ -77,23 +44,13 @@ private:
|
|
|
|
|
|
|
|
// Private helper method declarations
|
|
// Private helper method declarations
|
|
|
std::string resolve_key(int ch);
|
|
std::string resolve_key(int ch);
|
|
|
- std::vector<std::string>& get_current_history();
|
|
|
|
|
- void add_to_history(const std::string& entry);
|
|
|
|
|
- void previous_history();
|
|
|
|
|
- void next_history();
|
|
|
|
|
- void reset_history_navigation();
|
|
|
|
|
- void update_completion_candidates(const std::string& prefix);
|
|
|
|
|
- void reset_completion();
|
|
|
|
|
- void perform_search(bool find_next);
|
|
|
|
|
- bool handle_input(int ch);
|
|
|
|
|
|
|
+ bool handle_input(int ch); // This will be updated
|
|
|
bool process_key(const std::string& key_name);
|
|
bool process_key(const std::string& key_name);
|
|
|
- void execute_command(const std::string& cmd);
|
|
|
|
|
int get_attributes_for_face(const std::string& face_name);
|
|
int get_attributes_for_face(const std::string& face_name);
|
|
|
void render();
|
|
void render();
|
|
|
void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height);
|
|
void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height);
|
|
|
void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height);
|
|
void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height);
|
|
|
void render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active);
|
|
void render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active);
|
|
|
- void render_status_line();
|
|
|
|
|
void render_message_line();
|
|
void render_message_line();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -122,8 +79,9 @@ void TuiEditor::init() {
|
|
|
core_->active_theme()->initialize_ncurses_colors();
|
|
core_->active_theme()->initialize_ncurses_colors();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Set initial viewport size (leave room for status and message lines)
|
|
|
|
|
- int content_height = height_ - 2; // -1 for status, -1 for message
|
|
|
|
|
|
|
+ // Set initial viewport size (leave room for minibuffer/message line)
|
|
|
|
|
+ int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
|
|
|
|
|
+ int content_height = height_ - minibuffer_lines;
|
|
|
bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
|
|
bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
|
|
|
int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
|
|
int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
|
|
|
int content_width = width_ - line_number_width;
|
|
int content_width = width_ - line_number_width;
|
|
@@ -145,7 +103,8 @@ void TuiEditor::run() {
|
|
|
if (new_height != height_ || new_width != width_) {
|
|
if (new_height != height_ || new_width != width_) {
|
|
|
height_ = new_height;
|
|
height_ = new_height;
|
|
|
width_ = new_width;
|
|
width_ = new_width;
|
|
|
- int content_height = height_ - 2;
|
|
|
|
|
|
|
+ int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
|
|
|
|
|
+ int content_height = height_ - minibuffer_lines;
|
|
|
bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
|
|
bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
|
|
|
int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
|
|
int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
|
|
|
int content_width = width_ - line_number_width;
|
|
int content_width = width_ - line_number_width;
|
|
@@ -166,45 +125,100 @@ void TuiEditor::run() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void TuiEditor::handle_editor_event(EditorEvent event) { // existing method
|
|
|
|
|
|
|
+void TuiEditor::handle_editor_event(EditorEvent event) {
|
|
|
if (event == EditorEvent::Quit) {
|
|
if (event == EditorEvent::Quit) {
|
|
|
should_quit_ = true;
|
|
should_quit_ = true;
|
|
|
} else if (event == EditorEvent::Message) {
|
|
} else if (event == EditorEvent::Message) {
|
|
|
- message_line_ = core_->last_message();
|
|
|
|
|
|
|
+ message_line_ = core_->last_message(); // Still update local message_line_ for rendering
|
|
|
} else if (event == EditorEvent::CommandMode) {
|
|
} else if (event == EditorEvent::CommandMode) {
|
|
|
- mode_ = Mode::Command;
|
|
|
|
|
- command_buffer_.clear();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- reset_history_navigation();
|
|
|
|
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
|
|
+ MinibufferMode::Command, ":",
|
|
|
|
|
+ [this](const std::string& input) {
|
|
|
|
|
+ if (input == "q" || input == "quit") {
|
|
|
|
|
+ should_quit_ = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ auto result = core_->command_system().execute(input);
|
|
|
|
|
+ core_->set_message(result.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
|
|
+ );
|
|
|
} else if (event == EditorEvent::BufferSwitchMode) {
|
|
} else if (event == EditorEvent::BufferSwitchMode) {
|
|
|
- mode_ = Mode::BufferSwitch;
|
|
|
|
|
- command_buffer_.clear();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- reset_history_navigation();
|
|
|
|
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
|
|
+ MinibufferMode::BufferName, "Switch to buffer: ",
|
|
|
|
|
+ [this](const std::string& input) {
|
|
|
|
|
+ if (core_->switch_buffer_in_window(input)) {
|
|
|
|
|
+ core_->set_message("Switched to: " + input);
|
|
|
|
|
+ core_->lua_api()->execute("auto_activate_major_mode()");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ core_->set_message("Buffer not found: " + input);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
|
|
+ );
|
|
|
} else if (event == EditorEvent::KillBufferMode) {
|
|
} else if (event == EditorEvent::KillBufferMode) {
|
|
|
- mode_ = Mode::KillBuffer;
|
|
|
|
|
- command_buffer_.clear();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- reset_history_navigation();
|
|
|
|
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
|
|
+ MinibufferMode::BufferName, "Kill buffer: ",
|
|
|
|
|
+ [this](const std::string& input) {
|
|
|
|
|
+ auto buf = core_->get_buffer_by_name(input);
|
|
|
|
|
+ if (buf && buf->is_modified()) {
|
|
|
|
|
+ // Enter a confirmation sub-mode
|
|
|
|
|
+ mode_ = Mode::ConfirmKill; // Temporarily use local mode for confirmation
|
|
|
|
|
+ command_buffer_ = input; // Store buffer name for confirmation
|
|
|
|
|
+ core_->set_message("Buffer modified! Kill anyway? (y/n)");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (core_->close_buffer(input)) {
|
|
|
|
|
+ core_->set_message("Closed buffer: " + input);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ core_->set_message("Failed to close buffer: " + input);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
|
|
+ );
|
|
|
} else if (event == EditorEvent::FindFileMode) {
|
|
} else if (event == EditorEvent::FindFileMode) {
|
|
|
- mode_ = Mode::FindFile;
|
|
|
|
|
- command_buffer_.clear();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- reset_history_navigation();
|
|
|
|
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
|
|
+ MinibufferMode::FilePath, "Find file: ",
|
|
|
|
|
+ [this](const std::string& input) {
|
|
|
|
|
+ if (core_->load_file(input)) {
|
|
|
|
|
+ core_->set_message("Loaded: " + input);
|
|
|
|
|
+ core_->lua_api()->execute("auto_activate_major_mode()");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ core_->set_message("Failed to load: " + input);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if (event == EditorEvent::ThemeSelectionMode) {
|
|
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
|
|
+ MinibufferMode::ThemeName, "Set theme: ",
|
|
|
|
|
+ [this](const std::string& input) {
|
|
|
|
|
+ auto theme_names = core_->theme_manager().theme_names();
|
|
|
|
|
+ auto it = std::find(theme_names.begin(), theme_names.end(), input);
|
|
|
|
|
+ if (it != theme_names.end()) {
|
|
|
|
|
+ core_->set_theme(input);
|
|
|
|
|
+ core_->set_message("Switched to theme: " + input);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ core_->set_message("Theme not found: " + input);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
|
|
+ );
|
|
|
} else if (event == EditorEvent::ISearchMode) {
|
|
} else if (event == EditorEvent::ISearchMode) {
|
|
|
- mode_ = Mode::ISearch;
|
|
|
|
|
- isearch_query_.clear();
|
|
|
|
|
- isearch_forward_ = true;
|
|
|
|
|
- isearch_start_pos_ = core_->cursor();
|
|
|
|
|
- isearch_match_ = std::nullopt;
|
|
|
|
|
- isearch_failed_ = false;
|
|
|
|
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
|
|
+ MinibufferMode::ISearch, "I-search: ",
|
|
|
|
|
+ [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search query: " + input); },
|
|
|
|
|
+ [this]() { core_->set_message("Cancelled I-search"); }
|
|
|
|
|
+ );
|
|
|
} else if (event == EditorEvent::ISearchBackwardMode) {
|
|
} else if (event == EditorEvent::ISearchBackwardMode) {
|
|
|
- mode_ = Mode::ISearch;
|
|
|
|
|
- isearch_query_.clear();
|
|
|
|
|
- isearch_forward_ = false;
|
|
|
|
|
- isearch_start_pos_ = core_->cursor();
|
|
|
|
|
- isearch_match_ = std::nullopt;
|
|
|
|
|
- isearch_failed_ = false;
|
|
|
|
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
|
|
+ MinibufferMode::ISearch, "I-search backward: ",
|
|
|
|
|
+ [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search backward query: " + input); },
|
|
|
|
|
+ [this]() { core_->set_message("Cancelled I-search backward"); }
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if (event == EditorEvent::TransientMessageCleared) {
|
|
|
|
|
+ // Redraw to clear the message from the screen
|
|
|
|
|
+ render();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -219,623 +233,90 @@ std::string TuiEditor::resolve_key(int ch) {
|
|
|
debug_log << "=== NCURSES INPUT DEBUG ===" << std::endl;
|
|
debug_log << "=== NCURSES INPUT DEBUG ===" << std::endl;
|
|
|
debug_log << "Raw key code: " << ch << " (0x" << std::hex << ch << std::dec << ")" << std::endl;
|
|
debug_log << "Raw key code: " << ch << " (0x" << std::hex << ch << std::dec << ")" << std::endl;
|
|
|
|
|
|
|
|
- std::string key_name;
|
|
|
|
|
-
|
|
|
|
|
- // Handle special ncurses key codes first
|
|
|
|
|
- if (ch >= KEY_MIN) {
|
|
|
|
|
- switch (ch) {
|
|
|
|
|
- case KEY_UP: key_name = "ArrowUp"; break;
|
|
|
|
|
- case KEY_DOWN: key_name = "ArrowDown"; break;
|
|
|
|
|
- case KEY_LEFT: key_name = "ArrowLeft"; break;
|
|
|
|
|
- case KEY_RIGHT: key_name = "ArrowRight"; break;
|
|
|
|
|
- case KEY_HOME: key_name = "Home"; break;
|
|
|
|
|
- case KEY_END: key_name = "End"; break;
|
|
|
|
|
- case KEY_BACKSPACE: key_name = "Backspace"; break;
|
|
|
|
|
- case KEY_DC: key_name = "Delete"; break;
|
|
|
|
|
- case KEY_ENTER: key_name = "Return"; break;
|
|
|
|
|
- case KEY_F(3): key_name = "F3"; break;
|
|
|
|
|
- case KEY_F(4): key_name = "F4"; break;
|
|
|
|
|
- default:
|
|
|
|
|
- debug_log << "Unknown special key: " << ch << " (ignoring)" << std::endl;
|
|
|
|
|
- // Return empty string to ignore unknown special keys
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // Handle normal ASCII characters and control codes
|
|
|
|
|
- switch (ch) {
|
|
|
|
|
- case 127: // DEL
|
|
|
|
|
- case 8: // BS
|
|
|
|
|
- key_name = "Backspace"; break;
|
|
|
|
|
- case '\n':
|
|
|
|
|
- case '\r':
|
|
|
|
|
- key_name = "Return"; break;
|
|
|
|
|
- case '\t':
|
|
|
|
|
- key_name = "Tab"; break;
|
|
|
|
|
- case 27:
|
|
|
|
|
- key_name = "Escape"; break;
|
|
|
|
|
- default:
|
|
|
|
|
- // Control characters (1-26, excluding special cases)
|
|
|
|
|
- if (ch >= 1 && ch >= 1 && ch <= 26 && ch != 8 && ch != 9 && ch != 10 && ch != 13) {
|
|
|
|
|
- char letter = 'a' + (ch - 1);
|
|
|
|
|
- key_name = "C-" + std::string(1, letter);
|
|
|
|
|
- debug_log << "Control character detected: " << ch << " -> " << key_name << std::endl;
|
|
|
|
|
- }
|
|
|
|
|
- // Printable ASCII characters
|
|
|
|
|
- else if (ch >= 32 && ch <= 126) {
|
|
|
|
|
- key_name = std::string(1, static_cast<char>(ch));
|
|
|
|
|
- }
|
|
|
|
|
- // Extended characters (might be Meta combinations)
|
|
|
|
|
- else if (ch >= 128 && ch < 256) {
|
|
|
|
|
- char base_char = ch - 128;
|
|
|
|
|
- if (base_char >= 32 && base_char <= 126) {
|
|
|
|
|
- key_name = "M-" + std::string(1, base_char);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- debug_log << "Unhandled character code: " << ch << std::endl;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- debug_log << "Resolved key: '" << key_name << "'" << std::endl;
|
|
|
|
|
- debug_log << "============================" << std::endl;
|
|
|
|
|
-
|
|
|
|
|
- return key_name;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// History management
|
|
|
|
|
-std::vector<std::string>& TuiEditor::get_current_history() {
|
|
|
|
|
- switch (mode_) {
|
|
|
|
|
- case Mode::Command: return command_history_;
|
|
|
|
|
- case Mode::BufferSwitch: return buffer_switch_history_;
|
|
|
|
|
- case Mode::KillBuffer: return kill_buffer_history_;
|
|
|
|
|
- case Mode::ISearch: return isearch_history_;
|
|
|
|
|
- default: return command_history_;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void TuiEditor::add_to_history(const std::string& entry) {
|
|
|
|
|
- if (entry.empty()) return;
|
|
|
|
|
-
|
|
|
|
|
- auto& history = get_current_history();
|
|
|
|
|
-
|
|
|
|
|
- // Remove if already exists (move to front)
|
|
|
|
|
- auto it = std::find(history.begin(), history.end(), entry);
|
|
|
|
|
- if (it != history.end()) {
|
|
|
|
|
- history.erase(it);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Add to front
|
|
|
|
|
- history.insert(history.begin(), entry);
|
|
|
|
|
-
|
|
|
|
|
- // Limit history size
|
|
|
|
|
- const size_t MAX_HISTORY = 100;
|
|
|
|
|
- if (history.size() > MAX_HISTORY) {
|
|
|
|
|
- history.resize(MAX_HISTORY);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void TuiEditor::previous_history() {
|
|
|
|
|
- auto& history = get_current_history();
|
|
|
|
|
- if (history.empty()) return;
|
|
|
|
|
-
|
|
|
|
|
- if (history_index_ < history.size()) {
|
|
|
|
|
- command_buffer_ = history[history_index_];
|
|
|
|
|
- history_index_++;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void TuiEditor::next_history() {
|
|
|
|
|
- auto& history = get_current_history();
|
|
|
|
|
- if (history.empty() || history_index_ == 0) return;
|
|
|
|
|
-
|
|
|
|
|
- history_index_--;
|
|
|
|
|
- if (history_index_ == 0) {
|
|
|
|
|
- command_buffer_ = "";
|
|
|
|
|
- } else {
|
|
|
|
|
- command_buffer_ = history[history_index_ - 1];
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void TuiEditor::reset_history_navigation() {
|
|
|
|
|
- history_index_ = 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void TuiEditor::update_completion_candidates(const std::string& prefix) {
|
|
|
|
|
- std::vector<std::string> candidates;
|
|
|
|
|
-
|
|
|
|
|
- if (mode_ == Mode::Command) {
|
|
|
|
|
- // Get command names from Lua
|
|
|
|
|
- auto& lua = core_->lua_api()->state();
|
|
|
|
|
- sol::function get_names = lua["get_command_names"];
|
|
|
|
|
- if (get_names.valid()) {
|
|
|
|
|
- candidates = get_names.call<std::vector<std::string>>();
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // Default to buffer names for BufferSwitch/KillBuffer
|
|
|
|
|
- candidates = core_->get_buffer_names();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- completion_candidates_.clear();
|
|
|
|
|
-
|
|
|
|
|
- if (prefix.empty()) {
|
|
|
|
|
- completion_candidates_ = candidates;
|
|
|
|
|
- } else {
|
|
|
|
|
- for (const auto& name : candidates) {
|
|
|
|
|
- if (name.size() >= prefix.size() &&
|
|
|
|
|
- name.substr(0, prefix.size()) == prefix) {
|
|
|
|
|
- completion_candidates_.push_back(name);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- completion_index_ = 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Helper to reset completion state
|
|
|
|
|
-void TuiEditor::reset_completion() {
|
|
|
|
|
- completion_candidates_.clear();
|
|
|
|
|
- completion_index_ = 0;
|
|
|
|
|
- completion_prefix_.clear();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void TuiEditor::perform_search(bool find_next) {
|
|
|
|
|
- if (isearch_query_.empty()) {
|
|
|
|
|
- isearch_match_ = std::nullopt;
|
|
|
|
|
- isearch_failed_ = false;
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- Position start_search = core_->cursor();
|
|
|
|
|
- if (find_next) {
|
|
|
|
|
- if (isearch_forward_) {
|
|
|
|
|
- // Forward: move cursor forward 1 char to find next
|
|
|
|
|
- if (start_search.column < core_->buffer().line(start_search.line).size()) {
|
|
|
|
|
- start_search.column++;
|
|
|
|
|
- } else if (start_search.line < core_->buffer().line_count() - 1) {
|
|
|
|
|
- start_search.line++;
|
|
|
|
|
- start_search.column = 0;
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // Backward: move cursor backward 1 char
|
|
|
|
|
- if (start_search.column > 0) {
|
|
|
|
|
- start_search.column--;
|
|
|
|
|
- } else if (start_search.line > 0) {
|
|
|
|
|
- start_search.line--;
|
|
|
|
|
- start_search.column = core_->buffer().line(start_search.line).size();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- std::optional<Range> result;
|
|
|
|
|
- if (isearch_forward_) {
|
|
|
|
|
- result = core_->buffer().find(isearch_query_, start_search);
|
|
|
|
|
- } else {
|
|
|
|
|
- result = core_->buffer().find_backward(isearch_query_, start_search);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (result) {
|
|
|
|
|
- isearch_match_ = result;
|
|
|
|
|
- isearch_failed_ = false;
|
|
|
|
|
- core_->set_cursor(result->start);
|
|
|
|
|
- core_->adjust_scroll();
|
|
|
|
|
- } else {
|
|
|
|
|
- isearch_failed_ = true;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
bool TuiEditor::handle_input(int ch) {
|
|
bool TuiEditor::handle_input(int ch) {
|
|
|
- // Handle confirmation mode
|
|
|
|
|
- if (mode_ == Mode::ConfirmKill) {
|
|
|
|
|
|
|
+ // Handle confirmation mode - this remains local to TuiEditor for now
|
|
|
|
|
+ if (core_->minibuffer_manager().get_current_mode() == MinibufferMode::None && mode_ == Mode::ConfirmKill) {
|
|
|
if (ch == 'y' || ch == 'Y') {
|
|
if (ch == 'y' || ch == 'Y') {
|
|
|
if (core_->close_buffer(command_buffer_)) {
|
|
if (core_->close_buffer(command_buffer_)) {
|
|
|
- message_line_ = "Closed modified buffer: " + command_buffer_;
|
|
|
|
|
|
|
+ core_->set_message("Closed modified buffer: " + command_buffer_);
|
|
|
} else {
|
|
} else {
|
|
|
- message_line_ = "Failed to close buffer";
|
|
|
|
|
|
|
+ core_->set_message("Failed to close buffer");
|
|
|
}
|
|
}
|
|
|
mode_ = Mode::Normal;
|
|
mode_ = Mode::Normal;
|
|
|
command_buffer_.clear();
|
|
command_buffer_.clear();
|
|
|
- reset_completion();
|
|
|
|
|
} else if (ch == 'n' || ch == 'N' || ch == 27) { // n or ESC
|
|
} else if (ch == 'n' || ch == 'N' || ch == 27) { // n or ESC
|
|
|
mode_ = Mode::Normal;
|
|
mode_ = Mode::Normal;
|
|
|
- message_line_ = "Cancelled kill buffer";
|
|
|
|
|
|
|
+ core_->set_message("Cancelled kill buffer");
|
|
|
command_buffer_.clear();
|
|
command_buffer_.clear();
|
|
|
- reset_completion();
|
|
|
|
|
}
|
|
}
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Handle ISearch
|
|
|
|
|
- if (mode_ == Mode::ISearch) {
|
|
|
|
|
- // C-g (7) or ESC (27) -> Cancel
|
|
|
|
|
- if (ch == 27 || ch == 7) {
|
|
|
|
|
- core_->set_cursor(isearch_start_pos_);
|
|
|
|
|
- mode_ = Mode::Normal;
|
|
|
|
|
- message_line_ = "Quit";
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- // RET -> Accept
|
|
|
|
|
- if (ch == '\n' || ch == '\r') {
|
|
|
|
|
- mode_ = Mode::Normal;
|
|
|
|
|
- message_line_ = "Mark saved";
|
|
|
|
|
- core_->buffer().set_mark(isearch_start_pos_);
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- // C-s (19) -> Next
|
|
|
|
|
- if (ch == 19) {
|
|
|
|
|
- isearch_forward_ = true;
|
|
|
|
|
- perform_search(true);
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- // C-r (18) -> Prev
|
|
|
|
|
- if (ch == 18) {
|
|
|
|
|
- isearch_forward_ = false;
|
|
|
|
|
- perform_search(true);
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- // Backspace
|
|
|
|
|
- if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
|
|
|
|
|
- if (!isearch_query_.empty()) {
|
|
|
|
|
- isearch_query_.pop_back();
|
|
|
|
|
- perform_search(false);
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- // Printable
|
|
|
|
|
- if (ch >= 32 && ch <= 126) {
|
|
|
|
|
- isearch_query_ += static_cast<char>(ch);
|
|
|
|
|
- perform_search(false);
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Other keys -> Exit and process
|
|
|
|
|
- mode_ = Mode::Normal;
|
|
|
|
|
- return handle_input(ch);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Handle minibuffer/command mode
|
|
|
|
|
- if (mode_ == Mode::Command || mode_ == Mode::FindFile ||
|
|
|
|
|
- mode_ == Mode::BufferSwitch || mode_ == Mode::KillBuffer) {
|
|
|
|
|
-
|
|
|
|
|
- // ESC - cancel
|
|
|
|
|
- if (ch == 27) {
|
|
|
|
|
- mode_ = Mode::Normal;
|
|
|
|
|
- command_buffer_.clear();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- message_line_ = "Cancelled";
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // TAB - completion
|
|
|
|
|
- if (ch == '\t' && (mode_ == Mode::BufferSwitch || mode_ == Mode::KillBuffer || mode_ == Mode::Command)) {
|
|
|
|
|
- if (completion_candidates_.empty()) {
|
|
|
|
|
- // First TAB: save prefix and get candidates
|
|
|
|
|
- completion_prefix_ = command_buffer_;
|
|
|
|
|
- update_completion_candidates(completion_prefix_);
|
|
|
|
|
-
|
|
|
|
|
- if (!completion_candidates_.empty()) {
|
|
|
|
|
- command_buffer_ = completion_candidates_[0];
|
|
|
|
|
- completion_index_ = 0;
|
|
|
|
|
- } else {
|
|
|
|
|
- message_line_ = "No matches";
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // Cycle through candidates
|
|
|
|
|
- completion_index_ = (completion_index_ + 1) % completion_candidates_.size();
|
|
|
|
|
- command_buffer_ = completion_candidates_[completion_index_];
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Return - execute
|
|
|
|
|
- if (ch == '\n' || ch == '\r') {
|
|
|
|
|
- // Add to history before execution
|
|
|
|
|
- add_to_history(command_buffer_);
|
|
|
|
|
-
|
|
|
|
|
- if (mode_ == Mode::Command) {
|
|
|
|
|
- execute_command(command_buffer_);
|
|
|
|
|
- } else if (mode_ == Mode::FindFile) {
|
|
|
|
|
- if (core_->load_file(command_buffer_)) {
|
|
|
|
|
- message_line_ = "Loaded: " + command_buffer_;
|
|
|
|
|
- core_->lua_api()->execute("auto_activate_major_mode()") ;
|
|
|
|
|
- } else {
|
|
|
|
|
- message_line_ = "Failed to load: " + command_buffer_;
|
|
|
|
|
- }
|
|
|
|
|
- } else if (mode_ == Mode::BufferSwitch) {
|
|
|
|
|
- if (core_->switch_buffer_in_window(command_buffer_)) {
|
|
|
|
|
- message_line_ = "Switched to: " + command_buffer_;
|
|
|
|
|
- core_->lua_api()->execute("auto_activate_major_mode()") ;
|
|
|
|
|
- } else {
|
|
|
|
|
- message_line_ = "Buffer not found: " + command_buffer_;
|
|
|
|
|
- }
|
|
|
|
|
- } else if (mode_ == Mode::KillBuffer) {
|
|
|
|
|
- // Check for modification
|
|
|
|
|
- auto buf = core_->get_buffer_by_name(command_buffer_);
|
|
|
|
|
- if (buf && buf->is_modified()) {
|
|
|
|
|
- mode_ = Mode::ConfirmKill;
|
|
|
|
|
- message_line_ = "Buffer modified! Kill anyway? (y/n)";
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (core_->close_buffer(command_buffer_)) {
|
|
|
|
|
- message_line_ = "Closed buffer: " + command_buffer_;
|
|
|
|
|
- } else {
|
|
|
|
|
- message_line_ = "Failed to close buffer: " + command_buffer_;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- mode_ = Mode::Normal;
|
|
|
|
|
- command_buffer_.clear();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- reset_history_navigation();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // M-p (Alt+p) - Previous history
|
|
|
|
|
- if (ch == '\x90') { // Meta+p
|
|
|
|
|
- previous_history();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // M-n (Alt+n) - Next history
|
|
|
|
|
- if (ch == '\x8E') { // Meta+n
|
|
|
|
|
- next_history();
|
|
|
|
|
- reset_completion();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Backspace
|
|
|
|
|
- if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
|
|
|
|
|
- if (!command_buffer_.empty()) {
|
|
|
|
|
- command_buffer_.pop_back();
|
|
|
|
|
- reset_completion(); // Reset completion on edit
|
|
|
|
|
- reset_history_navigation(); // Reset history on edit
|
|
|
|
|
- } else {
|
|
|
|
|
- mode_ = Mode::Normal;
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Printable characters
|
|
|
|
|
- if (ch >= 32 && ch <= 126) {
|
|
|
|
|
- command_buffer_ += static_cast<char>(ch);
|
|
|
|
|
- reset_completion(); // Reset completion on new input
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Resolve key name, including Meta combinations
|
|
|
|
|
+ std::string key_name;
|
|
|
|
|
|
|
|
- // Check for expired meta key
|
|
|
|
|
|
|
+ // Check for expired meta key before processing current input
|
|
|
if (waiting_for_meta_) {
|
|
if (waiting_for_meta_) {
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
if (now - meta_time_ > META_TIMEOUT) {
|
|
if (now - meta_time_ > META_TIMEOUT) {
|
|
|
debug_log << "Meta timeout, treating ESC as Escape key" << std::endl;
|
|
debug_log << "Meta timeout, treating ESC as Escape key" << std::endl;
|
|
|
waiting_for_meta_ = false;
|
|
waiting_for_meta_ = false;
|
|
|
- // Process the ESC as a normal Escape key
|
|
|
|
|
- std::string final_key = "Escape";
|
|
|
|
|
- if (core_->lua_api()->execute_key_binding(final_key)) {
|
|
|
|
|
- core_->record_key_sequence(final_key);
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ // Process the ESC as a normal Escape key by pushing it back
|
|
|
|
|
+ // This is a bit hacky for ncurses, cleaner would be to simulate a key event
|
|
|
|
|
+ // For now, let's just make the original ESC be processed normally in next loop
|
|
|
|
|
+ key_name = "Escape"; // Treat previous ESC as an Escape key
|
|
|
|
|
+ if (core_->lua_api()->execute_key_binding(key_name)) {
|
|
|
|
|
+ core_->record_key_sequence(key_name);
|
|
|
|
|
+ return true; // Consume the "timed out ESC"
|
|
|
}
|
|
}
|
|
|
- message_line_ = "Key: " + final_key;
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+ core_->set_message("Key: " + key_name);
|
|
|
|
|
+ return false; // Not consumed by keybinding, so UI should re-evaluate
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (ch == 27 && !waiting_for_meta_ && !core_->minibuffer_manager().is_active()) { // ESC
|
|
|
|
|
+ waiting_for_meta_ = true;
|
|
|
|
|
+ meta_time_ = std::chrono::steady_clock::now();
|
|
|
|
|
+ debug_log << "ESC received, waiting for meta key..." << std::endl;
|
|
|
|
|
+ return true; // Consume ESC, wait for next key
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// If we're waiting for a meta key and got one, combine them
|
|
// If we're waiting for a meta key and got one, combine them
|
|
|
if (waiting_for_meta_) {
|
|
if (waiting_for_meta_) {
|
|
|
waiting_for_meta_ = false;
|
|
waiting_for_meta_ = false;
|
|
|
std::string base_key = resolve_key(ch);
|
|
std::string base_key = resolve_key(ch);
|
|
|
if (base_key.empty()) {
|
|
if (base_key.empty()) {
|
|
|
debug_log << "Empty base key after ESC, ignoring" << std::endl;
|
|
debug_log << "Empty base key after ESC, ignoring" << std::endl;
|
|
|
- return false;
|
|
|
|
|
|
|
+ // If the key after ESC is not recognized, treat ESC as a normal Escape key
|
|
|
|
|
+ key_name = "Escape";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ key_name = "M-" + base_key;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- std::string key_name = "M-" + base_key;
|
|
|
|
|
debug_log << "Meta sequence complete: " << key_name << std::endl;
|
|
debug_log << "Meta sequence complete: " << key_name << std::endl;
|
|
|
-
|
|
|
|
|
- // Continue processing this meta key below
|
|
|
|
|
- return process_key(key_name);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check if this is the start of a meta sequence
|
|
|
|
|
- if (ch == 27) { // ESC
|
|
|
|
|
- waiting_for_meta_ = true;
|
|
|
|
|
- meta_time_ = std::chrono::steady_clock::now();
|
|
|
|
|
- debug_log << "ESC received, waiting for meta key..." << std::endl;
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ key_name = resolve_key(ch);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Normal mode - resolve key and try bindings
|
|
|
|
|
- std::string key_name = resolve_key(ch);
|
|
|
|
|
|
|
+
|
|
|
if (key_name.empty()) {
|
|
if (key_name.empty()) {
|
|
|
debug_log << "Empty key name, ignoring input" << std::endl;
|
|
debug_log << "Empty key name, ignoring input" << std::endl;
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return process_key(key_name);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool TuiEditor::process_key(const std::string& key_name) {
|
|
|
|
|
- debug_log << "Processing key: " << key_name << std::endl;
|
|
|
|
|
-
|
|
|
|
|
- // Use the new keybinding system
|
|
|
|
|
- KeyResult result = core_->lua_api()->process_key(key_name);
|
|
|
|
|
-
|
|
|
|
|
- switch (result) {
|
|
|
|
|
- case KeyResult::Executed:
|
|
|
|
|
- debug_log << "Key binding executed successfully" << std::endl;
|
|
|
|
|
- message_line_.clear(); // Clear any partial sequence display
|
|
|
|
|
- // Record the key if we're recording a macro (but not if it's F3/F4 themselves)
|
|
|
|
|
- if (key_name != "F3" && key_name != "F4") {
|
|
|
|
|
- core_->record_key_sequence(key_name);
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
-
|
|
|
|
|
- case KeyResult::Failed:
|
|
|
|
|
- debug_log << "Key binding execution failed" << std::endl;
|
|
|
|
|
- message_line_ = "Command failed";
|
|
|
|
|
- return true; // Key was handled, even though it failed
|
|
|
|
|
-
|
|
|
|
|
- case KeyResult::Partial:
|
|
|
|
|
- // Building a multi-key sequence
|
|
|
|
|
- message_line_ = core_->keybinding_manager().current_sequence_display();
|
|
|
|
|
- debug_log << "Partial sequence: " << message_line_ << std::endl;
|
|
|
|
|
- return true;
|
|
|
|
|
-
|
|
|
|
|
- case KeyResult::Timeout:
|
|
|
|
|
- debug_log << "Key sequence timed out" << std::endl;
|
|
|
|
|
- message_line_ = "Sequence timeout";
|
|
|
|
|
- // Fall through to try fallback bindings
|
|
|
|
|
- break;
|
|
|
|
|
-
|
|
|
|
|
- case KeyResult::Unbound:
|
|
|
|
|
- debug_log << "No key binding found, trying C++ fallbacks" << std::endl;
|
|
|
|
|
- // Fall through to C++ fallback bindings
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Clear any sequence display since we're not in a partial state
|
|
|
|
|
- message_line_.clear();
|
|
|
|
|
-
|
|
|
|
|
- // C++ fallback bindings - these should eventually be moved to Lua
|
|
|
|
|
-
|
|
|
|
|
- // Quit
|
|
|
|
|
- if (key_name == "C-q") {
|
|
|
|
|
- core_->request_quit();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Command mode
|
|
|
|
|
- if (key_name == "M-x") {
|
|
|
|
|
- mode_ = Mode::Command;
|
|
|
|
|
- command_buffer_.clear();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Navigation fallbacks (these should be in Lua)
|
|
|
|
|
- if (key_name == "ArrowUp") {
|
|
|
|
|
- core_->move_up();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- if (key_name == "ArrowDown") {
|
|
|
|
|
- core_->move_down();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- if (key_name == "ArrowLeft") {
|
|
|
|
|
- core_->move_left();
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ // Handle Minibuffer Input Logic
|
|
|
|
|
+ if (core_->minibuffer_manager().is_active()) {
|
|
|
|
|
+ // Pass the key event to the MinibufferManager
|
|
|
|
|
+ return core_->minibuffer_manager().handle_key_event(key_name);
|
|
|
}
|
|
}
|
|
|
- if (key_name == "ArrowRight") {
|
|
|
|
|
- core_->move_right();
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- if (key_name == "Home") { core_->move_to_line_start(); return true; }
|
|
|
|
|
- if (key_name == "End") { core_->move_to_line_end(); return true; }
|
|
|
|
|
-
|
|
|
|
|
- // Editing fallbacks (these should also be in Lua)
|
|
|
|
|
- if (key_name == "Backspace") {
|
|
|
|
|
- auto cursor = core_->cursor();
|
|
|
|
|
- core_->buffer().erase_char(cursor);
|
|
|
|
|
- if (cursor.column > 0) {
|
|
|
|
|
- core_->set_cursor({cursor.line, cursor.column - 1});
|
|
|
|
|
- } else if (cursor.line > 0) {
|
|
|
|
|
- size_t prev_line_len = core_->buffer().line(cursor.line - 1).size();
|
|
|
|
|
- core_->set_cursor({cursor.line - 1, prev_line_len});
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (key_name == "Delete") {
|
|
|
|
|
- auto cursor = core_->cursor();
|
|
|
|
|
- if (cursor.column < core_->buffer().line(cursor.line).size()) {
|
|
|
|
|
- core_->buffer().erase_char({cursor.line, cursor.column + 1});
|
|
|
|
|
- } else if (cursor.line < core_->buffer().line_count() - 1) {
|
|
|
|
|
- core_->buffer().erase_char({cursor.line + 1, 0});
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (key_name == "Return") {
|
|
|
|
|
- auto cursor = core_->cursor();
|
|
|
|
|
- core_->buffer().insert_newline(cursor);
|
|
|
|
|
- core_->set_cursor({cursor.line + 1, 0});
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (key_name == "Tab") {
|
|
|
|
|
- auto cursor = core_->cursor();
|
|
|
|
|
- core_->buffer().insert(cursor, " ");
|
|
|
|
|
- core_->set_cursor({cursor.line, cursor.column + 4});
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Insert printable characters (use original key_name for this)
|
|
|
|
|
- if (key_name.size() == 1 && key_name[0] >= 32 && key_name[0] <= 126) {
|
|
|
|
|
- auto cursor = core_->cursor();
|
|
|
|
|
- core_->buffer().insert_char(cursor, key_name[0]);
|
|
|
|
|
- core_->set_cursor({cursor.line, cursor.column + 1});
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return false;
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
-void TuiEditor::execute_command(const std::string& cmd) {
|
|
|
|
|
- if (cmd.empty()) return;
|
|
|
|
|
-
|
|
|
|
|
- std::istringstream iss(cmd);
|
|
|
|
|
- std::string command;
|
|
|
|
|
- iss >> command;
|
|
|
|
|
-
|
|
|
|
|
- if (command == "q" || command == "quit") {
|
|
|
|
|
- core_->request_quit();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (command == "w" || command == "write") {
|
|
|
|
|
- core_->buffer().save();
|
|
|
|
|
- message_line_ = "Saved";
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (command == "wq") {
|
|
|
|
|
- core_->buffer().save();
|
|
|
|
|
- core_->request_quit();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (command == "e" || command == "edit") {
|
|
|
|
|
- std::string path;
|
|
|
|
|
- std::getline(iss >> std::ws, path);
|
|
|
|
|
- if (!path.empty()) {
|
|
|
|
|
- if (core_->load_file(path)) {
|
|
|
|
|
- message_line_ = "Loaded: " + path;
|
|
|
|
|
- core_->lua_api()->execute("auto_activate_major_mode()") ;
|
|
|
|
|
- } else { message_line_ = "Failed to load: " + path;
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- message_line_ = "Usage: :e <filename>";
|
|
|
|
|
- }
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Try executing via command registry first
|
|
|
|
|
- auto& lua = core_->lua_api()->state();
|
|
|
|
|
- sol::function exec_cmd = lua["execute_extended_command"];
|
|
|
|
|
- if (exec_cmd.valid()) {
|
|
|
|
|
- bool result = exec_cmd(cmd);
|
|
|
|
|
- if (result) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Normal mode processing (pass to keybinding system)
|
|
|
|
|
+ return process_key(key_name);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // Fallback: Try executing as Lua code
|
|
|
|
|
- if (core_->lua_api()->execute(cmd)) {
|
|
|
|
|
- message_line_ = "Lua executed";
|
|
|
|
|
- } else {
|
|
|
|
|
- message_line_ = "Unknown command: " + cmd;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+int TuiEditor::get_attributes_for_face(const std::string& face_name) {
|
|
|
|
|
+ auto theme = core_->active_theme();
|
|
|
|
|
+ if (!theme) return 0;
|
|
|
|
|
+ return theme->get_face_attributes_ncurses(face_name);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
int TuiEditor::get_attributes_for_face(const std::string& face_name) {
|
|
int TuiEditor::get_attributes_for_face(const std::string& face_name) {
|
|
@@ -856,8 +337,12 @@ void TuiEditor::render() {
|
|
|
}
|
|
}
|
|
|
clear();
|
|
clear();
|
|
|
|
|
|
|
|
- // Calculate content area (leave room for message line only)
|
|
|
|
|
- int content_height = height_ - 1;
|
|
|
|
|
|
|
+ // Calculate content area (leave room for message line and potentially a completion line)
|
|
|
|
|
+ int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
|
|
|
|
|
+ // if (core_->minibuffer_manager().is_active() && !core_->minibuffer_manager().get_completion_candidates().empty()) {
|
|
|
|
|
+ // minibuffer_lines++; // Reserve an extra line for completions
|
|
|
|
|
+ // }
|
|
|
|
|
+ int content_height = height_ - minibuffer_lines;
|
|
|
int content_width = width_;
|
|
int content_width = width_;
|
|
|
|
|
|
|
|
// Render the layout tree recursively (now includes per-window modelines)
|
|
// Render the layout tree recursively (now includes per-window modelines)
|
|
@@ -1112,33 +597,6 @@ void TuiEditor::render_window_modeline(std::shared_ptr<Window> window, int x, in
|
|
|
attroff(attrs);
|
|
attroff(attrs);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void TuiEditor::render_status_line() {
|
|
|
|
|
- const auto cursor = core_->cursor();
|
|
|
|
|
- const auto& buffer = core_->buffer();
|
|
|
|
|
-
|
|
|
|
|
- int status_y = height_ - 2;
|
|
|
|
|
- int attrs = get_attributes_for_face("mode-line");
|
|
|
|
|
- if (attrs == 0) attrs = A_REVERSE;
|
|
|
|
|
-
|
|
|
|
|
- attron(attrs);
|
|
|
|
|
- move(status_y, 0);
|
|
|
|
|
- clrtoeol();
|
|
|
|
|
-
|
|
|
|
|
- std::string status = buffer.name();
|
|
|
|
|
- if (buffer.is_modified()) status += " [+] ";
|
|
|
|
|
- status += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1);
|
|
|
|
|
- status += " | " + std::to_string(width_) + "x" + std::to_string(height_);
|
|
|
|
|
- if (mode_ == Mode::Command) status += " [CMD]";
|
|
|
|
|
- else if (mode_ == Mode::FindFile) status += " [FILE]";
|
|
|
|
|
- else if (mode_ == Mode::BufferSwitch) status += " [BUFFER]";
|
|
|
|
|
- else if (mode_ == Mode::KillBuffer) status += " [KILL]";
|
|
|
|
|
- else if (mode_ == Mode::ConfirmKill) status += " [CONFIRM]";
|
|
|
|
|
- else if (mode_ == Mode::ISearch) status += " [I-SEARCH]";
|
|
|
|
|
-
|
|
|
|
|
- mvprintw(status_y, 0, "%s", status.c_str());
|
|
|
|
|
- attroff(attrs);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
void TuiEditor::render_message_line() {
|
|
void TuiEditor::render_message_line() {
|
|
|
int msg_y = height_ - 1;
|
|
int msg_y = height_ - 1;
|
|
|
int attrs = get_attributes_for_face("minibuffer-prompt");
|
|
int attrs = get_attributes_for_face("minibuffer-prompt");
|
|
@@ -1147,32 +605,37 @@ void TuiEditor::render_message_line() {
|
|
|
move(msg_y, 0);
|
|
move(msg_y, 0);
|
|
|
clrtoeol();
|
|
clrtoeol();
|
|
|
|
|
|
|
|
- if (mode_ == Mode::Command) {
|
|
|
|
|
- mvprintw(msg_y, 0, ":%s", command_buffer_.c_str());
|
|
|
|
|
- } else if (mode_ == Mode::FindFile) {
|
|
|
|
|
- mvprintw(msg_y, 0, "Find file: %s", command_buffer_.c_str());
|
|
|
|
|
- } else if (mode_ == Mode::BufferSwitch) {
|
|
|
|
|
- std::string prompt = "Switch to buffer: " + command_buffer_;
|
|
|
|
|
- if (!completion_candidates_.empty()) {
|
|
|
|
|
- prompt += " [" + std::to_string(completion_index_ + 1) + "/" +
|
|
|
|
|
- std::to_string(completion_candidates_.size()) + "]";
|
|
|
|
|
- }
|
|
|
|
|
- mvprintw(msg_y, 0, "%s", prompt.c_str());
|
|
|
|
|
- } else if (mode_ == Mode::KillBuffer) {
|
|
|
|
|
- std::string prompt = "Kill buffer: " + command_buffer_;
|
|
|
|
|
- if (!completion_candidates_.empty()) {
|
|
|
|
|
- prompt += " [" + std::to_string(completion_index_ + 1) + "/" +
|
|
|
|
|
- std::to_string(completion_candidates_.size()) + "]";
|
|
|
|
|
|
|
+ if (core_->minibuffer_manager().is_active()) {
|
|
|
|
|
+ std::string prompt_part = core_->minibuffer_manager().get_prompt();
|
|
|
|
|
+ std::string input_part = core_->minibuffer_manager().get_input_buffer();
|
|
|
|
|
+ std::string display_text = prompt_part + input_part;
|
|
|
|
|
+
|
|
|
|
|
+ mvprintw(msg_y, 0, "%s", display_text.c_str());
|
|
|
|
|
+
|
|
|
|
|
+ // Display completion candidates below the input line
|
|
|
|
|
+ auto candidates = core_->minibuffer_manager().get_completion_candidates();
|
|
|
|
|
+ if (!candidates.empty()) {
|
|
|
|
|
+ std::string completion_display;
|
|
|
|
|
+ for (size_t i = 0; i < candidates.size() && completion_display.length() < width_ - 5; ++i) {
|
|
|
|
|
+ if (!completion_display.empty()) completion_display += " ";
|
|
|
|
|
+ completion_display += candidates[i].display_text; // Use display_text
|
|
|
|
|
+ }
|
|
|
|
|
+ if (completion_display.length() >= width_ - 5) {
|
|
|
|
|
+ completion_display = completion_display.substr(0, width_ - 8) + "...";
|
|
|
|
|
+ }
|
|
|
|
|
+ // Move up one line to display completions above the current minibuffer line
|
|
|
|
|
+ mvprintw(msg_y - 1, 0, "%s", completion_display.c_str());
|
|
|
}
|
|
}
|
|
|
- mvprintw(msg_y, 0, "%s", prompt.c_str());
|
|
|
|
|
- } else if (mode_ == Mode::ISearch) {
|
|
|
|
|
- std::string prompt = (isearch_failed_ ? "Failing " : "") + std::string("I-search: ") + isearch_query_;
|
|
|
|
|
- mvprintw(msg_y, 0, "%s", prompt.c_str());
|
|
|
|
|
|
|
+
|
|
|
} else if (!message_line_.empty()) {
|
|
} else if (!message_line_.empty()) {
|
|
|
|
|
+ // Display transient message
|
|
|
mvprintw(msg_y, 0, "%s", message_line_.c_str());
|
|
mvprintw(msg_y, 0, "%s", message_line_.c_str());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
attroff(attrs);
|
|
attroff(attrs);
|
|
|
|
|
+
|
|
|
|
|
+ // After rendering, check if it's time to clear the message.
|
|
|
|
|
+ core_->check_and_clear_message();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
namespace lumacs {
|
|
namespace lumacs {
|