summaryrefslogtreecommitdiff
path: root/lua/clever_f/feedback_service.lua
diff options
context:
space:
mode:
authorJackson Moore <jacksonmoore@tuta.io>2026-09-04 18:44:32 +0200
committerJackson Moore <jacksonmoore@tuta.io>2026-09-04 18:44:32 +0200
commitc7b14ffb6d14969c9c41864e827f33f8e80fc24e (patch)
treec7c0ac23dc93a4a8b75449d2be4d0e05c4f05f9e /lua/clever_f/feedback_service.lua
parent9013636a57144e1f57c9339e7888d588430aae6a (diff)
Rename plugin to clever-tee
Diffstat (limited to 'lua/clever_f/feedback_service.lua')
-rw-r--r--lua/clever_f/feedback_service.lua1003
1 files changed, 0 insertions, 1003 deletions
diff --git a/lua/clever_f/feedback_service.lua b/lua/clever_f/feedback_service.lua
deleted file mode 100644
index ca26746..0000000
--- a/lua/clever_f/feedback_service.lua
+++ /dev/null
@@ -1,1003 +0,0 @@
-local domain = require("clever_f.domain")
-local policy = require("clever_f.policy")
-local sequence_state = require("clever_f.sequence_state")
-local state_transitions = require("clever_f.state_transitions")
-local text_topology = require("clever_f.text_topology")
-
-local M = {}
-local FeedbackService = {}
-FeedbackService.__index = FeedbackService
-M.FeedbackService = FeedbackService
-local CursorPresentationLease = {}
-M.CursorPresentationLease = CursorPresentationLease
-
-M.DEFAULT_LABEL_GROUP = "CleverFDefaultLabel"
-M.Priority = {
- HIGH = "high",
- ORDINARY = "ordinary",
-}
-M.FINALIZER_EVENTS = {
- "CursorMoved",
- "InsertEnter",
- "TextChanged",
-}
-M.EAGER_EVENTS = {
- "WinEnter",
- "WinLeave",
- "CmdwinLeave",
-}
-M.FinalizerAction = {
- PRESERVE = "preserve",
- FINALIZE = "finalize",
-}
-M.MigrationReason = {
- LINE_CHANGE = "line_change",
- TILL_DIRECTION_CHANGE = "till_direction_change",
-}
-
-local OVERLAY_PRIORITIES = {
- CleverFCursor = M.Priority.HIGH,
- CleverFChar = M.Priority.HIGH,
- CleverFDirect = M.Priority.ORDINARY,
-}
-
-local service_records = setmetatable({}, { __mode = "k" })
-local cursor_lease_records = setmetatable({}, { __mode = "k" })
-local temporary_release_records = setmetatable({}, { __mode = "k" })
-
-local FEATURE_GROUPS = {
- "CleverFCursor",
- "CleverFChar",
- "CleverFDirect",
-}
-
-local LEGACY_NORMAL_EX_CONTEXTS = {
- cv = true,
- cvr = true,
-}
-
-local DIRECT_FINALIZER_EVENTS = {
- InsertEnter = true,
- TextChanged = true,
-}
-
-local EAGER_EVENT_SET = {}
-for _, event_name in ipairs(M.EAGER_EVENTS) do
- EAGER_EVENT_SET[event_name] = true
-end
-
-local DEFAULT_LABEL_DEFINITION = {
- guifg = "red",
- guibg = "NONE",
- gui = {
- bold = true,
- underline = true,
- },
- ctermfg = "red",
- ctermbg = "NONE",
- cterm = {
- bold = true,
- underline = true,
- },
-}
-
-local function copy(value)
- if type(value) ~= "table" then
- return value
- end
- local result = {}
- for key, item in pairs(value) do
- result[key] = copy(item)
- end
- return result
-end
-
-function M.default_label_definition()
- return copy(DEFAULT_LABEL_DEFINITION)
-end
-
-function M.overlay_priority(group)
- local priority = OVERLAY_PRIORITIES[group]
- if priority == nil then
- error("unknown feedback overlay group '" .. tostring(group) .. "'", 2)
- end
- return priority
-end
-
-local function fail(message, level)
- error(message, (level or 1) + 1)
-end
-
-local function normalize_options(options)
- if type(options) ~= "table" then
- fail("FeedbackService options must be a table", 3)
- end
- if options.host == nil then
- return { host = options }
- end
- return options
-end
-
-local function require_host(host)
- if type(host) ~= "table"
- or type(host.read_highlight_group) ~= "function"
- or type(host.define_highlight_group) ~= "function"
- or type(host.create_highlight) ~= "function"
- or type(host.remove_highlight) ~= "function"
- or type(host.read_buffer) ~= "function"
- or type(host.read_cursor) ~= "function"
- or type(host.read_window) ~= "function"
- or type(host.register_events) ~= "function"
- or type(host.remove_event_registration) ~= "function"
- or type(host.start_timer) ~= "function"
- or type(host.stop_timer) ~= "function"
- or type(host.supports_timers) ~= "function"
- or type(host.supports_cursor_presentation) ~= "function"
- or type(host.suppress_cursor_presentation) ~= "function"
- or type(host.restore_cursor_presentation) ~= "function"
- then
- fail("FeedbackService host must provide highlight groups", 3)
- end
- return host
-end
-
-local function require_transitions(transitions, state)
- transitions = transitions or state_transitions.new(state)
- if type(transitions) ~= "table"
- or type(transitions.AddTemporaryOverlay) ~= "function"
- or type(transitions.RemoveTemporaryOverlay) ~= "function"
- or type(transitions.AddTargetOverlay) ~= "function"
- or type(transitions.ClearTargetOverlays) ~= "function"
- or type(transitions.AddFinalizer) ~= "function"
- or type(transitions.RemoveFinalizer) ~= "function"
- or type(transitions.FullFinalization) ~= "function"
- or type(transitions.ClearTargetFeedback) ~= "function"
- or type(transitions.SetHighlightTimer) ~= "function"
- or type(transitions.ClearHighlightTimer) ~= "function"
- then
- fail("FeedbackService transitions must manage overlay resources", 3)
- end
- return transitions
-end
-
-local function require_policy(service, host)
- service = service or policy.new(host)
- if type(service) ~= "table"
- or type(service.evaluate_highlight_links) ~= "function"
- or type(service.sample_acquisition) ~= "function"
- or type(service.sample_markers) ~= "function"
- or type(service.sample_timeouts) ~= "function"
- or type(service.capture_activation) ~= "function"
- then
- fail("FeedbackService policy must evaluate highlight links", 3)
- end
- return service
-end
-
-function FeedbackService.new(options)
- if FeedbackService.is(options) then
- return options
- end
- options = normalize_options(options)
- local service = setmetatable({}, FeedbackService)
- local host = require_host(options.host)
- local state = options.state or sequence_state.get()
- if not sequence_state.is(state) then
- fail("FeedbackService state must be the plugin-global SequenceState", 2)
- end
- service_records[service] = {
- host = host,
- policy = require_policy(options.policy or options.policy_service, host),
- state = state,
- transitions = require_transitions(
- options.transitions or options.state_transitions,
- state
- ),
- persistent_requests = {},
- owned_finalizer = nil,
- activation = nil,
- eager_registration = nil,
- last_eager_decision = nil,
- }
- return service
-end
-
-function FeedbackService.is(value)
- return type(value) == "table" and service_records[value] ~= nil
-end
-
-local cursor_lease_metatable = {
- __index = function(lease, key)
- local method = CursorPresentationLease[key]
- if method ~= nil then
- return method
- end
- local record = cursor_lease_records[lease]
- if key == "identity" then
- return record.identity
- end
- if key == "active" then
- return record.active
- end
- return nil
- end,
- __newindex = function()
- fail("cursor presentation leases are read-only", 2)
- end,
- __metatable = "clever_f.feedback_service.CursorPresentationLease",
-}
-
-local function new_cursor_presentation_lease(host, suppress)
- local lease = setmetatable({}, cursor_lease_metatable)
- local identity = suppress and host:suppress_cursor_presentation() or nil
- cursor_lease_records[lease] = {
- host = host,
- identity = identity,
- active = identity ~= nil,
- }
- return lease
-end
-
-function CursorPresentationLease.is(value)
- return type(value) == "table" and cursor_lease_records[value] ~= nil
-end
-
-function CursorPresentationLease:release()
- local record = cursor_lease_records[self]
- if record == nil then
- fail("cursor presentation lease is invalid", 2)
- end
- if not record.active then
- return false
- end
- record.active = false
- record.host:restore_cursor_presentation(record.identity)
- return true
-end
-
-function FeedbackService:create_cursor_presentation_lease(enabled)
- local record = service_records[self]
- if enabled == nil then
- enabled = record.policy:sample_acquisition().hide_cursor_on_cmdline
- elseif type(enabled) ~= "boolean" then
- fail("cursor presentation policy must be a Boolean", 2)
- end
- local supported = enabled and record.host:supports_cursor_presentation()
- return new_cursor_presentation_lease(record.host, supported == true)
-end
-
-local function position_list(positions)
- if type(positions) ~= "table" then
- fail("direct marker positions must be a list", 3)
- end
- local result = {}
- local item_count = 0
- for key, position in pairs(positions) do
- if type(key) ~= "number"
- or key ~= math.floor(key)
- or key < 1
- or key > #positions
- then
- fail("direct marker positions must be a list", 3)
- end
- result[key] = domain.Position.coerce(position)
- item_count = item_count + 1
- end
- if item_count ~= #positions then
- fail("direct marker positions must be a list", 3)
- end
- return result
-end
-
-function FeedbackService:create_direct_markers(positions, window)
- positions = position_list(positions)
- if #positions == 0 then
- return nil
- end
- if window == nil then
- fail("direct marker window must identify its host window", 2)
- end
-
- local record = service_records[self]
- local identity = record.host:create_highlight({
- group = "CleverFDirect",
- window = window,
- positions = positions,
- priority = M.overlay_priority("CleverFDirect"),
- })
- record.transitions:AddTemporaryOverlay(identity, window, "CleverFDirect")
- local resource = {
- identity = identity,
- window = window,
- group = "CleverFDirect",
- positions = positions,
- }
- temporary_release_records[resource] = false
- return resource
-end
-
-function FeedbackService:create_cursor_marker(position, window)
- position = domain.Position.coerce(position)
- if window == nil then
- fail("cursor marker window must identify its host window", 2)
- end
-
- local record = service_records[self]
- local identity = record.host:create_highlight({
- group = "CleverFCursor",
- window = window,
- position = position,
- priority = M.overlay_priority("CleverFCursor"),
- })
- record.transitions:AddTemporaryOverlay(identity, window, "CleverFCursor")
- local resource = {
- identity = identity,
- window = window,
- group = "CleverFCursor",
- position = position,
- }
- temporary_release_records[resource] = false
- return resource
-end
-
-function FeedbackService:remove_temporary_overlay(resource)
- if type(resource) ~= "table" or resource.identity == nil then
- fail("temporary overlay resource must identify its highlight", 2)
- end
- if temporary_release_records[resource] == true then
- return false
- end
- temporary_release_records[resource] = true
- local record = service_records[self]
- local ok, removed = pcall(
- record.host.remove_highlight,
- record.host,
- resource.identity
- )
- record.transitions:RemoveTemporaryOverlay(resource.identity, resource.window)
- if not ok then
- error(removed, 0)
- end
- return removed
-end
-
-function M.persistent_context_eligible(context)
- context = domain.ModeContext.from_full_mode(context)
- return context.key == "n"
- or context.visual_kind ~= nil
- or context.select_kind ~= nil
- or LEGACY_NORMAL_EX_CONTEXTS[context.key] == true
-end
-
-function M.persistent_destination(
- view,
- target_position,
- descriptor,
- endpoint_policy
-)
- if not text_topology.TextView.is(view) then
- fail("persistent feedback requires a TextView", 2)
- end
- target_position = domain.Position.coerce(target_position)
- descriptor = domain.Descriptor.from_string(descriptor)
- endpoint_policy = domain.EndpointPolicy.from_string(endpoint_policy)
-
- if endpoint_policy == domain.EndpointPolicy.VISUAL_EXCLUSIVE
- and descriptor.direction == domain.Direction.FORWARD
- then
- if descriptor.family == domain.Family.FIND then
- return view:successor(target_position)
- end
- return target_position
- end
- if descriptor.family == domain.Family.FIND then
- return target_position
- end
- if descriptor.direction == domain.Direction.FORWARD then
- return view:predecessor(target_position)
- end
- return view:successor(target_position)
-end
-
-function M.persistent_match_positions(
- view,
- match_start_line,
- target_plan,
- descriptor,
- endpoint_policy
-)
- if not text_topology.TextView.is(view) then
- fail("persistent feedback requires a TextView", 2)
- end
- if not domain.TargetPlan.is(target_plan) then
- fail("persistent feedback requires a TargetPlan", 2)
- end
- local positions = {}
- local seen = {}
- local candidates = view:iter_line_forward(match_start_line)
- while true do
- local position, character = candidates()
- if position == nil then
- break
- end
- if target_plan:matches(character, position, view) then
- local destination = M.persistent_destination(
- view,
- position,
- descriptor,
- endpoint_policy
- )
- if destination ~= nil then
- local key = tostring(destination.line)
- .. ":"
- .. tostring(destination.byte_column)
- if not seen[key] then
- seen[key] = true
- positions[#positions + 1] = destination
- end
- end
- end
- end
- return positions
-end
-
-function FeedbackService:build_persistent(specification)
- if type(specification) ~= "table" then
- fail("persistent feedback request must be a table", 2)
- end
- local context = domain.ModeContext.from_full_mode(specification.context)
- if not M.persistent_context_eligible(context) then
- fail("persistent feedback request requires an eligible context", 2)
- end
- if not domain.TargetPlan.is(specification.target_plan) then
- fail("persistent feedback request requires a TargetPlan", 2)
- end
- if not domain.ResolvedMotionPlan.is(specification.motion_plan) then
- fail("persistent feedback request requires a ResolvedMotionPlan", 2)
- end
- if specification.motion_plan.target_plan ~= specification.target_plan then
- fail("persistent feedback must reuse the movement TargetPlan", 2)
- end
- local descriptor = domain.Descriptor.from_string(
- specification.descriptor or specification.motion_plan.descriptor
- )
- local endpoint_policy = domain.EndpointPolicy.from_string(
- specification.endpoint_policy or specification.motion_plan.endpoint_policy
- )
- local anchor = domain.Position.coerce(specification.anchor)
- local view = specification.text_view
- or text_topology.from_host(service_records[self].host)
- return {
- context = context,
- anchor = anchor,
- target_plan = specification.target_plan,
- motion_plan = specification.motion_plan,
- descriptor = descriptor,
- endpoint_policy = endpoint_policy,
- anchor_line = anchor.line,
- match_start_line = anchor.line,
- positions = M.persistent_match_positions(
- view,
- anchor.line,
- specification.target_plan,
- descriptor,
- endpoint_policy
- ),
- text_view = view,
- window = specification.window,
- }
-end
-
-local function release_target_overlays(record, resources)
- for _, resource in ipairs(resources) do
- record.host:remove_highlight(resource.identity)
- end
-end
-
-local function remove_target_overlays(record, window)
- local resources = record.transitions:ClearTargetOverlays(window)
- release_target_overlays(record, resources)
- return resources
-end
-
-function FeedbackService:remove_character_overlays(window)
- if window == nil then
- fail("character overlay window must identify its host window", 2)
- end
- return remove_target_overlays(service_records[self], window)
-end
-
-function FeedbackService:cursor_moved_decision()
- local record = service_records[self]
- local context = record.state.last_input_context
- local expected = context and record.state:get_previous_landing(context) or nil
- local actual = record.host:read_cursor()
- return {
- context = context,
- expected = expected,
- actual = actual,
- equal = expected ~= nil and domain.Position.equal(actual, expected),
- }
-end
-
-local function release_finalizers(record, resources)
- for _, resource in ipairs(resources) do
- record.host:remove_event_registration(resource.identity)
- end
-end
-
-local function release_highlight_timer(record, identity)
- if identity == nil then
- return false
- end
- return record.host:stop_timer(identity)
-end
-
-function FeedbackService:release_transition_cleanup(cleanup)
- if type(cleanup) ~= "table" then
- fail("feedback transition cleanup must be a table", 2)
- end
- local record = service_records[self]
- release_finalizers(record, cleanup.finalizers or {})
- release_highlight_timer(record, cleanup.highlight_timer)
- release_target_overlays(record, cleanup.target_overlays or {})
- if #(cleanup.finalizers or {}) > 0 then
- record.owned_finalizer = nil
- end
- return cleanup
-end
-
-function FeedbackService:full_finalize(window)
- local record = service_records[self]
- window = window or record.host:read_window()
- local cleanup = record.transitions:FullFinalization(window)
- self:release_transition_cleanup(cleanup)
- record.owned_finalizer = nil
- return cleanup
-end
-
-function FeedbackService:handle_finalizer_event(name, payload)
- if name == "CursorMoved" then
- local decision = self:cursor_moved_decision()
- if decision.equal then
- decision.action = M.FinalizerAction.PRESERVE
- else
- decision.action = M.FinalizerAction.FINALIZE
- decision.cleanup = self:full_finalize(payload and payload.window)
- end
- return decision
- end
- if DIRECT_FINALIZER_EVENTS[name] then
- return {
- action = M.FinalizerAction.FINALIZE,
- cleanup = self:full_finalize(payload and payload.window),
- }
- end
- return false
-end
-
-local function register_finalizers(service, record)
- local buffer = record.host:read_buffer()
- local owned = record.owned_finalizer
- if owned ~= nil and owned.buffer == buffer then
- return owned
- end
- if owned ~= nil then
- record.host:remove_event_registration(owned.identity)
- record.transitions:RemoveFinalizer(owned.identity, owned.buffer)
- end
-
- local identity = record.host:register_events(
- M.FINALIZER_EVENTS,
- function(name, payload)
- service:handle_finalizer_event(name, payload)
- end,
- { buffer = buffer }
- )
- record.transitions:AddFinalizer(identity, buffer)
- owned = {
- identity = identity,
- buffer = buffer,
- }
- record.owned_finalizer = owned
- return owned
-end
-
-local function materialize_persistent(service, request)
- if request.window == nil then
- fail("persistent feedback window must identify its host window", 3)
- end
- local record = service_records[service]
- remove_target_overlays(record, request.window)
- request.identity = record.host:create_highlight({
- group = "CleverFChar",
- window = request.window,
- positions = request.positions,
- priority = M.overlay_priority("CleverFChar"),
- target_plan = request.target_plan,
- descriptor = request.descriptor,
- endpoint_policy = request.endpoint_policy,
- anchor_line = request.anchor_line,
- match_start_line = request.match_start_line,
- })
- request.group = "CleverFChar"
- request.priority = M.overlay_priority("CleverFChar")
- local owned, ownership_error = pcall(function()
- record.transitions:AddTargetOverlay(
- request.identity,
- request.window,
- request.anchor.line
- )
- request.finalizers = register_finalizers(service, record)
- end)
- if not owned then
- local resources = record.transitions:RemoveTargetOverlay(
- request.identity,
- request.window
- )
- if #resources == 0 then
- resources[1] = {
- identity = request.identity,
- window = request.window,
- }
- end
- pcall(release_target_overlays, record, resources)
- error(ownership_error, 0)
- end
- local requests = record.persistent_requests
- requests[#requests + 1] = request
- return request
-end
-
-function FeedbackService:request_persistent(specification)
- return materialize_persistent(self, self:build_persistent(specification))
-end
-
-function FeedbackService:restore_primary(specification)
- local restoration = self:build_primary_restoration(specification)
- if restoration == nil then
- return nil
- end
- return materialize_persistent(self, restoration)
-end
-
-function FeedbackService:persistent_requests()
- local result = {}
- for index, request in ipairs(service_records[self].persistent_requests) do
- result[index] = request
- end
- return result
-end
-
-function M.repeated_till_migration_candidate(request)
- if type(request) ~= "table" then
- fail("command feedback migration request must be a table", 2)
- end
- local plan = request.resolved_motion_plan or request.plan
- return domain.ResolvedMotionPlan.is(plan)
- and plan.descriptor.family == domain.Family.TILL
- and request.first_move == false
-end
-
-function M.till_direction_changed(request)
- if not M.repeated_till_migration_candidate(request) then
- return false
- end
- if type(request.moved_forward) ~= "boolean"
- or type(request.previous_moved_forward) ~= "boolean"
- then
- fail("TILL feedback migration requires movement directions", 2)
- end
- return request.moved_forward ~= request.previous_moved_forward
-end
-
-function M.command_migration_reason(request)
- if type(request) ~= "table" then
- fail("command feedback migration request must be a table", 2)
- end
- local origin = domain.Position.coerce(request.origin)
- local destination = domain.Position.coerce(request.destination)
- if request.outcome ~= nil and request.outcome.complete ~= true then
- return nil
- end
- if origin.line ~= destination.line then
- return M.MigrationReason.LINE_CHANGE
- end
- if M.till_direction_changed(request) then
- return M.MigrationReason.TILL_DIRECTION_CHANGE
- end
- return nil
-end
-
-local function has_target_overlay(record, window)
- for _, resource in ipairs(record.state.target_overlays) do
- if resource.window == window then
- return true
- end
- end
- return false
-end
-
-function FeedbackService:primary_restoration_active(context, window)
- local record = service_records[self]
- context = domain.ModeContext.from_full_mode(context)
- local mark_char = record.policy:sample_markers().mark_char
- if not mark_char or not M.persistent_context_eligible(context) then
- return false
- end
- window = window or record.host:read_window()
- return not has_target_overlay(record, window)
-end
-
-function FeedbackService:build_primary_restoration(specification)
- if type(specification) ~= "table" then
- fail("primary feedback restoration must be a table", 2)
- end
- local window = specification.window
- or service_records[self].host:read_window()
- if not self:primary_restoration_active(specification.context, window) then
- return nil
- end
- if not domain.TargetPlan.is(specification.target_plan) then
- fail("primary feedback restoration requires an action TargetPlan", 2)
- end
- local action_motion_plan = specification.motion_plan
- local search_scope = specification.search_scope
- or (domain.ResolvedMotionPlan.is(action_motion_plan)
- and action_motion_plan.search_scope)
- or domain.SearchScope.BUFFER
- local motion_plan = domain.ResolvedMotionPlan.new({
- target_plan = specification.target_plan,
- descriptor = specification.stored_descriptor,
- search_scope = search_scope,
- endpoint_policy = specification.endpoint_policy,
- })
- return self:build_persistent({
- context = specification.context,
- anchor = specification.anchor,
- target_plan = specification.target_plan,
- motion_plan = motion_plan,
- descriptor = specification.stored_descriptor,
- endpoint_policy = specification.endpoint_policy,
- text_view = specification.text_view,
- window = window,
- })
-end
-
-function FeedbackService:migrate_command(request)
- local reason = M.command_migration_reason(request)
- local record = service_records[self]
- local window = request.window or record.host:read_window()
- if reason == nil or not has_target_overlay(record, window) then
- return {
- migrated = false,
- reason = reason,
- }
- end
-
- local plan = request.resolved_motion_plan or request.plan
- if not domain.ResolvedMotionPlan.is(plan) then
- fail("command feedback migration requires a ResolvedMotionPlan", 2)
- end
- local overlay = self:request_persistent({
- context = request.context,
- anchor = request.destination,
- target_plan = plan.target_plan,
- motion_plan = plan,
- descriptor = plan.descriptor,
- endpoint_policy = plan.endpoint_policy,
- window = window,
- })
- return {
- migrated = true,
- reason = reason,
- overlay = overlay,
- }
-end
-
-function FeedbackService:highlight_timer_delay()
- local record = service_records[self]
- if not record.policy:sample_markers().mark_char then
- return nil
- end
- local delay = record.policy:sample_timeouts().highlight_timeout_ms
- if delay == 0 or record.host:supports_timers() ~= true then
- return nil
- end
- return delay
-end
-
-function FeedbackService:cancel_highlight_timer()
- local record = service_records[self]
- local identity = record.transitions:ClearHighlightTimer()
- release_highlight_timer(record, identity)
- return identity
-end
-
-function FeedbackService:handle_highlight_timer(callback_identity, window)
- if callback_identity == nil then
- fail("highlight timer callback identity must be present", 2)
- end
- local record = service_records[self]
- if callback_identity ~= record.state.highlight_timer then
- return false
- end
- local _, current = record.transitions:ClearHighlightTimer(callback_identity)
- if not current then
- return false
- end
- self:remove_character_overlays(window or record.host:read_window())
- return true
-end
-
-function FeedbackService:start_highlight_timer(window)
- local record = service_records[self]
- local delay = self:highlight_timer_delay()
- if delay == nil then
- return nil
- end
- window = window or record.host:read_window()
- self:cancel_highlight_timer()
- local identity
- identity = record.host:start_timer(delay, function(callback_identity)
- self:handle_highlight_timer(callback_identity or identity, window)
- end)
- record.transitions:SetHighlightTimer(identity)
- return identity
-end
-
-function FeedbackService:refresh_primary(resolved_target, window)
- if resolved_target == nil then
- return nil
- end
- if not domain.TargetValue.is(resolved_target) then
- fail("primary timer refresh requires a resolved TargetValue", 2)
- end
- return self:start_highlight_timer(window)
-end
-
-function FeedbackService:handle_eager_event(name, payload)
- if not EAGER_EVENT_SET[name] then
- return false
- end
- local record = service_records[self]
- local decision = {
- event = name,
- payload = payload,
- mark_char = record.policy:sample_markers().mark_char,
- cleaned = false,
- }
- if decision.mark_char then
- local window = payload and payload.window or record.host:read_window()
- local cleanup = record.transitions:ClearTargetFeedback(window)
- release_highlight_timer(record, cleanup.highlight_timer)
- release_target_overlays(record, cleanup.target_overlays)
- decision.cleaned = true
- decision.cleanup = cleanup
- end
- record.last_eager_decision = decision
- return decision
-end
-
-function FeedbackService:last_eager_decision()
- local decision = service_records[self].last_eager_decision
- return decision and copy(decision) or nil
-end
-
-function FeedbackService:activate()
- local record = service_records[self]
- if record.activation ~= nil then
- return copy(record.activation)
- end
- local sampled = record.policy:capture_activation()
- local activation = {
- clean_labels_eagerly = sampled.clean_labels_eagerly,
- eager_registration = nil,
- }
- if sampled.clean_labels_eagerly then
- local identity = record.host:register_events(
- M.EAGER_EVENTS,
- function(name, payload)
- self:handle_eager_event(name, payload)
- end,
- { owner = "clever_f", lifecycle = "eager" }
- )
- record.eager_registration = identity
- activation.eager_registration = identity
- end
- record.activation = activation
- return copy(activation)
-end
-
-function FeedbackService:evaluate_feature_links()
- local record = service_records[self]
- local rules = record.policy:evaluate_highlight_links()
- local results = {}
- for _, group in ipairs(FEATURE_GROUPS) do
- local rule = rules[group]
- if rule.enabled then
- if rule.configured_target ~= nil then
- record.host:define_highlight_group(
- group,
- { link = rule.configured_target },
- { force = true }
- )
- results[group] = {
- group = group,
- target = rule.configured_target,
- source = "configured",
- applied = true,
- }
- else
- local existing = record.host:read_highlight_group(group)
- if existing ~= nil then
- results[group] = {
- group = group,
- definition = existing,
- source = "colorscheme",
- applied = false,
- }
- else
- record.host:define_highlight_group(
- group,
- { link = rule.target },
- { default = true }
- )
- results[group] = {
- group = group,
- target = rule.target,
- source = "fallback",
- applied = true,
- }
- end
- end
- end
- end
- return results
-end
-
-function FeedbackService:ensure_default_label()
- local existing = service_records[self].host:read_highlight_group(
- M.DEFAULT_LABEL_GROUP
- )
- if existing ~= nil then
- return {
- group = M.DEFAULT_LABEL_GROUP,
- definition = existing,
- source = "colorscheme",
- applied = false,
- }
- end
-
- local definition = M.default_label_definition()
- service_records[self].host:define_highlight_group(
- M.DEFAULT_LABEL_GROUP,
- definition,
- { default = true }
- )
- return {
- group = M.DEFAULT_LABEL_GROUP,
- definition = definition,
- source = "fallback",
- applied = true,
- }
-end
-
-function FeedbackService:evaluate_highlights()
- return {
- default_label = self:ensure_default_label(),
- feature_links = self:evaluate_feature_links(),
- }
-end
-
-function M.new(options)
- return FeedbackService.new(options)
-end
-
-setmetatable(M, {
- __call = function(_, options)
- return FeedbackService.new(options)
- end,
-})
-
-return M