Skip to content

Anchor match

anchor_match

Single owner of how a replace anchor is located in a file.

Both the engine (which applies edits) and the preflight (which predicts the engine's verdict) resolve anchors through :func:resolve_anchor, so the preflight can never block an anchor the engine would accept, nor let through one the engine would refuse.

Resolution order, first hit wins:

  1. the anchor as authored, then each normalized variant (smart quotes turned straight, wrapping double quotes stripped);
  2. for each candidate, an exact line match, then an indent-normalized one;
  3. with a line hint, the hint and ±5 lines around it first, then a full-file scan that only accepts a single match.

The whitespace model (:func:common_ws_prefix) is shared by indent detection, dedenting and :func:reindent, so detection and re-application always agree, tabs included.

AnchorMatch dataclass

Where, and how, an anchor resolved in a file.

Attributes:

Name Type Description
status AnchorStatus

found, not_found, or ambiguous (several matches and no line hint to pick one).

line int | None

1-indexed start line when found.

indent str

Indent prefix to re-apply to new after an indent-normalized match, "" otherwise.

old str

The anchor text that matched: the authored one, or the normalized variant.

normalizations tuple[str, ...]

What had to be normalized for the match, in order (smart_quotes, wrapping_quotes, indent); empty for a verbatim match.

hits tuple[int, ...]

Every candidate start line when ambiguous.

Source code in packages/axm-edit/src/axm_edit/core/anchor_match.py
Python
@dataclass(frozen=True)
class AnchorMatch:
    """Where, and how, an anchor resolved in a file.

    Attributes:
        status: ``found``, ``not_found``, or ``ambiguous`` (several matches
            and no ``line`` hint to pick one).
        line: 1-indexed start line when found.
        indent: Indent prefix to re-apply to ``new`` after an
            indent-normalized match, ``""`` otherwise.
        old: The anchor text that matched: the authored one, or the
            normalized variant.
        normalizations: What had to be normalized for the match, in order
            (``smart_quotes``, ``wrapping_quotes``, ``indent``); empty for a
            verbatim match.
        hits: Every candidate start line when ambiguous.
    """

    status: AnchorStatus
    line: int | None = None
    indent: str = ""
    old: str = ""
    normalizations: tuple[str, ...] = ()
    hits: tuple[int, ...] = ()

anchor_matches_at(lines, old, line)

Tell whether old still matches at line, exactly or indent-normalized.

Source code in packages/axm-edit/src/axm_edit/core/anchor_match.py
Python
def anchor_matches_at(lines: Sequence[str], old: str, line: int) -> bool:
    """Tell whether *old* still matches at *line*, exactly or indent-normalized."""
    match_exact, match_dedented, _ = _make_matchers(lines, old)
    return match_exact(line) or match_dedented(line)

common_ws_prefix(lines)

Return the literal longest common leading-whitespace prefix.

The prefix is computed across non-empty lines and is treated as an opaque character sequence: tabs and spaces are compared by identity, so a tab never collapses into spaces (or vice versa).

Source code in packages/axm-edit/src/axm_edit/core/anchor_match.py
Python
def common_ws_prefix(lines: Sequence[str]) -> str:
    """Return the literal longest common leading-whitespace prefix.

    The prefix is computed across *non-empty* lines and is treated as an
    opaque character sequence: tabs and spaces are compared by identity, so
    a tab never collapses into spaces (or vice versa).
    """
    non_empty = [ln for ln in lines if ln.strip()]
    if not non_empty:
        return ""
    leads = [ln[: len(ln) - len(ln.lstrip())] for ln in non_empty]
    prefix = leads[0]
    for lead in leads[1:]:
        limit = min(len(prefix), len(lead))
        i = 0
        while i < limit and prefix[i] == lead[i]:
            i += 1
        prefix = prefix[:i]
        if not prefix:
            break
    return prefix

old_line_count(old)

Return the number of file lines spanned by old.

Source code in packages/axm-edit/src/axm_edit/core/anchor_match.py
Python
def old_line_count(old: str) -> int:
    """Return the number of file lines spanned by *old*."""
    return old.rstrip("\n").count("\n") + 1

reindent(text, indent)

Dedent text fully, then prepend indent to each non-empty line.

Relative indentation within the block is preserved; blank lines are kept verbatim.

Source code in packages/axm-edit/src/axm_edit/core/anchor_match.py
Python
def reindent(text: str, indent: str) -> str:
    """Dedent *text* fully, then prepend *indent* to each non-empty line.

    Relative indentation within the block is preserved; blank lines are kept
    verbatim.
    """
    raw_lines = split_lines(text)
    width = len(common_ws_prefix(raw_lines))
    return "".join(
        indent + (line[width:] if width else line) if line.strip() else line
        for line in raw_lines
    )

resolve_anchor(lines, old, hint_line)

Resolve old in lines, the file split by :func:split_lines.

Parameters:

Name Type Description Default
lines Sequence[str]

File content as read, split by :func:split_lines.

required
old str

The anchor as authored.

required
hint_line int | None

Optional 1-indexed line hint.

required

Returns:

Type Description
AnchorMatch

The match, or why there is none: ambiguous when the authored

AnchorMatch

anchor matches several lines and no hint picks one, not_found

AnchorMatch

otherwise.

Source code in packages/axm-edit/src/axm_edit/core/anchor_match.py
Python
def resolve_anchor(
    lines: Sequence[str], old: str, hint_line: int | None
) -> AnchorMatch:
    """Resolve *old* in *lines*, the file split by :func:`split_lines`.

    Args:
        lines: File content as read, split by :func:`split_lines`.
        old: The anchor as authored.
        hint_line: Optional 1-indexed line hint.

    Returns:
        The match, or why there is none: ``ambiguous`` when the authored
        anchor matches several lines and no hint picks one, ``not_found``
        otherwise.
    """
    for variant, normalized in _variants(old):
        found = _find(lines, variant, hint_line)
        if found is not None:
            line, indent, dedented = found
            extra = (NORMALIZED_INDENT,) if dedented else ()
            return AnchorMatch(
                status="found",
                line=line,
                indent=indent,
                old=variant,
                normalizations=normalized + extra,
            )
    hits = _all_match_lines(lines, old)
    if hint_line is None and len(hits) >= _MIN_AMBIGUOUS_HITS:
        return AnchorMatch(status="ambiguous", old=old, hits=tuple(hits))
    return AnchorMatch(status="not_found", old=old)

split_lines(text)

Split text after each \n only, keeping the line ends.

The one line model of every edit. Unlike :meth:str.splitlines, a form feed, vertical tab, file separator or U+2028 stays inside its line, so an anchor carrying one matches the file byte for byte.

Source code in packages/axm-edit/src/axm_edit/core/anchor_match.py
Python
def split_lines(text: str) -> list[str]:
    """Split *text* after each ``\\n`` only, keeping the line ends.

    The one line model of every edit. Unlike :meth:`str.splitlines`, a form
    feed, vertical tab, file separator or U+2028 stays inside its line, so an
    anchor carrying one matches the file byte for byte.
    """
    pieces = text.split("\n")
    lines = [piece + "\n" for piece in pieces[:-1]]
    if pieces[-1]:
        lines.append(pieces[-1])
    return lines