root/src/viewer/ascii.c

/* [previous][next][first][last][top][bottom][index][help]  */

DEFINITIONS

This source file includes following definitions.
  1. mcview_wcwidth
  2. mcview_ismark
  3. mcview_is_non_spacing_mark
  4. mcview_is_spacing_mark
  5. mcview_isprint
  6. mcview_char_display
  7. mcview_get_next_char
  8. mcview_get_next_maybe_nroff_char
  9. mcview_next_combining_char_sequence
  10. mcview_display_line
  11. mcview_display_paragraph
  12. mcview_wrap_fixup
  13. mcview_display_text
  14. mcview_ascii_move_down
  15. mcview_ascii_move_up
  16. mcview_ascii_moveto_bol
  17. mcview_ascii_moveto_eol
  18. mcview_state_machine_init

   1 /*
   2    Internal file viewer for the Midnight Commander
   3    Function for plain view
   4 
   5    Copyright (C) 1994-2019
   6    Free Software Foundation, Inc.
   7 
   8    Written by:
   9    Miguel de Icaza, 1994, 1995, 1998
  10    Janne Kukonlehto, 1994, 1995
  11    Jakub Jelinek, 1995
  12    Joseph M. Hinkle, 1996
  13    Norbert Warmuth, 1997
  14    Pavel Machek, 1998
  15    Roland Illig <roland.illig@gmx.de>, 2004, 2005
  16    Slava Zanko <slavazanko@google.com>, 2009
  17    Andrew Borodin <aborodin@vmail.ru>, 2009-2014
  18    Ilia Maslakov <il.smind@gmail.com>, 2009
  19    Rewritten almost from scratch by:
  20    Egmont Koblinger <egmont@gmail.com>, 2014
  21 
  22    This file is part of the Midnight Commander.
  23 
  24    The Midnight Commander is free software: you can redistribute it
  25    and/or modify it under the terms of the GNU General Public License as
  26    published by the Free Software Foundation, either version 3 of the License,
  27    or (at your option) any later version.
  28 
  29    The Midnight Commander is distributed in the hope that it will be useful,
  30    but WITHOUT ANY WARRANTY; without even the implied warranty of
  31    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  32    GNU General Public License for more details.
  33 
  34    You should have received a copy of the GNU General Public License
  35    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  36 
  37    ------------------------------------------------------------------------------------------------
  38 
  39    The viewer is implemented along the following design principles:
  40 
  41    Goals: Always display simple scripts, double wide (CJK), combining accents and spacing marks
  42    (often used e.g. in Devanagari) perfectly. Make the arrow keys always work correctly.
  43 
  44    Absolutely non-goal: RTL.
  45 
  46    Terminology:
  47 
  48    - A "paragraph" is the text between two adjacent newline characters. A "line" or "row" is a
  49    visual row on the screen. In wrap mode, the viewer formats a paragraph into one or more lines.
  50 
  51    - The Unicode glossary <http://www.unicode.org/glossary/> doesn't seem to have a notion of "base
  52    character followed by zero or more combining characters". The closest matches are "Combining
  53    Character Sequence" meaning a base character followed by one or more combining characters, or
  54    "Grapheme" which seems to exclude non-printable characters such as newline. In this file,
  55    "combining character sequence" (or any obvious abbreviation thereof) means a base character
  56    followed by zero or more (up to a current limit of 4) combining characters.
  57 
  58    ------------------------------------------------------------------------------------------------
  59 
  60    The parser-formatter is designed to be stateless across paragraphs. This is so that we can walk
  61    backwards without having to reparse the whole file (although we still need to reparse and
  62    reformat the whole paragraph, but it's a lot better). This principle needs to be changed if we
  63    ever get to address tickets 1849/2977, but then we can still store (for efficiency) the parser
  64    state at the beginning of the paragraph, and safely walk backwards if we don't cross an escape
  65    character.
  66 
  67    The parser-formatter, however, definitely needs to carry a state across lines. Currently this
  68    state contains:
  69 
  70    - The logical column (as if we didn't wrap). This is used for handling TAB characters after a
  71    wordwrap consistently with less.
  72 
  73    - Whether the last nroff character was bold or underlined. This is used for displaying the
  74    ambiguous _\b_ sequence consistently with less.
  75 
  76    - Whether the desired way of displaying a lonely combining accent or spacing mark is to place it
  77    over a dotted circle (we do this at the beginning of the paragraph of after a TAB), or to ignore
  78    the combining char and show replacement char for the spacing mark (we do this if e.g. too many
  79    of these were encountered and hence we don't glue them with their base character).
  80 
  81    - (This state needs to be expanded if e.g. we decide to print verbose replacement characters
  82    (e.g. "<U+0080>") and allow these to wrap around lines.)
  83 
  84    The state also contains the file offset, as it doesn't make sense to ever know the state without
  85    knowing the corresponding offset.
  86 
  87    The state depends on various settings (viewer width, encoding, nroff mode, charwrap or wordwrap
  88    mode (if we'll have that one day) etc.), needs to be recomputed if any of these changes.
  89 
  90    Walking forwards is usually relatively easy both in the file and on the screen. Walking
  91    backwards within a paragraph would only be possible in some special cases and even then it would
  92    be painful, so we always walk back to the beginning of the paragraph and reparse-reformat from
  93    there.
  94 
  95    (Walking back within a line in the file would have at least the following difficulties: handling
  96    the parser state; processing invalid UTF-8; processing invalid nroff (e.g. what is "_\bA\bA"?).
  97    Walking back on the display: we wouldn't know where to display the last line of a paragraph, or
  98    where to display a line if its following line starts with a wide (CJK or Tab) character. Long
  99    story short: just forget this approach.)
 100 
 101    Most important variables:
 102 
 103    - dpy_start: Both in unwrap and wrap modes this points to the beginning of the topmost displayed
 104    paragraph.
 105 
 106    - dpy_text_column: Only in unwrap mode, an additional horizontal scroll.
 107 
 108    - dpy_paragraph_skip_lines: Only in wrap mode, an additional vertical scroll (the number of
 109    lines that are scrolled off at the top from the topmost paragraph).
 110 
 111    - dpy_state_top: Only in wrap mode, the offset and parser-formatter state at the line where
 112    displaying the file begins is cached here.
 113 
 114    - dpy_wrap_dirty: If some parameter has changed that makes it necessary to reparse-redisplay the
 115    topmost paragraph.
 116 
 117    In wrap mode, the three variables "dpy_start", "dpy_paragraph_skip_lines" and "dpy_state_top"
 118    are kept consistent. Think of the first two as the ones describing the position, and the third
 119    as a cached value for better performance so that we don't need to wrap the invisible beginning
 120    of the topmost paragraph over and over again. The third value needs to be recomputed each time a
 121    parameter that influences parsing or displaying the file (e.g. width of screen, encoding, nroff
 122    mode) changes, this is signaled by "dpy_wrap_dirty" to force recomputing "dpy_state_top" (and
 123    clamp "dpy_paragraph_skip_lines" if necessary).
 124 
 125    ------------------------------------------------------------------------------------------------
 126 
 127    Help integration
 128 
 129    I'm planning to port the help viewer to this codebase.
 130 
 131    Splitting at sections would still happen in the help viewer. It would either copy a section, or
 132    set force_max and a similar force_min to limit displaying to one section only.
 133 
 134    Parsing the help format would go next to the nroff parser. The colors, alternate character set,
 135    and emitting the version number would go to the "state". (The version number would be
 136    implemented by emitting remaining characters of a buffer in the "state" one by one, without
 137    advancing in the file position.)
 138 
 139    The active link would be drawn similarly to the search highlight. Other than that, the viewer
 140    wouldn't care about links (except for their color). help.c would keep track of which one is
 141    highlighted, how to advance to the next/prev on an arrow, how the scroll offset needs to be
 142    adjusted when moving, etc.
 143 
 144    Add wrapping at word boundaries to where wrapping at char boundaries happens now.
 145  */
 146 
 147 #include <config.h>
 148 
 149 #include "lib/global.h"
 150 #include "lib/tty/tty.h"
 151 #include "lib/skin.h"
 152 #include "lib/util.h"           /* is_printable() */
 153 #ifdef HAVE_CHARSET
 154 #include "lib/charsets.h"
 155 #endif
 156 
 157 #include "src/setup.h"          /* option_tab_spacing */
 158 
 159 #include "internal.h"
 160 
 161 /*** global variables ****************************************************************************/
 162 
 163 /*** file scope macro definitions ****************************************************************/
 164 
 165 #if GLIB_CHECK_VERSION (2, 30, 0)
 166 #define SPACING_MARK G_UNICODE_SPACING_MARK
 167 #else
 168 #define SPACING_MARK G_UNICODE_COMBINING_MARK
 169 #endif
 170 
 171 /* The Unicode standard recommends that lonely combining characters are printed over a dotted
 172  * circle. If the terminal is not UTF-8, this will be replaced by a dot anyway. */
 173 #define BASE_CHARACTER_FOR_LONELY_COMBINING 0x25CC      /* dotted circle */
 174 #define MAX_COMBINING_CHARS 4   /* both slang and ncurses support exactly 4 */
 175 
 176 /* I think anything other than space (e.g. arrows) just introduce visual clutter without actually
 177  * adding value. */
 178 #define PARTIAL_CJK_AT_LEFT_MARGIN  ' '
 179 #define PARTIAL_CJK_AT_RIGHT_MARGIN ' '
 180 
 181 /*
 182  * Wrap mode: This is for safety so that jumping to the end of file (which already includes
 183  * scrolling back by a page) and then walking backwards is reasonably fast, even if the file is
 184  * extremely large and consists of maybe full zeros or something like that. If there's no newline
 185  * found within this limit, just start displaying from there and see what happens. We might get
 186  * some displaying parameteres (most importantly the columns) incorrect, but at least will show the
 187  * file without spinning the CPU for ages. When scrolling back to that point, the user might see a
 188  * garbled first line (even starting with an invalid partial UTF-8), but then walking back by yet
 189  * another line should fix it.
 190  *
 191  * Unwrap mode: This is not used, we wouldn't be able to do anything reasonable without walking
 192  * back a whole paragraph (well, view->data_area.height paragraphs actually).
 193  */
 194 #define MAX_BACKWARDS_WALK_IN_PARAGRAPH (100 * 1000)
 195 
 196 /*** file scope type declarations ****************************************************************/
 197 
 198 /*** file scope variables ************************************************************************/
 199 
 200 /* --------------------------------------------------------------------------------------------- */
 201 /*** file scope functions ************************************************************************/
 202 /* --------------------------------------------------------------------------------------------- */
 203 
 204 /* TODO: These methods shouldn't be necessary, see ticket 3257 */
 205 
 206 static int
 207 mcview_wcwidth (const WView * view, int c)
     /* [previous][next][first][last][top][bottom][index][help]  */
 208 {
 209 #ifdef HAVE_CHARSET
 210     if (view->utf8)
 211     {
 212         if (g_unichar_iswide (c))
 213             return 2;
 214         if (g_unichar_iszerowidth (c))
 215             return 0;
 216     }
 217 #else
 218     (void) view;
 219     (void) c;
 220 #endif /* HAVE_CHARSET */
 221     return 1;
 222 }
 223 
 224 /* --------------------------------------------------------------------------------------------- */
 225 
 226 static gboolean
 227 mcview_ismark (const WView * view, int c)
     /* [previous][next][first][last][top][bottom][index][help]  */
 228 {
 229 #ifdef HAVE_CHARSET
 230     if (view->utf8)
 231         return g_unichar_ismark (c);
 232 #else
 233     (void) view;
 234     (void) c;
 235 #endif /* HAVE_CHARSET */
 236     return FALSE;
 237 }
 238 
 239 /* --------------------------------------------------------------------------------------------- */
 240 
 241 /* actually is_non_spacing_mark_or_enclosing_mark */
 242 static gboolean
 243 mcview_is_non_spacing_mark (const WView * view, int c)
     /* [previous][next][first][last][top][bottom][index][help]  */
 244 {
 245 #ifdef HAVE_CHARSET
 246     if (view->utf8)
 247     {
 248         GUnicodeType type;
 249 
 250         type = g_unichar_type (c);
 251 
 252         return type == G_UNICODE_NON_SPACING_MARK || type == G_UNICODE_ENCLOSING_MARK;
 253     }
 254 #else
 255     (void) view;
 256     (void) c;
 257 #endif /* HAVE_CHARSET */
 258     return FALSE;
 259 }
 260 
 261 /* --------------------------------------------------------------------------------------------- */
 262 
 263 #if 0
 264 static gboolean
 265 mcview_is_spacing_mark (const WView * view, int c)
     /* [previous][next][first][last][top][bottom][index][help]  */
 266 {
 267 #ifdef HAVE_CHARSET
 268     if (view->utf8)
 269         return g_unichar_type (c) == SPACING_MARK;
 270 #else
 271     (void) view;
 272     (void) c;
 273 #endif /* HAVE_CHARSET */
 274     return FALSE;
 275 }
 276 #endif /* 0 */
 277 
 278 /* --------------------------------------------------------------------------------------------- */
 279 
 280 static gboolean
 281 mcview_isprint (const WView * view, int c)
     /* [previous][next][first][last][top][bottom][index][help]  */
 282 {
 283 #ifdef HAVE_CHARSET
 284     if (!view->utf8)
 285         c = convert_from_8bit_to_utf_c ((unsigned char) c, view->converter);
 286     return g_unichar_isprint (c);
 287 #else
 288     (void) view;
 289     /* TODO this is very-very buggy by design: ticket 3257 comments 0-1 */
 290     return is_printable (c);
 291 #endif /* HAVE_CHARSET */
 292 }
 293 
 294 /* --------------------------------------------------------------------------------------------- */
 295 
 296 static int
 297 mcview_char_display (const WView * view, int c, char *s)
     /* [previous][next][first][last][top][bottom][index][help]  */
 298 {
 299 #ifdef HAVE_CHARSET
 300     if (mc_global.utf8_display)
 301     {
 302         if (!view->utf8)
 303             c = convert_from_8bit_to_utf_c ((unsigned char) c, view->converter);
 304         if (!g_unichar_isprint (c))
 305             c = '.';
 306         return g_unichar_to_utf8 (c, s);
 307     }
 308     if (view->utf8)
 309     {
 310         if (g_unichar_iswide (c))
 311         {
 312             s[0] = s[1] = '.';
 313             return 2;
 314         }
 315         if (g_unichar_iszerowidth (c))
 316             return 0;
 317         /* TODO the is_printable check below will be broken for this */
 318         c = convert_from_utf_to_current_c (c, view->converter);
 319     }
 320     else
 321     {
 322         /* TODO the is_printable check below will be broken for this */
 323         c = convert_to_display_c (c);
 324     }
 325 #else
 326     (void) view;
 327 #endif /* HAVE_CHARSET */
 328     /* TODO this is very-very buggy by design: ticket 3257 comments 0-1 */
 329     if (!is_printable (c))
 330         c = '.';
 331     *s = c;
 332     return 1;
 333 }
 334 
 335 /* --------------------------------------------------------------------------------------------- */
 336 
 337 /**
 338  * Just for convenience, a common interface in front of mcview_get_utf and mcview_get_byte, so that
 339  * the caller doesn't have to care about utf8 vs 8-bit modes.
 340  *
 341  * Normally: stores c, updates state, returns TRUE.
 342  * At EOF: state is unchanged, c is undefined, returns FALSE.
 343  *
 344  * Just as with mcview_get_utf(), invalid UTF-8 is reported using negative integers.
 345  *
 346  * Also, temporary hack: handle force_max here.
 347  * TODO: move it to lower layers (datasource.c)?
 348  */
 349 static gboolean
 350 mcview_get_next_char (WView * view, mcview_state_machine_t * state, int *c)
     /* [previous][next][first][last][top][bottom][index][help]  */
 351 {
 352     /* Pretend EOF if we reached force_max */
 353     if (view->force_max >= 0 && state->offset >= view->force_max)
 354         return FALSE;
 355 
 356 #ifdef HAVE_CHARSET
 357     if (view->utf8)
 358     {
 359         int char_length = 0;
 360 
 361         if (!mcview_get_utf (view, state->offset, c, &char_length))
 362             return FALSE;
 363         /* Pretend EOF if we crossed force_max */
 364         if (view->force_max >= 0 && state->offset + char_length > view->force_max)
 365             return FALSE;
 366 
 367         state->offset += char_length;
 368         return TRUE;
 369     }
 370 #endif /* HAVE_CHARSET */
 371     if (!mcview_get_byte (view, state->offset, c))
 372         return FALSE;
 373     state->offset++;
 374     return TRUE;
 375 }
 376 
 377 /* --------------------------------------------------------------------------------------------- */
 378 /**
 379  * This function parses the next nroff character and gives it to you along with its desired color,
 380  * so you never have to care about nroff again.
 381  *
 382  * The nroff mode does the backspace trick for every single character (Unicode codepoint). At least
 383  * that's what the GNU groff 1.22 package produces, and that's what less 458 expects. For
 384  * double-wide characters (CJK), still only a single backspace is emitted. For combining accents
 385  * and such, the print-backspace-print step is repeated for the base character and then for each
 386  * accent separately.
 387  *
 388  * So, the right place for this layer is after the bytes are interpreted in UTF-8, but before
 389  * joining a base character with its combining accents.
 390  *
 391  * Normally: stores c and color, updates state, returns TRUE.
 392  * At EOF: state is unchanged, c and color are undefined, returns FALSE.
 393  *
 394  * color can be null if the caller doesn't care.
 395  */
 396 static gboolean
 397 mcview_get_next_maybe_nroff_char (WView * view, mcview_state_machine_t * state, int *c, int *color)
     /* [previous][next][first][last][top][bottom][index][help]  */
 398 {
 399     mcview_state_machine_t state_after_nroff;
 400     int c2, c3;
 401 
 402     if (color != NULL)
 403         *color = VIEW_NORMAL_COLOR;
 404 
 405     if (!view->mode_flags.nroff)
 406         return mcview_get_next_char (view, state, c);
 407 
 408     if (!mcview_get_next_char (view, state, c))
 409         return FALSE;
 410     /* Don't allow nroff formatting around CR, LF, TAB or other special chars */
 411     if (!mcview_isprint (view, *c))
 412         return TRUE;
 413 
 414     state_after_nroff = *state;
 415 
 416     if (!mcview_get_next_char (view, &state_after_nroff, &c2))
 417         return TRUE;
 418     if (c2 != '\b')
 419         return TRUE;
 420 
 421     if (!mcview_get_next_char (view, &state_after_nroff, &c3))
 422         return TRUE;
 423     if (!mcview_isprint (view, c3))
 424         return TRUE;
 425 
 426     if (*c == '_' && c3 == '_')
 427     {
 428         *state = state_after_nroff;
 429         if (color != NULL)
 430             *color =
 431                 state->nroff_underscore_is_underlined ? VIEW_UNDERLINED_COLOR : VIEW_BOLD_COLOR;
 432     }
 433     else if (*c == c3)
 434     {
 435         *state = state_after_nroff;
 436         state->nroff_underscore_is_underlined = FALSE;
 437         if (color != NULL)
 438             *color = VIEW_BOLD_COLOR;
 439     }
 440     else if (*c == '_')
 441     {
 442         *c = c3;
 443         *state = state_after_nroff;
 444         state->nroff_underscore_is_underlined = TRUE;
 445         if (color != NULL)
 446             *color = VIEW_UNDERLINED_COLOR;
 447     }
 448 
 449     return TRUE;
 450 }
 451 
 452 /* --------------------------------------------------------------------------------------------- */
 453 /**
 454  * Get one base character, along with its combining or spacing mark characters.
 455  *
 456  * (A spacing mark is a character that extends the base character's width 1 into a combined
 457  * character of width 2, yet these two character cells should not be separated. E.g. Devanagari
 458  * <U+0939><U+094B>.)
 459  *
 460  * This method exists mainly for two reasons. One is to be able to tell if we fit on the current
 461  * line or need to wrap to the next one. The other is that both slang and ncurses seem to require
 462  * that the character and its combining marks are printed in a single call (or is it just a
 463  * limitation of mc's wrapper to them?).
 464  *
 465  * For convenience, this method takes care of converting CR or CR+LF into LF.
 466  * TODO this should probably happen later, when displaying the file?
 467  *
 468  * Normally: stores cs and color, updates state, returns >= 1 (entries in cs).
 469  * At EOF: state is unchanged, cs and color are undefined, returns 0.
 470  *
 471  * @param view ...
 472  * @param state the parser-formatter state machine's state, updated
 473  * @param cs store the characters here
 474  * @param clen the room available in cs (that is, at most clen-1 combining marks are allowed), must
 475  *   be at least 2
 476  * @param color if non-NULL, store the color here, taken from the first codepoint's color
 477  * @return the number of entries placed in cs, or 0 on EOF
 478  */
 479 static int
 480 mcview_next_combining_char_sequence (WView * view, mcview_state_machine_t * state, int *cs,
     /* [previous][next][first][last][top][bottom][index][help]  */
 481                                      int clen, int *color)
 482 {
 483     int i = 1;
 484 
 485     if (!mcview_get_next_maybe_nroff_char (view, state, cs, color))
 486         return 0;
 487 
 488     /* Process \r and \r\n newlines. */
 489     if (cs[0] == '\r')
 490     {
 491         int cnext;
 492 
 493         mcview_state_machine_t state_after_crlf = *state;
 494         if (mcview_get_next_maybe_nroff_char (view, &state_after_crlf, &cnext, NULL)
 495             && cnext == '\n')
 496             *state = state_after_crlf;
 497         cs[0] = '\n';
 498         return 1;
 499     }
 500 
 501     /* We don't want combining over non-printable characters. This includes '\n' and '\t' too. */
 502     if (!mcview_isprint (view, cs[0]))
 503         return 1;
 504 
 505     if (mcview_ismark (view, cs[0]))
 506     {
 507         if (!state->print_lonely_combining)
 508         {
 509             /* First character is combining. Either just return it, ... */
 510             return 1;
 511         }
 512         else
 513         {
 514             /* or place this (and subsequent combining ones) over a dotted circle. */
 515             cs[1] = cs[0];
 516             cs[0] = BASE_CHARACTER_FOR_LONELY_COMBINING;
 517             i = 2;
 518         }
 519     }
 520 
 521     if (mcview_wcwidth (view, cs[0]) == 2)
 522     {
 523         /* Don't allow combining or spacing mark for wide characters, is this okay? */
 524         return 1;
 525     }
 526 
 527     /* Look for more combining chars. Either at most clen-1 zero-width combining chars,
 528      * or at most 1 spacing mark. Is this logic correct? */
 529     for (; i < clen; i++)
 530     {
 531         mcview_state_machine_t state_after_combining;
 532 
 533         state_after_combining = *state;
 534         if (!mcview_get_next_maybe_nroff_char (view, &state_after_combining, &cs[i], NULL))
 535             return i;
 536         if (!mcview_ismark (view, cs[i]) || !mcview_isprint (view, cs[i]))
 537             return i;
 538         if (g_unichar_type (cs[i]) == SPACING_MARK)
 539         {
 540             /* Only allow as the first combining char. Stop processing in either case. */
 541             if (i == 1)
 542             {
 543                 *state = state_after_combining;
 544                 i++;
 545             }
 546             return i;
 547         }
 548         *state = state_after_combining;
 549     }
 550     return i;
 551 }
 552 
 553 /* --------------------------------------------------------------------------------------------- */
 554 /**
 555  * Parse, format and possibly display one visual line of text.
 556  *
 557  * Formatting starts at the given "state" (which encodes the file offset and parser and formatter's
 558  * internal state). In unwrap mode, this should point to the beginning of the paragraph with the
 559  * default state, the additional horizontal scrolling is added here. In wrap mode, this should
 560  * point to the beginning of the line, with the proper state at that point.
 561  *
 562  * In wrap mode, if a line ends in a newline, it is consumed, even if it's exactly at the right
 563  * edge. In unwrap mode, the whole remaining line, including the newline is consumed. Displaying
 564  * the next line should start at "state"'s new value, or if we displayed the bottom line then
 565  * state->offset tells the file offset to be shown in the top bar.
 566  *
 567  * If "row" is offscreen, don't actually display the line but still update "state" and return the
 568  * proper value. This is used by mcview_wrap_move_down to advance in the file.
 569  *
 570  * @param view ...
 571  * @param state the parser-formatter state machine's state, updated
 572  * @param row print to this row
 573  * @param paragraph_ended store TRUE if paragraph ended by newline or EOF, FALSE if wraps to next
 574  *   line
 575  * @param linewidth store the width of the line here
 576  * @return the number of rows, that is, 0 if we were already at EOF, otherwise 1
 577  */
 578 static int
 579 mcview_display_line (WView * view, mcview_state_machine_t * state, int row,
     /* [previous][next][first][last][top][bottom][index][help]  */
 580                      gboolean * paragraph_ended, off_t * linewidth)
 581 {
 582     const screen_dimen left = view->data_area.left;
 583     const screen_dimen top = view->data_area.top;
 584     const screen_dimen width = view->data_area.width;
 585     const screen_dimen height = view->data_area.height;
 586     off_t dpy_text_column = view->mode_flags.wrap ? 0 : view->dpy_text_column;
 587     screen_dimen col = 0;
 588     int cs[1 + MAX_COMBINING_CHARS];
 589     char str[(1 + MAX_COMBINING_CHARS) * UTF8_CHAR_LEN + 1];
 590     int i, j;
 591 
 592     if (paragraph_ended != NULL)
 593         *paragraph_ended = TRUE;
 594 
 595     if (!view->mode_flags.wrap && (row < 0 || row >= (int) height) && linewidth == NULL)
 596     {
 597         /* Optimization: Fast forward to the end of the line, rather than carefully
 598          * parsing and then not actually displaying it. */
 599         off_t eol;
 600         int retval;
 601 
 602         eol = mcview_eol (view, state->offset);
 603         retval = (eol > state->offset) ? 1 : 0;
 604 
 605         mcview_state_machine_init (state, eol);
 606         return retval;
 607     }
 608 
 609     while (TRUE)
 610     {
 611         int charwidth = 0;
 612         mcview_state_machine_t state_saved;
 613         int n;
 614         int color;
 615 
 616         state_saved = *state;
 617         n = mcview_next_combining_char_sequence (view, state, cs, 1 + MAX_COMBINING_CHARS, &color);
 618         if (n == 0)
 619         {
 620             if (linewidth != NULL)
 621                 *linewidth = col;
 622             return (col > 0) ? 1 : 0;
 623         }
 624 
 625         if (view->search_start <= state->offset && state->offset < view->search_end)
 626             color = VIEW_SELECTED_COLOR;
 627 
 628         if (cs[0] == '\n')
 629         {
 630             /* New line: reset all formatting state for the next paragraph. */
 631             mcview_state_machine_init (state, state->offset);
 632             if (linewidth != NULL)
 633                 *linewidth = col;
 634             return 1;
 635         }
 636 
 637         if (mcview_is_non_spacing_mark (view, cs[0]))
 638         {
 639             /* Lonely combining character. Probably leftover after too many combining chars. Just ignore. */
 640             continue;
 641         }
 642 
 643         /* Nonprintable, or lonely spacing mark */
 644         if ((!mcview_isprint (view, cs[0]) || mcview_ismark (view, cs[0])) && cs[0] != '\t')
 645             cs[0] = '.';
 646 
 647         for (i = 0; i < n; i++)
 648             charwidth += mcview_wcwidth (view, cs[i]);
 649 
 650         /* Adjust the width for TAB. It's handled below along with the normal characters,
 651          * so that it's wrapped consistently with them, and is painted with the proper
 652          * attributes (although currently it can't have a special color). */
 653         if (cs[0] == '\t')
 654         {
 655             charwidth = option_tab_spacing - state->unwrapped_column % option_tab_spacing;
 656             state->print_lonely_combining = TRUE;
 657         }
 658         else
 659             state->print_lonely_combining = FALSE;
 660 
 661         /* In wrap mode only: We're done with this row if the character sequence wouldn't fit.
 662          * Except if at the first column, because then it wouldn't fit in the next row either.
 663          * In this extreme case let the unwrapped code below do its best to display it. */
 664         if (view->mode_flags.wrap && (off_t) col + charwidth > dpy_text_column + (off_t) width
 665             && col > 0)
 666         {
 667             *state = state_saved;
 668             if (paragraph_ended != NULL)
 669                 *paragraph_ended = FALSE;
 670             if (linewidth != NULL)
 671                 *linewidth = col;
 672             return 1;
 673         }
 674 
 675         /* Display, unless outside of the viewport. */
 676         if (row >= 0 && row < (int) height)
 677         {
 678             if ((off_t) col >= dpy_text_column &&
 679                 (off_t) col + charwidth <= dpy_text_column + (off_t) width)
 680             {
 681                 /* The combining character sequence fits entirely in the viewport. Print it. */
 682                 tty_setcolor (color);
 683                 widget_move (view, top + row, left + ((off_t) col - dpy_text_column));
 684                 if (cs[0] == '\t')
 685                 {
 686                     for (i = 0; i < charwidth; i++)
 687                         tty_print_char (' ');
 688                 }
 689                 else
 690                 {
 691                     j = 0;
 692                     for (i = 0; i < n; i++)
 693                         j += mcview_char_display (view, cs[i], str + j);
 694                     str[j] = '\0';
 695                     /* This is probably a bug in our tty layer, but tty_print_string
 696                      * normalizes the string, whereas tty_printf doesn't. Don't normalize,
 697                      * since we handle combining characters ourselves correctly, it's
 698                      * better if they are copy-pasted correctly. Ticket 3255. */
 699                     tty_printf ("%s", str);
 700                 }
 701             }
 702             else if ((off_t) col < dpy_text_column && (off_t) col + charwidth > dpy_text_column)
 703             {
 704                 /* The combining character sequence would cross the left edge of the viewport.
 705                  * This cannot happen with wrap mode. Print replacement character(s),
 706                  * or spaces with the correct attributes for partial Tabs. */
 707                 tty_setcolor (color);
 708                 for (i = dpy_text_column;
 709                      i < (off_t) col + charwidth && i < dpy_text_column + (off_t) width; i++)
 710                 {
 711                     widget_move (view, top + row, left + (i - dpy_text_column));
 712                     tty_print_anychar ((cs[0] == '\t') ? ' ' : PARTIAL_CJK_AT_LEFT_MARGIN);
 713                 }
 714             }
 715             else if ((off_t) col < dpy_text_column + (off_t) width &&
 716                      (off_t) col + charwidth > dpy_text_column + (off_t) width)
 717             {
 718                 /* The combining character sequence would cross the right edge of the viewport
 719                  * and we're not wrapping. Print replacement character(s),
 720                  * or spaces with the correct attributes for partial Tabs. */
 721                 tty_setcolor (color);
 722                 for (i = col; i < dpy_text_column + (off_t) width; i++)
 723                 {
 724                     widget_move (view, top + row, left + (i - dpy_text_column));
 725                     tty_print_anychar ((cs[0] == '\t') ? ' ' : PARTIAL_CJK_AT_RIGHT_MARGIN);
 726                 }
 727             }
 728         }
 729 
 730         col += charwidth;
 731         state->unwrapped_column += charwidth;
 732 
 733         if (!view->mode_flags.wrap && (off_t) col >= dpy_text_column + (off_t) width
 734             && linewidth == NULL)
 735         {
 736             /* Optimization: Fast forward to the end of the line, rather than carefully
 737              * parsing and then not actually displaying it. */
 738             off_t eol;
 739 
 740             eol = mcview_eol (view, state->offset);
 741             mcview_state_machine_init (state, eol);
 742             return 1;
 743         }
 744     }
 745 }
 746 
 747 /* --------------------------------------------------------------------------------------------- */
 748 /**
 749  * Parse, format and possibly display one paragraph (perhaps not from the beginning).
 750  *
 751  * Formatting starts at the given "state" (which encodes the file offset and parser and formatter's
 752  * internal state). In unwrap mode, this should point to the beginning of the paragraph with the
 753  * default state, the additional horizontal scrolling is added here. In wrap mode, this may point
 754  * to the beginning of the line within a paragraph (to display the partial paragraph at the top),
 755  * with the proper state at that point.
 756  *
 757  * Displaying the next paragraph should start at "state"'s new value, or if we displayed the bottom
 758  * line then state->offset tells the file offset to be shown in the top bar.
 759  *
 760  * If "row" is negative, don't display the first abs(row) lines and display the rest from the top.
 761  * This was a nice idea but it's now unused :)
 762  *
 763  * If "row" is too large, don't display the paragraph at all but still return the number of lines.
 764  * This is used when moving upwards.
 765  *
 766  * @param view ...
 767  * @param state the parser-formatter state machine's state, updated
 768  * @param row print starting at this row
 769  * @return the number of rows the paragraphs is wrapped to, that is, 0 if we were already at EOF,
 770  *   otherwise 1 in unwrap mode, >= 1 in wrap mode. We stop when reaching the bottom of the
 771  *   viewport, it's not counted how many more lines the paragraph would occupy
 772  */
 773 static int
 774 mcview_display_paragraph (WView * view, mcview_state_machine_t * state, int row)
     /* [previous][next][first][last][top][bottom][index][help]  */
 775 {
 776     const screen_dimen height = view->data_area.height;
 777     int lines = 0;
 778 
 779     while (TRUE)
 780     {
 781         gboolean paragraph_ended;
 782 
 783         lines += mcview_display_line (view, state, row, &paragraph_ended, NULL);
 784         if (paragraph_ended)
 785             return lines;
 786 
 787         if (row < (int) height)
 788         {
 789             row++;
 790             /* stop if bottom of screen reached */
 791             if (row >= (int) height)
 792                 return lines;
 793         }
 794     }
 795 }
 796 
 797 /* --------------------------------------------------------------------------------------------- */
 798 /**
 799  * Recompute dpy_state_top from dpy_start and dpy_paragraph_skip_lines. Clamp
 800  * dpy_paragraph_skip_lines if necessary.
 801  *
 802  * This method should be called in wrap mode after changing one of the parsing or formatting
 803  * properties (e.g. window width, encoding, nroff), or when switching to wrap mode from unwrap or
 804  * hex.
 805  *
 806  * If we stayed within the same paragraph then try to keep the vertical offset within that
 807  * paragraph as well. It might happen though that the paragraph became shorter than our desired
 808  * vertical position, in that case move to its last row.
 809  */
 810 static void
 811 mcview_wrap_fixup (WView * view)
     /* [previous][next][first][last][top][bottom][index][help]  */
 812 {
 813     int lines = view->dpy_paragraph_skip_lines;
 814 
 815     if (!view->dpy_wrap_dirty)
 816         return;
 817     view->dpy_wrap_dirty = FALSE;
 818 
 819     view->dpy_paragraph_skip_lines = 0;
 820     mcview_state_machine_init (&view->dpy_state_top, view->dpy_start);
 821 
 822     while (lines-- != 0)
 823     {
 824         mcview_state_machine_t state_prev;
 825         gboolean paragraph_ended;
 826 
 827         state_prev = view->dpy_state_top;
 828         if (mcview_display_line (view, &view->dpy_state_top, -1, &paragraph_ended, NULL) == 0)
 829             break;
 830         if (paragraph_ended)
 831         {
 832             view->dpy_state_top = state_prev;
 833             break;
 834         }
 835         view->dpy_paragraph_skip_lines++;
 836     }
 837 }
 838 
 839 /* --------------------------------------------------------------------------------------------- */
 840 /*** public functions ****************************************************************************/
 841 /* --------------------------------------------------------------------------------------------- */
 842 
 843 /**
 844  * In both wrap and unwrap modes, dpy_start points to the beginning of the paragraph.
 845  *
 846  * In unwrap mode, start displaying from this position, probably applying an additional horizontal
 847  * scroll.
 848  *
 849  * In wrap mode, an additional dpy_paragraph_skip_lines lines are skipped from the top of this
 850  * paragraph. dpy_state_top contains the position and parser-formatter state corresponding to the
 851  * top left corner so we can just start rendering from here. Unless dpy_wrap_dirty is set in which
 852  * case dpy_state_top is invalid and we need to recompute first.
 853  */
 854 void
 855 mcview_display_text (WView * view)
     /* [previous][next][first][last][top][bottom][index][help]  */
 856 {
 857     const screen_dimen left = view->data_area.left;
 858     const screen_dimen top = view->data_area.top;
 859     const screen_dimen height = view->data_area.height;
 860     int row;
 861     mcview_state_machine_t state;
 862     gboolean again;
 863 
 864     do
 865     {
 866         int n;
 867 
 868         again = FALSE;
 869 
 870         mcview_display_clean (view);
 871         mcview_display_ruler (view);
 872 
 873         if (!view->mode_flags.wrap)
 874             mcview_state_machine_init (&state, view->dpy_start);
 875         else
 876         {
 877             mcview_wrap_fixup (view);
 878             state = view->dpy_state_top;
 879         }
 880 
 881         for (row = 0; row < (int) height; row += n)
 882         {
 883             n = mcview_display_paragraph (view, &state, row);
 884             if (n == 0)
 885             {
 886                 /* In the rare case that displaying didn't start at the beginning
 887                  * of the file, yet there are some empty lines at the bottom,
 888                  * scroll the file and display again. This happens when e.g. the
 889                  * window is made bigger, or the file becomes shorter due to
 890                  * charset change or enabling nroff. */
 891                 if ((view->mode_flags.wrap ? view->dpy_state_top.offset : view->dpy_start) > 0)
 892                 {
 893                     mcview_ascii_move_up (view, height - row);
 894                     again = TRUE;
 895                 }
 896                 break;
 897             }
 898         }
 899     }
 900     while (again);
 901 
 902     view->dpy_end = state.offset;
 903     view->dpy_state_bottom = state;
 904 
 905     tty_setcolor (VIEW_NORMAL_COLOR);
 906     if (mcview_show_eof != NULL && mcview_show_eof[0] != '\0')
 907         while (row < (int) height)
 908         {
 909             widget_move (view, top + row, left);
 910             /* TODO: should make it no wider than the viewport */
 911             tty_print_string (mcview_show_eof);
 912             row++;
 913         }
 914 }
 915 
 916 /* --------------------------------------------------------------------------------------------- */
 917 /**
 918  * Move down.
 919  *
 920  * It's very simple. Just invisibly format the next "lines" lines, carefully carrying the formatter
 921  * state in wrap mode. But before each step we need to check if we've already hit the end of the
 922  * file, in that case we can no longer move. This is done by walking from dpy_state_bottom.
 923  *
 924  * Note that this relies on mcview_display_text() setting dpy_state_bottom to its correct value
 925  * upon rendering the screen contents. So don't call this function from other functions (e.g. at
 926  * the bottom of mcview_ascii_move_up()) which invalidate this value.
 927  */
 928 void
 929 mcview_ascii_move_down (WView * view, off_t lines)
     /* [previous][next][first][last][top][bottom][index][help]  */
 930 {
 931     while (lines-- != 0)
 932     {
 933         gboolean paragraph_ended;
 934 
 935         /* See if there's still data below the bottom line, by imaginarily displaying one
 936          * more line. This takes care of reading more data into growbuf, if required.
 937          * If the end position didn't advance, we're at EOF and hence bail out. */
 938         if (mcview_display_line (view, &view->dpy_state_bottom, -1, &paragraph_ended, NULL) == 0)
 939             break;
 940 
 941         /* Okay, there's enough data. Move by 1 row at the top, too. No need to check for
 942          * EOF, that can't happen. */
 943         if (!view->mode_flags.wrap)
 944         {
 945             view->dpy_start = mcview_eol (view, view->dpy_start);
 946             view->dpy_paragraph_skip_lines = 0;
 947             view->dpy_wrap_dirty = TRUE;
 948         }
 949         else
 950         {
 951             mcview_display_line (view, &view->dpy_state_top, -1, &paragraph_ended, NULL);
 952             if (!paragraph_ended)
 953                 view->dpy_paragraph_skip_lines++;
 954             else
 955             {
 956                 view->dpy_start = view->dpy_state_top.offset;
 957                 view->dpy_paragraph_skip_lines = 0;
 958             }
 959         }
 960     }
 961 }
 962 
 963 /* --------------------------------------------------------------------------------------------- */
 964 /**
 965  * Move up.
 966  *
 967  * Unwrap mode: Piece of cake. Wrap mode: If we'd walk back more than the current line offset
 968  * within the paragraph, we need to jump back to the previous paragraph and compute its height to
 969  * see if we start from that paragraph, and repeat this if necessary. Once we're within the desired
 970  * paragraph, we still need to format it from its beginning to know the state.
 971  *
 972  * See the top of this file for comments about MAX_BACKWARDS_WALK_IN_PARAGRAPH.
 973  *
 974  * force_max is a nice protection against the rare extreme case that the file underneath us
 975  * changes, we don't want to endlessly consume a file of maybe full of zeros upon moving upwards.
 976  */
 977 void
 978 mcview_ascii_move_up (WView * view, off_t lines)
     /* [previous][next][first][last][top][bottom][index][help]  */
 979 {
 980     if (!view->mode_flags.wrap)
 981     {
 982         while (lines-- != 0)
 983             view->dpy_start = mcview_bol (view, view->dpy_start - 1, 0);
 984         view->dpy_paragraph_skip_lines = 0;
 985         view->dpy_wrap_dirty = TRUE;
 986     }
 987     else
 988     {
 989         int i;
 990 
 991         while (lines > view->dpy_paragraph_skip_lines)
 992         {
 993             /* We need to go back to the previous paragraph. */
 994             if (view->dpy_start == 0)
 995             {
 996                 /* Oops, we're already in the first paragraph. */
 997                 view->dpy_paragraph_skip_lines = 0;
 998                 mcview_state_machine_init (&view->dpy_state_top, 0);
 999                 return;
1000             }
1001             lines -= view->dpy_paragraph_skip_lines;
1002             view->force_max = view->dpy_start;
1003             view->dpy_start =
1004                 mcview_bol (view, view->dpy_start - 1,
1005                             view->dpy_start - MAX_BACKWARDS_WALK_IN_PARAGRAPH);
1006             mcview_state_machine_init (&view->dpy_state_top, view->dpy_start);
1007             /* This is a tricky way of denoting that we're at the end of the paragraph.
1008              * Normally we'd jump to the next paragraph and reset paragraph_skip_lines. But for
1009              * walking backwards this is exactly what we need. */
1010             view->dpy_paragraph_skip_lines =
1011                 mcview_display_paragraph (view, &view->dpy_state_top, view->data_area.height);
1012             view->force_max = -1;
1013         }
1014 
1015         /* Okay, we have have dpy_start pointing to the desired paragraph, and we still need to
1016          * walk back "lines" lines from the current "dpy_paragraph_skip_lines" offset. We can't do
1017          * that, so walk from the beginning of the paragraph. */
1018         mcview_state_machine_init (&view->dpy_state_top, view->dpy_start);
1019         view->dpy_paragraph_skip_lines -= lines;
1020         for (i = 0; i < view->dpy_paragraph_skip_lines; i++)
1021             mcview_display_line (view, &view->dpy_state_top, -1, NULL, NULL);
1022     }
1023 }
1024 
1025 /* --------------------------------------------------------------------------------------------- */
1026 
1027 void
1028 mcview_ascii_moveto_bol (WView * view)
     /* [previous][next][first][last][top][bottom][index][help]  */
