adding functionalities on the buffer
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Lua bytecode (just in case)
|
||||
*.luac
|
||||
|
||||
*test*
|
||||
todo*
|
||||
# OS junk
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
235
README.md
235
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 |
|
||||
|--------|---------|-------------|
|
||||
| `<leader>id` | `:IdeaRight` | Open today's idea in right buffer |
|
||||
| `<leader>in` | `:IdeaRight ` | Open named idea in right buffer |
|
||||
| `<leader>it` | `:IdeaTree` | Open tree browser |
|
||||
| `<leader>is` | `:IdeaSearch ` | Search ideas |
|
||||
| `<leader>ig` | `:IdeaTags` | Browse tags |
|
||||
| `<leader>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
|
||||
|
||||
@@ -67,3 +225,30 @@ MIT License - feel free to use this plugin in your own projects!
|
||||
## 🤝 Contributing
|
||||
|
||||
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! 🚀**
|
||||
248
lua/ideaDrop/core/init.lua
Normal file
248
lua/ideaDrop/core/init.lua
Normal file
@@ -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
|
||||
@@ -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
|
||||
294
lua/ideaDrop/features/search.lua
Normal file
294
lua/ideaDrop/features/search.lua
Normal file
@@ -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
|
||||
283
lua/ideaDrop/features/tags.lua
Normal file
283
lua/ideaDrop/features/tags.lua
Normal file
@@ -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
|
||||
@@ -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")
|
||||
@@ -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", "<leader>id", function()
|
||||
vim.notify("💡 Idea captured!")
|
||||
end, { desc = "Drop idea (demo)" })
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -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
|
||||
275
lua/ideaDrop/ui/sidebar.lua
Normal file
275
lua/ideaDrop/ui/sidebar.lua
Normal file
@@ -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", "<C-t>", "", {
|
||||
callback = function()
|
||||
M.toggle_tree()
|
||||
end,
|
||||
noremap = true,
|
||||
silent = true,
|
||||
})
|
||||
|
||||
vim.api.nvim_buf_set_keymap(right_side_buf, "n", "<C-r>", "", {
|
||||
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
|
||||
138
lua/ideaDrop/ui/tree.lua
Normal file
138
lua/ideaDrop/ui/tree.lua
Normal file
@@ -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", "<CR>", 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", "<C-]>", api.tree.change_root_to_node, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<C-e>", api.node.open.replace_tree_buffer, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<C-k>", api.node.show_info_popup, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<C-r>", api.fs.rename_sub, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<C-t>", api.node.open.tab, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<C-v>", api.node.open.vertical, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<C-x>", api.node.open.horizontal, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<BS>", api.node.navigate.parent_close, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set("n", "<Tab>", 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
|
||||
151
lua/ideaDrop/utils/constants.lua
Normal file
151
lua/ideaDrop/utils/constants.lua
Normal file
@@ -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 = "<C-t>",
|
||||
REFRESH_FILE = "<C-r>",
|
||||
CLOSE_WINDOW = "q",
|
||||
SELECT_ITEM = "<CR>",
|
||||
}
|
||||
|
||||
-- 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
|
||||
20
lua/ideaDrop/utils/keymaps.lua
Normal file
20
lua/ideaDrop/utils/keymaps.lua
Normal file
@@ -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", "<leader>id", ":IdeaRight<CR>", { desc = "Open today's idea" })
|
||||
-- vim.keymap.set("n", "<leader>it", ":IdeaTree<CR>", { desc = "Open idea tree" })
|
||||
-- vim.keymap.set("n", "<leader>is", ":IdeaSearch ", { desc = "Search ideas" })
|
||||
-- vim.keymap.set("n", "<leader>it", ":IdeaTags<CR>", { 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
|
||||
181
lua/ideaDrop/utils/utils.lua
Normal file
181
lua/ideaDrop/utils/utils.lua
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user