local util = require("luasnip.util.util")
local ext_util = require("luasnip.util.ext_opts")
local types = require("luasnip.util.types")
local key_indexer = require("luasnip.nodes.key_indexer")
local session = require("luasnip.session")
local feedkeys = require("luasnip.util.feedkeys")

local function subsnip_init_children(parent, children)
	for _, child in ipairs(children) do
		if child.type == types.snippetNode then
			child.snippet = parent.snippet
			child:resolve_child_ext_opts()
		end
		child:resolve_node_ext_opts()
		child:subsnip_init()
	end
end

local function init_child_positions_func(
	key,
	node_children_key,
	child_func_name
)
	-- maybe via load()?
	return function(node, position_so_far)
		node[key] = vim.deepcopy(position_so_far)
		local pos_depth = #position_so_far + 1

		for indx, child in ipairs(node[node_children_key]) do
			position_so_far[pos_depth] = indx
			child[child_func_name](child, position_so_far)
		end
		-- undo changes to position_so_far.
		position_so_far[pos_depth] = nil
	end
end

local function make_args_absolute(args, parent_insert_position, target)
	for i, arg in ipairs(args) do
		if type(arg) == "number" then
			-- the arg is a number, should be interpreted relative to direct
			-- parent.
			local t = vim.deepcopy(parent_insert_position)
			table.insert(t, arg)
			target[i] = { absolute_insert_position = t }
		else
			-- insert node, absolute_indexer, or key itself, node's
			-- absolute_insert_position may be nil, check for that during
			-- usage.
			target[i] = arg
		end
	end
end

local function wrap_args(args)
	-- stylua: ignore
	if type(args) ~= "table" or
	  (type(args) == "table" and args.absolute_insert_position) or
	  key_indexer.is_key(args) then
		-- args is one single arg, wrap it.
		return { args }
	else
		return args
	end
end

