2525
2626from contextlib import contextmanager
2727from dataclasses import dataclass , field , fields
28- import unicodedata
2928from _colorize import can_colorize , ANSIColors
3029
3130
3231from . import commands , console , input
33- from .utils import wlen , unbracket , str_width
32+ from .utils import wlen , unbracket , disp_str
3433from .trace import trace
3534
3635
3938from .types import Callback , SimpleContextManager , KeySpec , CommandName
4039
4140
42- def disp_str (buffer : str ) -> tuple [str , list [int ]]:
43- """disp_str(buffer:string) -> (string, [int])
44-
45- Return the string that should be the printed representation of
46- |buffer| and a list detailing where the characters of |buffer|
47- get used up. E.g.:
48-
49- >>> disp_str(chr(3))
50- ('^C', [1, 0])
51-
52- """
53- b : list [int ] = []
54- s : list [str ] = []
55- for c in buffer :
56- if c == '\x1a ' :
57- s .append (c )
58- b .append (2 )
59- elif ord (c ) < 128 :
60- s .append (c )
61- b .append (1 )
62- elif unicodedata .category (c ).startswith ("C" ):
63- c = r"\u%04x" % ord (c )
64- s .append (c )
65- b .append (len (c ))
66- else :
67- s .append (c )
68- b .append (str_width (c ))
69- return "" .join (s ), b
70-
71-
7241# syntax classes:
7342
7443SYNTAX_WHITESPACE , SYNTAX_WORD , SYNTAX_SYMBOL = range (3 )
@@ -347,14 +316,12 @@ def calc_screen(self) -> list[str]:
347316 pos -= offset
348317
349318 prompt_from_cache = (offset and self .buffer [offset - 1 ] != "\n " )
350-
351319 lines = "" .join (self .buffer [offset :]).split ("\n " )
352-
353320 cursor_found = False
354321 lines_beyond_cursor = 0
355322 for ln , line in enumerate (lines , num_common_lines ):
356- ll = len (line )
357- if 0 <= pos <= ll :
323+ line_len = len (line )
324+ if 0 <= pos <= line_len :
358325 self .lxy = pos , ln
359326 cursor_found = True
360327 elif cursor_found :
@@ -368,34 +335,34 @@ def calc_screen(self) -> list[str]:
368335 prompt_from_cache = False
369336 prompt = ""
370337 else :
371- prompt = self .get_prompt (ln , ll >= pos >= 0 )
338+ prompt = self .get_prompt (ln , line_len >= pos >= 0 )
372339 while "\n " in prompt :
373340 pre_prompt , _ , prompt = prompt .partition ("\n " )
374341 last_refresh_line_end_offsets .append (offset )
375342 screen .append (pre_prompt )
376343 screeninfo .append ((0 , []))
377- pos -= ll + 1
378- prompt , lp = self .process_prompt (prompt )
379- l , l2 = disp_str (line )
380- wrapcount = (wlen (l ) + lp ) // self .console .width
381- if wrapcount == 0 :
382- offset += ll + 1 # Takes all of the line plus the newline
344+ pos -= line_len + 1
345+ prompt , prompt_len = self .process_prompt (prompt )
346+ chars , char_widths = disp_str (line )
347+ wrapcount = (sum (char_widths ) + prompt_len ) // self .console .width
348+ trace ("wrapcount = {wrapcount}" , wrapcount = wrapcount )
349+ if wrapcount == 0 or not char_widths :
350+ offset += line_len + 1 # Takes all of the line plus the newline
383351 last_refresh_line_end_offsets .append (offset )
384- screen .append (prompt + l )
385- screeninfo .append ((lp , l2 ))
352+ screen .append (prompt + "" . join ( chars ) )
353+ screeninfo .append ((prompt_len , char_widths ))
386354 else :
387- i = 0
388- while l :
389- prelen = lp if i == 0 else 0
355+ pre = prompt
356+ prelen = prompt_len
357+ for wrap in range ( wrapcount + 1 ):
390358 index_to_wrap_before = 0
391359 column = 0
392- for character_width in l2 :
393- if column + character_width >= self .console .width - prelen :
360+ for char_width in char_widths :
361+ if column + char_width + prelen >= self .console .width :
394362 break
395363 index_to_wrap_before += 1
396- column += character_width
397- pre = prompt if i == 0 else ""
398- if len (l ) > index_to_wrap_before :
364+ column += char_width
365+ if len (chars ) > index_to_wrap_before :
399366 offset += index_to_wrap_before
400367 post = "\\ "
401368 after = [1 ]
@@ -404,11 +371,14 @@ def calc_screen(self) -> list[str]:
404371 post = ""
405372 after = []
406373 last_refresh_line_end_offsets .append (offset )
407- screen .append (pre + l [:index_to_wrap_before ] + post )
408- screeninfo .append ((prelen , l2 [:index_to_wrap_before ] + after ))
409- l = l [index_to_wrap_before :]
410- l2 = l2 [index_to_wrap_before :]
411- i += 1
374+ render = pre + "" .join (chars [:index_to_wrap_before ]) + post
375+ render_widths = char_widths [:index_to_wrap_before ] + after
376+ screen .append (render )
377+ screeninfo .append ((prelen , render_widths ))
378+ chars = chars [index_to_wrap_before :]
379+ char_widths = char_widths [index_to_wrap_before :]
380+ pre = ""
381+ prelen = 0
412382 self .screeninfo = screeninfo
413383 self .cxy = self .pos2xy ()
414384 if self .msg :
@@ -537,9 +507,9 @@ def setpos_from_xy(self, x: int, y: int) -> None:
537507 pos = 0
538508 i = 0
539509 while i < y :
540- prompt_len , character_widths = self .screeninfo [i ]
541- offset = len (character_widths ) - character_widths . count ( 0 )
542- in_wrapped_line = prompt_len + sum (character_widths ) >= self .console .width
510+ prompt_len , char_widths = self .screeninfo [i ]
511+ offset = len (char_widths )
512+ in_wrapped_line = prompt_len + sum (char_widths ) >= self .console .width
543513 if in_wrapped_line :
544514 pos += offset - 1 # -1 cause backslash is not in buffer
545515 else :
@@ -560,29 +530,33 @@ def setpos_from_xy(self, x: int, y: int) -> None:
560530
561531 def pos2xy (self ) -> tuple [int , int ]:
562532 """Return the x, y coordinates of position 'pos'."""
563- # this *is* incomprehensible, yes.
564- p , y = 0 , 0
565- l2 : list [int ] = []
533+
534+ prompt_len , y = 0 , 0
535+ char_widths : list [int ] = []
566536 pos = self .pos
567537 assert 0 <= pos <= len (self .buffer )
538+
539+ # optimize for the common case: typing at the end of the buffer
568540 if pos == len (self .buffer ) and len (self .screeninfo ) > 0 :
569541 y = len (self .screeninfo ) - 1
570- p , l2 = self .screeninfo [y ]
571- return p + sum (l2 ) + l2 .count (0 ), y
542+ prompt_len , char_widths = self .screeninfo [y ]
543+ return prompt_len + sum (char_widths ), y
544+
545+ for prompt_len , char_widths in self .screeninfo :
546+ offset = len (char_widths )
547+ in_wrapped_line = prompt_len + sum (char_widths ) >= self .console .width
548+ if in_wrapped_line :
549+ offset -= 1 # need to remove line-wrapping backslash
572550
573- for p , l2 in self .screeninfo :
574- l = len (l2 ) - l2 .count (0 )
575- in_wrapped_line = p + sum (l2 ) >= self .console .width
576- offset = l - 1 if in_wrapped_line else l # need to remove backslash
577551 if offset >= pos :
578552 break
579553
580- if p + sum ( l2 ) >= self . console . width :
581- pos -= l - 1 # -1 cause backslash is not in buffer
582- else :
583- pos -= l + 1 # +1 cause newline is in buffer
554+ if not in_wrapped_line :
555+ offset += 1 # there's a newline in buffer
556+
557+ pos -= offset
584558 y += 1
585- return p + sum (l2 [:pos ]), y
559+ return prompt_len + sum (char_widths [:pos ]), y
586560
587561 def insert (self , text : str | list [str ]) -> None :
588562 """Insert 'text' at the insertion point."""
0 commit comments