Skip to content

Commit 264b83e

Browse files
committed
new observer
1 parent 9bd6561 commit 264b83e

8 files changed

Lines changed: 261 additions & 232 deletions

File tree

lua/fittencode/fn/buf.lua

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,20 +126,21 @@ function M.is_valid_buf(buf)
126126
return true
127127
end
128128

129-
-- 检测一个 buffer 是否是一个文件,并且可读
129+
-- 检测一个 buffer 是否是一个文件
130130
-- 并在满足这个条件下返回该文件的路径 (路径是和平台相关的)
131131
---@return boolean, string?
132132
function M.is_filebuf(buf)
133133
if not M.is_valid_buf(buf) then
134134
return false
135135
end
136136
if vim.api.nvim_buf_is_loaded(buf) and vim.fn.buflisted(buf) == 1 then
137-
local path
138-
vim.api.nvim_buf_call(buf, function()
139-
path = vim.fn.expand('%:p')
140-
end)
141-
if vim.api.nvim_buf_get_name(buf) ~= '' and path and vim.fn.filereadable(path) == 1 then
142-
return true, path
137+
local bufname = vim.api.nvim_buf_get_name(buf)
138+
if bufname == '' then
139+
return false
140+
end
141+
local stat = vim.uv.fs_stat(bufname)
142+
if stat and stat.type == 'file' then
143+
return true, bufname
143144
end
144145
end
145146
return false

lua/fittencode/fn/observer.lua

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
local Log = require('fittencode.log')
2+
3+
---@class FittenCode.StateMachine
4+
---@field private current string
5+
---@field private transitions table<string, string[]>
6+
---@field private subscribers function[]
7+
local StateMachine = {}
8+
StateMachine.__index = StateMachine
9+
10+
---@class FittenCode.StateMachine.Options
11+
---@field initial string
12+
---@field transitions table<string, string[]>
13+
14+
---@param options FittenCode.StateMachine.Options
15+
---@return FittenCode.StateMachine
16+
function StateMachine.new(options)
17+
local self = setmetatable({}, StateMachine)
18+
self.current = options.initial
19+
self.transitions = options.transitions
20+
self.subscribers = {}
21+
return self
22+
end
23+
24+
---@return string
25+
function StateMachine:state()
26+
return self.current
27+
end
28+
29+
---@param name string
30+
---@return boolean
31+
function StateMachine:is(name)
32+
return self.current == name
33+
end
34+
35+
---@param target string
36+
---@return boolean
37+
function StateMachine:transition(target)
38+
local from = self.current
39+
if self.current ~= target then
40+
local valid = self.transitions[self.current]
41+
if valid and not vim.tbl_contains(valid, target) then
42+
Log.warn('Invalid state transition: {} -> {}', self.current, target)
43+
return false
44+
end
45+
self.current = target
46+
end
47+
for _, fn in ipairs(self.subscribers) do
48+
fn({ from = from, to = target })
49+
end
50+
return true
51+
end
52+
53+
---@param fn fun(state: { from: string, to: string })
54+
function StateMachine:subscribe(fn)
55+
self.subscribers[#self.subscribers + 1] = fn
56+
end
57+
58+
function StateMachine:unsubscribe(fn)
59+
for i, cb in ipairs(self.subscribers) do
60+
if cb == fn then
61+
table.remove(self.subscribers, i)
62+
return
63+
end
64+
end
65+
end
66+
67+
return StateMachine

lua/fittencode/inline/_types.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,20 @@
6969
---@field model FittenCode.Inline.Model
7070
---@field version number
7171
---@field headless boolean
72+
73+
--[[
74+
'start', -- 创建了 Session,COMPLETION
75+
'generating_prompt', -- 正在构建补全请求的提示词(如代码片段、自然语言问题)。
76+
'getting_completion_version', -- 正在获取补全服务版本。
77+
'generate_one_stage', -- 向补全服务发送请求(如 HTTP 请求),等待响应。
78+
'suggestions_ready', -- 成功获取补全建议,可渲染到 UI。
79+
'no_more_suggestions', -- 补全服务返回无结果。
80+
'error', -- 补全流程失败(如网络错误、参数无效)。
81+
]]
82+
---@alias FittenCode.Inline.CompletionEvent.Type 'start' | 'generating_prompt' | 'getting_completion_version' | 'generate_one_stage' |'suggestions_ready' | 'no_more_suggestions' | 'error'
83+
84+
--[[
85+
'semantic_segment_pre', -- 开始语义分割(如中文分词)
86+
'semantic_segment_post', -- 完成语义分割
87+
]]
88+
---@alias FittenCode.Inline.SessionTaskEvent.Type'semantic_segment_pre' |'semantic_segment_post'

lua/fittencode/inline/controller.lua

Lines changed: 91 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@ local Promise = require('fittencode.fn.promise')
2222
local Session = require('fittencode.inline.session')
2323
local i18n = require('fittencode.i18n')
2424
local Log = require('fittencode.log')
25-
local Definitions = require('fittencode.inline.definitions')
2625
local CtrlObserver = require('fittencode.inline.ctrl_observer')
2726
local ProgressIndicator = require('fittencode.fn.progress_indicator')
28-
local Observer = require('fittencode.fn.observer')
2927
local Color = require('fittencode.color')
3028
local LspServer = require('fittencode.integrations.completion.lsp_server')
29+
local StateMachine = require('fittencode.fn.state_machine')
3130

32-
-- local Status = CtrlObserver.Status
31+
local Status = CtrlObserver.Status
3332
-- local ProgressIndicatorObserver = CtrlObserver.ProgressIndicatorObserver
3433
-- local TimingObserver = CtrlObserver.TimingObserver
3534

@@ -38,11 +37,13 @@ local LspServer = require('fittencode.integrations.completion.lsp_server')
3837
---@field rhs function
3938
---@field options? table<string, any>
4039

40+
---@class FittenCode.Observer
41+
---@field update function
42+
4143
---@class FittenCode.Inline.Controller
4244
---@field observers FittenCode.Observer[]
4345
---@field sessions FittenCode.Inline.Session[]
4446
---@field filter_events table<string, boolean>
45-
---@field status_observer FittenCode.Inline.Status
4647
---@field keymaps FittenCode.Keymap[]
4748
local Controller = {}
4849
Controller.__index = Controller
@@ -63,8 +64,8 @@ function Controller:_initialize(options)
6364
self.current_session = nil
6465
self.filter_events = {}
6566
self.keymaps = {}
66-
-- self.status_observer = Status.new()
67-
-- self:add_observer(self.status_observer)
67+
self.status_observer = Status.new()
68+
self:on(self.status_observer)
6869
-- self.pi = ProgressIndicator.new()
6970
-- self.progress_observer = ProgressIndicatorObserver.new({ pi = self.pi })
7071
-- self:add_observer(self.progress_observer)
@@ -116,6 +117,13 @@ function Controller:_initialize(options)
116117
self:edit_completion_cancel({ vimev = args })
117118
end,
118119
})
120+
vim.api.nvim_create_autocmd({ 'BufEnter', 'BufFilePost', 'FileType' }, {
121+
group = vim.api.nvim_create_augroup('FittenCode.Inline.ServiceUpdate', { clear = true }),
122+
pattern = '*',
123+
callback = function(args)
124+
self:sync_state()
125+
end,
126+
})
119127
if Config.integrations.completion.lsp_server then
120128
Log.debug('Completion integration: LSP server enabled')
121129
-- vim.lsp.enable('FittenCode')
@@ -139,53 +147,53 @@ function Controller:_initialize(options)
139147
})
140148
-- If {fn} returns an empty string, {key} is discarded/ignored.
141149
vim.on_key(function(key)
142-
local buf = vim.api.nvim_get_current_buf()
143150
self.filter_events = {}
144-
if vim.api.nvim_get_mode().mode:sub(1, 1) == 'i' and self:is_enabled(buf) then
151+
if vim.api.nvim_get_mode().mode:sub(1, 1) == 'i' and not self.state:is('disabled') then
145152
if vim.tbl_contains(filtered, key) and Config.inline_completion.disable_completion_when_delete then
146153
self.filter_events = { 'CursorMovedI', 'TextChangedI', }
147154
return
148155
end
149156
end
150157
end)
151-
end
152158

