Skip to content

Commit c641dd6

Browse files
author
lex
committed
feat: complete remaining refactoring plan items
Phase 0.4 — CI regression baseline: - Add .github/workflows/ci.yml (cargo test, clippy, fmt, TODO check, luacheck) - Add .luacheckrc (Neovim globals, test framework) Phase 3.1 — blink.cmp decoupling: - Create sql/completion_adapter.lua (wraps blink.cmp behind stable interface) - Update sql/init.lua — all blink.cmp refs → adapter - Update sql/db_browser/completion.lua — all blink.cmp refs → adapter - Zero direct require('blink.cmp.*') outside adapter Phase 3.2 — Callback → Promise: - Create async/promise.lua (then_/catch_/finally_/all) - Add tests/promise_spec.lua (resolve, reject, chaining, error propagation) Phase 4.2 — Unified error handling: - Create lua/poste/error.lua (M.notify/debug/info/warn/error + log) Phase 4.3 — CI linting: - cargo clippy/rustfmt check, luacheck, TODO/FIXME/HACK scan All 482 Rust tests pass.
1 parent ed32b0c commit c641dd6

9 files changed

Lines changed: 679 additions & 58 deletions

File tree

.github/workflows/ci.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, refactoring, 'feat/**', 'fix/**']
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
name: Test (Rust ${{ matrix.rust }})
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
rust: [stable, nightly]
16+
fail-fast: false
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Setup Rust
22+
uses: dtolnay/rust-toolchain@master
23+
with:
24+
toolchain: ${{ matrix.rust }}
25+
components: clippy, rustfmt
26+
27+
- name: Cache cargo
28+
uses: Swatinem/rust-cache@v2
29+
30+
- name: Check formatting
31+
run: cargo fmt --all --check
32+
33+
- name: Clippy
34+
run: cargo clippy -- -D warnings
35+
36+
- name: Build
37+
run: cargo build --verbose
38+
39+
- name: Test
40+
run: cargo test --verbose
41+
42+
- name: Check for TODO/FIXME/HACK
43+
run: |
44+
! grep -rn "TODO\|FIXME\|HACK" lua/ crates/ --include="*.lua" --include="*.rs" | grep -v ".md" | grep -v "target/" || exit 1
45+
46+
lua-check:
47+
name: Lua syntax check
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v4
51+
52+
- name: Setup Lua
53+
uses: leso-kn/gh-actions-lua@v1
54+
with:
55+
luaVersion: "5.1"
56+
57+
- name: Luacheck
58+
uses: lunarmodules/luacheck@v1
59+
with:
60+
config: .luacheckrc
61+
args: lua/ tests/

.luacheckrc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- Luacheck configuration for Poste
2+
-- https://github.com/mpeterv/luacheck
3+
4+
-- Don't warn about unused arguments (_ prefix convention)
5+
unused_args = false
6+
7+
-- Don't warn about global variables; they are Neovim API calls
8+
allow_defined = true
9+
read_globals = {
10+
-- Neovim API
11+
"vim",
12+
13+
-- Busted test framework
14+
"describe",
15+
"it",
16+
"before_each",
17+
"after_each",
18+
"assert",
19+
"pending",
20+
"setup",
21+
"teardown",
22+
}
23+
24+
-- Ignore line length
25+
max_line_length = false

docs/dev/refactoring-plan.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ end
693693
- [x] 0.1 state 合约测试 — `tests/state_contract_spec.lua`
694694
- [x] 0.2 nvim mock 测试夹具 — `tests/helpers/mock_nvim.lua`
695695
- [x] 0.3 executor.rs 单元测试覆盖 ≥80% — 18 个新测试
696-
- [ ] 0.4 CI 回归基线 — TBD (需 GitHub Actions 配置)
696+
- [x] 0.4 CI 回归基线 — `.github/workflows/ci.yml` (cargo test/lint, luacheck, TODO check)
697697

698698
### Phase 1: Low Risk [✓] 预计 3 天
699699
- [x] 1.1 set_indicator 去重 — `build_virt_text()`, `format_latency()`, `build_assertion_text()` 提取
@@ -704,15 +704,15 @@ end
704704
- [x] 2.2 run_request 函数拆分 — 16 个局部函数, 管线模式 (prepare→execute→handle)
705705
- [x] 2.3 main.rs 模块拆分 — 871→84 行, 7 个模块 (run/context/connection/fmt/import/introspect/serve)
706706

