local case_policy = require("clever_f.case_policy") local domain = require("clever_f.domain") local text_topology = require("clever_f.text_topology") local M = {} local DirectPreviewPlanner = {} DirectPreviewPlanner.__index = DirectPreviewPlanner M.DirectPreviewPlanner = DirectPreviewPlanner local planner_records = setmetatable({}, { __mode = "k" }) local function fail(message, level) error(message, (level or 1) + 1) end local function require_view(view) if not text_topology.TextView.is(view) then fail("direct preview text must be a TextView", 3) end return view end local function require_case_resolver(options) options = options or {} if type(options) == "function" then options = { lowercase = options } end if type(options) ~= "table" then fail("DirectPreviewPlanner options must be a table", 3) end local resolver = options.case_resolver if resolver == nil then resolver = case_policy.new({ lowercase = options.lowercase }) end if type(resolver) ~= "table" or type(resolver.lowercase) ~= "function" then fail("DirectPreviewPlanner case resolver must provide lowercase", 3) end return resolver end function DirectPreviewPlanner.new(options) if DirectPreviewPlanner.is(options) then return options end local planner = setmetatable({}, DirectPreviewPlanner) planner_records[planner] = { case_resolver = require_case_resolver(options), } return planner end function DirectPreviewPlanner.is(value) return type(value) == "table" and planner_records[value] ~= nil end function DirectPreviewPlanner:scan_current_line(view, origin, direction) require_view(view) origin = domain.Position.coerce(origin) direction = domain.Direction.from_string(direction) local records = {} for position, character, span in view:iter_strict( origin, direction, domain.SearchScope.CURRENT_LINE ) do records[#records + 1] = { position = position, character = character, span = span, } end return records end function M.normalize_count(count) return domain.Count.to_number(count) end function DirectPreviewPlanner:normalize_count(count) return M.normalize_count(count) end function M.direction_for_descriptor(descriptor) descriptor = domain.Descriptor.from_string(descriptor) if domain.Descriptor.is_uppercase(descriptor) then return domain.Direction.BACKWARD end return domain.Direction.FORWARD end function DirectPreviewPlanner:scan_for_descriptor(view, origin, descriptor) return self:scan_current_line( view, origin, M.direction_for_descriptor(descriptor) ) end local function increment_counter(counters, character) local value = (counters[character] or 0) + 1 counters[character] = value return value end local function is_upper_ascii(character) if type(character) ~= "string" or #character ~= 1 then return false end local code = character:byte(1) return code >= string.byte("A") and code <= string.byte("Z") end local function increment_smart_counters(counters, character) local exact_count = increment_counter(counters, character) local lowercase_count if is_upper_ascii(character) then local lowercase = string.char(character:byte(1) + 32) lowercase_count = increment_counter(counters, lowercase) end return exact_count, lowercase_count end M.is_upper_ascii = is_upper_ascii local function require_case_setting(settings, name) local value = settings[name] if value == nil then return false end if type(value) ~= "boolean" then fail("direct preview " .. name .. " must be a Boolean", 3) end return value end function DirectPreviewPlanner:plan(view, origin, descriptor, count, settings) settings = settings or {} if type(settings) ~= "table" then fail("direct preview case settings must be a table", 2) end local ignore_case = require_case_setting(settings, "ignore_case") local smart_case = require_case_setting(settings, "smart_case") local selected_occurrence = self:normalize_count(count) local counters = {} local positions = {} local resolver = planner_records[self].case_resolver for _, record in ipairs(self:scan_for_descriptor(view, origin, descriptor)) do local selected if ignore_case then selected = increment_counter( counters, resolver:lowercase(record.character) ) == selected_occurrence elseif smart_case then selected = increment_smart_counters( counters, record.character ) == selected_occurrence else selected = increment_counter( counters, record.character ) == selected_occurrence end if selected then positions[#positions + 1] = record.position end end return positions end function M.new(options) return DirectPreviewPlanner.new(options) end function M.scan_current_line(view, origin, direction) return DirectPreviewPlanner.new():scan_current_line(view, origin, direction) end function M.scan_for_descriptor(view, origin, descriptor) return DirectPreviewPlanner.new():scan_for_descriptor(view, origin, descriptor) end function M.plan(view, origin, descriptor, count, settings, options) return DirectPreviewPlanner.new(options):plan( view, origin, descriptor, count, settings ) end setmetatable(M, { __call = function(_, options) return DirectPreviewPlanner.new(options) end, }) return M