153-
---@param observer FittenCode.Observer | function
154-
function Controller:add_observer(observer)
155-
if type(observer) == 'function' then
156-
local id = 'callback_observer' .. Fn.generate_short_id_as_string()
157-
observer = setmetatable({
158-
id = id,
159-
update = function(_, ctrl, event)
160-
observer(ctrl, event)
161-
end
162-
}, { __index = Observer })
163-
end
164-
self.observers[observer.id] = observer
165-
return observer
166-
end
167-
168-
---@param identifier string | FittenCode.Observer
169-
function Controller:remove_observer(identifier)
170-
local id = type(identifier) == 'string' and identifier or identifier.id
171-
self.observers[id] = nil
159+
-- 进入buffer检测类型或手动更改类型,决定 disabled/enabled
160+
-- current_session.id == source.id { != terminated } running/idle
161+
self.state = StateMachine.new({
162+
initial = 'idle',
163+
transitions = {
164+
idle = { 'running', 'disabled' },
165+
running = { 'idle', 'disabled' },
166+
disabled = { 'idle', 'running' },
167+
},
168+
})
169+
self.state:subscribe(function(value)
170+
self:emit({
171+
ctrl = value.to,
172+
})
173+
end)
172174
end
173175

174-
---@param id string
175-
function Controller:get_observer(id)
176-
return self.observers[id]
176+
function Controller:on(observer)
177+
table.insert(self.observers, observer)
177178
end
178179

179-
---@param event FittenCode.Inline.Event
180-
function Controller:notify_observers(event)
181-
vim.tbl_map(function(observer)
182-
observer:update(self, event)
183-
end, self.observers)
180+
function Controller:off(observer)
181+
for i, obs in ipairs(self.observers) do
182+
if obs == observer then
183+
table.remove(self.observers, i)
184+
break
185+
end
186+
end
184187
end
185188

