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