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