Browse Source

feat(completion): enhance file path autocompletion

Implemented robust file path completion in CompletionSystem. Features include tilde expansion (HOME), proper handling of absolute/relative paths, directory suffixing, and filtering of hidden files unless requested.
Bernardo Magri 1 tháng trước cách đây
mục cha
commit
24d017b21c
2 tập tin đã thay đổi với 86 bổ sung27 xóa
  1. 1 0
      documentation/PLAN.md
  2. 85 27
      src/completion_system.cpp

+ 1 - 0
documentation/PLAN.md

@@ -141,6 +141,7 @@ 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.
 - ✅ **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.