"""Find the section of a Markdown document the cursor sits in. The refinement endpoint refines exactly one section at a time (FIM-style), so this is the AUTHORITATIVE boundary computation — the client mirrors it for a visual highlight, but the server owns it. A section runs from the nearest heading at or above the cursor down to the line before the next heading of the same or higher level; content before the first heading is its own section. A section whose body exceeds the chunk cap narrows to the blank-line paragraph at the cursor, so a large document never refines as one giant block. Shares the heading regex, fence-awareness and cap with `rag/chunking.py` so "what is a section" means the same thing to refinement and to indexing. """ from dataclasses import dataclass from app.rag.chunking import HEADING_RE, TARGET_CHUNK_CHARS @dataclass(frozen=True) class ActiveSection: start_line: int # 1-based, inclusive, into content_md end_line: int # 1-based, inclusive def _heading_lines(lines: list[str]) -> list[tuple[int, int]]: """(line_index_0based, level) for every heading line, ignoring fences.""" headings: list[tuple[int, int]] = [] in_fence = False for i, line in enumerate(lines): if line.lstrip().startswith("```"): in_fence = not in_fence continue if in_fence: continue match = HEADING_RE.match(line) if match: headings.append((i, len(match.group(1)))) return headings def _paragraph_at( lines: list[str], start0: int, end0: int, cursor0: int ) -> tuple[int, int] | None: """The blank-line-delimited block (fence-aware) at the cursor, within [start0, end0]. Falls back to the block just before the cursor when it sits on a blank gap, else the first block.""" blocks: list[tuple[int, int]] = [] block_start: int | None = None in_fence = False for i in range(start0, end0 + 1): line = lines[i] if line.lstrip().startswith("```"): in_fence = not in_fence if block_start is None: block_start = i continue if not line.strip() and not in_fence: if block_start is not None: blocks.append((block_start, i - 1)) block_start = None elif block_start is None: block_start = i if block_start is not None: blocks.append((block_start, end0)) if not blocks: return None for b_start, b_end in blocks: if b_start <= cursor0 <= b_end: return b_start, b_end for b_start, b_end in reversed(blocks): if b_end < cursor0: return b_start, b_end return blocks[0] def active_section(content_md: str, cursor_line: int) -> ActiveSection: lines = content_md.splitlines() n = len(lines) if n == 0: return ActiveSection(1, 1) cursor0 = max(1, min(cursor_line, n)) - 1 headings = _heading_lines(lines) owner: tuple[int, int] | None = None for idx, level in headings: if idx <= cursor0: owner = (idx, level) else: break if owner is None: # Preamble before the first heading (or a document with no headings). start0 = 0 end0 = headings[0][0] - 1 if headings else n - 1 else: start0, owner_level = owner end0 = n - 1 for idx, level in headings: if idx > start0 and level <= owner_level: end0 = idx - 1 break # Trailing blank lines belong to the separation before the next section, # not to this one: keeping them in the range would let an accepted # suggestion swallow the blank line above the next heading. while end0 > start0 and not lines[end0].strip(): end0 -= 1 body = "\n".join(lines[start0 : end0 + 1]) if len(body) > TARGET_CHUNK_CHARS: narrowed = _paragraph_at(lines, start0, end0, cursor0) if narrowed is not None: start0, end0 = narrowed return ActiveSection(start_line=start0 + 1, end_line=end0 + 1) def slice_lines( content_md: str, start_line: int, end_line: int ) -> tuple[str, str, str]: """(prefix, section, suffix) split at the 1-based inclusive line range. The section is the lines the model refines; prefix/suffix are the rest of the document, handed to the model as context it must not re-emit. """ lines = content_md.splitlines() prefix = "\n".join(lines[: start_line - 1]) section = "\n".join(lines[start_line - 1 : end_line]) suffix = "\n".join(lines[end_line:]) return prefix, section, suffix