adding functionalities on the buffer
This commit is contained in:
30
lua/ideaDrop/features/list.lua
Normal file
30
lua/ideaDrop/features/list.lua
Normal 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
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user