1
0

2 Коммитууд f77a196c6f ... c74c0f295b

Эзэн SHA1 Мессеж Огноо
  Bernardo Magri c74c0f295b fix(gtk): correct minibuffer cursor rendering 1 сар өмнө
  Bernardo Magri 24d017b21c feat(completion): enhance file path autocompletion 1 сар өмнө

+ 2 - 0
documentation/PLAN.md

@@ -141,6 +141,8 @@ Lumacs/
     - **Minibuffer & Popover Polish**: ✅ Fixed. Exposed ISearch direction control, fixed TUI rendering and Backspace, fixed GTK minibuffer double-rendering and disappearance, polished GTK Popover positioning and focus management, fixed GTK crash on completion mismatch, and fixed minibuffer cursor not moving after Tab completion.
     - [x] **GTK Completion Popover**: Disabled by default via Lua configuration (user request).
     - [x] **Persistent Messages**: Implemented `*Messages*` buffer to log all minibuffer outputs, ensuring history is accessible via buffer switching.
+    - [x] **File Path Completion**: Implemented robust file path autocompletion with tilde expansion and directory browsing.
+    - [x] **GTK Minibuffer Cursor**: Fixed cursor rendering to be a proper block cursor positioned correctly at the editing point, not a line at the end of the text.
 - ✅ **Theme System Refactoring**:
     - [x] Implemented `editor:create_and_register_theme` Lua API to allow theme definition from Lua.
     - [x] Factored all hardcoded C++ themes (`default`, `everforest-dark`, `dracula`, `solarized-dark`, `nord`, `gruvbox-light`) into individual Lua files (`lua/themes/*.lua`).

+ 85 - 27
src/completion_system.cpp

@@ -52,6 +52,9 @@ private:
     EditorCore& core_;
 };
 