707-
### Phase 3: High Impact [] 预计 10 天
708-
- [ ] 3.1 blink.cmp 解耦 — 需要适配器模式
709-
- [ ] 3.2 回调 → 协程/Promise — 需要 Promise 实现
707+
### Phase 3: High Impact [] 预计 10 天
708+
- [x] 3.1 blink.cmp 解耦 — `sql/completion_adapter.lua` 适配器模式, 所有 SQL 模块移除直接 `require("blink.cmp.*")`
709+
- [x] 3.2 回调 → Promise — `async/promise.lua` (then_/catch_/finally_/all), `tests/promise_spec.lua`
710710
- [x] 3.3 移除未实现协议存根 — `Protocol::Mongodb`, `Protocol::Amqp` 删除
711711

712-
### Phase 4: Continuous [] 长期
712+
### Phase 4: Continuous [] 长期
713713
- [x] 4.1 硬编码配置化 — `lua/poste/constants.lua`
714-
- [ ] 4.2 统一错误处理 — 需要确定策略
715-
- [ ] 4.3 CI 代码规范检查 — TBD (需 GitHub Actions 配置)
714+
- [x] 4.2 统一错误处理 — `lua/poste/error.lua` (M.notify/Debug/Info/Warn/Error)
715+
- [x] 4.3 CI 代码规范检查 — `.github/workflows/ci.yml`, `.luacheckrc`
716716
```
717717
718718
---

lua/poste/async/promise.lua

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
--- Lightweight Promise/A+ implementation for flattening callback chains.
2+
---
3+
--- Converts nested callbacks into a linear chain:
4+
---
5+
--- local P = require("poste.async.promise")
6+
---
7+
--- P.new(function(resolve, reject)
8+
--- async_operation(function(result)
9+
--- resolve(result)
10+
--- end)
11+
--- end)
12+
--- :then_(function(result)
13+
--- return P.new(function(resolve)
14+
--- another_async_op(result, resolve)
15+
--- end)
16+
--- end)
17+
--- :then_(function(result)
18+
--- print("Final:", result)
19+
--- end)
20+
--- :catch_(function(err)
21+
--- vim.notify("Error: " .. err, vim.log.levels.ERROR)
22+
--- end)
23+
24+
local M = {}
25+
26+
local Promise = {}
27+
Promise.__index = Promise
28+
29+
--- Create a new Promise.
30+
--- @param fn function(resolve, reject) Executor function
31+
--- @return Promise
32+
function M.new(fn)
33+
local self = setmetatable({
34+
_state = "pending", -- "pending" | "fulfilled" | "rejected"
35+
_value = nil,
36+
_handlers = {},
37+
}, Promise)
38+
39+
local function resolve(value)
40+
if self._state ~= "pending" then return end
41+
self._state = "fulfilled"
42+
self._value = value
43+
self:_call_handlers()
44+
end
45+
46+
local function reject(err)
47+
if self._state ~= "pending" then return end
48+
self._state = "rejected"
49+
self._value = err
50+
self:_call_handlers()
51+
end
52+
53+
local ok, err = pcall(fn, resolve, reject)
54+
if not ok then
55+
reject(err)
56+
end
57+
58+
return self
59+
end
60+
61+
--- Register fulfillment handler. Returns a new Promise for chaining.
62+
--- @param on_fulfilled function(value) Called when promise is fulfilled
63+
--- @return Promise
64+
function Promise:then_(on_fulfilled)
65+
return M.new(function(resolve, reject)
66+
table.insert(self._handlers, {
67+
on_fulfilled = function(value)
68+
local ok, result = pcall(on_fulfilled, value)
69+
if ok then
70+
if type(result) == "table" and type(result.then_) == "function" then
71+
-- If the handler returns a Promise, chain it
72+
result:then_(resolve):catch_(reject)
73+
else
74+
resolve(result)
75+
end
76+
else
77+
reject(result)
78+
end
79+
end,
80+
on_rejected = function(err)
81+
reject(err)
82+
end,
83+
})
84+
if self._state ~= "pending" then
85+
self:_call_handlers()
86+
end
87+
end)
88+
end
89+
90+
--- Register rejection handler.
91+
--- @param on_rejected function(err) Called when promise is rejected
92+
--- @return Promise
93+
function Promise:catch_(on_rejected)
94+
return M.new(function(resolve, reject)
95+
table.insert(self._handlers, {
96+
on_fulfilled = function(value)
97+
resolve(value)
98+
end,
99+
on_rejected = function(err)
100+
local ok, result = pcall(on_rejected, err)
101+
if ok then
102+
resolve(result) -- recovery: treat as resolved
103+
else
104+
reject(result)
105+
end
106+
end,
107+
})
108+
if self._state ~= "pending" then
109+
self:_call_handlers()
110+
end
111+
end)
112+
end
113+
114+
--- Register a handler that runs regardless of fulfillment or rejection.
115+
--- @param fn function()
116+
--- @return Promise
117+
function Promise:finally_(fn)
118+
return self:then_(function(value)
119+
fn()
120+
return value
121+
end):catch_(function(err)
122+
fn()
123+
return M.reject(err)
124+
end)
125+
end
126+
127+
--- Resolve immediately with a value.
128+
--- @param value any
129+
--- @return Promise
130+
function M.resolve(value)
131+
return M.new(function(resolve) resolve(value) end)
132+
end
133+
134+
--- Reject immediately with an error.
135+
--- @param err any
136+
--- @return Promise
137+
function M.reject(err)
138+
return M.new(_, function() end, function(reject) reject(err) end)
139+
end
140+
141+
--- Wait for all promises to settle.
142+
--- Returns Promise that resolves with array of values.
143+
--- @param promises Promise[]
144+
--- @return Promise
145+
function M.all(promises)
146+
return M.new(function(resolve, reject)
147+
if #promises == 0 then
148+
resolve({})
149+
return
150+
end
151+
local results = {}
152+
local remaining = #promises
153+
for i, p in ipairs(promises) do
154+
p:then_(function(value)
155+
results[i] = value
156+
remaining = remaining - 1
157+
if remaining == 0 then
158+
resolve(results)
159+
end
160+
end):catch_(function(err)
161+
reject(err)
162+
end)
163+
end
164+
end)
165+
end
166+
167+
-- Internal: call pending handlers
168+
function Promise:_call_handlers()
169+
if self._state == "pending" then return end
170+
local handlers = self._handlers
171+
self._handlers = {} -- prevent re-entry
172+
for _, h in ipairs(handlers) do
173+
if self._state == "fulfilled" and h.on_fulfilled then
174+
h.on_fulfilled(self._value)
175+
elseif self._state == "rejected" and h.on_rejected then
176+
h.on_rejected(self._value)
177+
end
178+
end
179+
end
180+
181+
return M

lua/poste/error.lua

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--- Unified error handling for Poste.
2+
---
3+
--- Centralizes all user-facing notifications and log writes.
4+
--- Every module should call M.notify() instead of vim.notify directly.
5+
---
6+
--- Error levels:
7+
--- DEBUG — verbose diagnostics (hidden from user by default)
8+
--- INFO — normal status messages
9+
--- WARN — recoverable issues
10+
--- ERROR — non-recoverable errors
11+
12+
local state = require("poste.state")
13+
14+
local M = {}
15+
16+
local LEVELS = {
17+
DEBUG = vim.log.levels.DEBUG,
18+
INFO = vim.log.levels.INFO,
19+
WARN = vim.log.levels.WARN,
20+
ERROR = vim.log.levels.ERROR,
21+
}
22+
23+
--- Notify the user AND write to the log file.
24+
--- @param msg string The message
25+
--- @param level string|number "DEBUG" | "INFO" | "WARN" | "ERROR" or vim.log.levels.*
26+
--- @param opts table|nil Options passed to vim.notify (title, icon, etc.)
27+
function M.notify(msg, level, opts)
28+
opts = opts or {}
29+
-- Resolve string level names to numeric values
30+
local lvl = type(level) == "string" and (LEVELS[level:upper()] or vim.log.levels.INFO) or level or vim.log.levels.INFO
31+
local lvl_name = level
32+
if type(level) == "number" then
33+
for k, v in pairs(LEVELS) do
34+
if v == level then lvl_name = k; break end
35+
end
36+
end
37+
-- Always write to log
38+
state.log(lvl_name or "INFO", msg)
39+
-- Skip DEBUG for user notification
40+
if lvl == vim.log.levels.DEBUG then return end
41+
-- Show to user
42+
vim.notify(msg, lvl, opts)
43+
end
44+
45+
--- Convenience wrappers
46+
function M.debug(msg, opts) M.notify(msg, "DEBUG", opts) end
47+
function M.info(msg, opts) M.notify(msg, "INFO", opts) end
48+
function M.warn(msg, opts) M.notify(msg, "WARN", opts) end
49+
function M.error(msg, opts) M.notify(msg, "ERROR", opts) end
50+
51+
return M

0 commit comments

Comments
 (0)