-- includes child, does not include parent.
local function get_nodes_between(parent, child)
	local nodes = {}

	-- special case for nodes without absolute_position (which is only
	-- start_node).
	if child.pos == -1 then
		-- no nodes between, only child.
		nodes[1] = child
		return nodes
	end

	local child_pos = child.absolute_position

	local indx = #parent.absolute_position + 1
	local prev = parent
	while child_pos[indx] do
		local next = prev:resolve_position(child_pos[indx])
		nodes[#nodes + 1] = next
		prev = next
		indx = indx + 1
	end

	return nodes
end

-- assumes that children of child are not even active.
-- If they should also be left, do that separately.
-- Does not leave the parent.
local function leave_nodes_between(parent, child, no_move)
	local nodes = get_nodes_between(parent, child)
	if #nodes == 0 then
		return
	end

	-- reverse order, leave child first.
	for i = #nodes, 2, -1 do
		-- this only happens for nodes where the parent will also be left
		-- entirely (because we stop at nodes[2], and handle nodes[1]
		-- separately)
		nodes[i]:input_leave(no_move)
		nodes[i - 1]:input_leave_children()
	end
	nodes[1]:input_leave(no_move)
end

local function enter_nodes_between(parent, child, no_move)
	local nodes = get_nodes_between(parent, child)
	if #nodes == 0 then
		return
	end

	for i = 1, #nodes - 1 do
		-- only enter children for nodes before the last (lowest) one.
		nodes[i]:input_enter(no_move)
		nodes[i]:input_enter_children()
	end
	nodes[#nodes]:input_enter(no_move)
end

local function select_node(node)
	local node_begin, node_end = node.mark:pos_begin_end_raw()
	feedkeys.select_range(node_begin, node_end)
end

local function print_dict(dict)
	print(vim.inspect(dict, {
		process = function(item, path)
			if path[#path] == "node" or path[#path] == "dependent" then
				return "node@" .. vim.inspect(item.absolute_position)
			elseif path[#path] ~= vim.inspect.METATABLE then
				return item
			end
		end,
	}))
end

local function init_node_opts(opts)
	local in_node = {}
	if not opts then
		opts = {}
	end

	-- copy once here, the opts might be reused.
	in_node.node_ext_opts =
		ext_util.complete(vim.deepcopy(opts.node_ext_opts or {}))

	if opts.merge_node_ext_opts == nil then
		in_node.merge_node_ext_opts = true
	else
		in_node.merge_node_ext_opts = opts.merge_node_ext_opts
	end

	in_node.key = opts.key

	in_node.node_callbacks = opts.node_callbacks or {}

	return in_node
end

local function snippet_extend_context(arg, extend)
	if type(arg) == "string" then
		arg = { trig = arg }
	end

	-- both are table or nil now.
	return vim.tbl_extend("keep", arg or {}, extend or {})
end

local function wrap_context(context)
	if type(context) == "string" then
		return { trig = context }
	else
		return context
	end
end

local function linkable_node(node)
	-- node.type has to be one of insertNode, exitNode.
	return vim.tbl_contains(
		{ types.insertNode, types.exitNode },
		rawget(node, "type")
	)
end

-- mainly used internally, by binarysearch_pos.
-- these are the nodes that are definitely not linkable, there are nodes like
-- dynamicNode or snippetNode that might be linkable, depending on their
-- content. Could look into that to make this more complete, but that does not
-- feel appropriate (higher runtime), most cases should be served well by this
-- heuristic.
local function non_linkable_node(node)
	return vim.tbl_contains(
		{ types.textNode, types.functionNode },
		rawget(node, "type")
	)
end
-- return whether a node is certainly (not) interactive.
-- Coincindentially, the same nodes as (non-)linkable ones, but since there is a
-- semantic difference, use separate names.
local interactive_node = linkable_node
local non_interactive_node = non_linkable_node

local function prefer_nodes(prefer_func, reject_func)
	return function(cmp_mid_to, cmp_mid_from, mid_node)
		local reject_mid = reject_func(mid_node)
		local prefer_mid = prefer_func(mid_node)

		-- if we can choose which node to continue in, prefer the one that
		-- may be linkable/interactive.
		if cmp_mid_to == 0 and reject_mid then
			return true, false
		elseif cmp_mid_from == 0 and reject_mid then
			return false, true
		elseif (cmp_mid_to == 0 or cmp_mid_from == 0) and prefer_mid then
			return false, false
		else
			return cmp_mid_to >= 0, cmp_mid_from < 0
		end
	end
end

-- functions for resolving conflicts, if `pos` is on the boundary of two nodes.
-- Return whether to continue behind or before mid (in that order).
-- At most one of those may be true, of course.
local binarysearch_preference = {
	outside = function(cmp_mid_to, cmp_mid_from, _)
		return cmp_mid_to >= 0, cmp_mid_from <= 0
	end,
	inside = function(cmp_mid_to, cmp_mid_from, _)
		return cmp_mid_to > 0, cmp_mid_from < 0
	end,
	linkable = prefer_nodes(linkable_node, non_linkable_node),
	interactive = prefer_nodes(interactive_node, non_interactive_node),
}
-- `nodes` is a list of nodes ordered by their occurrence in the buffer.
-- `pos` is a row-column-tuble, byte-columns, and we return the node the LEFT
-- EDGE(/side) of `pos` is inside.
-- This convention is chosen since a snippet inserted at `pos` will move the
-- character at `pos` to the right.
-- The exact meaning of "inside" can be influenced with `respect_rgravs` and
-- `boundary_resolve_mode`:
-- * if `respect_rgravs` is true, "inside" emulates the shifting-behaviour of
--   extmarks:
--   First of all, we compare the left edge of `pos` with the left/right edges
--   of from/to, depending on rgrav.
--   If the left edge is <= left/right edge of from, and < left/right edge of
--   to, `pos` is inside the node.
--
-- * if `respect_rgravs` is false, pos has to be fully inside a node to be
--   considered inside it. If pos is on the left endpoint, it is considered to be
--   left of the node, and likewise for the right endpoint.
--
-- * `boundary_resolve_mode` changes how a position on the boundary of a node
-- is treated:
-- * for `"prefer_linkable/interactive"`, we assume that the nodes in `nodes` are
-- contiguous, and prefer falling into the previous/next node if `pos` is on
-- mid's boundary, and mid is not linkable/interactie.
-- This way, we are more likely to return a node that can handle a new
-- snippet/is interactive.
-- * `"prefer_outside"` makes sense when the nodes are not contiguous, and we'd
-- like to find a position between two nodes.
-- This mode makes sense for finding the snippet a new snippet should be
-- inserted in, since we'd like to prefer inserting before/after a snippet, if
-- the position is ambiguous.
--
-- In general:
-- These options are useful for making this function more general: When
-- searching in the contiguous nodes of a snippet, we'd like this routine to
-- return any of them (obviously the one pos is inside/or on the border of, and
-- we'd like to prefer returning a node that can be linked), but in no case
-- fail.
-- However! when searching the top-level snippets with the intention of finding
-- the snippet/node a new snippet should be expanded inside, it seems better to
-- shift an existing snippet to the right/left than expand the new snippet
-- inside it (when the expand-point is on the boundary).
local function binarysearch_pos(
	nodes,
	pos,
	respect_rgravs,
	boundary_resolve_mode
)
	local left = 1
	local right = #nodes

	-- actual search-routine from
	-- https://github.com/Roblox/Wiki-Lua-Libraries/blob/master/StandardLibraries/BinarySearch.lua
	if #nodes == 0 then
		return nil, 1
	end
	while true do
		local mid = left + math.floor((right - left) / 2)
		local mid_mark = nodes[mid].mark
		local ok, mid_from, mid_to = pcall(mid_mark.pos_begin_end_raw, mid_mark)

		if not ok then
			-- error while running this procedure!
			-- return false (because I don't know how to do this with `error`
			-- and the offending node).
			-- (returning data instead of a message in `error` seems weird..)
			return false, mid
		end

		if respect_rgravs then
			-- if rgrav is set on either endpoint, the node considers its
			-- endpoint to be the right, not the left edge.
			-- We only want to work with left edges but since the right edge is
			-- the left edge of the next column, this is not an issue :)
			-- TODO: does this fail with multibyte characters???
			if mid_mark:get_rgrav(-1) then
				mid_from[2] = mid_from[2] + 1
			end
			if mid_mark:get_rgrav(1) then
				mid_to[2] = mid_to[2] + 1
			end
		end

		local cmp_mid_to = util.pos_cmp(pos, mid_to)
		local cmp_mid_from = util.pos_cmp(pos, mid_from)

		local cont_behind_mid, cont_before_mid =
			boundary_resolve_mode(cmp_mid_to, cmp_mid_from, nodes[mid])

		if cont_behind_mid then
			-- make sure right-left becomes smaller.
			left = mid + 1
			if left > right then
				return nil, mid + 1
			end
		elseif cont_before_mid then
			-- continue search on left side
			right = mid - 1
			if left > right then
				return nil, mid
			end
		else
			-- greater-equal than mid_from, smaller or equal to mid_to => left edge
			-- of pos is inside nodes[mid] :)
			return nodes[mid], mid
		end
	end
end

-- a and b have to be in the same snippet, return their first (as seen from
-- them) common parent.
local function first_common_node(a, b)
	local a_pos = a.absolute_position
	local b_pos = b.absolute_position

	-- last as seen from root.
	local i = 0
	local last_common = a.parent.snippet
	-- invariant: last_common is parent of both a and b.
	while (a_pos[i + 1] ~= nil) and a_pos[i + 1] == b_pos[i + 1] do
		last_common = last_common:resolve_position(a_pos[i + 1])
		i = i + 1
	end

	return last_common
end

-- roots at depth 0, children of root at depth 1, their children at 2, ...
local function snippettree_depth(snippet)
	local depth = 0
	while snippet.parent_node ~= nil do
		snippet = snippet.parent_node.parent.snippet
		depth = depth + 1
	end
	return depth
end

-- find the first common snippet a and b have on their respective unique paths
-- to the snippet-roots.
-- if no common ancestor exists (ie. a and b are roots of their buffers'
-- forest, or just in different trees), return nil.
-- in both cases, the paths themselves are returned as well.
-- The common ancestor is included in the paths, except if there is none.
-- Instead of storing the snippets in the paths, they are represented by the
-- node which contains the next-lower snippet in the path (or `from`/`to`, if it's
-- the first node of the path)
-- This is a bit complicated, but this representation contains more information
-- (or, more easily accessible information) than storing snippets: the
-- immediate parent of the child along the path cannot be easily retrieved if
-- the snippet is stored, but the snippet can be easily retrieved if the child
-- is stored (.parent.snippet).
-- And, so far this is pretty specific to refocus, and thus modeled so there is
-- very little additional work in that method.
-- At most one of a,b may be nil.
local function first_common_snippet_ancestor_path(a, b)
	local a_path = {}
	local b_path = {}

	-- general idea: we find the depth of a and b, walk upward with the deeper
	-- one until we find its first ancestor with the same depth as the less
	-- deep snippet, and then follow both paths until they arrive at the same
	-- snippet (or at the root of their respective trees).
	-- if either is nil, we treat it like it's one of the roots (the code will
	-- behave correctly this way, and return an empty path for the nil-node,
	-- and the correct path for the non-nil one).
	local a_depth = a ~= nil and snippettree_depth(a) or 0
	local b_depth = b ~= nil and snippettree_depth(b) or 0

	-- bit subtle: both could be 0, but one could be nil.
	-- deeper should not be nil! (this allows us to do the whole walk for the
	-- non-nil node in the first for-loop, as opposed to needing some special
	-- handling).
	local deeper, deeper_path, other, other_path
	if b == nil or (a ~= nil and a_depth > b_depth) then
		deeper = a
		other = b
		deeper_path = a_path
		other_path = b_path
	else
		-- we land here if `b ~= nil and (a == nil or a_depth >= b_depth)`, so
		-- exactly what we want.
		deeper = b
		other = a
		deeper_path = b_path
		other_path = a_path
	end

	for _ = 1, math.abs(a_depth - b_depth) do
		table.insert(deeper_path, deeper.parent_node)
		deeper = deeper.parent_node.parent.snippet
	end
	-- here: deeper and other are at the same depth.
	-- If we walk upwards one step at a time, they will meet at the same
	-- parent, or hit their respective roots.

	-- deeper can't be nil, if other is, we are done here and can return the
	-- paths (and there is no shared node)
	if other == nil then
		return nil, a_path, b_path
	end
	-- beyond here, deeper and other are not nil.

	while deeper ~= other do
		if deeper.parent_node == nil then
			-- deeper is at depth 0 => other as well => both are roots.
			return nil, a_path, b_path
		end

		table.insert(deeper_path, deeper.parent_node)
		table.insert(other_path, other.parent_node)

		-- walk one step towards root.
		deeper = deeper.parent_node.parent.snippet
		other = other.parent_node.parent.snippet
	end

	-- either one will do here.
	return deeper, a_path, b_path
end

-- removes focus from `from` and upwards up to the first common ancestor
-- (node!) of `from` and `to`, and then focuses nodes between that ancestor and
-- `to`.
-- Requires that `from` is currently entered/focused, and that no snippet
-- between `to` and its root is invalid.
local function refocus(from, to)
	if from == nil and to == nil then
		-- absolutely nothing to do, should not happen.
		return
	end
	-- pass nil if from/to is nil.
	-- if either is nil, first_common_node is nil, and the corresponding list empty.
	local first_common_snippet, from_snip_path, to_snip_path =
		first_common_snippet_ancestor_path(
			from and from.parent.snippet,
			to and to.parent.snippet
		)

	-- we want leave/enter_path to be s.t. leaving/entering all nodes between
	-- each entry and its snippet (and the snippet itself) will leave/enter all
	-- nodes between the first common snippet (or the root-snippet) and
	-- from/to.
	-- Then, the nodes between the first common node and the respective
	-- entrypoints (also nodes) into the first common snippet will have to be
	-- left/entered, which is handled by final_leave_/first_enter_/common_node.

	-- from, to are not yet in the paths.
	table.insert(from_snip_path, 1, from)
	table.insert(to_snip_path, 1, to)

	-- determine how far to leave: if there is a common snippet, only up to the
	-- first common node of from and to, otherwise leave the one snippet, and
	-- enter the other completely.
	local final_leave_node, first_enter_node, common_node
	if first_common_snippet then
		-- there is a common snippet => there is a common node => we have to
		-- set final_leave_node, first_enter_node, and common_node.
		final_leave_node = from_snip_path[#from_snip_path]
		first_enter_node = to_snip_path[#to_snip_path]
		common_node = first_common_node(first_enter_node, final_leave_node)

		-- Also remove these last nodes from the lists, their snippet is not
		-- supposed to be left entirely.
		from_snip_path[#from_snip_path] = nil
		to_snip_path[#to_snip_path] = nil
	end

	-- now do leave/enter, set no_move on all operations.
	-- if one of from/to was nil, there are no leave/enter-operations done for
	-- it (from/to_snip_path is {}, final_leave/first_enter_* is nil).

	-- leave_children on all from-nodes except the original from.
	if #from_snip_path > 0 then
		local ok1, ok2
		if from.type == types.exitNode then
			ok1 = pcall(from.input_leave, from, true)
			ok2 = true
		else
			-- we know that the first node is from.
			ok1 = pcall(leave_nodes_between, from.parent.snippet, from, true)
			-- leave_nodes_between does not affect snippet, so that has to be left
			-- here.
			-- snippet does not have input_leave_children, so only input_leave
			-- needs to be called.
			ok2 = pcall(
				from.parent.snippet.input_leave,
				from.parent.snippet,
				true
			)
		end
		if not ok1 or not ok2 then
			from.parent.snippet:remove_from_jumplist()
		end
	end
	for i = 2, #from_snip_path do
		local node = from_snip_path[i]
		local ok1, ok2, ok3
		ok1 = pcall(node.input_leave_children, node)

		if node.type == types.exitNode then
			ok2 = pcall(node.input_leave, node, true)
			ok3 = true
		else
			ok2 = pcall(leave_nodes_between, node.parent.snippet, node, true)
			ok3 = pcall(
				node.parent.snippet.input_leave,
				node.parent.snippet,
				true
			)
		end

		if not ok1 or not ok2 or not ok3 then
			from.parent.snippet:remove_from_jumplist()
		end
	end

	-- this leave, and the following enters should be safe: the path to `to`
	-- was verified via extmarks_valid (precondition).
	if common_node and final_leave_node then
		-- if the final_leave_node is from, its children are not active (which
		-- stems from the requirement that from is the currently active node),
		-- and so don't have to be left.
		if final_leave_node ~= from then
			final_leave_node:input_leave_children()
		end
		leave_nodes_between(common_node, final_leave_node, true)
	end

	if common_node and first_enter_node then
		-- In general we assume that common_node is active when we are here.
		-- This may not be the case if we are currently inside the i(0) or
		-- i(-1), since the snippet might be the common node and in this case,
		-- it is inactive.
		-- This means that, if we want to enter a non-exitNode, we have to
		-- explicitly activate the snippet for all jumps to behave correctly.
		-- (if we enter a i(0)/i(-1), this is not necessary, of course).
		if
			final_leave_node.type == types.exitNode
			and first_enter_node.type ~= types.exitNode
		then
			common_node:input_enter(true)
		end
		-- symmetrically, entering an i(0)/i(-1) requires leaving the snippet.
		if
			final_leave_node.type ~= types.exitNode
			and first_enter_node.type == types.exitNode
		then
			common_node:input_leave(true)
		end

		enter_nodes_between(common_node, first_enter_node, true)

		-- if the `first_enter_node` is already `to` (occurs if `to` is in the
		-- common snippet of to and from), we should not enter its children.
		-- (we only want to `input_enter` to.)
		if first_enter_node ~= to then
			first_enter_node:input_enter_children()
		end
	end

	-- same here, input_enter_children has to be called manually for the
	-- to-nodes of the path we are entering (since enter_nodes_between does not
	-- call it for the child-node).

	for i = #to_snip_path, 2, -1 do
		local node = to_snip_path[i]
		if node.type ~= types.exitNode then
			node.parent.snippet:input_enter(true)
			enter_nodes_between(node.parent.snippet, node, true)
		else
			node:input_enter(true)
		end
		node:input_enter_children()
	end
	if #to_snip_path > 0 then
		if to.type ~= types.exitNode then
			to.parent.snippet:input_enter(true)
		else
			to.parent.snippet:input_leave(true)
		end
		enter_nodes_between(to.parent.snippet, to, true)
	end

	-- it may be that we only leave nodes in this process (happens if to is a
	-- parent of from).
	-- If that is the case, we will not explicitly focus on to, and it may be
	-- that focus is even lost if it was focused previously (leave may trigger
	-- update, update may change focus)
	-- To prevent this, just call focus here, which is pretty close to a noop
	-- if to is already focused.
	if to then
		to:focus()
	end
end

local function generic_extmarks_valid(node, child)
	-- valid if
	-- - extmark-extents match.
	-- - current choice is valid
	local ok1, self_from, self_to =
		pcall(node.mark.pos_begin_end_raw, node.mark)
	local ok2, child_from, child_to =
		pcall(child.mark.pos_begin_end_raw, child.mark)

	if
		not ok1
		or not ok2
		or util.pos_cmp(self_from, child_from) ~= 0
		or util.pos_cmp(self_to, child_to) ~= 0
	then
		return false
	end
	return child:extmarks_valid()
end

-- returns: * the smallest known snippet `pos` is inside.
--          * the list of other snippets inside the snippet of this smallest
--            node
--          * the index this snippet would be at if inserted into that list
--          * the node of this snippet pos is on.
local function snippettree_find_undamaged_node(pos, opts)
	local prev_parent, child_indx, found_parent
	local prev_parent_children =
		session.snippet_roots[vim.api.nvim_get_current_buf()]

	while true do
		-- false: don't respect rgravs.
		-- Prefer inserting the snippet outside an existing one.
		found_parent, child_indx = binarysearch_pos(
			prev_parent_children,
			pos,
			opts.tree_respect_rgravs,
			opts.tree_preference
		)
		if found_parent == false then
			-- if the procedure returns false, there was an error getting the
			-- position of a node (in this case, that node is a snippet).
			-- The position of the offending snippet is returned in child_indx,
			-- and we can remove it here.
			prev_parent_children[child_indx]:remove_from_jumplist()
		elseif found_parent ~= nil and not found_parent:extmarks_valid() then
			-- found snippet damaged (the idea to sidestep the damaged snippet,
			-- even if no error occurred _right now_, is to ensure that we can
			-- input_enter all the nodes along the insertion-path correctly).
			found_parent:remove_from_jumplist()
			-- continue again with same parent, but one less snippet in its
			-- children => shouldn't cause endless loop.
		elseif found_parent == nil then
			break
		else
			prev_parent = found_parent
			-- can index prev_parent, since found_parent is not nil, and
			-- assigned to prev_parent.
			prev_parent_children = prev_parent.child_snippets
		end
	end

	local node
	if prev_parent then
		-- if found, find node to insert at, prefer receiving a linkable node.
		node = prev_parent:node_at(pos, opts.snippet_mode)
	end

	return prev_parent, prev_parent_children, child_indx, node
end

local function root_path(node)
	local path = {}

	while node do
		local node_snippet = node.parent.snippet
		local snippet_node_path = get_nodes_between(node_snippet, node)
		-- get_nodes_between gives parent -> node, but we need
		-- node -> parent => insert back to front.
		for i = #snippet_node_path, 1, -1 do
			table.insert(path, snippet_node_path[i])
		end
		-- parent not in get_nodes_between.
		table.insert(path, node_snippet)

		node = node_snippet.parent_node
	end

	return path
end

-- adjust rgravs of siblings of the node with indx child_from_indx in nodes.
local function nodelist_adjust_rgravs(
	nodes,
	child_from_indx,
	child_endpoint,
	direction,
	rgrav,
	nodes_adjacent
)
	-- only handle siblings, not the node with child_from_indx itself.
	local i = child_from_indx
	local node = nodes[i]
	while node do
		local direction_node_endpoint = node.mark:get_endpoint(direction)
		if util.pos_equal(direction_node_endpoint, child_endpoint) then
			-- both endpoints of node are on top of child_endpoint (we wouldn't
			-- be in the loop with `node` if the -direction-endpoint didn't
			-- match), so update rgravs of the entire subtree to match rgrav
			node:subtree_set_rgrav(rgrav)
		else
			-- either assume that they are adjacent, or check.
			if
				nodes_adjacent
				or util.pos_equal(
					node.mark:get_endpoint(-direction),
					child_endpoint
				)
			then
				-- only the -direction-endpoint matches child_endpoint, adjust its
				-- position and break the loop (don't need to look at any other
				-- siblings).
				node:subtree_set_pos_rgrav(child_endpoint, direction, rgrav)
			end
			break
		end

		i = i + direction
		node = nodes[i]
	end
end

return {
	subsnip_init_children = subsnip_init_children,
	init_child_positions_func = init_child_positions_func,
	make_args_absolute = make_args_absolute,
	wrap_args = wrap_args,
	wrap_context = wrap_context,
	get_nodes_between = get_nodes_between,
	leave_nodes_between = leave_nodes_between,
	enter_nodes_between = enter_nodes_between,
	select_node = select_node,
	print_dict = print_dict,
	init_node_opts = init_node_opts,
	snippet_extend_context = snippet_extend_context,
	linkable_node = linkable_node,
	binarysearch_pos = binarysearch_pos,
	binarysearch_preference = binarysearch_preference,
	refocus = refocus,
	generic_extmarks_valid = generic_extmarks_valid,
	snippettree_find_undamaged_node = snippettree_find_undamaged_node,
	interactive_node = interactive_node,
	root_path = root_path,
	nodelist_adjust_rgravs = nodelist_adjust_rgravs,
}
