xref: /freebsd/contrib/less/input.c (revision d5cb458b4b58b0f0b3c058a32439f232fd5455ca)
1 /*
2  * Copyright (C) 1984-2025  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, see the README file.
8  */
9 
10 /*
11  * High level routines dealing with getting lines of input
12  * from the file being viewed.
13  *
14  * When we speak of "lines" here, we mean PRINTABLE lines;
15  * lines processed with respect to the screen width.
16  * We use the term "raw line" to refer to lines simply
17  * delimited by newlines; not processed with respect to screen width.
18  */
19 
20 #include "less.h"
21 
22 extern int squeeze;
23 extern int hshift;
24 extern int quit_if_one_screen;
25 extern int ignore_eoi;
26 extern int status_col;
27 extern int wordwrap;
28 extern POSITION start_attnpos;
29 extern POSITION end_attnpos;
30 #if HILITE_SEARCH
31 extern int hilite_search;
32 extern int show_attn;
33 #endif
34 
35 /*
36  * Set the status column.
37  *  base  Position of first char in line.
38  *  disp  First visible char.
39  *        Different than base_pos if line is shifted.
40  *  edisp Last visible char.
41  *  eol   End of line. Normally the newline.
42  *        Different than edisp if line is chopped.
43  */
init_status_col(POSITION base_pos,POSITION disp_pos,POSITION edisp_pos,POSITION eol_pos)44 static void init_status_col(POSITION base_pos, POSITION disp_pos, POSITION edisp_pos, POSITION eol_pos)
45 {
46 	int hl_before = (chop_line() && disp_pos != NULL_POSITION) ?
47 	    is_hilited_attr(base_pos, disp_pos, TRUE, NULL) : 0;
48 	int hl_after = (chop_line() && edisp_pos != NULL_POSITION) ?
49 	    is_hilited_attr(edisp_pos, eol_pos, TRUE, NULL) : 0;
50 	int attr;
51 	char ch;
52 
53 	if (hl_before && hl_after)
54 	{
55 		attr = hl_after;
56 		ch = '=';
57 	} else if (hl_before)
58 	{
59 		attr = hl_before;
60 		ch = '<';
61 	} else if (hl_after)
62 	{
63 		attr = hl_after;
64 		ch = '>';
65 	} else if (disp_pos != NULL_POSITION)
66 	{
67 		attr = is_hilited_attr(disp_pos, edisp_pos, TRUE, NULL);
68 		ch = '*';
69 	} else
70 	{
71 		attr = 0;
72 	}
73 	if (attr)
74 		set_status_col(ch, attr);
75 }
76 
77 /*
78  * Get the next line.
79  * A "current" position is passed and a "new" position is returned.
80  * The current position is the position of the first character of
81  * a line.  The new position is the position of the first character
82  * of the NEXT line.  The line obtained is the line starting at curr_pos.
83  */
forw_line_seg(POSITION curr_pos,lbool skipeol,lbool rscroll,lbool nochop,POSITION * p_linepos,lbool * p_newline)84 public POSITION forw_line_seg(POSITION curr_pos, lbool skipeol, lbool rscroll, lbool nochop, POSITION *p_linepos, lbool *p_newline)
85 {
86 	POSITION base_pos;
87 	POSITION new_pos;
88 	POSITION edisp_pos;
89 	int c;
90 	lbool blankline;
91 	lbool endline;
92 	lbool chopped;
93 	int backchars;
94 	POSITION wrap_pos;
95 	lbool skipped_leading;
96 
97 	if (p_linepos != NULL)
98 		*p_linepos = NULL_POSITION;
99 	if (p_newline != NULL)
100 		*p_newline = TRUE;
101 
102 get_forw_line:
103 	if (curr_pos == NULL_POSITION)
104 	{
105 		null_line();
106 		return (NULL_POSITION);
107 	}
108 #if HILITE_SEARCH
109 	if (hilite_search == OPT_ONPLUS || is_filtering() || (status_col && hilite_search != OPT_ON))
110 	{
111 		/*
112 		 * If we are ignoring EOI (command F), only prepare
113 		 * one line ahead, to avoid getting stuck waiting for
114 		 * slow data without displaying the data we already have.
115 		 * If we're not ignoring EOI, we *could* do the same, but
116 		 * for efficiency we prepare several lines ahead at once.
117 		 */
118 		prep_hilite(curr_pos, NULL_POSITION, 1);
119 	}
120 #endif
121 	if (ch_seek(curr_pos))
122 	{
123 		null_line();
124 		return (NULL_POSITION);
125 	}
126 
127 	/*
128 	 * Step back to the beginning of the line.
129 	 */
130 	base_pos = curr_pos;
131 	for (;;)
132 	{
133 		c = ch_back_get();
134 		if (c == EOI)
135 			break;
136 		if (c == '\n')
137 		{
138 			(void) ch_forw_get();
139 			break;
140 		}
141 		--base_pos;
142 	}
143 
144 	/*
145 	 * Read forward again to the position we should start at.
146 	 */
147 	if (is_line_contig_pos(curr_pos))
148 	{
149 		prewind(TRUE);
150 		plinestart(base_pos);
151 		ch_seek(curr_pos);
152 		new_pos = curr_pos;
153 	} else
154 	{
155 		prewind(FALSE);
156 		plinestart(base_pos);
157 		ch_seek(base_pos);
158 		new_pos = base_pos;
159 		while (new_pos < curr_pos)
160 		{
161 			c = ch_forw_get();
162 			if (c == EOI)
163 			{
164 				null_line();
165 				return (NULL_POSITION);
166 			}
167 			backchars = pappend((char) c, new_pos);
168 			new_pos++;
169 			if (backchars > 0)
170 			{
171 				pshift_all();
172 				if (wordwrap && (c == ' ' || c == '\t'))
173 				{
174 					do
175 					{
176 						new_pos++;
177 						c = ch_forw_get(); /* {{ what if c == EOI? }} */
178 					} while (c == ' ' || c == '\t');
179 					backchars = 1;
180 				}
181 				new_pos -= backchars;
182 				while (--backchars >= 0)
183 					(void) ch_back_get();
184 			}
185 		}
186 		pshift_all();
187 	}
188 	(void) pflushmbc();
189 
190 	/*
191 	 * Read the first character to display.
192 	 */
193 	c = ch_forw_get();
194 	if (c == EOI)
195 	{
196 		null_line();
197 		return (NULL_POSITION);
198 	}
199 	blankline = (c == '\n' || c == '\r');
200 	wrap_pos = NULL_POSITION;
201 	skipped_leading = FALSE;
202 
203 	/*
204 	 * Read each character in the line and append to the line buffer.
205 	 */
206 	chopped = FALSE;
207 	for (;;)
208 	{
209 		if (c == '\n' || c == EOI)
210 		{
211 			/*
212 			 * End of the line.
213 			 */
214 			backchars = pflushmbc();
215 			new_pos = ch_tell();
216 			if (backchars > 0 && (nochop || !chop_line()) && hshift == 0)
217 			{
218 				new_pos -= backchars + 1;
219 				endline = FALSE;
220 			} else
221 				endline = TRUE;
222 			edisp_pos = new_pos;
223 			break;
224 		}
225 		if (c != '\r')
226 			blankline = FALSE;
227 
228 		/*
229 		 * Append the char to the line and get the next char.
230 		 */
231 		backchars = pappend((char) c, ch_tell()-1);
232 		if (backchars > 0)
233 		{
234 			/*
235 			 * The char won't fit in the line; the line
236 			 * is too long to print in the screen width.
237 			 * End the line here.
238 			 */
239 			if (skipeol)
240 			{
241 				/* Read to end of line. */
242 				edisp_pos = ch_tell() - backchars;
243 				do
244 				{
245 					c = ch_forw_get();
246 				} while (c != '\n' && c != EOI);
247 				new_pos = ch_tell();
248 				endline = TRUE;
249 				quit_if_one_screen = FALSE;
250 				chopped = TRUE;
251 			} else
252 			{
253 				if (!wordwrap)
254 					new_pos = ch_tell() - backchars;
255 				else
256 				{
257 					/*
258 					 * We're word-wrapping, so go back to the last space.
259 					 * However, if it's the space itself that couldn't fit,
260 					 * simply ignore it and any subsequent spaces.
261 					 */
262 					if (c == ' ' || c == '\t')
263 					{
264 						do
265 						{
266 							new_pos = ch_tell();
267 							c = ch_forw_get(); /* {{ what if c == EOI? }} */
268 						} while (c == ' ' || c == '\t');
269 						if (c == '\r')
270 							c = ch_forw_get(); /* {{ what if c == EOI? }} */
271 						if (c == '\n')
272 							new_pos = ch_tell();
273 					} else if (wrap_pos == NULL_POSITION)
274 						new_pos = ch_tell() - backchars;
275 					else
276 					{
277 						new_pos = wrap_pos;
278 						loadc();
279 					}
280 				}
281 				endline = FALSE;
282 				edisp_pos = new_pos;
283 			}
284 			break;
285 		}
286 		if (wordwrap)
287 		{
288 			if (c == ' ' || c == '\t')
289 			{
290 				if (skipped_leading)
291 				{
292 					wrap_pos = ch_tell();
293 					savec();
294 				}
295 			} else
296 				skipped_leading = TRUE;
297 		}
298 		c = ch_forw_get();
299 	}
300 
301 #if HILITE_SEARCH
302 	if (blankline && show_attn)
303 	{
304 		/* Add spurious space to carry possible attn hilite.
305 		 * Use pappend_b so that if line ended with \r\n,
306 		 * we insert the space before the \r. */
307 		pappend_b(' ', ch_tell()-1, TRUE);
308 	}
309 #endif
310 	pdone(endline, rscroll && chopped, TRUE);
311 
312 #if HILITE_SEARCH
313 	if (is_filtered(base_pos))
314 	{
315 		/*
316 		 * We don't want to display this line.
317 		 * Get the next line.
318 		 */
319 		curr_pos = new_pos;
320 		goto get_forw_line;
321 	}
322 	if (status_col)
323 		init_status_col(base_pos, line_position(), edisp_pos, new_pos);
324 #endif
325 
326 	if (squeeze && blankline)
327 	{
328 		/*
329 		 * This line is blank.
330 		 * Skip down to the last contiguous blank line
331 		 * and pretend it is the one which we are returning.
332 		 */
333 		while ((c = ch_forw_get()) == '\n' || c == '\r')
334 			continue;
335 		if (c != EOI)
336 			(void) ch_back_get();
337 		new_pos = ch_tell();
338 	}
339 	if (p_linepos != NULL)
340 		*p_linepos = curr_pos;
341 	if (p_newline != NULL)
342 		*p_newline = endline;
343 	set_line_contig_pos(endline ? NULL_POSITION : new_pos);
344 	return (new_pos);
345 }
346 
forw_line(POSITION curr_pos,POSITION * p_linepos,lbool * p_newline)347 public POSITION forw_line(POSITION curr_pos, POSITION *p_linepos, lbool *p_newline)
348 {
349 	return forw_line_seg(curr_pos, (chop_line() || hshift > 0), TRUE, FALSE, p_linepos, p_newline);
350 }
351 
352 /*
353  * Get the previous line.
354  * A "current" position is passed and a "new" position is returned.
355  * The current position is the position of the first character of
356  * a line.  The new position is the position of the first character
357  * of the PREVIOUS line.  The line obtained is the one starting at new_pos.
358  */
back_line(POSITION curr_pos,lbool * p_newline)359 public POSITION back_line(POSITION curr_pos, lbool *p_newline)
360 {
361 	POSITION base_pos;
362 	POSITION new_pos;
363 	POSITION edisp_pos;
364 	POSITION begin_new_pos;
365 	int c;
366 	lbool endline;
367 	lbool chopped;
368 	int backchars;
369 	POSITION wrap_pos;
370 	lbool skipped_leading;
371 
372 get_back_line:
373 	if (p_newline != NULL)
374 		*p_newline = TRUE;
375 	if (curr_pos == NULL_POSITION || curr_pos <= ch_zero())
376 	{
377 		null_line();
378 		return (NULL_POSITION);
379 	}
380 	if (ch_seek(curr_pos-1))
381 	{
382 		null_line();
383 		return (NULL_POSITION);
384 	}
385 
386 	if (squeeze)
387 	{
388 		/*
389 		 * Find out if the "current" line was blank.
390 		 */
391 		(void) ch_forw_get();    /* Skip the newline */
392 		c = ch_forw_get();       /* First char of "current" line */
393 		/* {{ what if c == EOI? }} */
394 		(void) ch_back_get();    /* Restore our position */
395 		(void) ch_back_get();
396 
397 		if (c == '\n' || c == '\r')
398 		{
399 			/*
400 			 * The "current" line was blank.
401 			 * Skip over any preceding blank lines,
402 			 * since we skipped them in forw_line().
403 			 */
404 			while ((c = ch_back_get()) == '\n' || c == '\r')
405 				continue;
406 			if (c == EOI)
407 			{
408 				null_line();
409 				return (NULL_POSITION);
410 			}
411 			(void) ch_forw_get();
412 		}
413 	}
414 
415 	/*
416 	 * Scan backwards until we hit the beginning of the line.
417 	 */
418 	for (;;)
419 	{
420 		c = ch_back_get();
421 		if (c == '\n')
422 		{
423 			/*
424 			 * This is the newline ending the previous line.
425 			 * We have hit the beginning of the line.
426 			 */
427 			base_pos = ch_tell() + 1;
428 			break;
429 		}
430 		if (c == EOI)
431 		{
432 			/*
433 			 * We have hit the beginning of the file.
434 			 * This must be the first line in the file.
435 			 * This must, of course, be the beginning of the line.
436 			 */
437 			base_pos = ch_tell();
438 			break;
439 		}
440 	}
441 
442 #if HILITE_SEARCH
443 	if (hilite_search == OPT_ONPLUS || is_filtering() || (status_col && hilite_search != OPT_ON))
444 		prep_hilite(base_pos, NULL_POSITION, 1);
445 #endif
446 
447 	/*
448 	 * Now scan forwards from the beginning of this line.
449 	 * We keep discarding "printable lines" (based on screen width)
450 	 * until we reach the curr_pos.
451 	 *
452 	 * {{ This algorithm is pretty inefficient if the lines
453 	 *    are much longer than the screen width,
454 	 *    but I don't know of any better way. }}
455 	 */
456 	new_pos = base_pos;
457 	if (ch_seek(new_pos))
458 	{
459 		null_line();
460 		return (NULL_POSITION);
461 	}
462 	endline = FALSE;
463 	prewind(FALSE);
464 	plinestart(new_pos);
465     loop:
466 	wrap_pos = NULL_POSITION;
467 	skipped_leading = FALSE;
468 	begin_new_pos = new_pos;
469 	(void) ch_seek(new_pos);
470 	chopped = FALSE;
471 
472 	for (;;)
473 	{
474 		c = ch_forw_get();
475 		if (c == EOI)
476 		{
477 			null_line();
478 			return (NULL_POSITION);
479 		}
480 		new_pos++;
481 		if (c == '\n')
482 		{
483 			backchars = pflushmbc();
484 			if (backchars > 0 && !chop_line() && hshift == 0)
485 			{
486 				backchars++;
487 				goto shift;
488 			}
489 			endline = TRUE;
490 			edisp_pos = new_pos;
491 			break;
492 		}
493 		backchars = pappend((char) c, ch_tell()-1);
494 		if (backchars > 0)
495 		{
496 			/*
497 			 * Got a full printable line, but we haven't
498 			 * reached our curr_pos yet.  Discard the line
499 			 * and start a new one.
500 			 */
501 			if (chop_line() || hshift > 0)
502 			{
503 				endline = TRUE;
504 				chopped = TRUE;
505 				quit_if_one_screen = FALSE;
506 				edisp_pos = new_pos;
507 				break;
508 			}
509 			if (p_newline != NULL)
510 				*p_newline = FALSE;
511 		shift:
512 			if (!wordwrap)
513 			{
514 				pshift_all();
515 				new_pos -= backchars;
516 			} else
517 			{
518 				if (c == ' ' || c == '\t')
519 				{
520 					for (;;)
521 					{
522 						c = ch_forw_get(); /* {{ what if c == EOI? }} */
523 						if (c == ' ' || c == '\t')
524 							new_pos++;
525 						else
526 						{
527 							if (c == '\r')
528 							{
529 								c = ch_forw_get(); /* {{ what if c == EOI? }} */
530 								if (c == '\n')
531 									new_pos++;
532 							}
533 							if (c == '\n')
534 								new_pos++;
535 							edisp_pos = new_pos;
536 							break;
537 						}
538 					}
539 					if (new_pos >= curr_pos)
540 					{
541 						edisp_pos = new_pos;
542 						break;
543 					}
544 					pshift_all();
545 				} else
546 				{
547 					pshift_all();
548 					if (wrap_pos == NULL_POSITION)
549 						new_pos -= backchars;
550 					else
551 						new_pos = wrap_pos;
552 				}
553 			}
554 			goto loop;
555 		}
556 		if (wordwrap)
557 		{
558 			if (c == ' ' || c == '\t')
559 			{
560 				if (skipped_leading)
561 					wrap_pos = new_pos;
562 			} else
563 				skipped_leading = TRUE;
564 		}
565 		if (new_pos >= curr_pos)
566 		{
567 			edisp_pos = new_pos;
568 			break;
569 		}
570 	}
571 
572 	pdone(endline, chopped, FALSE);
573 
574 #if HILITE_SEARCH
575 	if (is_filtered(base_pos))
576 	{
577 		/*
578 		 * We don't want to display this line.
579 		 * Get the previous line.
580 		 */
581 		curr_pos = begin_new_pos;
582 		goto get_back_line;
583 	}
584 	if (status_col)
585 		init_status_col(base_pos, line_position(), edisp_pos, new_pos);
586 #endif
587 	return (begin_new_pos);
588 }
589 
590 /*
591  * Set attnpos.
592  */
set_attnpos(POSITION pos)593 public void set_attnpos(POSITION pos)
594 {
595 	int c;
596 
597 	if (pos != NULL_POSITION)
598 	{
599 		if (ch_seek(pos))
600 			return;
601 		for (;;)
602 		{
603 			c = ch_forw_get();
604 			if (c == EOI)
605 				break;
606 			if (c == '\n' || c == '\r')
607 			{
608 				(void) ch_back_get();
609 				break;
610 			}
611 			pos++;
612 		}
613 		end_attnpos = pos;
614 		for (;;)
615 		{
616 			c = ch_back_get();
617 			if (c == EOI || c == '\n' || c == '\r')
618 				break;
619 			pos--;
620 		}
621 	}
622 	start_attnpos = pos;
623 }
624