xref: /freebsd/contrib/less/line.c (revision 64db83a8ab2d1f72a9b2174b39d2ef42b5b0580c)
1 /*
2  * Copyright (C) 1984-2000  Mark Nudelman
3  *
4  * You may distribute under the terms of either the GNU General Public
5  * License or the Less License, as specified in the README file.
6  *
7  * For more information about less, or for information on how to
8  * contact the author, see the README file.
9  */
10 
11 
12 /*
13  * Routines to manipulate the "line buffer".
14  * The line buffer holds a line of output as it is being built
15  * in preparation for output to the screen.
16  */
17 
18 #include "less.h"
19 
20 #define IS_CONT(c)  (((c) & 0xC0) == 0x80)
21 
22 /* Buffer which holds the current output line */
23 public char linebuf[LINEBUF_SIZE];
24 public int size_linebuf = sizeof(linebuf);
25 
26 public int cshift;		/* Current left-shift of output line buffer */
27 public int hshift;		/* Desired left-shift of output line buffer */
28 
29 static char attr[LINEBUF_SIZE];	/* Extension of linebuf to hold attributes */
30 static int curr;		/* Index into linebuf */
31 static int column;		/* Printable length, accounting for
32 				   backspaces, etc. */
33 static int overstrike;		/* Next char should overstrike previous char */
34 static int is_null_line;	/* There is no current line */
35 static char pendc;
36 static POSITION pendpos;
37 static char *end_ansi_chars;
38 
39 static int do_append();
40 
41 extern int bs_mode;
42 extern int tabstop;
43 extern int linenums;
44 extern int ctldisp;
45 extern int twiddle;
46 extern int binattr;
47 extern int auto_wrap, ignaw;
48 extern int bo_s_width, bo_e_width;
49 extern int ul_s_width, ul_e_width;
50 extern int bl_s_width, bl_e_width;
51 extern int so_s_width, so_e_width;
52 extern int sc_width, sc_height;
53 extern int utf_mode;
54 
55 /*
56  * Initialize from environment variables.
57  */
58 	public void
59 init_line()
60 {
61 	end_ansi_chars = lgetenv("LESSANSIENDCHARS");
62 	if (end_ansi_chars == NULL || *end_ansi_chars == '\0')
63 		end_ansi_chars = "m";
64 }
65 
66 /*
67  * Rewind the line buffer.
68  */
69 	public void
70 prewind()
71 {
72 	curr = 0;
73 	column = 0;
74 	overstrike = 0;
75 	is_null_line = 0;
76 	pendc = '\0';
77 }
78 
79 /*
80  * Insert the line number (of the given position) into the line buffer.
81  */
82 	public void
83 plinenum(pos)
84 	POSITION pos;
85 {
86 	register int lno;
87 	register int i;
88 	register int n;
89 
90 	/*
91 	 * We display the line number at the start of each line
92 	 * only if the -N option is set.
93 	 */
94 	if (linenums != OPT_ONPLUS)
95 		return;
96 
97 	/*
98 	 * Get the line number and put it in the current line.
99 	 * {{ Note: since find_linenum calls forw_raw_line,
100 	 *    it may seek in the input file, requiring the caller
101 	 *    of plinenum to re-seek if necessary. }}
102 	 */
103 	lno = find_linenum(pos);
104 
105 	sprintf(&linebuf[curr], "%6d", lno);
106 	n = strlen(&linebuf[curr]);
107 	column += n;
108 	for (i = 0;  i < n;  i++)
109 		attr[curr++] = 0;
110 
111 	/*
112 	 * Append enough spaces to bring us to the next tab stop.
113 	 * {{ We could avoid this at the cost of adding some
114 	 *    complication to the tab stop logic in pappend(). }}
115 	 */
116 	if (tabstop == 0)
117 		tabstop = 1;
118 	do
119 	{
120 		linebuf[curr] = ' ';
121 		attr[curr++] = AT_NORMAL;
122 		column++;
123 	} while (((column + cshift) % tabstop) != 0);
124 }
125 
126 /*
127  *
128  */
129 	static int
130 utf_len(char *s, int len)
131 {
132 	int ulen = 0;
133 
134 	while (*s != '\0' && len > 0)
135 	{
136 		if (!IS_CONT(*s))
137 			len--;
138 		s++;
139 		ulen++;
140 	}
141 	while (IS_CONT(*s))
142 	{
143 		s++;
144 		ulen++;
145 	}
146 	return (ulen);
147 }
148 
149 /*
150  * Shift the input line left.
151  * This means discarding N printable chars at the start of the buffer.
152  */
153 	static void
154 pshift(shift)
155 	int shift;
156 {
157 	int i;
158 	int real_shift;
159 
160 	if (shift > column)
161 		shift = column;
162 	if (shift > curr)
163 		shift = curr;
164 
165 	if (!utf_mode)
166 		real_shift = shift;
167 	else
168 	{
169 		real_shift = utf_len(linebuf, shift);
170 		if (real_shift > curr)
171 			real_shift = curr;
172 	}
173 	for (i = 0;  i < curr - real_shift;  i++)
174 	{
175 		linebuf[i] = linebuf[i + real_shift];
176 		attr[i] = attr[i + real_shift];
177 	}
178 	column -= shift;
179 	curr -= real_shift;
180 	cshift += shift;
181 }
182 
183 /*
184  * Return the printing width of the start (enter) sequence
185  * for a given character attribute.
186  */
187 	static int
188 attr_swidth(a)
189 	int a;
190 {
191 	switch (a)
192 	{
193 	case AT_BOLD:		return (bo_s_width);
194 	case AT_UNDERLINE:	return (ul_s_width);
195 	case AT_BLINK:		return (bl_s_width);
196 	case AT_STANDOUT:	return (so_s_width);
197 	}
198 	return (0);
199 }
200 
201 /*
202  * Return the printing width of the end (exit) sequence
203  * for a given character attribute.
204  */
205 	static int
206 attr_ewidth(a)
207 	int a;
208 {
209 	switch (a)
210 	{
211 	case AT_BOLD:		return (bo_e_width);
212 	case AT_UNDERLINE:	return (ul_e_width);
213 	case AT_BLINK:		return (bl_e_width);
214 	case AT_STANDOUT:	return (so_e_width);
215 	}
216 	return (0);
217 }
218 
219 /*
220  * Return the printing width of a given character and attribute,
221  * if the character were added to the current position in the line buffer.
222  * Adding a character with a given attribute may cause an enter or exit
223  * attribute sequence to be inserted, so this must be taken into account.
224  */
225 	static int
226 pwidth(c, a)
227 	int c;
228 	int a;
229 {
230 	register int w;
231 
232 	if (utf_mode && IS_CONT(c))
233 		return (0);
234 
235 	if (c == '\b')
236 		/*
237 		 * Backspace moves backwards one position.
238 		 */
239 		return (-1);
240 
241 	if (control_char(c))
242 		/*
243 		 * Control characters do unpredicatable things,
244 		 * so we don't even try to guess; say it doesn't move.
245 		 * This can only happen if the -r flag is in effect.
246 		 */
247 		return (0);
248 
249 	/*
250 	 * Other characters take one space,
251 	 * plus the width of any attribute enter/exit sequence.
252 	 */
253 	w = 1;
254 	if (curr > 0 && attr[curr-1] != a)
255 		w += attr_ewidth(attr[curr-1]);
256 	if (a && (curr == 0 || attr[curr-1] != a))
257 		w += attr_swidth(a);
258 	return (w);
259 }
260 
261 /*
262  * Delete the previous character in the line buffer.
263  */
264 	static void
265 backc()
266 {
267 	curr--;
268 	column -= pwidth(linebuf[curr], attr[curr]);
269 }
270 
271 /*
272  * Are we currently within a recognized ANSI escape sequence?
273  */
274 	static int
275 in_ansi_esc_seq()
276 {
277 	int i;
278 
279 	/*
280 	 * Search backwards for either an ESC (which means we ARE in a seq);
281 	 * or an end char (which means we're NOT in a seq).
282 	 */
283 	for (i = curr-1;  i >= 0;  i--)
284 	{
285 		if (linebuf[i] == ESC)
286 			return (1);
287 		if (strchr(end_ansi_chars, linebuf[i]) != NULL)
288 			return (0);
289 	}
290 	return (0);
291 }
292 
293 /*
294  * Append a character and attribute to the line buffer.
295  */
296 	static int
297 storec(c, a, pos)
298 	int c;
299 	int a;
300 	POSITION pos;
301 {
302 	register int w;
303 
304 #if HILITE_SEARCH
305 	if (is_hilited(pos, pos+1, 0))
306 		/*
307 		 * This character should be highlighted.
308 		 * Override the attribute passed in.
309 		 */
310 		a = AT_STANDOUT;
311 #endif
312 	if (ctldisp == OPT_ONPLUS && in_ansi_esc_seq())
313 		w = 0;
314 	else
315 		w = pwidth(c, a);
316 	if (ctldisp != OPT_ON && column + w + attr_ewidth(a) > sc_width)
317 		/*
318 		 * Won't fit on screen.
319 		 */
320 		return (1);
321 
322 	if (curr >= sizeof(linebuf)-2)
323 		/*
324 		 * Won't fit in line buffer.
325 		 */
326 		return (1);
327 
328 	/*
329 	 * Special handling for "magic cookie" terminals.
330 	 * If an attribute enter/exit sequence has a printing width > 0,
331 	 * and the sequence is adjacent to a space, delete the space.
332 	 * We just mark the space as invisible, to avoid having too
333 	 * many spaces deleted.
334 	 * {{ Note that even if the attribute width is > 1, we
335 	 *    delete only one space.  It's not worth trying to do more.
336 	 *    It's hardly worth doing this much. }}
337 	 */
338 	if (curr > 0 && a != AT_NORMAL &&
339 		linebuf[curr-1] == ' ' && attr[curr-1] == AT_NORMAL &&
340 		attr_swidth(a) > 0)
341 	{
342 		/*
343 		 * We are about to append an enter-attribute sequence
344 		 * just after a space.  Delete the space.
345 		 */
346 		attr[curr-1] = AT_INVIS;
347 		column--;
348 	} else if (curr > 0 && attr[curr-1] != AT_NORMAL &&
349 		attr[curr-1] != AT_INVIS && c == ' ' && a == AT_NORMAL &&
350 		attr_ewidth(attr[curr-1]) > 0)
351 	{
352 		/*
353 		 * We are about to append a space just after an
354 		 * exit-attribute sequence.  Delete the space.
355 		 */
356 		a = AT_INVIS;
357 		column--;
358 	}
359 	/* End of magic cookie handling. */
360 
361 	linebuf[curr] = c;
362 	attr[curr] = a;
363 	column += w;
364 	return (0);
365 }
366 
367 /*
368  * Append a character to the line buffer.
369  * Expand tabs into spaces, handle underlining, boldfacing, etc.
370  * Returns 0 if ok, 1 if couldn't fit in buffer.
371  */
372 	public int
373 pappend(c, pos)
374 	register int c;
375 	POSITION pos;
376 {
377 	int r;
378 
379 	if (pendc)
380 	{
381 		if (do_append(pendc, pendpos))
382 			/*
383 			 * Oops.  We've probably lost the char which
384 			 * was in pendc, since caller won't back up.
385 			 */
386 			return (1);
387 		pendc = '\0';
388 	}
389 
390 	if (c == '\r' && bs_mode == BS_SPECIAL)
391 	{
392 		/*
393 		 * Don't put the CR into the buffer until we see
394 		 * the next char.  If the next char is a newline,
395 		 * discard the CR.
396 		 */
397 		pendc = c;
398 		pendpos = pos;
399 		return (0);
400 	}
401 
402 	r = do_append(c, pos);
403 	/*
404 	 * If we need to shift the line, do it.
405 	 * But wait until we get to at least the middle of the screen,
406 	 * so shifting it doesn't affect the chars we're currently
407 	 * pappending.  (Bold & underline can get messed up otherwise.)
408 	 */
409 	if (cshift < hshift && column > sc_width / 2)
410 		pshift(hshift - cshift);
411 	return (r);
412 }
413 
414 	static int
415 do_append(c, pos)
416 	int c;
417 	POSITION pos;
418 {
419 	register char *s;
420 	register int a;
421 
422 #define	STOREC(c,a) \
423 	if (storec((c),(a),pos)) return (1); else curr++
424 
425 	if (c == '\b')
426 	{
427 		switch (bs_mode)
428 		{
429 		case BS_NORMAL:
430 			STOREC(c, AT_NORMAL);
431 			break;
432 		case BS_CONTROL:
433 			goto do_control_char;
434 		case BS_SPECIAL:
435 			if (curr == 0)
436 				break;
437 			backc();
438 			overstrike = 1;
439 			break;
440 		}
441 	} else if (overstrike)
442 	{
443 		/*
444 		 * Overstrike the character at the current position
445 		 * in the line buffer.  This will cause either
446 		 * underline (if a "_" is overstruck),
447 		 * bold (if an identical character is overstruck),
448 		 * or just deletion of the character in the buffer.
449 		 */
450 		overstrike = 0;
451 		if ((char)c == linebuf[curr])
452 			STOREC(linebuf[curr], AT_BOLD);
453 		else if (c == '_')
454 			STOREC(linebuf[curr], AT_UNDERLINE);
455 		else if (linebuf[curr] == '_')
456 			STOREC(c, AT_UNDERLINE);
457 		else if (control_char(c))
458 			goto do_control_char;
459 		else
460 			STOREC(c, AT_NORMAL);
461 	} else if (c == '\t')
462 	{
463 		/*
464 		 * Expand a tab into spaces.
465 		 */
466 		if (tabstop == 0)
467 			tabstop = 1;
468 		switch (bs_mode)
469 		{
470 		case BS_CONTROL:
471 			goto do_control_char;
472 		case BS_NORMAL:
473 		case BS_SPECIAL:
474 			do
475 			{
476 				STOREC(' ', AT_NORMAL);
477 			} while (((column + cshift) % tabstop) != 0);
478 			break;
479 		}
480 	} else if (control_char(c))
481 	{
482 	do_control_char:
483 		if (ctldisp == OPT_ON || (ctldisp == OPT_ONPLUS && c == ESC))
484 		{
485 			/*
486 			 * Output as a normal character.
487 			 */
488 			STOREC(c, AT_NORMAL);
489 		} else
490 		{
491 			/*
492 			 * Convert to printable representation.
493 			 */
494 			s = prchar(c);
495 			a = binattr;
496 
497 			/*
498 			 * Make sure we can get the entire representation
499 			 * of the character on this line.
500 			 */
501 			if (column + (int) strlen(s) +
502 			    attr_swidth(a) + attr_ewidth(a) > sc_width)
503 				return (1);
504 
505 			for ( ;  *s != 0;  s++)
506 				STOREC(*s, a);
507 		}
508 	} else
509 	{
510 		STOREC(c, AT_NORMAL);
511 	}
512 
513 	return (0);
514 }
515 
516 /*
517  * Terminate the line in the line buffer.
518  */
519 	public void
520 pdone(endline)
521 	int endline;
522 {
523 	if (pendc && (pendc != '\r' || !endline))
524 		/*
525 		 * If we had a pending character, put it in the buffer.
526 		 * But discard a pending CR if we are at end of line
527 		 * (that is, discard the CR in a CR/LF sequence).
528 		 */
529 		(void) do_append(pendc, pendpos);
530 
531 	/*
532 	 * Make sure we've shifted the line, if we need to.
533 	 */
534 	if (cshift < hshift)
535 		pshift(hshift - cshift);
536 
537 	/*
538 	 * Add a newline if necessary,
539 	 * and append a '\0' to the end of the line.
540 	 */
541 	if (column < sc_width || !auto_wrap || ignaw || ctldisp == OPT_ON)
542 	{
543 		linebuf[curr] = '\n';
544 		attr[curr] = AT_NORMAL;
545 		curr++;
546 	}
547 	linebuf[curr] = '\0';
548 	attr[curr] = AT_NORMAL;
549 	/*
550 	 * If we are done with this line, reset the current shift.
551 	 */
552 	if (endline)
553 		cshift = 0;
554 }
555 
556 /*
557  * Get a character from the current line.
558  * Return the character as the function return value,
559  * and the character attribute in *ap.
560  */
561 	public int
562 gline(i, ap)
563 	register int i;
564 	register int *ap;
565 {
566 	char *s;
567 
568 	if (is_null_line)
569 	{
570 		/*
571 		 * If there is no current line, we pretend the line is
572 		 * either "~" or "", depending on the "twiddle" flag.
573 		 */
574 		*ap = AT_BOLD;
575 		s = (twiddle) ? "~\n" : "\n";
576 		return (s[i]);
577 	}
578 
579 	*ap = attr[i];
580 	return (linebuf[i] & 0377);
581 }
582 
583 /*
584  * Indicate that there is no current line.
585  */
586 	public void
587 null_line()
588 {
589 	is_null_line = 1;
590 	cshift = 0;
591 }
592 
593 /*
594  * Analogous to forw_line(), but deals with "raw lines":
595  * lines which are not split for screen width.
596  * {{ This is supposed to be more efficient than forw_line(). }}
597  */
598 	public POSITION
599 forw_raw_line(curr_pos, linep)
600 	POSITION curr_pos;
601 	char **linep;
602 {
603 	register char *p;
604 	register int c;
605 	POSITION new_pos;
606 
607 	if (curr_pos == NULL_POSITION || ch_seek(curr_pos) ||
608 		(c = ch_forw_get()) == EOI)
609 		return (NULL_POSITION);
610 
611 	p = linebuf;
612 
613 	for (;;)
614 	{
615 		if (c == '\n' || c == EOI)
616 		{
617 			new_pos = ch_tell();
618 			break;
619 		}
620 		if (p >= &linebuf[sizeof(linebuf)-1])
621 		{
622 			/*
623 			 * Overflowed the input buffer.
624 			 * Pretend the line ended here.
625 			 * {{ The line buffer is supposed to be big
626 			 *    enough that this never happens. }}
627 			 */
628 			new_pos = ch_tell() - 1;
629 			break;
630 		}
631 		*p++ = c;
632 		c = ch_forw_get();
633 	}
634 	*p = '\0';
635 	if (linep != NULL)
636 		*linep = linebuf;
637 	return (new_pos);
638 }
639 
640 /*
641  * Analogous to back_line(), but deals with "raw lines".
642  * {{ This is supposed to be more efficient than back_line(). }}
643  */
644 	public POSITION
645 back_raw_line(curr_pos, linep)
646 	POSITION curr_pos;
647 	char **linep;
648 {
649 	register char *p;
650 	register int c;
651 	POSITION new_pos;
652 
653 	if (curr_pos == NULL_POSITION || curr_pos <= ch_zero() ||
654 		ch_seek(curr_pos-1))
655 		return (NULL_POSITION);
656 
657 	p = &linebuf[sizeof(linebuf)];
658 	*--p = '\0';
659 
660 	for (;;)
661 	{
662 		c = ch_back_get();
663 		if (c == '\n')
664 		{
665 			/*
666 			 * This is the newline ending the previous line.
667 			 * We have hit the beginning of the line.
668 			 */
669 			new_pos = ch_tell() + 1;
670 			break;
671 		}
672 		if (c == EOI)
673 		{
674 			/*
675 			 * We have hit the beginning of the file.
676 			 * This must be the first line in the file.
677 			 * This must, of course, be the beginning of the line.
678 			 */
679 			new_pos = ch_zero();
680 			break;
681 		}
682 		if (p <= linebuf)
683 		{
684 			/*
685 			 * Overflowed the input buffer.
686 			 * Pretend the line ended here.
687 			 */
688 			new_pos = ch_tell() + 1;
689 			break;
690 		}
691 		*--p = c;
692 	}
693 	if (linep != NULL)
694 		*linep = p;
695 	return (new_pos);
696 }
697