From c949c05e7d28e6003ed60c51615e9e5744f1bd5b Mon Sep 17 00:00:00 2001 From: n451 <2020200706@ruc.edu.cn> Date: Thu, 20 Feb 2025 00:16:17 +0800 Subject: [PATCH 1/4] feat: query syntax support for `~`, meaning not matching a feed --- lua/feed/db/local.lua | 67 ++++++++++++++++++++++++------------------- lua/feed/db/query.lua | 3 ++ tests/test_query.lua | 4 ++- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/lua/feed/db/local.lua b/lua/feed/db/local.lua index 7bc7bf2..4121b29 100644 --- a/lua/feed/db/local.lua +++ b/lua/feed/db/local.lua @@ -9,8 +9,8 @@ local ut = require("feed.utils") ---@field index table ---@field tags table> ---@field add fun(db: feed.db, entry: feed.entry, tags: string[]?) ----@field rm fun(db: feed.db, id: integer) ----@field iter Iter +---@field rm fun(db: feed.db, id: string) +---@field iter fun(db: feed.db, sort: boolean): Iter ---@field filter fun(db: feed.db, query: string) : string[] ---@field save_entry fun(db: feed.db, id: string): boolean ---@field save_feeds fun(db: feed.db): boolean @@ -19,9 +19,8 @@ local ut = require("feed.utils") ---@field blowup fun(db: feed.db) ---@field update fun(db: feed.db) ---@field lastUpdated fun(db: feed.db) - -local DB = {} -DB.__index = DB +local M = {} +M.__index = M local uv = vim.uv @@ -45,7 +44,7 @@ local function if_path(k, dir) return vim.fs.find({ k }, { path = tostring(dir / "object"), type = "file" })[1] -- TODO: remove end -function DB:append_time_id(time, id) +function M:append_time_id(time, id) local fp = tostring(self.dir / "index") local f = io.open(fp, "a") assert(f) @@ -68,7 +67,7 @@ local function parse_index(fp) return res end -function DB:save_index() +function M:save_index() local buf = {} for i, v in ipairs(self.index) do buf[i] = tostring(v[2]) .. " " .. v[1] @@ -79,7 +78,7 @@ end local mem = {} ---@return feed.db -function DB.new(db_dir) +function M.new(db_dir) db_dir = Path.new(db_dir or Config.db_dir) local data_dir = db_dir / "data" local object_dir = db_dir / "object" @@ -103,14 +102,14 @@ function DB.new(db_dir) return rawget(t, tag) end, }), - }, DB) + }, M) end ---@param k any ----@return function | feed.entry | string -function DB:__index(k) - if rawget(DB, k) then - return DB[k] +---@return function | feed.entry +function M:__index(k) + if rawget(M, k) then + return M[k] elseif k == "index" then local index = parse_index(self.dir / "index") rawset(self, "index", index) @@ -125,7 +124,7 @@ function DB:__index(k) end end -function DB:update() +function M:update() local feeds = Path.load(self.dir / "feeds.lua") rawset(self, "feeds", feeds) local index = parse_index(self.dir / "index") @@ -140,13 +139,13 @@ function DB:update() rawset(self, "tags", tags) end -function DB:lastUpdated() +function M:lastUpdated() return os.date("%c", vim.fn.getftime(tostring(self.dir / "feeds.lua"))) end ---@param id string ---@param entry feed.entry -function DB:__newindex(id, entry) +function M:__newindex(id, entry) if not id or if_path(id, self.dir) then return end @@ -157,7 +156,7 @@ end ---@param id string | string[] ---@param tag string -function DB:tag(id, tag) +function M:tag(id, tag) local function tag_one(t) self.tags[t][id] = true self:save_tags() @@ -179,14 +178,14 @@ end ---@param id string | string[] ---@param tag string -function DB:untag(id, tag) +function M:untag(id, tag) local function tag_one(t) self.tags[t][id] = nil self:save_tags() end if type(tag) == "string" then if tag:find(",") then - for t in split_comma(tag) do + for t in ut.split_comma(tag) do tag_one(t) end else @@ -199,13 +198,14 @@ function DB:untag(id, tag) end end -function DB:sort() +function M:sort() table.sort(self.index, function(a, b) return a[2] > b[2] end) end -function DB:rm(id) +---@param id string +function M:rm(id) for i, v in ipairs(self.index) do if v[1] == id then table.remove(self.index, i) @@ -226,7 +226,7 @@ end ---@param sort any ---@return Iter -function DB:iter(sort) +function M:iter(sort) if sort then self:sort() end @@ -239,7 +239,7 @@ end ---return a list of db ids base on query ---@param str string ---@return string[] -function DB:filter(str) +function M:filter(str) if str == "" then return {} end @@ -299,7 +299,6 @@ function DB:filter(str) if q.re then iter = iter:filter(function(id) - mem[id] = Path.load(self.dir / "object" / id) local entry = self[id] if not entry or not entry.title then return false @@ -315,7 +314,6 @@ function DB:filter(str) if q.feed then iter = iter:filter(function(id) - mem[id] = Path.load(self.dir / "object" / id) local url = self[id].feed local feed_name = self.feeds[url] and self.feeds[url].title if q.feed:match_str(url) or (feed_name and q.feed:match_str(feed_name)) then @@ -325,6 +323,17 @@ function DB:filter(str) end) end + if q.not_feed then + iter = iter:filter(function(id) + local url = self[id].feed + local feed_name = self.feeds[url] and self.feeds[url].title + if q.not_feed:match_str(url) or (feed_name and q.not_feed:match_str(feed_name)) then + return false + end + return true + end) + end + local ret = iter:fold({}, function(acc, id) if not mem[id] then mem[id] = Path.load(self.dir / "object" / id) @@ -342,18 +351,18 @@ function DB:filter(str) return ret end -function DB:save_feeds() +function M:save_feeds() return Path.save(self.dir / "feeds.lua", self.feeds) end -function DB:save_tags() +function M:save_tags() local tags = vim.deepcopy(self.tags) setmetatable(tags, nil) return Path.save(self.dir / "tags.lua", tags) end -function DB:blowup() +function M:blowup() Path.rm(self.dir) end -return DB.new() +return M.new() diff --git a/lua/feed/db/query.lua b/lua/feed/db/query.lua index 4dd32f2..b2705f9 100644 --- a/lua/feed/db/query.lua +++ b/lua/feed/db/query.lua @@ -8,6 +8,7 @@ local M = {} ---@field must_have? string[] #+ ---@field must_not_have? string[] #- ---@field feed? vim.regex #= +---@field not_feed? vim.regex #~ ---@field limit? number ## ---@field re? vim.regex[] @@ -42,6 +43,8 @@ function M.parse_query(str) table.insert(query.must_not_have, q:sub(2)) elseif kind == "=" then query.feed = build_regex(q:sub(2)) + elseif kind == "~" then + query.not_feed = build_regex(q:sub(2)) else if not query.re then query.re = {} diff --git a/tests/test_query.lua b/tests/test_query.lua index 8907538..e3b31c5 100644 --- a/tests/test_query.lua +++ b/tests/test_query.lua @@ -6,11 +6,13 @@ local T = MiniTest.new_set() T["parse"] = MiniTest.new_set() T["parse"]["splits query into parts"] = function() - local query = M.parse_query("+read -star @5-days-ago linu[xs]") + local query = M.parse_query("+read -star @5-days-ago linu[xs] =vim ~emacs") eq("read", query.must_have[1]) eq("star", query.must_not_have[1]) eq("number", type(query.after)) eq("userdata", type(query.re[1])) + assert(query.feed:match_str("vim")) + assert(query.not_feed:match_str("emacs")) end T["parse"]["allows imcomplete query for live searching"] = function() From 37c96074ef0f7e528e8d95e5dc4a171b51c2df0e Mon Sep 17 00:00:00 2001 From: n451 <2020200706@ruc.edu.cn> Date: Thu, 20 Feb 2025 00:49:33 +0800 Subject: [PATCH 2/4] refactor: clean up path and db logic --- lua/feed/db/local.lua | 105 ++++++++++++++++--------------------- lua/feed/db/path.lua | 47 +++++------------ lua/feed/utils/shared.lua | 73 +++++++++----------------- lua/feed/utils/strings.lua | 14 ++--- 4 files changed, 89 insertions(+), 150 deletions(-) diff --git a/lua/feed/db/local.lua b/lua/feed/db/local.lua index 4121b29..68f1761 100644 --- a/lua/feed/db/local.lua +++ b/lua/feed/db/local.lua @@ -2,6 +2,7 @@ local Path = require("feed.db.path") local Config = require("feed.config") local query = require("feed.db.query") local ut = require("feed.utils") +local uv = vim.uv ---@class feed.db ---@field dir string @@ -20,39 +21,26 @@ local ut = require("feed.utils") ---@field update fun(db: feed.db) ---@field lastUpdated fun(db: feed.db) local M = {} -M.__index = M - -local uv = vim.uv ---@param fp string ---@param t any -local ensure_path = function(fp, t) - local fpstr = tostring(fp) - if not uv.fs_stat(fpstr) then +local ensure_exists = function(fp, t) + if not uv.fs_stat(tostring(fp)) then if t == "dir" then Path.mkdir(fp) elseif t == "file" then - Path.touch(fp) + Path.save(fp, "") elseif t == "obj" then - Path.touch(fp) Path.save(fp, {}) end end end local function if_path(k, dir) - return vim.fs.find({ k }, { path = tostring(dir / "object"), type = "file" })[1] -- TODO: remove + return vim.fs.find({ k }, { path = tostring(dir / "object"), type = "file" })[1] end -function M:append_time_id(time, id) - local fp = tostring(self.dir / "index") - local f = io.open(fp, "a") - assert(f) - f:write(time .. " " .. id .. "\n") - f:close() -end - -local function parse_index(fp) +local function load_index(fp) local res = {} fp = tostring(fp) if not uv.fs_stat(fp) then @@ -67,34 +55,27 @@ local function parse_index(fp) return res end -function M:save_index() - local buf = {} - for i, v in ipairs(self.index) do - buf[i] = tostring(v[2]) .. " " .. v[1] - end - Path.save(self.dir / "index", table.concat(buf, "\n")) -end - local mem = {} ---@return feed.db -function M.new(db_dir) - db_dir = Path.new(db_dir or Config.db_dir) - local data_dir = db_dir / "data" - local object_dir = db_dir / "object" - local feeds_fp = db_dir / "feeds.lua" - local tags_fp = db_dir / "tags.lua" - local index_fp = db_dir / "index" - - ensure_path(db_dir, "dir") - ensure_path(data_dir, "dir") - ensure_path(object_dir, "dir") - ensure_path(feeds_fp, "obj") - ensure_path(tags_fp, "obj") - ensure_path(index_fp, "file") +function M.new(dir) + dir = Path.new(dir or Config.db_dir) + local data_dir = dir / "data" + local object_dir = dir / "object" + local feeds_fp = dir / "feeds.lua" + local tags_fp = dir / "tags.lua" + local index_fp = dir / "index" + + ensure_exists(dir, "dir") + ensure_exists(data_dir, "dir") + ensure_exists(object_dir, "dir") + ensure_exists(feeds_fp, "obj") + ensure_exists(tags_fp, "obj") + ensure_exists(index_fp, "file") return setmetatable({ - dir = db_dir, + dir = dir, + index = load_index(dir / "index"), feeds = feeds_fp:load(), tags = setmetatable(tags_fp:load(), { __index = function(t, tag) @@ -108,26 +89,22 @@ end ---@param k any ---@return function | feed.entry function M:__index(k) - if rawget(M, k) then - return M[k] - elseif k == "index" then - local index = parse_index(self.dir / "index") - rawset(self, "index", index) - return rawget(self, "index") - else - local r = mem[k] - if not r then - r = Path.load(self.dir / "object" / k) - mem[k] = r - end - return r + local ms = rawget(M, k) + if ms then + return ms end + local r = mem[k] + if not r then + r = Path.load(self.dir / "object" / k) + mem[k] = r + end + return r end function M:update() local feeds = Path.load(self.dir / "feeds.lua") rawset(self, "feeds", feeds) - local index = parse_index(self.dir / "index") + local index = load_index(self.dir / "index") rawset(self, "index", index) local tags = Path.load(self.dir / "tags.lua") setmetatable(tags, { @@ -149,8 +126,10 @@ function M:__newindex(id, entry) if not id or if_path(id, self.dir) then return end - table.insert(self.index, { id, entry.time }) - self:append_time_id(entry.time, id) + mem[id] = entry + local time = entry.time + table.insert(self.index, { id, time }) + Path.append(self.dir / "index", time .. " " .. id .. "\n") Path.save(self.dir / "object" / id, entry) end @@ -163,7 +142,7 @@ function M:tag(id, tag) end if type(tag) == "string" then if tag:find(",") then - for t in ut.split_comma(tag) do + for t in ut.split(tag, ",") do tag_one(t) end else @@ -185,7 +164,7 @@ function M:untag(id, tag) end if type(tag) == "string" then if tag:find(",") then - for t in ut.split_comma(tag) do + for t in ut.split(tag, ",") do tag_one(t) end else @@ -361,6 +340,14 @@ function M:save_tags() return Path.save(self.dir / "tags.lua", tags) end +function M:save_index() + local buf = {} + for i, v in ipairs(self.index) do + buf[i] = tostring(v[2]) .. " " .. v[1] + end + Path.save(self.dir / "index", table.concat(buf, "\n")) +end + function M:blowup() Path.rm(self.dir) end diff --git a/lua/feed/db/path.lua b/lua/feed/db/path.lua index b2b04a0..4e0601e 100644 --- a/lua/feed/db/path.lua +++ b/lua/feed/db/path.lua @@ -4,6 +4,10 @@ local Path = {} local uv = vim.uv +local ut = require("feed.utils") +local save_file = ut.save_file +local read_file = ut.read_file +local load_file = ut.load_file local sep = package.config:sub(1, 1) @@ -27,22 +31,6 @@ Path.new = function(path) }) end -local function savefile(fp, str) - local f = io.open(fp, "w") - assert(f, fp) - f:write(str) - f:close() -end - -local function readfile(fp) - local ret - local f = io.open(fp, "r") - assert(f) - ret = f:read("*a") - f:close() - return ret -end - ---@param dir local function rmdir(dir) dir = type(dir) == "table" and tostring(dir) or dir @@ -56,19 +44,6 @@ local function rmdir(dir) return uv.fs_rmdir(dir) end ----@return table -local function pload(str) - local ok, res = pcall(dofile, tostring(str)) - if not ok then - return {} - end - return res -end - -Path.touch = function(self) - self:save("") -end - Path.rm = function(self) local fp = tostring(self) if vim.fs.rm then @@ -86,20 +61,26 @@ end Path.save = function(self, content) local fp = tostring(self) if type(content) == "string" then - savefile(fp, content) + save_file(fp, content, "w") else - savefile(fp, "return " .. vim.inspect(content)) + save_file(fp, "return " .. vim.inspect(content), "w") end end +---@param line string +Path.append = function(self, line) + local fp = tostring(self) + save_file(fp, line, "a") +end + ---@return table Path.load = function(self) - return pload(tostring(self)) + return load_file(tostring(self)) end ---@return table Path.read = function(self) - return readfile(tostring(self)) + return read_file(tostring(self)) end Path.mkdir = function(self) diff --git a/lua/feed/utils/shared.lua b/lua/feed/utils/shared.lua index 6d1a9e6..556b563 100644 --- a/lua/feed/utils/shared.lua +++ b/lua/feed/utils/shared.lua @@ -2,12 +2,17 @@ local M = {} local vim = vim local api, fn = vim.api, vim.fn -local ipairs, tostring = ipairs, tostring +local ipairs, pcall, dofile, type = ipairs, pcall, dofile, type +local io = io -M.load_file = function(fp) - if type(fp) == "table" then - fp = tostring(fp) +M.listify = function(t) + if type(t) ~= "table" then + return { t } end + return (#t == 0 and not vim.islist(t)) and { t } or t +end + +M.load_file = function(fp) local ok, res = pcall(dofile, fp) if ok and res then return res @@ -17,55 +22,25 @@ M.load_file = function(fp) end end -M.listify = function(t) - if type(t) ~= "table" then - return { t } - end - return (#t == 0 and not vim.islist(t)) and { t } or t -end - ----@param fp string | PathlibPath ----@param object table -M.save_obj = function(fp, object) - M.save_file(fp, "return " .. vim.inspect(object)) -end - ----@param path string | PathlibPath ----@param content string -M.save_file = function(path, content) - if not path then - return - end - content = content or "" - if type(path) == "table" then - ---@diagnostic disable-next-line: param-type-mismatch - path = tostring(path) - end - ---@cast path string - local f = io.open(path, "w") - if f then - f:write(content) - f:close() - return true - else - return false - end +---@param fp string +---@param str string +---@param mode "w" | "a" +M.save_file = function(fp, str, mode) + mode = mode or "w" + local f = io.open(fp, mode) + assert(f, fp) + f:write(str) + f:close() end ---@param path string ---@return string? M.read_file = function(path) local ret - - if type(path) == "table" then - ---@diagnostic disable-next-line: param-type-mismatch - path = tostring(path) - end local f = io.open(path, "r") - if f then - ret = f:read("*a") - f:close() - end + assert(f, "could not open " .. path) + ret = f:read("*a") + f:close() return ret end @@ -149,7 +124,7 @@ end ---@param wo vim.wo function M.wo(win, wo) for k, v in pairs(wo or {}) do - vim.api.nvim_set_option_value(k, v, { scope = "local", win = win }) + api.nvim_set_option_value(k, v, { scope = "local", win = win }) end end @@ -158,7 +133,7 @@ end ---@param bo vim.bo function M.bo(buf, bo) for k, v in pairs(bo or {}) do - vim.api.nvim_set_option_value(k, v, { buf = buf }) + api.nvim_set_option_value(k, v, { buf = buf }) end end @@ -172,7 +147,7 @@ end ---@return boolean M.is_headless = function() - return vim.tbl_isempty(vim.api.nvim_list_uis()) + return vim.tbl_isempty(api.nvim_list_uis()) end return M diff --git a/lua/feed/utils/strings.lua b/lua/feed/utils/strings.lua index cd8dc86..ec14200 100644 --- a/lua/feed/utils/strings.lua +++ b/lua/feed/utils/strings.lua @@ -1,12 +1,5 @@ local M = {} --- TODO: edge case --- [观点&评…] --- [Articles, 新…] --- [Social Media…] --- [Articles, 新…] --- [Articles, 新…] - --- from plenary.nvim local truncate = function(str, len, dots, direction) if vim.fn.strdisplaywidth(str) <= len then @@ -87,8 +80,11 @@ M.capticalize = function(str) return str:sub(1, 1):upper() .. str:sub(2) end -M.split_comma = function(str) - return vim.iter(vim.split(str, ",")) +---@param str string +---@param sep string +---@return Iter +M.split = function(str, sep) + return vim.iter(vim.split(str, sep)) :map(function(v) return vim.trim(v) end) From 8d48ccc0f588f24c149b7614ca93bcbde4dbb719 Mon Sep 17 00:00:00 2001 From: n451 <2020200706@ruc.edu.cn> Date: Thu, 20 Feb 2025 01:36:51 +0800 Subject: [PATCH 3/4] feat: query syntax support for `!`, meaning inverse regex match --- lua/feed/db/query.lua | 21 ++++++++++++++++----- tests/test_query.lua | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lua/feed/db/query.lua b/lua/feed/db/query.lua index b2705f9..d61001b 100644 --- a/lua/feed/db/query.lua +++ b/lua/feed/db/query.lua @@ -12,14 +12,25 @@ local M = {} ---@field limit? number ## ---@field re? vim.regex[] +---wrapper arround vim.regex, ! is inverse, respects vim.o.ignorecase +---@param str string local function build_regex(str) - -- local rev = str:sub(0, 1) == "!" - -- if rev then - -- str = str:sub(2) - -- end - return vim.regex(str .. "\\c") + local pattern + if str:sub(0, 1) == "!" then + pattern = [[^\(.*]] .. vim.fn.escape(str:sub(2), "\\") .. [[.*\)\@!.*]] + else + pattern = str + end + if vim.o.ignorecase then + pattern = pattern .. "\\c" + else + pattern = pattern .. "\\C" + end + return vim.regex(pattern) end +M._build_regex = build_regex + ---@param str string ---@return feed.query function M.parse_query(str) diff --git a/tests/test_query.lua b/tests/test_query.lua index e3b31c5..fe36788 100644 --- a/tests/test_query.lua +++ b/tests/test_query.lua @@ -5,6 +5,22 @@ local T = MiniTest.new_set() T["parse"] = MiniTest.new_set() +T["regex"] = function() + vim.o.ignorecase = true -- vim == Vim + local regex_vim = M._build_regex("vim") + eq(true, regex_vim:match_str("Vim") ~= nil) + vim.o.ignorecase = false -- vim ~= Vim + local regex_vim2 = M._build_regex("vim") + eq(false, regex_vim2:match_str("Vim") ~= nil) + + vim.o.ignorecase = true -- vim == Vim + local regex_not_vim = M._build_regex("!vim") + eq(false, regex_not_vim:match_str("Vim") ~= nil) + vim.o.ignorecase = false -- vim ~= Vim + local regex_not_vim2 = M._build_regex("!vim") + eq(true, regex_not_vim2:match_str("Vim") ~= nil) +end + T["parse"]["splits query into parts"] = function() local query = M.parse_query("+read -star @5-days-ago linu[xs] =vim ~emacs") eq("read", query.must_have[1]) From 293b0226f6784c5ff550ec5a05bdb420495d3eea Mon Sep 17 00:00:00 2001 From: n451 <2020200706@ruc.edu.cn> Date: Thu, 20 Feb 2025 02:02:32 +0800 Subject: [PATCH 4/4] feat: regex also matches a feed's link like in elfeed --- lua/feed/db/local.lua | 2 +- tests/test_query.lua | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/feed/db/local.lua b/lua/feed/db/local.lua index 68f1761..5fab24e 100644 --- a/lua/feed/db/local.lua +++ b/lua/feed/db/local.lua @@ -283,7 +283,7 @@ function M:filter(str) return false end for _, reg in ipairs(q.re) do - if not reg:match_str(entry.title) then + if not reg:match_str(entry.title) or not reg:match(entry.link) then return false end end diff --git a/tests/test_query.lua b/tests/test_query.lua index fe36788..73dc436 100644 --- a/tests/test_query.lua +++ b/tests/test_query.lua @@ -19,6 +19,11 @@ T["regex"] = function() vim.o.ignorecase = false -- vim ~= Vim local regex_not_vim2 = M._build_regex("!vim") eq(true, regex_not_vim2:match_str("Vim") ~= nil) + + local maybe_nvim = M._build_regex("^n\\=vim") + eq(true, maybe_nvim:match_str("vim") ~= nil) + eq(true, maybe_nvim:match_str("nvim") ~= nil) + eq(false, maybe_nvim:match_str("neovim") ~= nil) end T["parse"]["splits query into parts"] = function()