1029 {
1030     if (!view->mode_flags.wrap)
1031         view->dpy_text_column = 0;
1032 }
1033 
1034 /* --------------------------------------------------------------------------------------------- */
1035 
1036 void
1037 mcview_ascii_moveto_eol (WView * view)
     /* [previous][next][first][last][top][bottom][index][help]  */
1038 {
1039     if (!view->mode_flags.wrap)
1040     {
1041         mcview_state_machine_t state;
1042         off_t linewidth;
1043 
1044         /* Get the width of the topmost paragraph. */
1045         mcview_state_machine_init (&state, view->dpy_start);
1046         mcview_display_line (view, &state, -1, NULL, &linewidth);
1047         view->dpy_text_column = mcview_offset_doz (linewidth, (off_t) view->data_area.width);
1048     }
1049 }
1050 
1051 /* --------------------------------------------------------------------------------------------- */
1052 
1053 void
1054 mcview_state_machine_init (mcview_state_machine_t * state, off_t offset)
     /* [previous][next][first][last][top][bottom][index][help]  */
1055 {
1056     memset (state, 0, sizeof (*state));
1057     state->offset = offset;
1058     state->print_lonely_combining = TRUE;
1059 }
1060 
1061 /* --------------------------------------------------------------------------------------------- */

/* [previous][next][first][last][top][bottom][index][help]  */