mirror of
https://github.com/python/cpython.git
synced 2026-04-20 02:40:59 +00:00
136 lines
3.8 KiB
Python
136 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from .utils import ColorSpan, StyleRef, THEME, iter_display_chars, unbracket, wlen
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ContentFragment:
|
|
"""A single display character with its visual width and style.
|
|
|
|
The body of ``>>> def greet`` becomes one fragment per character::
|
|
|
|
d e f g r e e t
|
|
╰──┴──╯ ╰──┴──┴──┴──╯
|
|
keyword (unstyled)
|
|
|
|
e.g. ``ContentFragment("d", 1, StyleRef(tag="keyword"))``.
|
|
"""
|
|
|
|
text: str
|
|
width: int
|
|
style: StyleRef = StyleRef()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PromptContent:
|
|
"""The prompt split into leading full-width lines and an inline portion.
|
|
|
|
For the common ``">>> "`` prompt (no newlines)::
|
|
|
|
>>> def greet(name):
|
|
╰─╯
|
|
text=">>> ", width=4, leading_lines=()
|
|
|
|
If ``sys.ps1`` contains newlines, e.g. ``"Python 3.13\\n>>> "``::
|
|
|
|
Python 3.13 ← leading_lines[0]
|
|
>>> def greet(name):
|
|
╰─╯
|
|
text=">>> ", width=4
|
|
"""
|
|
|
|
leading_lines: tuple[ContentFragment, ...]
|
|
text: str
|
|
width: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SourceLine:
|
|
"""One logical line from the editor buffer, before styling.
|
|
|
|
Given this two-line input in the REPL::
|
|
|
|
>>> def greet(name):
|
|
... return name
|
|
▲ cursor
|
|
|
|
The buffer ``"def greet(name):\\n return name"`` yields::
|
|
|
|
SourceLine(lineno=0, text="def greet(name):",
|
|
start_offset=0, has_newline=True)
|
|
SourceLine(lineno=1, text=" return name",
|
|
start_offset=17, cursor_index=14)
|
|
"""
|
|
|
|
lineno: int
|
|
text: str
|
|
start_offset: int
|
|
has_newline: bool
|
|
cursor_index: int | None = None
|
|
|
|
@property
|
|
def cursor_on_line(self) -> bool:
|
|
return self.cursor_index is not None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ContentLine:
|
|
"""A logical line paired with its prompt and styled body.
|
|
|
|
For ``>>> def greet(name):``::
|
|
|
|
>>> def greet(name):
|
|
╰─╯ ╰──────────────╯
|
|
prompt body: one ContentFragment per character
|
|
"""
|
|
|
|
source: SourceLine
|
|
prompt: PromptContent
|
|
body: tuple[ContentFragment, ...]
|
|
|
|
|
|
def process_prompt(prompt: str) -> PromptContent:
|
|
r"""Return prompt content with width measured without zero-width markup."""
|
|
|
|
prompt_text = unbracket(prompt, including_content=False)
|
|
visible_prompt = unbracket(prompt, including_content=True)
|
|
leading_lines: list[ContentFragment] = []
|
|
|
|
while "\n" in prompt_text:
|
|
leading_text, _, prompt_text = prompt_text.partition("\n")
|
|
visible_leading, _, visible_prompt = visible_prompt.partition("\n")
|
|
leading_lines.append(ContentFragment(leading_text, wlen(visible_leading)))
|
|
|
|
return PromptContent(tuple(leading_lines), prompt_text, wlen(visible_prompt))
|
|
|
|
|
|
def build_body_fragments(
|
|
buffer: str,
|
|
colors: list[ColorSpan] | None,
|
|
start_index: int,
|
|
) -> tuple[ContentFragment, ...]:
|
|
"""Convert a line's text into styled content fragments."""
|
|
# Two separate loops to avoid the THEME() call in the common uncolored path.
|
|
if colors is None:
|
|
return tuple(
|
|
ContentFragment(
|
|
styled_char.text,
|
|
styled_char.width,
|
|
StyleRef(),
|
|
)
|
|
for styled_char in iter_display_chars(buffer, colors, start_index)
|
|
)
|
|
|
|
theme = THEME()
|
|
return tuple(
|
|
ContentFragment(
|
|
styled_char.text,
|
|
styled_char.width,
|
|
StyleRef.from_tag(styled_char.tag, theme[styled_char.tag])
|
|
if styled_char.tag
|
|
else StyleRef(),
|
|
)
|
|
for styled_char in iter_display_chars(buffer, colors, start_index)
|
|
)
|