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