adding functionalities on the buffer

This commit is contained in:
Carlos
2025-07-28 19:28:46 -04:00
parent 2baa8ee836
commit 658a56ca55
16 changed files with 1809 additions and 178 deletions

View File

@@ -0,0 +1,30 @@
-- ideaDrop/features/list.lua
local config = require("ideaDrop.core.config")
local sidebar = require("ideaDrop.ui.sidebar")
---@class List
---@field list_all fun(): nil
local M = {}
---Lists all idea files and allows user to select one to open
---@return nil
function M.list_all()
local path = config.options.idea_dir
-- Find all .md files recursively
local files = vim.fn.glob(path .. "**/*.md", false, true)
if #files == 0 then
vim.notify("📂 No idea files found", vim.log.levels.INFO)
return
end
-- Present file selection UI
vim.ui.select(files, { prompt = "📂 Select an idea file to open:" }, function(choice)
if choice then
sidebar.open(choice) -- Open the selected file in sidebar
end
end)
end
return M

View 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

View 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