-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
Expand file tree
/
Copy pathcontent.py
More file actions
136 lines (100 loc) · 3.82 KB
/
content.py
File metadata and controls
136 lines (100 loc) · 3.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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)
)