|
7 | 7 |
|
8 | 8 | import asyncio |
9 | 9 | import errno |
| 10 | +import io |
10 | 11 | import os |
11 | 12 | import signal |
12 | 13 | import stat |
|
19 | 20 | Dict, |
20 | 21 | List, |
21 | 22 | IO, |
| 23 | + Iterable, |
22 | 24 | Iterator, |
23 | 25 | NamedTuple, |
24 | 26 | Optional, |
|
37 | 39 |
|
38 | 40 | from edgacknowledg import ( |
39 | 41 | MAX_DISPLAY_CONTENT_SIZE, |
| 42 | + MAX_DISPLAY_LABEL_SIZE, |
| 43 | + MAX_DISPLAY_LINE_SIZE, |
40 | 44 | ProtocolMessage, |
41 | 45 | RequestTags, |
42 | 46 | RichName, |
@@ -83,6 +87,8 @@ async def handle_request(self, channel: Channel) -> None: |
83 | 87 | ReviewClientHandler = Any # type: ignore[misc,assignment] |
84 | 88 |
|
85 | 89 |
|
| 90 | +import edgutil |
| 91 | + |
86 | 92 | from edgutil import get_running_event_loop, is_path_relative_to |
87 | 93 |
|
88 | 94 | CoroFactory = Callable[[], Awaitable[Any]] |
@@ -149,32 +155,111 @@ async def announce_section(channel: Channel, |
149 | 155 | )) |
150 | 156 |
|
151 | 157 |
|
152 | | -def trunc_display_content(content: str) -> Dict[str, Union[str, bool]]: |
153 | | - '''Truncate ``content`` for UI display; may set ``truncated``.''' |
154 | | - max_content_size = MAX_DISPLAY_CONTENT_SIZE |
155 | | - if len(content) > max_content_size: |
156 | | - # Attempt to truncate at line boundaries. |
157 | | - # |
158 | | - # To save time, find the newline closest to the boundary before walking |
159 | | - # the file size back. |
160 | | - last_newline_permitted = content.find('\n', max_content_size) |
161 | | - if last_newline_permitted != -1: |
162 | | - content = content[:last_newline_permitted] |
163 | | - # Now walk the file size back. |
164 | | - while len(content) > max_content_size: |
165 | | - last_new_line_idx = content.rfind('\n') |
166 | | - if last_new_line_idx != -1: |
167 | | - content = content[:(last_new_line_idx - 1)] |
168 | | - else: |
169 | | - # If there are no more newlines, resort to truncating within |
170 | | - # the line. |
171 | | - content = content[:(max_content_size - 4)] |
172 | | - content += '...' |
173 | | - return { |
174 | | - 'truncated': True, |
175 | | - 'content': content |
176 | | - } |
177 | | - return { 'content': content } |
| 158 | +def trunc_display_label(text: str) -> str: |
| 159 | + '''``text`` (one line, e.g. a status) cut for UI display to |
| 160 | + ``MAX_DISPLAY_LABEL_SIZE`` characters, a cut ending in an ellipsis. |
| 161 | + ''' |
| 162 | + if len(text) > MAX_DISPLAY_LABEL_SIZE: |
| 163 | + text = text[:MAX_DISPLAY_LABEL_SIZE - 1] + '\N{HORIZONTAL ELLIPSIS}' |
| 164 | + return text |
| 165 | + |
| 166 | + |
| 167 | +def _display_lines( |
| 168 | + pieces: Iterable[Tuple[str, bool]]) -> Iterator[Tuple[str, str, bool]]: |
| 169 | + '''Join ``(piece, at_line_start)`` pieces (as ``edgutil.iter_line_pieces`` |
| 170 | + yields them) into lines, yielding ``(text, line_end, cut)`` for each: at |
| 171 | + most ``MAX_DISPLAY_LINE_SIZE`` characters of its text (``cut`` if it had |
| 172 | + more, which are dropped as they come), and its end ('\\n', or '' for a |
| 173 | + last line without one). |
| 174 | +
|
| 175 | + A piece that starts a line ends the one before it, so a text that doesn't |
| 176 | + end in a line end (e.g. a command's output) never runs into the next. |
| 177 | + ''' |
| 178 | + # The line being joined (as much of it as is kept), and whether some was |
| 179 | + # dropped; ``None`` between lines. |
| 180 | + line: Optional[str] = None |
| 181 | + cut = False |
| 182 | + for piece, at_line_start in pieces: |
| 183 | + if line is not None and at_line_start: |
| 184 | + yield line, '\n', cut |
| 185 | + line = None |
| 186 | + if line is None: |
| 187 | + line = '' |
| 188 | + cut = False |
| 189 | + text = piece[:-1] if piece.endswith('\n') else piece |
| 190 | + room = MAX_DISPLAY_LINE_SIZE - len(line) |
| 191 | + if len(text) > room: |
| 192 | + cut = True |
| 193 | + line += text[:room] |
| 194 | + if piece.endswith('\n'): |
| 195 | + yield line, '\n', cut |
| 196 | + line = None |
| 197 | + if line is not None: |
| 198 | + yield line, '', cut |
| 199 | + |
| 200 | + |
| 201 | +def display_content(pieces: Iterable[Tuple[str, bool]]) -> Dict[str, Any]: |
| 202 | + '''The ``content`` of a display payload, from ``(piece, at_line_start)`` |
| 203 | + pieces (as ``edgutil.iter_line_pieces`` yields them), and as needed |
| 204 | + ``truncated`` and ``lines_truncated``: |
| 205 | +
|
| 206 | + - Each line is cut to ``MAX_DISPLAY_LINE_SIZE`` characters, its 1-based |
| 207 | + number listed in ``lines_truncated``. |
| 208 | + - The whole is cut to ``MAX_DISPLAY_CONTENT_SIZE``, after the last line |
| 209 | + that fits (setting ``truncated``); no more pieces are taken then, so |
| 210 | + what they come from is read no further. (Closing ``pieces``, should it |
| 211 | + be a generator with files open, is left to the caller.) |
| 212 | +
|
| 213 | + Neither key is set when nothing was cut, and nothing marks a cut in the |
| 214 | + content itself: the UI does. |
| 215 | + ''' |
| 216 | + kept: List[str] = [] |
| 217 | + size = 0 |
| 218 | + lines_truncated: List[int] = [] |
| 219 | + truncated = False |
| 220 | + for line_number, (text, line_end, cut) in enumerate( |
| 221 | + _display_lines(pieces), |
| 222 | + start = 1 |
| 223 | + ): |
| 224 | + num_chars_for_line = len(text) + len(line_end) |
| 225 | + if size + num_chars_for_line > MAX_DISPLAY_CONTENT_SIZE: |
| 226 | + truncated = True |
| 227 | + if not kept: |
| 228 | + # The first line alone doesn't fit (MAX_DISPLAY_LINE_SIZE is |
| 229 | + # larger than MAX_DISPLAY_CONTENT_SIZE): keep what does of it, as |
| 230 | + # a cut line. |
| 231 | + kept.append(text[:MAX_DISPLAY_CONTENT_SIZE]) |
| 232 | + lines_truncated.append(line_number) |
| 233 | + break |
| 234 | + kept.append(text + line_end) |
| 235 | + size += num_chars_for_line |
| 236 | + if cut: |
| 237 | + lines_truncated.append(line_number) |
| 238 | + |
| 239 | + content = ''.join(kept) |
| 240 | + if truncated and content.endswith('\n'): |
| 241 | + # Without the end of the last line kept. |
| 242 | + content = content[:-1] |
| 243 | + result: Dict[str, Any] = { 'content': content } |
| 244 | + if truncated: |
| 245 | + result['truncated'] = True |
| 246 | + if lines_truncated: |
| 247 | + result['lines_truncated'] = lines_truncated |
| 248 | + return result |
| 249 | + |
| 250 | + |
| 251 | +def display_text(handle: IO[str]) -> Dict[str, Any]: |
| 252 | + '''``display_content`` of the text ``handle`` reads: only as much as is |
| 253 | + shown, and of a line, at most a character more. |
| 254 | + ''' |
| 255 | + return display_content( |
| 256 | + edgutil.iter_line_pieces(handle, MAX_DISPLAY_LINE_SIZE + 1) |
| 257 | + ) |
| 258 | + |
| 259 | + |
| 260 | +def trunc_display_content(content: str) -> Dict[str, Any]: |
| 261 | + '''``display_content`` of ``content``, already in memory.''' |
| 262 | + return display_text(io.StringIO(content)) |
178 | 263 |
|
179 | 264 |
|
180 | 265 | def reviewer_of(request: Any) -> Optional[str]: |
|
0 commit comments