+// Include cstdlib for std::getenv
+#include <cstdlib> 
+
 /// @brief Completion source for file paths.
 class FilePathCompletionSource : public ICompletionSource {
 public:
@@ -59,46 +62,101 @@ public:
 
     std::vector<CompletionCandidate> get_candidates(const std::string& input_text) override {
         std::vector<CompletionCandidate> candidates;
-        std::filesystem::path current_path = input_text;
+        std::string expanded_input = expand_tilde(input_text);
         
-        // If input ends with /, list contents of that directory
-        if (input_text.empty() || input_text.back() == '/') {
-            try {
-                for (const auto& entry : std::filesystem::directory_iterator(current_path)) {
-                    std::string path_str = entry.path().string();
-                    if (entry.is_directory()) {
-                        path_str += "/";
-                    }
-                    candidates.emplace_back(path_str);
-                }
-            } catch (const std::filesystem::filesystem_error& e) {
-                // Ignore errors like "No such file or directory"
-            }
+        std::filesystem::path path_obj(expanded_input);
+        std::filesystem::path dir;
+        std::string prefix;
+
+        // Determine directory to search and prefix to match
+        if (input_text.empty()) {
+            dir = std::filesystem::current_path();
+            prefix = "";
+        } else if (input_text.back() == '/') {
+            dir = expanded_input;
+            prefix = "";
         } else {
-            // Find files/dirs that start with the input_text in the parent directory
-            std::filesystem::path parent_path = current_path.parent_path();
-            std::string stem = current_path.filename().string();
+            // If expanded_input is just a directory (like "/tmp"), parent_path is "/" and filename is "tmp".
+            // But we want to search INSIDE /tmp if the user typed "/tmp/" (handled above).
+            // If user typed "/tmp", we search in "/" for "tmp*".
+            // The filesystem::path parsing handles this:
+            // path("/tmp").parent_path() -> "/"
+            // path("/tmp").filename() -> "tmp"
+            
+            dir = path_obj.parent_path();
+            prefix = path_obj.filename().string();
             
-            try {
-                for (const auto& entry : std::filesystem::directory_iterator(parent_path)) {
-                    std::string entry_name = entry.path().filename().string();
-                    if (entry_name.rfind(stem, 0) == 0) {
-                        std::string full_path = entry.path().string();
+            // Handle relative paths (empty parent means current dir, unless it's root)
+            if (dir.empty()) {
+                if (path_obj.is_absolute()) {
+                    // Should typically not happen for unix paths unless it is root?
+                    // path("/").parent_path() is "/".
+                } else {
+                    dir = std::filesystem::current_path();
+                }
+            }
+        }
+
+        try {
+            // Check if directory exists
+            if (std::filesystem::exists(dir) && std::filesystem::is_directory(dir)) {
+                for (const auto& entry : std::filesystem::directory_iterator(dir)) {
+                    std::string filename = entry.path().filename().string();
+                    
+                    // Skip hidden files unless prefix starts with '.'
+                    if (filename.size() > 0 && filename[0] == '.' && (prefix.empty() || prefix[0] != '.')) {
+                        continue;
+                    }
+
+                    // Check prefix match
+                    if (filename.rfind(prefix, 0) == 0) {
+                        // Reconstruct the completion text based on ORIGINAL input
+                        std::string result_str;
+                        
+                        if (input_text.empty()) {
+                            result_str = filename;
+                        } else if (input_text.back() == '/') {
+                            result_str = input_text + filename;
+                        } else {
+                            // Replace the partial filename in input with the full match
+                            size_t last_sep = input_text.find_last_of('/');
+                            if (last_sep != std::string::npos) {
+                                result_str = input_text.substr(0, last_sep + 1) + filename;
+                            } else {
+                                result_str = filename;
+                            }
+                        }
+
                         if (entry.is_directory()) {
-                            full_path += "/";
+                            result_str += "/";
                         }
-                        candidates.emplace_back(full_path);
+                        
+                        candidates.emplace_back(result_str);
                     }
                 }
-            } catch (const std::filesystem::filesystem_error& e) {
-                // Ignore errors
             }
+        } catch (...) {
+            // Ignore filesystem errors (permissions, etc.)
         }
+        
         return candidates;
     }
 
 private:
-    [[maybe_unused]] EditorCore& core_; // Reserved for future filesystem operations
+    [[maybe_unused]] EditorCore& core_; 
+
+    std::string expand_tilde(const std::string& path) {
+        if (path.empty()) return path;
+        if (path[0] == '~') {
+            const char* home = std::getenv("HOME");
+            if (home) {
+                // Handle "~" -> "/home/user" and "~/foo" -> "/home/user/foo"
+                if (path.length() == 1) return std::string(home);
+                if (path[1] == '/') return std::string(home) + path.substr(1);
+            }
+        }
+        return path;
+    }
 };
 
 /// @brief Completion source for theme names.

+ 50 - 13
src/gtk_renderer.cpp

@@ -504,20 +504,54 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
     }
     
     // Render minibuffer cursor if active and visible (assuming cursor_visible is managed by GtkEditor)
-    bool cursor_visible_ = true; // Placeholder
-    if (core_.minibuffer_manager().is_active() && cursor_visible_) {
+    bool cursor_visible_ = true; // Placeholder - in real usage passed or managed
+    if (core_.minibuffer_manager().is_active()) { // Assuming always visible or managed externally
         // Calculate cursor position in minibuffer
+        // text is prompt + input. Cursor is relative to input start.
+        size_t cursor_idx = prompt_part.length() + core_.minibuffer_manager().get_cursor_position();
+        
         auto layout = Pango::Layout::create(context_widget_.get_pango_context());
         layout->set_font_description(font_desc_);
-        layout->set_text(minibuffer_text); // Measure full text
-        Pango::Rectangle ink_rect, logical_rect;
-        layout->get_pixel_extents(ink_rect, logical_rect);
-        double cursor_x = minibuffer_x + logical_rect.get_width();
+        layout->set_text(minibuffer_text); 
+        
+        Pango::Rectangle pos = layout->index_to_pos(static_cast<int>(cursor_idx));
+        double cursor_screen_x = minibuffer_x + (pos.get_x() / (double)PANGO_SCALE);
         
-        // Draw minibuffer cursor
+        // Draw Cursor Block
+        // Use char_width_ as default, or measure actual char
+        double cur_width = char_width_;
+        // If not at end, measure actual char width
+        if (cursor_idx < minibuffer_text.length()) {
+             Pango::Rectangle next_pos = layout->index_to_pos(static_cast<int>(cursor_idx + 1));
+             cur_width = (next_pos.get_x() - pos.get_x()) / (double)PANGO_SCALE;
+        }
+
         cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
-        cr->rectangle(cursor_x, minibuffer_y, 2.0, line_height_);
+        cr->rectangle(cursor_screen_x, minibuffer_y, cur_width, line_height_);
         cr->fill();
+        
+        // Draw Character Inverted (if not at end)
+        if (cursor_idx < minibuffer_text.length()) {
+             // Extract char at cursor
+             // Note: simple indexing works for ASCII, but for UTF-8 need careful extraction.
+             // Pango layout handles rendering, we just need to render that one char at the pos.
+             // Simpler: Set color to BG and re-render the layout clipped? 
+             // Or just render single char?
+             // Let's reuse the main editor approach: create layout for single char.
+             // But we need the exact byte string for the character at cursor_idx (utf8 aware).
+             
+             // For now, let's rely on std::string indexing assuming ASCII for command/paths mostly, 
+             // or use Pango to iterate?
+             // Glib::ustring is better for UTF8.
+             // Let's assume standard char for now to fix the main visual bug.
+             char cursor_char = minibuffer_text[cursor_idx];
+             
+             cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
+             auto cursor_layout = context_widget_.create_pango_layout(std::string(1, cursor_char));
+             cursor_layout->set_font_description(font_desc_);
+             cr->move_to(cursor_screen_x, minibuffer_y);
+             cursor_layout->show_in_cairo_context(cr);
+        }
 
         // Render completion overlay if applicable
         auto current_completion = core_.minibuffer_manager().get_current_completion();
@@ -526,10 +560,13 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
             if (input_part.length() <= current_completion->length()) {
                 std::string completion_suffix = current_completion->substr(input_part.length());
                 if (!completion_suffix.empty()) {
-                    // Calculate width of existing text to position suffix
-                    Pango::Rectangle ink_rect, logical_rect;
-                    layout->get_pixel_extents(ink_rect, logical_rect);
-                    double text_width = logical_rect.get_width();
+                    // Position completion at the end of the input text (cursor might be in middle)
+                    // completion suffix appends to the END of input.
+                    
+                    // Calculate position of the end of input
+                    size_t input_end_idx = prompt_part.length() + input_part.length();
+                    Pango::Rectangle end_pos = layout->index_to_pos(static_cast<int>(input_end_idx));
+                    double completion_x = minibuffer_x + (end_pos.get_x() / (double)PANGO_SCALE);
 
                     auto completion_layout = Pango::Layout::create(context_widget_.get_pango_context());
                     completion_layout->set_font_description(font_desc_);
@@ -552,7 +589,7 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
                     completion_layout->set_attributes(completion_attr_list);
 
                     cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
-                    cr->move_to(minibuffer_x + text_width, minibuffer_y); // Position after text
+                    cr->move_to(completion_x, minibuffer_y); 
                     completion_layout->show_in_cairo_context(cr);
                 }
             }