diff options
Diffstat (limited to 'lua/clever_tee/direct_preview_planner.lua')
| -rw-r--r-- | lua/clever_tee/direct_preview_planner.lua | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/lua/clever_tee/direct_preview_planner.lua b/lua/clever_tee/direct_preview_planner.lua new file mode 100644 index 0000000..699fcf4 --- /dev/null +++ b/lua/clever_tee/direct_preview_planner.lua @@ -0,0 +1,250 @@ +local case_policy = require("clever_tee.case_policy") +local domain = require("clever_tee.domain") +local text_topology = require("clever_tee.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.marker_position(descriptor, target_position) + domain.Descriptor.from_string(descriptor) + return domain.Position.coerce(target_position) +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 M.case_grouping_settings(settings) + settings = settings or {} + if type(settings) ~= "table" then + fail("direct preview case settings must be a table", 2) + end + return { + ignore_case = require_case_setting(settings, "ignore_case"), + smart_case = require_case_setting(settings, "smart_case"), + } +end + +function M.validate_marker_positions(view, positions) + require_view(view) + if type(positions) ~= "table" then + fail("direct preview markers must be a list of positions", 2) + end + + local result = {} + local seen = {} + 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 preview markers must be a list of positions", 2) + end + position = domain.Position.coerce(position) + if not view:is_character_start(position) then + fail("direct preview marker must start an editor character", 2) + end + local identity = tostring(position.line) .. ":" .. tostring(position.byte_column) + if seen[identity] then + fail("direct preview marker positions must be unique", 2) + end + seen[identity] = true + result[key] = position + item_count = item_count + 1 + end + if item_count ~= #positions then + fail("direct preview markers must be a list of positions", 2) + end + return result +end + +function DirectPreviewPlanner:plan(view, origin, descriptor, count, settings) + local grouping = M.case_grouping_settings(settings) + local ignore_case = grouping.ignore_case + local smart_case = grouping.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 + local exact_count, lowercase_count = increment_smart_counters( + counters, + record.character + ) + selected = exact_count == selected_occurrence + or lowercase_count == selected_occurrence + else + selected = increment_counter( + counters, + record.character + ) == selected_occurrence + end + if selected then + positions[#positions + 1] = M.marker_position(descriptor, record.position) + end + end + return M.validate_marker_positions(view, 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 |