186-
---@param event FittenCode.Inline.Event
187-
function Controller:_emit(event)
188-
self:notify_observers(event)
189+
function Controller:emit(...)
190+
for _, observer in ipairs(self.observers) do
191+
if type(observer) == 'function' then
192+
observer(...)
193+
elseif observer.update then
194+
observer:update(...)
195+
end
196+
end
189197
end
190198

191199
function Controller:has_completions()
@@ -264,7 +272,7 @@ function Controller:trigger_inline_suggestion(options)
264272
local force = (options.force == nil) and false or options.force
265273

266274
local is_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
267-
local is_enabled = self:is_enabled(buf)
275+
local is_enabled = not self.state:is('disabled')
268276
local has_access_token = api_key_manager:has_fitten_access_token()
269277
local is_filtered_event = options.vimev and vim.tbl_contains(self.filter_events, options.vimev.event)
270278
local is_within_the_line = Config.inline_completion.disable_completion_within_the_line and check_is_within_the_line(position)
@@ -287,33 +295,32 @@ function Controller:trigger_inline_suggestion(options)
287295
id = assert(Fn.generate_short_id(13)),
288296
trigger_inline_suggestion = function(...) self:trigger_inline_suggestion_auto(...) end,
289297
is_outdated = function(s)
290-
if s.id ~= self.current_session.id then
298+
if not self.current_session or s.id ~= self.current_session.id then
291299
Log.debug('latest_session id = {}, other id = {}', self.current_session.id, s.id)
292300
return true
293301
end
294302
return false
295303
end
296304
})
305+
self.current_session:on(function(value)
306+
local id = (self.current_session and not self.current_session:is_terminated()) and self.current_session.id
307+
if not self.state:is('disabled') then
308+
if not id then
309+
self.state:transition('idle')
310+
elseif id == value.id then
311+
self.state:transition('running')
312+
end
313+
end
314+
self:emit({
315+
ctrl = self.state:state(),
316+
current_session_id = id,
317+
session = value
318+
})
319+
end)
297320

298321
return self.current_session:send_completions()
299322
end
300323

301-
-- ---@param data FittenCode.Inline.Event.Data
302-
-- function Controller:on_session_event(data)
303-
-- if data.session_event == SESSION_EVENT.CREATED then
304-
-- self:_emit({ event = CONTROLLER_EVENT.INLINE_RUNNING, data = { id = data.id } })
305-
-- self:_emit({ event = CONTROLLER_EVENT.SESSION_ADDED, data = { id = data.id } })
306-
-- elseif data.session_event == SESSION_EVENT.TERMINATED then
307-
-- Log.debug('Controller received session terminated event, event session id = {}, selected_session_id = {}', data.id, self.selected_session_id)
308-
-- self:_emit({ event = CONTROLLER_EVENT.SESSION_DELETED, data = { id = data.id } })
309-
-- self.sessions[data.id] = nil
310-
-- if not self.selected_session_id or self.selected_session_id == data.id then
311-
-- self.selected_session_id = nil
312-
-- self:_emit({ event = CONTROLLER_EVENT.INLINE_IDLE, data = { id = data.id } })
313-
-- end
314-
-- end
315-
-- end
316-
317324
---@return FittenCode.Inline.Session?
318325
function Controller:get_active_session()
319326
local session = self:get_current_session()
@@ -327,15 +334,30 @@ function Controller:get_current_session()
327334
return self.current_session
328335
end
329336

330-
---@param buf integer
331-
function Controller:is_enabled(buf)
337+
---@param buf integer?
338+
function Controller:should_enable(buf)
339+
if buf == nil then
340+
buf = vim.api.nvim_get_current_buf()
341+
end
332342
local filebuf = true
333343
if Config.inline_completion.disable_completion_when_nofile_buffer then
334344
filebuf = F.is_filebuf(buf)
335345
end
336346
return Config.inline_completion.enable and filebuf and not self:is_ft_disabled(buf)
337347
end
338348

349+
function Controller:sync_state()
350+
if self:should_enable() then
351+
if self.current_session and not self.current_session:is_terminated() then
352+
self.state:transition('running')
353+
else
354+
self.state:transition('idle')
355+
end
356+
else
357+
self.state:transition('disabled')
358+
end
359+
end
360+
339361
---@param msg string
340362
---@param timeout number
341363
---@param timestamp number
@@ -459,11 +481,19 @@ function Controller:set_suffix_permissions(enable, suffixes)
459481
end
460482
end
461483
Config.disable_specific_inline_completion.suffixes = vim.tbl_keys(suffix_map)
484+
self:sync_state()
462485
end
463486

464-
---@return FittenCode.Inline.Status
465487
function Controller:get_status()
466-
return self.status_observer
488+
return self.status_observer:get_snapshot()
489+
-- if self:is_enabled() then
490+
-- if self.current_session and not self.current_session:is_terminated() then
491+
-- return 'running'
492+
-- end
493+
-- return 'idle'
494+
-- else
495+
-- return 'disabled'
496+
-- end
467497
end
468498

469499
return Controller

0 commit comments

Comments
 (0)