xref: /freebsd/contrib/less/input.c (revision 8a7df6c58d549bd2eaa5cad30b2ef93c3eca3c18)
1 /*
2  * Copyright (C) 1984-2026  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  */
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  */
83 public POSITION forw_line_seg(POSITION curr_pos, lbool skipeol, lbool rscroll, lbool nochop, lbool full_pad, lbool rforw, 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, curr_pos);
150 		ch_seek(curr_pos);
151 		new_pos = curr_pos;
152 	} else
153 	{
154 		prewind(FALSE);
155 		plinestart(base_pos, curr_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 = 0;
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, rforw, full_pad);
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 
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, FALSE, TRUE, 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  */
358 public POSITION back_line(POSITION curr_pos, POSITION *p_linepos, 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 	if (p_linepos != NULL)
372 		*p_linepos = NULL_POSITION;
373 get_back_line:
374 	if (p_newline != NULL)
375 		*p_newline = TRUE;
376 	if (curr_pos == NULL_POSITION || curr_pos <= ch_zero())
377 	{
378 		null_line();
379 		return (NULL_POSITION);
380 	}
381 	if (ch_seek(curr_pos-1))
382 	{
383 		null_line();
384 		return (NULL_POSITION);
385 	}
386 
387 	if (squeeze)
388 	{
389 		/*
390 		 * Find out if the "current" line was blank.
391 		 */
392 		(void) ch_forw_get();    /* Skip the newline */
393 		c = ch_forw_get();       /* First char of "current" line */
394 		/* {{ what if c == EOI? }} */
395 		(void) ch_back_get();    /* Restore our position */
396 		(void) ch_back_get();
397 
398 		if (c == '\n' || c == '\r')
399 		{
400 			/*
401 			 * The "current" line was blank.
402 			 * Skip over any preceding blank lines,
403 			 * since we skipped them in forw_line().
404 			 */
405 			while ((c = ch_back_get()) == '\n' || c == '\r')
406 				continue;
407 			if (c == EOI)
408 			{
409 				null_line();
410 				return (NULL_POSITION);
411 			}
412 			(void) ch_forw_get();
413 		}
414 	}
415 
416 	/*
417 	 * Scan backwards until we hit the beginning of the line.
418 	 */
419 	for (;;)
420 	{
421 		c = ch_back_get();
422 		if (c == '\n')
423 		{
424 			/*
425 			 * This is the newline ending the previous line.
426 			 * We have hit the beginning of the line.
427 			 */
428 			base_pos = ch_tell() + 1;
429 			break;
430 		}
431 		if (c == EOI)
432 		{
433 			/*
434 			 * We have hit the beginning of the file.
435 			 * This must be the first line in the file.
436 			 * This must, of course, be the beginning of the line.
437 			 */
438 			base_pos = ch_tell();
439 			break;
440 		}
441 	}
442 
443 #if HILITE_SEARCH
444 	if (hilite_search == OPT_ONPLUS || is_filtering() || (status_col && hilite_search != OPT_ON))
445 		prep_hilite(base_pos, NULL_POSITION, 1);
446 #endif
447 
448 	/*
449 	 * Now scan forwards from the beginning of this line.
450 	 * We keep discarding "printable lines" (based on screen width)
451 	 * until we reach the curr_pos.
452 	 *
453 	 * {{ This algorithm is pretty inefficient if the lines
454 	 *    are much longer than the screen width,
455 	 *    but I don't know of any better way. }}
456 	 */
457 	new_pos = base_pos;
458 	if (ch_seek(new_pos))
459 	{
460 		null_line();
461 		return (NULL_POSITION);
462 	}
463 	endline = FALSE;
464     loop:
465 	prewind(FALSE);
466 	plinestart(new_pos, new_pos);
467 	wrap_pos = NULL_POSITION;
468 	skipped_leading = FALSE;
469 	begin_new_pos = new_pos;
470 	(void) ch_seek(new_pos);
471 	chopped = FALSE;
472 
473 	for (;;)
474 	{
475 		c = ch_forw_get();
476 		if (c == EOI)
477 		{
478 			null_line();
479 			return (NULL_POSITION);
480 		}
481 		new_pos++;
482 		if (c == '\n')
483 		{
484 			backchars = pflushmbc();
485 			if (backchars > 0 && !chop_line() && hshift == 0)
486 			{
487 				backchars++;
488 				goto shift;
489 			}
490 			endline = TRUE;
491 			edisp_pos = new_pos;
492 			break;
493 		}
494 		backchars = pappend((char) c, ch_tell()-1);
495 		if (backchars > 0)
496 		{
497 			/*
498 			 * Got a full printable line, but we haven't
499 			 * reached our curr_pos yet.  Discard the line
500 			 * and start a new one.
501 			 */
502 			if (chop_line() || hshift > 0)
503 			{
504 				endline = TRUE;
505 				chopped = TRUE;
506 				quit_if_one_screen = 0;
507 				edisp_pos = new_pos;
508 				break;
509 			}
510 			if (p_newline != NULL)
511 				*p_newline = FALSE;
512 		shift:
513 			if (!wordwrap)
514 			{
515 				pshift_all();
516 				new_pos -= backchars;
517 			} else
518 			{
519 				if (c == ' ' || c == '\t')
520 				{
521 					for (;;)
522 					{
523 						c = ch_forw_get(); /* {{ what if c == EOI? }} */
524 						if (c == ' ' || c == '\t')
525 							new_pos++;
526 						else
527 						{
528 							if (c == '\r')
529 							{
530 								c = ch_forw_get(); /* {{ what if c == EOI? }} */
531 								if (c == '\n')
532 									new_pos++;
533 							}
534 							if (c == '\n')
535 								new_pos++;
536 							edisp_pos = new_pos;
537 							break;
538 						}
539 					}
540 					if (new_pos >= curr_pos)
541 					{
542 						edisp_pos = new_pos;
543 						break;
544 					}
545 					pshift_all();
546 				} else
547 				{
548 					pshift_all();
549 					if (wrap_pos == NULL_POSITION)
550 						new_pos -= backchars;
551 					else
552 						new_pos = wrap_pos;
553 				}
554 			}
555 			goto loop;
556 		}
557 		if (wordwrap)
558 		{
559 			if (c == ' ' || c == '\t')
560 			{
561 				if (skipped_leading)
562 					wrap_pos = new_pos;
563 			} else
564 				skipped_leading = TRUE;
565 		}
566 		if (new_pos >= curr_pos)
567 		{
568 			edisp_pos = new_pos;
569 			break;
570 		}
571 	}
572 
573 	pdone(endline, chopped, FALSE, FALSE);
574 
575 #if HILITE_SEARCH
576 	if (is_filtered(base_pos))
577 	{
578 		/*
579 		 * We don't want to display this line.
580 		 * Get the previous line.
581 		 */
582 		curr_pos = begin_new_pos;
583 		goto get_back_line;
584 	}
585 	if (status_col)
586 		init_status_col(base_pos, line_position(), edisp_pos, new_pos);
587 #endif
588 	if (p_linepos != NULL)
589 		*p_linepos = curr_pos;
590 	return (begin_new_pos);
591 }
592 
593 /*
594  * Set attnpos.
595  */
596 public void set_attnpos(POSITION pos)
597 {
598 	int c;
599 
600 	if (pos != NULL_POSITION)
601 	{
602 		if (ch_seek(pos))
603 			return;
604 		for (;;)
605 		{
606 			c = ch_forw_get();
607 			if (c == EOI)
608 				break;
609 			if (c == '\n' || c == '\r')
610 			{
611 				(void) ch_back_get();
612 				break;
613 			}
614 			pos++;
615 		}
616 		end_attnpos = pos;
617 		for (;;)
618 		{
619 			c = ch_back_get();
620 			if (c == EOI || c == '\n' || c == '\r')
621 				break;
622 			pos--;
623 		}
624 	}
625 	start_attnpos = pos;
626 }
627