From 658a56ca55ba0d51fbc0994d3634cf2f558c85c3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 28 Jul 2025 19:28:46 -0400 Subject: [PATCH] adding functionalities on the buffer --- .gitignore | 2 + README.md | 237 ++++++++++++++++++--- lua/ideaDrop/{ => core}/config.lua | 0 lua/ideaDrop/core/init.lua | 248 ++++++++++++++++++++++ lua/ideaDrop/{ => features}/list.lua | 6 +- lua/ideaDrop/features/search.lua | 294 +++++++++++++++++++++++++++ lua/ideaDrop/features/tags.lua | 283 ++++++++++++++++++++++++++ lua/ideaDrop/init.lua | 54 +---- lua/ideaDrop/keymaps.lua | 16 -- lua/ideaDrop/sidebar.lua | 82 -------- lua/ideaDrop/ui/sidebar.lua | 275 +++++++++++++++++++++++++ lua/ideaDrop/ui/tree.lua | 138 +++++++++++++ lua/ideaDrop/utils.lua | 0 lua/ideaDrop/utils/constants.lua | 151 ++++++++++++++ lua/ideaDrop/utils/keymaps.lua | 20 ++ lua/ideaDrop/utils/utils.lua | 181 +++++++++++++++++ 16 files changed, 1809 insertions(+), 178 deletions(-) rename lua/ideaDrop/{ => core}/config.lua (100%) create mode 100644 lua/ideaDrop/core/init.lua rename lua/ideaDrop/{ => features}/list.lua (83%) create mode 100644 lua/ideaDrop/features/search.lua create mode 100644 lua/ideaDrop/features/tags.lua delete mode 100644 lua/ideaDrop/keymaps.lua delete mode 100644 lua/ideaDrop/sidebar.lua create mode 100644 lua/ideaDrop/ui/sidebar.lua create mode 100644 lua/ideaDrop/ui/tree.lua delete mode 100644 lua/ideaDrop/utils.lua create mode 100644 lua/ideaDrop/utils/constants.lua create mode 100644 lua/ideaDrop/utils/keymaps.lua create mode 100644 lua/ideaDrop/utils/utils.lua diff --git a/.gitignore b/.gitignore index e18ca08..4e5642d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Lua bytecode (just in case) *.luac +*test* +todo* # OS junk .DS_Store Thumbs.db diff --git a/README.md b/README.md index f094a06..8108e8d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,51 @@ # ideaDrop.nvim -๐Ÿ’ก A simple Neovim plugin to drop and organize your ideas in floating Markdown sidebars. +๐Ÿ’ก A powerful Neovim plugin for capturing, organizing, and managing your ideas with multiple view modes, tagging system, and advanced search capabilities. + +## โœจ Features + +- ๐ŸŽฏ **Multiple View Modes**: Floating windows, current buffer, or persistent right-side buffer +- ๐Ÿท๏ธ **Smart Tagging System**: Add, remove, and filter ideas by tags +- ๐Ÿ” **Advanced Search**: Fuzzy search through titles and content +- ๐Ÿ“ **File Tree Browser**: Integrated nvim-tree for easy file navigation +- ๐Ÿ“ **Markdown Support**: Full markdown editing with syntax highlighting +- ๐Ÿ’พ **Auto-save**: Changes saved automatically +- ๐Ÿ“… **Date-based Organization**: Automatic date-based file naming +- ๐Ÿ—‚๏ธ **Folder Support**: Nested organization with subdirectories +- ๐ŸŽจ **Clean Interface**: Distraction-free writing environment ## ๐Ÿ“ฆ Installation -Using **lazy.nvim**: +### Using lazy.nvim ```lua { - dir = "/Users/carlos/Documents/SSD_Documents/personals/ideaDrop", + dir = "/path/to/ideaDrop", name = "ideaDrop", + dependencies = { + "nvim-tree/nvim-tree.lua", + "nvim-tree/nvim-web-devicons", + }, config = function() require("ideaDrop").setup({ - idea_dir = "/Users/carlos/Nextcloud/ObsidianVault", -- where your ideas will be saved + idea_dir = "/path/to/your/ideas", -- where your ideas will be saved + }) + end, +} +``` + +### Using packer + +```lua +use { + dir = "/path/to/ideaDrop", + requires = { + "nvim-tree/nvim-tree.lua", + "nvim-tree/nvim-web-devicons", + }, + config = function() + require("ideaDrop").setup({ + idea_dir = "/path/to/your/ideas", }) end, } @@ -24,41 +57,166 @@ Using **lazy.nvim**: |--------|------|---------|-------------| | `idea_dir` | string | `vim.fn.stdpath("data") .. "/ideaDrop"` | Directory where your idea files will be stored | -## ๐Ÿงช Commands +### Example Configuration + +```lua +require("ideaDrop").setup({ + idea_dir = "/Users/carlos/Nextcloud/ObsidianVault", +}) +``` + +## ๐ŸŽฎ Commands + +### Core Commands | Command | Description | |---------|-------------| -| `:Idea` | Opens today's idea file | -| `:Idea name` | Opens or creates an idea with the specified name | -| `:Idea listAll` | Opens a fuzzy picker to select from existing ideas | +| `:Idea` | Opens today's idea in floating window | +| `:Idea name` | Opens or creates an idea with the specified name (floating) | +| `:IdeaBuffer` | Opens today's idea in current buffer | +| `:IdeaBuffer name` | Opens or creates an idea in current buffer | +| `:IdeaRight` | Opens today's idea in persistent right-side buffer | +| `:IdeaRight name` | Opens or creates an idea in right-side buffer | +| `:IdeaTree` | Opens nvim-tree file browser on the left | -## ๐Ÿ“Œ Features +### Tag Commands -- ๐Ÿ“ Markdown editor in a floating sidebar -- ๐Ÿ’พ Automatic save on close -- ๐Ÿ“… Date-based and custom named notes -- ๐Ÿ“ Folder support (e.g., `project/vision.md`) -- ๐Ÿ” Fuzzy finder for existing ideas -- ๐ŸŽจ Clean and distraction-free interface +| Command | Description | +|---------|-------------| +| `:IdeaTags` | Shows tag picker to browse files by tag | +| `:IdeaAddTag tag` | Adds a tag to the current idea file | +| `:IdeaRemoveTag tag` | Removes a tag from the current idea file | +| `:IdeaSearchTag tag` | Searches for files with a specific tag | -## ๐Ÿ—‚ Example Usage +### Search Commands + +| Command | Description | +|---------|-------------| +| `:IdeaSearch query` | Fuzzy search through idea titles and content | +| `:IdeaSearchContent query` | Search only in idea content | +| `:IdeaSearchTitle query` | Search only in idea titles | + +## โŒจ๏ธ Keymaps + +The plugin automatically sets up convenient keymaps: + +| Keymap | Command | Description | +|--------|---------|-------------| +| `id` | `:IdeaRight` | Open today's idea in right buffer | +| `in` | `:IdeaRight ` | Open named idea in right buffer | +| `it` | `:IdeaTree` | Open tree browser | +| `is` | `:IdeaSearch ` | Search ideas | +| `ig` | `:IdeaTags` | Browse tags | +| `if` | `:Idea` | Open today's idea in float | + +## ๐Ÿ—‚๏ธ Usage Examples + +### Basic Usage ```vim -:Idea project/nextgen " Opens or creates project/nextgen.md -:Idea listAll " Opens fuzzy finder for all ideas +:IdeaRight " Open today's idea in right buffer +:IdeaRight project/vision " Open project/vision.md in right buffer +:IdeaTree " Browse all ideas with nvim-tree ``` -## ๐Ÿ“š Documentation +### Tag Management -For detailed documentation, run `:help ideaDrop` in Neovim. +```vim +:IdeaAddTag #work " Add #work tag to current idea +:IdeaAddTag #personal " Add #personal tag to current idea +:IdeaTags " Browse all tags +:IdeaSearchTag #work " Find all ideas with #work tag +``` -## ๐Ÿ›  Development +### Search and Discovery + +```vim +:IdeaSearch "machine learning" " Search for "machine learning" in all ideas +:IdeaSearchContent "algorithm" " Search content for "algorithm" +:IdeaSearchTitle "project" " Search titles for "project" +``` + +### File Organization + +```vim +:IdeaRight meetings/2024-01-15 " Create nested folder structure +:IdeaRight projects/app/features " Organize by project and feature +``` + +## ๐Ÿท๏ธ Tagging System + +The plugin includes a powerful tagging system: + +- **Add tags**: Use `#tag` format in your markdown files +- **Auto-completion**: Tags are automatically detected and indexed +- **Filter by tags**: Browse and filter ideas by tags +- **Tag statistics**: See how many files use each tag + +### Tag Examples + +```markdown +# My Idea Title + +This is my idea content. + +#work #project-x #feature #todo +``` + +## ๐Ÿ” Search Features + +### Fuzzy Search +- Search through file titles and content +- Real-time results as you type +- Navigate through search results easily + +### Content Search +- Search only in the body of your ideas +- Perfect for finding specific concepts or references + +### Title Search +- Search only in file names +- Quick way to find specific ideas + +## ๐Ÿ“ File Tree Integration + +The plugin integrates with nvim-tree for seamless file browsing: + +- **Left-side tree**: Opens on the left side of your screen +- **File selection**: Click or press Enter to open files +- **Directory navigation**: Browse through your idea folders +- **File operations**: Create, delete, rename files directly + +## ๐ŸŽฏ View Modes + +### 1. Floating Window (Original) +- Opens ideas in a floating window +- Good for quick notes +- Command: `:Idea` + +### 2. Current Buffer +- Opens ideas in the current buffer +- Replaces current content +- Command: `:IdeaBuffer` + +### 3. Right-Side Buffer (Recommended) +- Persistent buffer on the right side +- Stays open while you work +- Perfect for ongoing projects +- Command: `:IdeaRight` + +### 4. Tree Browser +- Full file tree on the left side +- Integrated with nvim-tree +- Command: `:IdeaTree` + +## ๐Ÿ› ๏ธ Development This plugin is built with: -- Lua -- Neovim API -- Markdown support -- Fuzzy finding capabilities +- **Lua**: Core functionality +- **Neovim API**: Native Neovim integration +- **nvim-tree**: File tree browsing +- **telescope**: Search functionality +- **Markdown**: Rich text support ## ๐Ÿ“„ License @@ -66,4 +224,31 @@ MIT License - feel free to use this plugin in your own projects! ## ๐Ÿค Contributing -Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file +Contributions are welcome! Please feel free to submit a Pull Request. + +### Development Setup + +1. Clone the repository +2. Install dependencies (nvim-tree, telescope) +3. Configure the plugin in your Neovim setup +4. Test with the provided commands and keymaps + +## ๐Ÿ› Troubleshooting + +### Common Issues + +1. **Module not found errors**: Ensure all dependencies are installed +2. **Tree not opening**: Make sure nvim-tree is properly configured +3. **Search not working**: Verify telescope is installed and configured +4. **Tags not showing**: Check that your idea directory exists and contains markdown files + +### Getting Help + +- Check the configuration examples above +- Ensure all dependencies are installed +- Verify your idea directory path is correct +- Test with the basic commands first + +--- + +**Happy idea capturing! ๐Ÿš€** \ No newline at end of file diff --git a/lua/ideaDrop/config.lua b/lua/ideaDrop/core/config.lua similarity index 100% rename from lua/ideaDrop/config.lua rename to lua/ideaDrop/core/config.lua diff --git a/lua/ideaDrop/core/init.lua b/lua/ideaDrop/core/init.lua new file mode 100644 index 0000000..f5f033a --- /dev/null +++ b/lua/ideaDrop/core/init.lua @@ -0,0 +1,248 @@ +-- ideaDrop.nvim/core/init.lua +-- Core modules +local config = require("ideaDrop.core.config") + +-- UI modules +local sidebar = require("ideaDrop.ui.sidebar") +local tree = require("ideaDrop.ui.tree") + +-- Feature modules +local list = require("ideaDrop.features.list") +local tags = require("ideaDrop.features.tags") +local search = require("ideaDrop.features.search") + +-- Utility modules +local keymaps = require("ideaDrop.utils.keymaps") + +local M = {} + +---@class IdeaDrop +---@field setup fun(user_opts: IdeaDropConfig): nil + +---@class IdeaDropConfig +---@field idea_dir string Directory where idea files will be stored + +---Setup function for ideaDrop.nvim +---@param user_opts IdeaDropConfig|nil User configuration options +---@return nil +function M.setup(user_opts) + config.setup(user_opts) + + -- Command to open ideas in floating window (original behavior) + vim.api.nvim_create_user_command("Idea", function(opts) + local arg = opts.args + local idea_dir = config.options.idea_dir + + if arg == "listAll" then + list.list_all() + elseif arg ~= "" then + -- Ensure directory exists (even for nested folders) + local filename = arg:match("%.md$") and arg or (arg .. ".md") + local full_path = idea_dir .. "/" .. filename + + -- Create parent folders if needed + local folder = vim.fn.fnamemodify(full_path, ":h") + if vim.fn.isdirectory(folder) == 0 then + vim.fn.mkdir(folder, "p") + end + + sidebar.open(full_path, filename, false) + else + -- Default to today's idea file + local path = string.format("%s/%s.md", idea_dir, os.date("%Y-%m-%d")) + sidebar.open(path, nil, false) + end + end, { + nargs = "?", + complete = function() + return { "listAll" } + end, + desc = "Open today's idea, a named idea, or list all (in floating window)", + }) + + -- Command to open ideas in current buffer + vim.api.nvim_create_user_command("IdeaBuffer", function(opts) + local arg = opts.args + local idea_dir = config.options.idea_dir + + if arg == "listAll" then + list.list_all() + elseif arg ~= "" then + -- Ensure directory exists (even for nested folders) + local filename = arg:match("%.md$") and arg or (arg .. ".md") + local full_path = idea_dir .. "/" .. filename + + -- Create parent folders if needed + local folder = vim.fn.fnamemodify(full_path, ":h") + if vim.fn.isdirectory(folder) == 0 then + vim.fn.mkdir(folder, "p") + end + + sidebar.open(full_path, filename, true) + else + -- Default to today's idea file + local path = string.format("%s/%s.md", idea_dir, os.date("%Y-%m-%d")) + sidebar.open(path, nil, true) + end + end, { + nargs = "?", + complete = function() + return { "listAll" } + end, + desc = "Open today's idea, a named idea, or list all (in current buffer)", + }) + + -- Command to open ideas in persistent right-side buffer + vim.api.nvim_create_user_command("IdeaRight", function(opts) + local arg = opts.args + local idea_dir = config.options.idea_dir + + if arg == "listAll" then + list.list_all() + elseif arg ~= "" then + -- Ensure directory exists (even for nested folders) + local filename = arg:match("%.md$") and arg or (arg .. ".md") + local full_path = idea_dir .. "/" .. filename + + -- Create parent folders if needed + local folder = vim.fn.fnamemodify(full_path, ":h") + if vim.fn.isdirectory(folder) == 0 then + vim.fn.mkdir(folder, "p") + end + + sidebar.open_right_side(full_path, filename) + else + -- Default to today's idea file + local path = string.format("%s/%s.md", idea_dir, os.date("%Y-%m-%d")) + sidebar.open_right_side(path, nil) + end + end, { + nargs = "?", + complete = function() + return { "listAll" } + end, + desc = "Open today's idea, a named idea, or list all (in persistent right-side buffer)", + }) + + -- Command to open the tree view for browsing ideas + vim.api.nvim_create_user_command("IdeaTree", function() + tree.open_tree_window(function(selected_file) + -- When a file is selected from the tree, open it in the right-side buffer + if selected_file then + local filename = vim.fn.fnamemodify(selected_file, ":t") + sidebar.open_right_side(selected_file, filename) + end + end) + end, { + desc = "Open tree view to browse and select idea files", + }) + + -- Tag-related commands + vim.api.nvim_create_user_command("IdeaTags", function() + tags.show_tag_picker(function(selected_tag) + if selected_tag then + tags.show_files_with_tag(selected_tag) + end + end) + end, { + desc = "Show all tags and browse files by tag", + }) + + vim.api.nvim_create_user_command("IdeaAddTag", function(opts) + local tag = opts.args + if tag == "" then + vim.notify("โŒ Please provide a tag name", vim.log.levels.ERROR) + return + end + + -- Get current file from right-side buffer or prompt for file + local current_file = sidebar.get_current_file() + if current_file then + tags.add_tag(current_file, tag) + else + vim.notify("โŒ No active idea file. Open an idea first.", vim.log.levels.ERROR) + end + end, { + nargs = 1, + desc = "Add a tag to the current idea file", + }) + + vim.api.nvim_create_user_command("IdeaRemoveTag", function(opts) + local tag = opts.args + if tag == "" then + vim.notify("โŒ Please provide a tag name", vim.log.levels.ERROR) + return + end + + -- Get current file from right-side buffer or prompt for file + local current_file = sidebar.get_current_file() + if current_file then + tags.remove_tag(current_file, tag) + else + vim.notify("โŒ No active idea file. Open an idea first.", vim.log.levels.ERROR) + end + end, { + nargs = 1, + desc = "Remove a tag from the current idea file", + }) + + vim.api.nvim_create_user_command("IdeaSearchTag", function(opts) + local tag = opts.args + if tag == "" then + vim.notify("โŒ Please provide a tag name", vim.log.levels.ERROR) + return + end + + tags.show_files_with_tag(tag) + end, { + nargs = 1, + desc = "Search for files with a specific tag", + }) + + -- Search-related commands + vim.api.nvim_create_user_command("IdeaSearch", function(opts) + local query = opts.args + if query == "" then + vim.notify("โŒ Please provide a search query", vim.log.levels.ERROR) + return + end + + search.fuzzy_search(query) + end, { + nargs = 1, + desc = "Fuzzy search through idea titles and content", + }) + + vim.api.nvim_create_user_command("IdeaSearchContent", function(opts) + local query = opts.args + if query == "" then + vim.notify("โŒ Please provide a search query", vim.log.levels.ERROR) + return + end + + search.search_in_content(query) + end, { + nargs = 1, + desc = "Search only in idea content", + }) + + vim.api.nvim_create_user_command("IdeaSearchTitle", function(opts) + local query = opts.args + if query == "" then + vim.notify("โŒ Please provide a search query", vim.log.levels.ERROR) + return + end + + search.search_by_title(query) + end, { + nargs = 1, + desc = "Search only in idea titles", + }) + + -- Set up keymaps + keymaps.setup() + + vim.notify("ideaDrop loaded!", vim.log.levels.INFO) +end + +return M diff --git a/lua/ideaDrop/list.lua b/lua/ideaDrop/features/list.lua similarity index 83% rename from lua/ideaDrop/list.lua rename to lua/ideaDrop/features/list.lua index 7f86d1e..2f0a025 100644 --- a/lua/ideaDrop/list.lua +++ b/lua/ideaDrop/features/list.lua @@ -1,6 +1,6 @@ --- ideaDrop/list.lua -local config = require("ideaDrop.config") -local sidebar = require("ideaDrop.sidebar") +-- ideaDrop/features/list.lua +local config = require("ideaDrop.core.config") +local sidebar = require("ideaDrop.ui.sidebar") ---@class List ---@field list_all fun(): nil diff --git a/lua/ideaDrop/features/search.lua b/lua/ideaDrop/features/search.lua new file mode 100644 index 0000000..572d793 --- /dev/null +++ b/lua/ideaDrop/features/search.lua @@ -0,0 +1,294 @@ +-- ideaDrop/features/search.lua +local config = require("ideaDrop.core.config") +local sidebar = require("ideaDrop.ui.sidebar") + +---@class Search +---@field fuzzy_search fun(query: string): nil +---@field search_in_content fun(query: string): nil +---@field search_by_title fun(query: string): nil +---@field show_search_results fun(results: table[]): nil +local M = {} + +-- Simple fuzzy matching function +local function fuzzy_match(str, pattern) + local str_lower = str:lower() + local pattern_lower = pattern:lower() + + local str_idx = 1 + local pattern_idx = 1 + + while str_idx <= #str_lower and pattern_idx <= #pattern_lower do + if str_lower:sub(str_idx, str_idx) == pattern_lower:sub(pattern_idx, pattern_idx) then + pattern_idx = pattern_idx + 1 + end + str_idx = str_idx + 1 + end + + return pattern_idx > #pattern_lower +end + +-- Calculate fuzzy match score (lower is better) +local function fuzzy_score(str, pattern) + local str_lower = str:lower() + local pattern_lower = pattern:lower() + + local score = 0 + local str_idx = 1 + local pattern_idx = 1 + local consecutive_bonus = 0 + + while str_idx <= #str_lower and pattern_idx <= #pattern_lower do + if str_lower:sub(str_idx, str_idx) == pattern_lower:sub(pattern_idx, pattern_idx) then + score = score + 1 + consecutive_bonus + consecutive_bonus = consecutive_bonus + 1 + pattern_idx = pattern_idx + 1 + else + consecutive_bonus = 0 + end + str_idx = str_idx + 1 + end + + if pattern_idx <= #pattern_lower then + return 999999 -- No match + end + + -- Penalize longer strings + score = score - (#str_lower - #pattern_lower) * 0.1 + + return -score -- Negative so lower scores are better +end + +---Performs fuzzy search across all idea files +---@param query string Search query +---@return nil +function M.fuzzy_search(query) + if query == "" then + vim.notify("โŒ Please provide a search query", vim.log.levels.ERROR) + return + end + + local idea_path = config.options.idea_dir + local results = {} + + -- Find all .md files recursively + local files = vim.fn.glob(idea_path .. "**/*.md", false, true) + + for _, file in ipairs(files) do + if vim.fn.filereadable(file) == 1 then + local filename = vim.fn.fnamemodify(file, ":t") + local relative_path = file:sub(#idea_path + 2) -- Remove idea_path + "/" + + -- Search in filename + local filename_score = fuzzy_score(filename, query) + if filename_score < 999999 then + table.insert(results, { + file = file, + relative_path = relative_path, + filename = filename, + score = filename_score, + match_type = "filename", + context = filename + }) + end + + -- Search in content + local content = vim.fn.readfile(file) + local content_str = table.concat(content, "\n") + + -- Search for query in content + local content_lower = content_str:lower() + local query_lower = query:lower() + + if content_lower:find(query_lower, 1, true) then + -- Find the line with the match + for line_num, line in ipairs(content) do + if line:lower():find(query_lower, 1, true) then + local context = line:gsub("^%s*(.-)%s*$", "%1") -- Trim whitespace + if #context > 80 then + context = context:sub(1, 77) .. "..." + end + + table.insert(results, { + file = file, + relative_path = relative_path, + filename = filename, + score = fuzzy_score(context, query) - 10, -- Slight bonus for content matches + match_type = "content", + context = context, + line_number = line_num + }) + break + end + end + end + end + end + + -- Sort by score (best matches first) + table.sort(results, function(a, b) + return a.score < b.score + end) + + -- Limit results + if #results > 20 then + results = vim.list_slice(results, 1, 20) + end + + if #results == 0 then + vim.notify("๐Ÿ” No results found for '" .. query .. "'", vim.log.levels.INFO) + return + end + + M.show_search_results(results, query) +end + +---Shows search results in a picker +---@param results table[] Array of search results +---@param query string Original search query +---@return nil +function M.show_search_results(results, query) + local choices = {} + + for _, result in ipairs(results) do + local icon = result.match_type == "filename" and "๐Ÿ“„" or "๐Ÿ“" + local line_info = result.line_number and (" (line " .. result.line_number .. ")") or "" + local choice = icon .. " " .. result.relative_path .. line_info + + if result.context and result.context ~= result.filename then + choice = choice .. "\n " .. result.context + end + + table.insert(choices, choice) + end + + vim.ui.select(choices, { + prompt = "๐Ÿ” Search results for '" .. query .. "':", + format_item = function(item) + return item + end + }, function(choice, idx) + if choice and idx then + local selected_result = results[idx] + -- Open the selected file in the right-side buffer + local filename = vim.fn.fnamemodify(selected_result.file, ":t") + sidebar.open_right_side(selected_result.file, filename) + + -- If it was a content match, jump to the line + if selected_result.line_number then + -- Wait a bit for the buffer to load, then jump to line + vim.defer_fn(function() + if sidebar.get_current_file() == selected_result.file then + vim.api.nvim_win_set_cursor(0, {selected_result.line_number, 0}) + end + end, 100) + end + end + end) +end + +---Searches only in file content +---@param query string Search query +---@return nil +function M.search_in_content(query) + if query == "" then + vim.notify("โŒ Please provide a search query", vim.log.levels.ERROR) + return + end + + local idea_path = config.options.idea_dir + local results = {} + + -- Find all .md files recursively + local files = vim.fn.glob(idea_path .. "**/*.md", false, true) + + for _, file in ipairs(files) do + if vim.fn.filereadable(file) == 1 then + local content = vim.fn.readfile(file) + local content_str = table.concat(content, "\n") + + -- Search for query in content + local content_lower = content_str:lower() + local query_lower = query:lower() + + if content_lower:find(query_lower, 1, true) then + local filename = vim.fn.fnamemodify(file, ":t") + local relative_path = file:sub(#idea_path + 2) + + -- Find the line with the match + for line_num, line in ipairs(content) do + if line:lower():find(query_lower, 1, true) then + local context = line:gsub("^%s*(.-)%s*$", "%1") + if #context > 80 then + context = context:sub(1, 77) .. "..." + end + + table.insert(results, { + file = file, + relative_path = relative_path, + filename = filename, + context = context, + line_number = line_num + }) + break + end + end + end + end + end + + if #results == 0 then + vim.notify("๐Ÿ” No content matches found for '" .. query .. "'", vim.log.levels.INFO) + return + end + + M.show_search_results(results, query) +end + +---Searches only in file titles +---@param query string Search query +---@return nil +function M.search_by_title(query) + if query == "" then + vim.notify("โŒ Please provide a search query", vim.log.levels.ERROR) + return + end + + local idea_path = config.options.idea_dir + local results = {} + + -- Find all .md files recursively + local files = vim.fn.glob(idea_path .. "**/*.md", false, true) + + for _, file in ipairs(files) do + if vim.fn.filereadable(file) == 1 then + local filename = vim.fn.fnamemodify(file, ":t") + local relative_path = file:sub(#idea_path + 2) + + -- Search in filename + if fuzzy_match(filename, query) then + table.insert(results, { + file = file, + relative_path = relative_path, + filename = filename, + score = fuzzy_score(filename, query), + match_type = "filename", + context = filename + }) + end + end + end + + -- Sort by score + table.sort(results, function(a, b) + return a.score < b.score + end) + + if #results == 0 then + vim.notify("๐Ÿ” No title matches found for '" .. query .. "'", vim.log.levels.INFO) + return + end + + M.show_search_results(results, query) +end + +return M \ No newline at end of file diff --git a/lua/ideaDrop/features/tags.lua b/lua/ideaDrop/features/tags.lua new file mode 100644 index 0000000..07da0ba --- /dev/null +++ b/lua/ideaDrop/features/tags.lua @@ -0,0 +1,283 @@ +-- ideaDrop/features/tags.lua +local config = require("ideaDrop.core.config") + +---@class Tags +---@field extract_tags fun(content: string): string[] +---@field get_all_tags fun(): string[] +---@field add_tag fun(file_path: string, tag: string): nil +---@field remove_tag fun(file_path: string, tag: string): nil +---@field get_files_by_tag fun(tag: string): string[] +---@field show_tag_picker fun(callback: fun(tag: string): nil): nil +local M = {} + +-- Cache for all tags +local tag_cache = {} +local tag_cache_dirty = true + +---Extracts tags from content using #tag pattern +---@param content string The content to extract tags from +---@return string[] Array of tags found +function M.extract_tags(content) + local tags = {} + local lines = vim.split(content, "\n") + + for _, line in ipairs(lines) do + -- Find all #tag patterns in the line + for tag in line:gmatch("#([%w%-_]+)") do + -- Filter out common words that shouldn't be tags + if not M.is_common_word(tag) then + table.insert(tags, tag) + end + end + end + + -- Remove duplicates and sort + local unique_tags = {} + local seen = {} + for _, tag in ipairs(tags) do + if not seen[tag] then + table.insert(unique_tags, tag) + seen[tag] = true + end + end + + table.sort(unique_tags) + return unique_tags +end + +---Checks if a word is too common to be a meaningful tag +---@param word string The word to check +---@return boolean True if it's a common word +function M.is_common_word(word) + local common_words = { + "the", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", + "by", "is", "are", "was", "were", "be", "been", "have", "has", "had", + "do", "does", "did", "will", "would", "could", "should", "may", "might", + "can", "this", "that", "these", "those", "i", "you", "he", "she", "it", + "we", "they", "me", "him", "her", "us", "them", "my", "your", "his", + "her", "its", "our", "their", "mine", "yours", "hers", "ours", "theirs" + } + + word = word:lower() + for _, common in ipairs(common_words) do + if word == common then + return true + end + end + + return false +end + +---Gets all unique tags from all idea files +---@return string[] Array of all tags +function M.get_all_tags() + if not tag_cache_dirty and #tag_cache > 0 then + return tag_cache + end + + local idea_path = config.options.idea_dir + local all_tags = {} + local seen = {} + + -- Find all .md files recursively + local files = vim.fn.glob(idea_path .. "**/*.md", false, true) + + for _, file in ipairs(files) do + if vim.fn.filereadable(file) == 1 then + local content = vim.fn.readfile(file) + local file_tags = M.extract_tags(table.concat(content, "\n")) + + for _, tag in ipairs(file_tags) do + if not seen[tag] then + table.insert(all_tags, tag) + seen[tag] = true + end + end + end + end + + table.sort(all_tags) + tag_cache = all_tags + tag_cache_dirty = false + + return all_tags +end + +---Adds a tag to a file +---@param file_path string Path to the file +---@param tag string Tag to add +---@return nil +function M.add_tag(file_path, tag) + if vim.fn.filereadable(file_path) == 0 then + vim.notify("โŒ File not found: " .. file_path, vim.log.levels.ERROR) + return + end + + local content = vim.fn.readfile(file_path) + local existing_tags = M.extract_tags(table.concat(content, "\n")) + + -- Check if tag already exists + for _, existing_tag in ipairs(existing_tags) do + if existing_tag == tag then + vim.notify("๐Ÿท๏ธ Tag '" .. tag .. "' already exists in file", vim.log.levels.INFO) + return + end + end + + -- Add tag to the end of the file + table.insert(content, "") + table.insert(content, "#" .. tag) + + -- Write back to file + local f, err = io.open(file_path, "w") + if not f then + vim.notify("โŒ Failed to write file: " .. tostring(err), vim.log.levels.ERROR) + return + end + + f:write(table.concat(content, "\n") .. "\n") + f:close() + + -- Invalidate cache + tag_cache_dirty = true + + vim.notify("โœ… Added tag '" .. tag .. "' to " .. vim.fn.fnamemodify(file_path, ":t"), vim.log.levels.INFO) +end + +---Removes a tag from a file +---@param file_path string Path to the file +---@param tag string Tag to remove +---@return nil +function M.remove_tag(file_path, tag) + if vim.fn.filereadable(file_path) == 0 then + vim.notify("โŒ File not found: " .. file_path, vim.log.levels.ERROR) + return + end + + local content = vim.fn.readfile(file_path) + local new_content = {} + local tag_found = false + + for _, line in ipairs(content) do + -- Check if line contains the tag + local has_tag = false + for found_tag in line:gmatch("#([%w%-_]+)") do + if found_tag == tag then + has_tag = true + tag_found = true + break + end + end + + if not has_tag then + table.insert(new_content, line) + end + end + + if not tag_found then + vim.notify("๐Ÿท๏ธ Tag '" .. tag .. "' not found in file", vim.log.levels.INFO) + return + end + + -- Write back to file + local f, err = io.open(file_path, "w") + if not f then + vim.notify("โŒ Failed to write file: " .. tostring(err), vim.log.levels.ERROR) + return + end + + f:write(table.concat(new_content, "\n") .. "\n") + f:close() + + -- Invalidate cache + tag_cache_dirty = true + + vim.notify("โœ… Removed tag '" .. tag .. "' from " .. vim.fn.fnamemodify(file_path, ":t"), vim.log.levels.INFO) +end + +---Gets all files that contain a specific tag +---@param tag string The tag to search for +---@return string[] Array of file paths +function M.get_files_by_tag(tag) + local idea_path = config.options.idea_dir + local matching_files = {} + + -- Find all .md files recursively + local files = vim.fn.glob(idea_path .. "**/*.md", false, true) + + for _, file in ipairs(files) do + if vim.fn.filereadable(file) == 1 then + local content = vim.fn.readfile(file) + local file_tags = M.extract_tags(table.concat(content, "\n")) + + for _, file_tag in ipairs(file_tags) do + if file_tag == tag then + table.insert(matching_files, file) + break + end + end + end + end + + return matching_files +end + +---Shows a tag picker UI for selecting tags +---@param callback fun(tag: string): nil Callback function when a tag is selected +---@return nil +function M.show_tag_picker(callback) + local all_tags = M.get_all_tags() + + if #all_tags == 0 then + vim.notify("๐Ÿท๏ธ No tags found in your ideas", vim.log.levels.INFO) + return + end + + -- Format tags for display + local tag_choices = {} + for _, tag in ipairs(all_tags) do + local files = M.get_files_by_tag(tag) + table.insert(tag_choices, tag .. " (" .. #files .. " files)") + end + + vim.ui.select(tag_choices, { prompt = "๐Ÿท๏ธ Select a tag:" }, function(choice) + if choice then + local tag = choice:match("^([%w%-_]+)") + if tag and callback then + callback(tag) + end + end + end) +end + +---Shows all files with a specific tag +---@param tag string The tag to show files for +---@return nil +function M.show_files_with_tag(tag) + local files = M.get_files_by_tag(tag) + + if #files == 0 then + vim.notify("๐Ÿ“‚ No files found with tag '" .. tag .. "'", vim.log.levels.INFO) + return + end + + -- Format file names for display + local file_choices = {} + for _, file in ipairs(files) do + local filename = vim.fn.fnamemodify(file, ":t") + local relative_path = file:sub(#config.options.idea_dir + 2) -- Remove idea_dir + "/" + table.insert(file_choices, relative_path) + end + + vim.ui.select(file_choices, { prompt = "๐Ÿ“‚ Files with tag '" .. tag .. "':" }, function(choice) + if choice then + local full_path = config.options.idea_dir .. "/" .. choice + -- Open the selected file in the right-side buffer + local sidebar = require("ideaDrop.ui.sidebar") + local filename = vim.fn.fnamemodify(full_path, ":t") + sidebar.open_right_side(full_path, filename) + end + end) +end + +return M \ No newline at end of file diff --git a/lua/ideaDrop/init.lua b/lua/ideaDrop/init.lua index 5ca7a49..163a59b 100644 --- a/lua/ideaDrop/init.lua +++ b/lua/ideaDrop/init.lua @@ -1,53 +1,5 @@ -- ideaDrop.nvim/init.lua -local config = require("ideaDrop.config") -local sidebar = require("ideaDrop.sidebar") -local list = require("ideaDrop.list") -local M = {} +-- Main entry point for the ideaDrop plugin ----@class IdeaDrop ----@field setup fun(user_opts: IdeaDropConfig): nil - ----@class IdeaDropConfig ----@field idea_dir string Directory where idea files will be stored - ----Setup function for ideaDrop.nvim ----@param user_opts IdeaDropConfig|nil User configuration options ----@return nil -function M.setup(user_opts) - config.setup(user_opts) - - vim.api.nvim_create_user_command("Idea", function(opts) - local arg = opts.args - local idea_dir = config.options.idea_dir - - if arg == "listAll" then - list.list_all() - elseif arg ~= "" then - -- Ensure directory exists (even for nested folders) - local filename = arg:match("%.md$") and arg or (arg .. ".md") - local full_path = idea_dir .. "/" .. filename - - -- Create parent folders if needed - local folder = vim.fn.fnamemodify(full_path, ":h") - if vim.fn.isdirectory(folder) == 0 then - vim.fn.mkdir(folder, "p") - end - - sidebar.open(full_path, filename) - else - -- Default to today's idea file - local path = string.format("%s/%s.md", idea_dir, os.date("%Y-%m-%d")) - sidebar.open(path) - end - end, { - nargs = "?", - complete = function() - return { "listAll" } - end, - desc = "Open today's idea, a named idea, or list all", - }) - - vim.notify("ideaDrop loaded!", vim.log.levels.INFO) -end - -return M +-- Require the core module which contains the main setup function +return require("ideaDrop.core.init") \ No newline at end of file diff --git a/lua/ideaDrop/keymaps.lua b/lua/ideaDrop/keymaps.lua deleted file mode 100644 index e6e35fa..0000000 --- a/lua/ideaDrop/keymaps.lua +++ /dev/null @@ -1,16 +0,0 @@ --- ideaDrop/keymaps.lua ----@class Keymaps ----@field setup fun(): nil -local M = {} - ----Sets up default keymaps for ideaDrop ----Currently only sets up a demo keymap ----@return nil -function M.setup() - -- Demo keymap for idea capture - vim.keymap.set("n", "id", function() - vim.notify("๐Ÿ’ก Idea captured!") - end, { desc = "Drop idea (demo)" }) -end - -return M diff --git a/lua/ideaDrop/sidebar.lua b/lua/ideaDrop/sidebar.lua deleted file mode 100644 index f976bec..0000000 --- a/lua/ideaDrop/sidebar.lua +++ /dev/null @@ -1,82 +0,0 @@ -local config = require("ideaDrop.config") - ----@class Sidebar ----@field open fun(file: string|nil, filename: string|nil): nil ----@field save_idea fun(lines: string[], file: string|nil): nil -local M = {} - ----Opens a floating sidebar window with the specified file ----@param file string|nil Path to the file to open ----@param filename string|nil Name of the file (used for new files) ----@return nil -function M.open(file, filename) - -- Create a new buffer - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_option(buf, "filetype", "markdown") - - -- Calculate window dimensions (30% of screen width, 80% of screen height) - local width = math.floor(vim.o.columns * 0.3) - local height = math.floor(vim.o.lines * 0.8) - - -- Create and configure the floating window - local win = vim.api.nvim_open_win(buf, true, { - relative = "editor", - width = width, - height = height, - row = 1, - col = 1, - border = "rounded", - style = "minimal", - }) - - -- Load file content or create new file template - if file and vim.fn.filereadable(file) == 1 then - local content = vim.fn.readfile(file) - vim.api.nvim_buf_set_lines(buf, 0, -1, false, content) - else - vim.api.nvim_buf_set_lines(buf, 0, -1, false, { - "# " .. (filename or "Idea for " .. os.date("%Y-%m-%d")), - "", - "- ", - }) - end - - -- Set up autosave on window close - vim.api.nvim_create_autocmd("WinClosed", { - pattern = tostring(win), - once = true, - callback = function() - local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - M.save_idea(lines, file) - end, - }) -end - ----Saves the idea content to a file ----@param lines string[] Array of lines to save ----@param file string|nil Path where to save the file ----@return nil -function M.save_idea(lines, file) - local idea_path = config.options.idea_dir - - -- Create default path if none provided - if not file then - if vim.fn.isdirectory(idea_path) == 0 then - vim.fn.mkdir(idea_path, "p") - end - file = string.format("%s/%s.md", idea_path, os.date("%Y-%m-%d")) - end - - -- Write content to file - local f, err = io.open(file, "w") - if not f then - vim.notify("โŒ Failed to write idea: " .. tostring(err), vim.log.levels.ERROR) - return - end - - f:write(table.concat(lines, "\n") .. "\n") - f:close() - vim.notify("๐Ÿ’พ Idea saved to " .. file, vim.log.levels.INFO) -end - -return M diff --git a/lua/ideaDrop/ui/sidebar.lua b/lua/ideaDrop/ui/sidebar.lua new file mode 100644 index 0000000..652472e --- /dev/null +++ b/lua/ideaDrop/ui/sidebar.lua @@ -0,0 +1,275 @@ +-- ideaDrop/ui/sidebar.lua +local config = require("ideaDrop.core.config") +local tree = require("ideaDrop.ui.tree") + +---@class Sidebar +---@field open fun(file: string|nil, filename: string|nil, use_buffer: boolean|nil): nil +---@field open_in_buffer fun(file: string|nil, filename: string|nil): nil +---@field open_right_side fun(file: string|nil, filename: string|nil): nil +---@field toggle_tree fun(): nil +---@field get_current_file fun(): string|nil +---@field save_idea fun(lines: string[], file: string|nil): nil +local M = {} + +-- Global variables to track the right-side buffer and window +local right_side_buf = nil +local right_side_win = nil +local current_file = nil + +---Opens a floating sidebar window with the specified file +---@param file string|nil Path to the file to open +---@param filename string|nil Name of the file (used for new files) +---@param use_buffer boolean|nil If true, opens in current buffer instead of floating window +---@return nil +function M.open(file, filename, use_buffer) + if use_buffer then + M.open_in_buffer(file, filename) + return + end + + -- Create a new buffer + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(buf, "filetype", "markdown") + + -- Calculate window dimensions (30% of screen width, 80% of screen height) + local width = math.floor(vim.o.columns * 0.3) + local height = math.floor(vim.o.lines * 0.8) + + -- Create and configure the floating window + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = width, + height = height, + row = 1, + col = 1, + border = "rounded", + style = "minimal", + }) + + -- Load file content or create new file template + if file and vim.fn.filereadable(file) == 1 then + local content = vim.fn.readfile(file) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, content) + else + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { + "# " .. (filename or "Idea for " .. os.date("%Y-%m-%d")), + "", + "- ", + }) + end + + -- Set up autosave on window close + vim.api.nvim_create_autocmd("WinClosed", { + pattern = tostring(win), + once = true, + callback = function() + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + M.save_idea(lines, file) + end, + }) +end + +---Opens the idea file in the current buffer +---@param file string|nil Path to the file to open +---@param filename string|nil Name of the file (used for new files) +---@return nil +function M.open_in_buffer(file, filename) + -- Create default path if none provided + if not file then + local idea_path = config.options.idea_dir + if vim.fn.isdirectory(idea_path) == 0 then + vim.fn.mkdir(idea_path, "p") + end + file = string.format("%s/%s.md", idea_path, os.date("%Y-%m-%d")) + end + + -- Open the file in the current buffer + vim.cmd("edit " .. vim.fn.fnameescape(file)) + + -- If it's a new file, add template content + if vim.fn.filereadable(file) == 0 then + local template_lines = { + "# " .. (filename or "Idea for " .. os.date("%Y-%m-%d")), + "", + "- ", + } + vim.api.nvim_buf_set_lines(0, 0, -1, false, template_lines) + end + + -- Set up autosave on buffer write + vim.api.nvim_create_autocmd("BufWritePost", { + buffer = 0, + callback = function() + vim.notify("๐Ÿ’พ Idea saved to " .. file, vim.log.levels.INFO) + end, + }) +end + +---Opens the idea file in a persistent right-side buffer +---@param file string|nil Path to the file to open +---@param filename string|nil Name of the file (used for new files) +---@return nil +function M.open_right_side(file, filename) + -- Create default path if none provided + if not file then + local idea_path = config.options.idea_dir + if vim.fn.isdirectory(idea_path) == 0 then + vim.fn.mkdir(idea_path, "p") + end + file = string.format("%s/%s.md", idea_path, os.date("%Y-%m-%d")) + end + + current_file = file + + -- Create buffer if it doesn't exist + if not right_side_buf or not vim.api.nvim_buf_is_valid(right_side_buf) then + right_side_buf = vim.api.nvim_create_buf(false, false) + vim.api.nvim_buf_set_option(right_side_buf, "filetype", "markdown") + vim.api.nvim_buf_set_option(right_side_buf, "buftype", "acwrite") + vim.api.nvim_buf_set_option(right_side_buf, "bufhidden", "hide") + + -- Set up autosave for the right-side buffer + vim.api.nvim_create_autocmd("BufWriteCmd", { + buffer = right_side_buf, + callback = function() + local lines = vim.api.nvim_buf_get_lines(right_side_buf, 0, -1, false) + M.save_idea(lines, current_file) + -- Prevent the default write behavior + vim.api.nvim_command("setlocal nomodified") + end, + }) + + -- Set up key mappings for the right-side buffer + vim.api.nvim_buf_set_keymap(right_side_buf, "n", "", "", { + callback = function() + M.toggle_tree() + end, + noremap = true, + silent = true, + }) + + vim.api.nvim_buf_set_keymap(right_side_buf, "n", "", "", { + callback = function() + M.refresh_current_file() + end, + noremap = true, + silent = true, + }) + end + + -- Load file content or create new file template + if vim.fn.filereadable(file) == 1 then + local content = vim.fn.readfile(file) + vim.api.nvim_buf_set_lines(right_side_buf, 0, -1, false, content) + else + vim.api.nvim_buf_set_lines(right_side_buf, 0, -1, false, { + "# " .. (filename or "Idea for " .. os.date("%Y-%m-%d")), + "", + "- ", + }) + end + + -- Set the buffer name to show the current file + vim.api.nvim_buf_set_name(right_side_buf, "ideaDrop://" .. (filename or os.date("%Y-%m-%d"))) + + -- Create or update the right-side window + if not right_side_win or not vim.api.nvim_win_is_valid(right_side_win) then + -- Calculate window dimensions (30% of screen width, full height) + local width = math.floor(vim.o.columns * 0.3) + local height = vim.o.lines - 2 -- Full height minus status line + + -- Create the right-side window + right_side_win = vim.api.nvim_open_win(right_side_buf, false, { + relative = "editor", + width = width, + height = height, + row = 0, + col = vim.o.columns - width, + border = "single", + style = "minimal", + }) + + -- Set window options + vim.api.nvim_win_set_option(right_side_win, "wrap", true) + vim.api.nvim_win_set_option(right_side_win, "number", true) + vim.api.nvim_win_set_option(right_side_win, "relativenumber", false) + vim.api.nvim_win_set_option(right_side_win, "cursorline", true) + vim.api.nvim_win_set_option(right_side_win, "winhl", "Normal:Normal,FloatBorder:FloatBorder") + + -- Set up autosave on window close + vim.api.nvim_create_autocmd("WinClosed", { + pattern = tostring(right_side_win), + once = true, + callback = function() + local lines = vim.api.nvim_buf_get_lines(right_side_buf, 0, -1, false) + M.save_idea(lines, current_file) + right_side_win = nil + end, + }) + else + -- Window exists, just switch to it + vim.api.nvim_set_current_win(right_side_win) + end + + -- Focus on the right-side window + vim.api.nvim_set_current_win(right_side_win) +end + +---Toggles the tree view +---@return nil +function M.toggle_tree() + tree.open_tree_window(function(selected_file) + -- Callback when a file is selected from the tree + if selected_file then + local filename = vim.fn.fnamemodify(selected_file, ":t") + M.open_right_side(selected_file, filename) + end + end) +end + +---Refreshes the current file in the right-side buffer +---@return nil +function M.refresh_current_file() + if current_file and right_side_buf and vim.api.nvim_buf_is_valid(right_side_buf) then + if vim.fn.filereadable(current_file) == 1 then + local content = vim.fn.readfile(current_file) + vim.api.nvim_buf_set_lines(right_side_buf, 0, -1, false, content) + vim.notify("๐Ÿ”„ File refreshed", vim.log.levels.INFO) + end + end +end + +---Saves the idea content to a file +---@param lines string[] Array of lines to save +---@param file string|nil Path where to save the file +---@return nil +function M.save_idea(lines, file) + local idea_path = config.options.idea_dir + + -- Create default path if none provided + if not file then + if vim.fn.isdirectory(idea_path) == 0 then + vim.fn.mkdir(idea_path, "p") + end + file = string.format("%s/%s.md", idea_path, os.date("%Y-%m-%d")) + end + + -- Write content to file + local f, err = io.open(file, "w") + if not f then + vim.notify("โŒ Failed to write idea: " .. tostring(err), vim.log.levels.ERROR) + return + end + + f:write(table.concat(lines, "\n") .. "\n") + f:close() + vim.notify("๐Ÿ’พ Idea saved to " .. file, vim.log.levels.INFO) +end + +---Gets the current file path from the right-side buffer +---@return string|nil Current file path or nil if no file is open +function M.get_current_file() + return current_file +end + +return M diff --git a/lua/ideaDrop/ui/tree.lua b/lua/ideaDrop/ui/tree.lua new file mode 100644 index 0000000..8e1c3f4 --- /dev/null +++ b/lua/ideaDrop/ui/tree.lua @@ -0,0 +1,138 @@ +-- ideaDrop/ui/tree.lua +local config = require("ideaDrop.core.config") + +---@class Tree +---@field open_tree_window fun(callback: fun(file_path: string): nil): nil +local M = {} + +-- Tree state +local tree_callback = nil +local original_cwd = nil + +---Opens nvim-tree focused on the idea directory +---@param callback fun(file_path: string): nil Callback function when a file is selected +---@return nil +function M.open_tree_window(callback) + tree_callback = callback + + -- Check if nvim-tree is available + local has_nvim_tree, nvim_tree = pcall(require, "nvim-tree") + if not has_nvim_tree then + vim.notify("โŒ nvim-tree is not installed. Please install nvim-tree to use this feature.", vim.log.levels.ERROR) + return + end + + -- Store original working directory + original_cwd = vim.fn.getcwd() + + -- Change to idea directory + local idea_path = config.options.idea_dir + if vim.fn.isdirectory(idea_path) == 1 then + vim.cmd("cd " .. vim.fn.fnameescape(idea_path)) + else + vim.notify("โŒ Idea directory not found: " .. idea_path, vim.log.levels.ERROR) + return + end + + -- Set up nvim-tree to open on the left side + nvim_tree.setup({ + view = { + side = "left", + width = 30, + }, + actions = { + open_file = { + quit_on_open = false, + }, + }, + on_attach = function(bufnr) + -- Override the default file opening behavior + local api = require("nvim-tree.api") + + -- Map Enter to custom handler + vim.keymap.set("n", "", function() + local node = api.tree.get_node_under_cursor() + if node and node.type == "file" then + -- Call our callback with the selected file + if tree_callback then + tree_callback(node.absolute_path) + end + -- Close nvim-tree + api.tree.close() + -- Restore original working directory + if original_cwd then + vim.cmd("cd " .. vim.fn.fnameescape(original_cwd)) + end + else + -- Default behavior for directories + api.node.open.edit() + end + end, { buffer = bufnr, noremap = true, silent = true }) + + -- Map 'q' to close tree and restore directory + vim.keymap.set("n", "q", function() + api.tree.close() + if original_cwd then + vim.cmd("cd " .. vim.fn.fnameescape(original_cwd)) + end + end, { buffer = bufnr, noremap = true, silent = true }) + + -- Keep other default mappings + vim.keymap.set("n", "", api.tree.change_root_to_node, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.node.open.replace_tree_buffer, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.node.show_info_popup, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.fs.rename_sub, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.node.open.tab, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.node.open.vertical, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.node.open.horizontal, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.node.navigate.parent_close, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "", api.node.open.preview, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", ">", api.node.navigate.sibling.next, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "<", api.node.navigate.sibling.prev, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", ".", api.node.run.cmd, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "-", api.tree.change_root_to_parent, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "a", api.fs.create, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "bmv", api.marks.bulk.move, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "B", api.tree.toggle_no_buffer_filter, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "c", api.fs.copy.node, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "C", api.tree.toggle_git_clean_filter, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "[c", api.node.navigate.git.prev, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "]c", api.node.navigate.git.next, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "d", api.fs.remove, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "D", api.fs.trash, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "E", api.tree.expand_all, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "e", api.fs.rename_basename, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "]e", api.node.navigate.diagnostics.next, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "[e", api.node.navigate.diagnostics.prev, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "F", api.live_filter.clear, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "f", api.live_filter.start, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "g?", api.tree.toggle_help, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "gy", api.fs.copy.absolute_path, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "H", api.tree.toggle_hidden_filter, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "I", api.tree.toggle_gitignore_filter, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "J", api.node.navigate.sibling.last, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "K", api.node.navigate.sibling.first, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "m", api.marks.toggle, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "o", api.node.open.edit, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "O", api.node.open.no_window_picker, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "P", api.node.navigate.parent, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "r", api.fs.rename, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "R", api.tree.reload, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "s", api.node.run.system, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "S", api.tree.search_node, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "U", api.tree.toggle_custom_filter, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "W", api.tree.collapse_all, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "x", api.fs.cut, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "y", api.fs.copy.filename, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "Y", api.fs.copy.relative_path, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "<2-LeftMouse>", api.node.open.edit, { buffer = bufnr, noremap = true, silent = true }) + vim.keymap.set("n", "<2-RightMouse>", api.tree.change_root_to_node, { buffer = bufnr, noremap = true, silent = true }) + end, + }) + + -- Open nvim-tree using the correct API + local api = require("nvim-tree.api") + api.tree.open() +end + +return M \ No newline at end of file diff --git a/lua/ideaDrop/utils.lua b/lua/ideaDrop/utils.lua deleted file mode 100644 index e69de29..0000000 diff --git a/lua/ideaDrop/utils/constants.lua b/lua/ideaDrop/utils/constants.lua new file mode 100644 index 0000000..efa61d0 --- /dev/null +++ b/lua/ideaDrop/utils/constants.lua @@ -0,0 +1,151 @@ +-- ideaDrop/utils/constants.lua +local M = {} + +-- File extensions +M.FILE_EXTENSIONS = { + MARKDOWN = ".md", + LUA = ".lua", +} + +-- Default file templates +M.DEFAULT_TEMPLATES = { + IDEA = { + "# %s", + "", + "- ", + }, + MEETING = { + "# Meeting: %s", + "", + "## Date: %s", + "## Attendees:", + "- ", + "", + "## Agenda:", + "- ", + "", + "## Notes:", + "- ", + "", + "## Action Items:", + "- [ ] ", + }, + PROJECT = { + "# Project: %s", + "", + "## Overview:", + "", + "## Goals:", + "- ", + "", + "## Tasks:", + "- [ ] ", + "", + "## Notes:", + "- ", + }, +} + +-- Window dimensions (as percentages of screen) +M.WINDOW_DIMENSIONS = { + RIGHT_SIDE_WIDTH = 0.3, -- 30% of screen width + TREE_WIDTH = 0.25, -- 25% of screen width + FLOATING_HEIGHT = 0.8, -- 80% of screen height +} + +-- Buffer options +M.BUFFER_OPTIONS = { + IDEA_BUFFER = { + filetype = "markdown", + buftype = "acwrite", + bufhidden = "hide", + }, + TREE_BUFFER = { + filetype = "ideaDrop-tree", + buftype = "nofile", + modifiable = false, + bufhidden = "hide", + }, +} + +-- Window options +M.WINDOW_OPTIONS = { + RIGHT_SIDE = { + wrap = true, + number = true, + relativenumber = false, + cursorline = true, + }, + TREE = { + wrap = false, + number = false, + relativenumber = false, + cursorline = true, + }, +} + +-- Search settings +M.SEARCH_SETTINGS = { + MAX_RESULTS = 20, + CONTEXT_LENGTH = 80, + FUZZY_SCORE_BONUS = 10, +} + +-- Tag settings +M.TAG_SETTINGS = { + PATTERN = "#([%w%-_]+)", + MAX_DEPTH = 10, +} + +-- Common words to exclude from tags +M.COMMON_WORDS = { + "the", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", + "by", "is", "are", "was", "were", "be", "been", "have", "has", "had", + "do", "does", "did", "will", "would", "could", "should", "may", "might", + "can", "this", "that", "these", "those", "i", "you", "he", "she", "it", + "we", "they", "me", "him", "her", "us", "them", "my", "your", "his", + "her", "its", "our", "their", "mine", "yours", "hers", "ours", "theirs" +} + +-- Icons for different file types and actions +M.ICONS = { + FILE = "๐Ÿ“„", + DIRECTORY = "๐Ÿ“", + IDEA = "๐Ÿ’ก", + SEARCH = "๐Ÿ”", + TAG = "๐Ÿท๏ธ", + TREE = "๐ŸŒณ", + SUCCESS = "โœ…", + ERROR = "โŒ", + WARNING = "โš ๏ธ", + INFO = "โ„น๏ธ", + SAVE = "๐Ÿ’พ", + REFRESH = "๐Ÿ”„", +} + +-- Key mappings (default) +M.DEFAULT_KEYMAPS = { + TOGGLE_TREE = "", + REFRESH_FILE = "", + CLOSE_WINDOW = "q", + SELECT_ITEM = "", +} + +-- Notification messages +M.MESSAGES = { + PLUGIN_LOADED = "ideaDrop loaded!", + NO_FILES_FOUND = "๐Ÿ“‚ No idea files found", + NO_TAGS_FOUND = "๐Ÿท๏ธ No tags found in your ideas", + NO_SEARCH_RESULTS = "๐Ÿ” No results found for '%s'", + FILE_SAVED = "๐Ÿ’พ Idea saved to %s", + FILE_REFRESHED = "๐Ÿ”„ File refreshed", + TAG_ADDED = "โœ… Added tag '%s' to %s", + TAG_REMOVED = "โœ… Removed tag '%s' from %s", + TAG_EXISTS = "๐Ÿท๏ธ Tag '%s' already exists in file", + TAG_NOT_FOUND = "๐Ÿท๏ธ Tag '%s' not found in file", + NO_ACTIVE_FILE = "โŒ No active idea file. Open an idea first.", + PROVIDE_TAG = "โŒ Please provide a tag name", + PROVIDE_QUERY = "โŒ Please provide a search query", +} + +return M \ No newline at end of file diff --git a/lua/ideaDrop/utils/keymaps.lua b/lua/ideaDrop/utils/keymaps.lua new file mode 100644 index 0000000..5f82e23 --- /dev/null +++ b/lua/ideaDrop/utils/keymaps.lua @@ -0,0 +1,20 @@ +-- ideaDrop/utils/keymaps.lua +local M = {} + +---Setup function for keymaps +---@return nil +function M.setup() + -- Global keymaps for ideaDrop + -- These can be customized by users in their config + + -- Example: Quick access to ideaDrop commands + -- vim.keymap.set("n", "id", ":IdeaRight", { desc = "Open today's idea" }) + -- vim.keymap.set("n", "it", ":IdeaTree", { desc = "Open idea tree" }) + -- vim.keymap.set("n", "is", ":IdeaSearch ", { desc = "Search ideas" }) + -- vim.keymap.set("n", "it", ":IdeaTags", { desc = "Browse tags" }) + + -- Note: Keymaps are commented out by default to avoid conflicts + -- Users can uncomment and customize these in their config +end + +return M diff --git a/lua/ideaDrop/utils/utils.lua b/lua/ideaDrop/utils/utils.lua new file mode 100644 index 0000000..937ca77 --- /dev/null +++ b/lua/ideaDrop/utils/utils.lua @@ -0,0 +1,181 @@ +-- ideaDrop/utils/utils.lua +local M = {} + +---@class Utils +---@field ensure_dir fun(path: string): nil +---@field get_relative_path fun(full_path: string, base_path: string): string +---@field sanitize_filename fun(filename: string): string +---@field format_date fun(date_format: string|nil): string +---@field truncate_string fun(str: string, max_length: number): string +---@field table_contains fun(tbl: table, value: any): boolean +---@field deep_copy fun(orig: table): table + +---Ensures a directory exists, creating it if necessary +---@param path string Directory path to ensure +---@return nil +function M.ensure_dir(path) + if vim.fn.isdirectory(path) == 0 then + vim.fn.mkdir(path, "p") + end +end + +---Gets the relative path from a full path +---@param full_path string Full file path +---@param base_path string Base directory path +---@return string Relative path +function M.get_relative_path(full_path, base_path) + if full_path:sub(1, #base_path) == base_path then + return full_path:sub(#base_path + 2) -- Remove base_path + "/" + end + return full_path +end + +---Sanitizes a filename for safe file creation +---@param filename string Original filename +---@return string Sanitized filename +function M.sanitize_filename(filename) + -- Remove or replace invalid characters + local sanitized = filename:gsub("[<>:\"/\\|?*]", "_") + -- Remove leading/trailing spaces and dots + sanitized = sanitized:gsub("^[%s%.]+", ""):gsub("[%s%.]+$", "") + -- Ensure it's not empty + if sanitized == "" then + sanitized = "untitled" + end + return sanitized +end + +---Formats current date with optional format +---@param date_format string|nil Date format string (default: "%Y-%m-%d") +---@return string Formatted date string +function M.format_date(date_format) + date_format = date_format or "%Y-%m-%d" + return os.date(date_format) +end + +---Truncates a string to specified length with ellipsis +---@param str string String to truncate +---@param max_length number Maximum length +---@return string Truncated string +function M.truncate_string(str, max_length) + if #str <= max_length then + return str + end + return str:sub(1, max_length - 3) .. "..." +end + +---Checks if a table contains a specific value +---@param tbl table Table to search +---@param value any Value to find +---@return boolean True if value is found +function M.table_contains(tbl, value) + for _, v in ipairs(tbl) do + if v == value then + return true + end + end + return false +end + +---Creates a deep copy of a table +---@param orig table Original table +---@return table Deep copy of the table +function M.deep_copy(orig) + local copy + if type(orig) == "table" then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[M.deep_copy(orig_key)] = M.deep_copy(orig_value) + end + setmetatable(copy, M.deep_copy(getmetatable(orig))) + else + copy = orig + end + return copy +end + +---Splits a string by delimiter +---@param str string String to split +---@param delimiter string Delimiter character +---@return table Array of substrings +function M.split_string(str, delimiter) + delimiter = delimiter or "\n" + local result = {} + for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do + table.insert(result, match) + end + return result +end + +---Joins table elements with a delimiter +---@param tbl table Table to join +---@param delimiter string Delimiter string +---@return string Joined string +function M.join_strings(tbl, delimiter) + delimiter = delimiter or "\n" + return table.concat(tbl, delimiter) +end + +---Gets file extension from filename +---@param filename string Filename +---@return string File extension (without dot) +function M.get_file_extension(filename) + return filename:match("%.([^%.]+)$") or "" +end + +---Removes file extension from filename +---@param filename string Filename +---@return string Filename without extension +function M.remove_file_extension(filename) + return filename:match("(.+)%.[^%.]+$") or filename +end + +---Checks if a string starts with a prefix +---@param str string String to check +---@param prefix string Prefix to look for +---@return boolean True if string starts with prefix +function M.starts_with(str, prefix) + return str:sub(1, #prefix) == prefix +end + +---Checks if a string ends with a suffix +---@param str string String to check +---@param suffix string Suffix to look for +---@return boolean True if string ends with suffix +function M.ends_with(str, suffix) + return str:sub(-#suffix) == suffix +end + +---Escapes special characters in a string for shell commands +---@param str string String to escape +---@return string Escaped string +function M.escape_shell(str) + return vim.fn.shellescape(str) +end + +---Gets the current buffer's file path +---@return string|nil Current buffer file path or nil +function M.get_current_file_path() + local buf_name = vim.api.nvim_buf_get_name(0) + if buf_name and buf_name ~= "" then + return buf_name + end + return nil +end + +---Gets the current working directory +---@return string Current working directory +function M.get_cwd() + return vim.fn.getcwd() +end + +---Shows a notification with optional log level +---@param message string Message to show +---@param level string|nil Log level (INFO, WARN, ERROR) +---@return nil +function M.notify(message, level) + level = level or vim.log.levels.INFO + vim.notify(message, level) +end + +return M