search_for_pattern resolved each match's line number by rescanning the
file from the beginning (TextUtils.get_line_from_index walks a
TextStepper from index 0), making coordinate resolution O(n) per match
and O(n*m) for m matches. search_text now precomputes line start
offsets once per file (O(n)) and resolves each match via binary search
(O(log n) per match).
The precomputed table mirrors TextStepper line semantics exactly:
\n, \r\n and bare \r are line separators, and an index pointing at the
\n of a \r\n pair resolves to (next line, 0) — the same rule
TextUtils.get_line_col_from_index implements, now pinned by regression
tests (including the mixed line-ending edge cases from the
insert_text_at_position fix).
On a synthetic 12k-line file (494 KB) with 723 matches, match-coordinate
resolution drops from ~1764 ms to ~2 ms (~1000x); the one-off
precompute costs ~1.1 ms. Small files regress nothing (100 lines, 5
matches: 70 µs -> 11 µs). Results are byte-for-byte identical to the
previous implementation.
Note: compatible with PR #1899 (interruptible agent regexes): that PR
touches the re.compile call site in search_text and the ContentReplacer
helpers, this PR touches the post-match line-coordinate resolution; the
two changes are complementary and rebase cleanly onto each other.
* perf(search_text): add reproducible benchmark script
scripts/profile_search_text.py regenerates the synthetic benchmark
content with a fixed seed (no real-world code), asserts both resolution
paths produce identical results, and prints the before/after timings
quoted in the PR description. Default: 12k lines / ~1830 matches;
optional CLI args scale the file down for quick runs.
* refactor(ls): encapsulate cached text coordinates in TextCoordinates abstraction
Address review feedback on the search_text line-lookup PR: the previously
added module-level helpers duplicated TextStepper's line-ending logic and
exposed raw offsets as a low-level data structure shared between two
functions, violating the project's software design principles (each
concern in exactly one home; dataclasses instead of tuples for simple
data storage).
Introduce in solidlsp/ls_utils.py:
- LineCol: frozen, kw-only dataclass for a 0-based line/column position
- TextCoordinates: encapsulates the cached table of line start offsets,
built via TextStepper (step_line + line_start_idx), with a single
public method line_col_at_index() resolving an index via binary
search, including the CRLF rule (an index pointing at the '\n' of a
'\r\n' pair denotes the beginning of the next line, column 0) and
InvalidTextLocationError for out-of-range indices
search_text now instantiates TextCoordinates once per file and resolves
each match's coordinates through the public method; the private helpers
(_compute_line_starts, _line_col_at_index) are removed.
Equivalence with TextUtils.get_line_col_from_index verified across
mixed line endings, empty content and boundary indices (0 mismatches).
Tests are behaviour-anchored: the implementation-coupled parametrized
test importing the private helpers is replaced by public-contract tests
of TextCoordinates (test/solidlsp/test_ls_utils.py) and absolute line
assertions through the public search_text API. The benchmark script
uses the public API as well.
Benchmark (synthetic 12k lines, ~723 matches, best of 5): coordinate
resolution ~1764 ms -> ~2 ms; precompute ~1.1 ms; small files unchanged.
Semantics are unchanged; results are identical to the previously
proposed implementation and to the upstream reference implementation.
* fix(test): rename test_ls_utils to test_text_coordinates to avoid basename collision
The new test file collided with the pre-existing upstream
test/solidlsp/util/test_ls_utils.py: pytest (prepend import mode, no
__init__.py in the test dirs) requires unique basenames, and collecting
both modules produced an import-file-mismatch error that aborted
collection with exit code 2 across every CI matrix job.
Rename to test_text_coordinates.py (unique, and matches the class under
test). Full-suite collection verified locally (2252 tests, 0 errors).