xref: /freebsd/contrib/less/linenum.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 /*
12  * Code to handle displaying line numbers.
13  *
14  * Finding the line number of a given file position is rather tricky.
15  * We don't want to just start at the beginning of the file and
16  * count newlines, because that is slow for large files (and also
17  * wouldn't work if we couldn't get to the start of the file; e.g.
18  * if input is a long pipe).
19  *
20  * So we use the function add_lnum to cache line numbers.
21  * We try to be very clever and keep only the more interesting
22  * line numbers when we run out of space in our table.  A line
23  * number is more interesting than another when it is far from
24  * other line numbers.   For example, we'd rather keep lines
25  * 100,200,300 than 100,101,300.  200 is more interesting than
26  * 101 because 101 can be derived very cheaply from 100, while
27  * 200 is more expensive to derive from 100.
28  *
29  * The function currline() returns the line number of a given
30  * position in the file.  As a side effect, it calls add_lnum
31  * to cache the line number.  Therefore currline is occasionally
32  * called to make sure we cache line numbers often enough.
33  */
34 
35 #include "less.h"
36 
37 /*
38  * Structure to keep track of a line number and the associated file position.
39  * A doubly-linked circular list of line numbers is kept ordered by line number.
40  */
41 struct linenum_info
42 {
43 	struct linenum_info *next;      /* Link to next in the list */
44 	struct linenum_info *prev;      /* Line to previous in the list */
45 	POSITION pos;                   /* File position */
46 	POSITION gap;                   /* Gap between prev and next */
47 	LINENUM line;                   /* Line number */
48 };
49 /*
50  * "gap" needs some explanation: the gap of any particular line number
51  * is the distance between the previous one and the next one in the list.
52  * ("Distance" means difference in file position.)  In other words, the
53  * gap of a line number is the gap which would be introduced if this
54  * line number were deleted.  It is used to decide which one to replace
55  * when we have a new one to insert and the table is full.
56  */
57 
58 #define LONGTIME        (2)             /* In seconds */
59 
60 static struct linenum_info anchor;      /* Anchor of the list */
61 static struct linenum_info *freelist;   /* Anchor of the unused entries */
62 static struct linenum_info pool[LINENUM_POOL]; /* The pool itself */
63 static struct linenum_info *spare;      /* We always keep one spare entry */
64 public lbool scanning_eof = FALSE;
65 
66 extern int linenums;
67 extern int sigs;
68 extern int sc_height;
69 extern int header_lines;
70 extern int nonum_headers;
71 extern POSITION header_start_pos;
72 
73 /*
74  * Initialize the line number structures.
75  */
clr_linenum(void)76 public void clr_linenum(void)
77 {
78 	struct linenum_info *p;
79 
80 	/*
81 	 * Put all the entries on the free list.
82 	 * Leave one for the "spare".
83 	 */
84 	for (p = pool;  p < &pool[LINENUM_POOL-2];  p++)
85 		p->next = p+1;
86 	pool[LINENUM_POOL-2].next = NULL;
87 	freelist = pool;
88 
89 	spare = &pool[LINENUM_POOL-1];
90 
91 	/*
92 	 * Initialize the anchor.
93 	 */
94 	anchor.next = anchor.prev = &anchor;
95 	anchor.gap = 0;
96 	anchor.pos = (POSITION)0;
97 	anchor.line = 1;
98 }
99 
100 /*
101  * Calculate the gap for an entry.
102  */
calcgap(struct linenum_info * p)103 static void calcgap(struct linenum_info *p)
104 {
105 	/*
106 	 * Don't bother to compute a gap for the anchor.
107 	 * Also don't compute a gap for the last one in the list.
108 	 * The gap for that last one should be considered infinite,
109 	 * but we never look at it anyway.
110 	 */
111 	if (p == &anchor || p->next == &anchor)
112 		return;
113 	p->gap = p->next->pos - p->prev->pos;
114 }
115 
116 /*
117  * Add a new line number to the cache.
118  * The specified position (pos) should be the file position of the
119  * FIRST character in the specified line.
120  */
add_lnum(LINENUM linenum,POSITION pos)121 public void add_lnum(LINENUM linenum, POSITION pos)
122 {
123 	struct linenum_info *p;
124 	struct linenum_info *new;
125 	struct linenum_info *nextp;
126 	struct linenum_info *prevp;
127 	POSITION mingap;
128 
129 	/*
130 	 * Find the proper place in the list for the new one.
131 	 * The entries are sorted by position.
132 	 */
133 	for (p = anchor.next;  p != &anchor && p->pos < pos;  p = p->next)
134 		if (p->line == linenum)
135 			/* We already have this one. */
136 			return;
137 	nextp = p;
138 	prevp = p->prev;
139 
140 	if (freelist != NULL)
141 	{
142 		/*
143 		 * We still have free (unused) entries.
144 		 * Use one of them.
145 		 */
146 		new = freelist;
147 		freelist = freelist->next;
148 	} else
149 	{
150 		/*
151 		 * No free entries.
152 		 * Use the "spare" entry.
153 		 */
154 		new = spare;
155 		spare = NULL;
156 	}
157 
158 	/*
159 	 * Fill in the fields of the new entry,
160 	 * and insert it into the proper place in the list.
161 	 */
162 	new->next = nextp;
163 	new->prev = prevp;
164 	new->pos = pos;
165 	new->line = linenum;
166 
167 	nextp->prev = new;
168 	prevp->next = new;
169 
170 	/*
171 	 * Recalculate gaps for the new entry and the neighboring entries.
172 	 */
173 	calcgap(new);
174 	calcgap(nextp);
175 	calcgap(prevp);
176 
177 	if (spare == NULL)
178 	{
179 		/*
180 		 * We have used the spare entry.
181 		 * Scan the list to find the one with the smallest
182 		 * gap, take it out and make it the spare.
183 		 * We should never remove the last one, so stop when
184 		 * we get to p->next == &anchor.  This also avoids
185 		 * looking at the gap of the last one, which is
186 		 * not computed by calcgap.
187 		 */
188 		mingap = anchor.next->gap;
189 		for (p = anchor.next;  p->next != &anchor;  p = p->next)
190 		{
191 			if (p->gap <= mingap)
192 			{
193 				spare = p;
194 				mingap = p->gap;
195 			}
196 		}
197 		spare->next->prev = spare->prev;
198 		spare->prev->next = spare->next;
199 	}
200 }
201 
202 /*
203  * If we get stuck in a long loop trying to figure out the
204  * line number, print a message to tell the user what we're doing.
205  */
longloopmessage(void)206 static void longloopmessage(void)
207 {
208 	ierror("Calculating line numbers", NULL_PARG);
209 }
210 
211 struct delayed_msg
212 {
213 	void (*message)(void);
214 	int loopcount;
215 #if HAVE_TIME
216 	time_type startime;
217 #endif
218 };
219 
start_delayed_msg(struct delayed_msg * dmsg,void (* message)(void))220 static void start_delayed_msg(struct delayed_msg *dmsg, void (*message)(void))
221 {
222 	dmsg->loopcount = 0;
223 	dmsg->message = message;
224 #if HAVE_TIME
225 	dmsg->startime = get_time();
226 #endif
227 }
228 
delayed_msg(struct delayed_msg * dmsg)229 static void delayed_msg(struct delayed_msg *dmsg)
230 {
231 #if HAVE_TIME
232 	if (dmsg->loopcount >= 0 && ++(dmsg->loopcount) > 100)
233 	{
234 		dmsg->loopcount = 0;
235 		if (get_time() >= dmsg->startime + LONGTIME)
236 		{
237 			dmsg->message();
238 			dmsg->loopcount = -1;
239 		}
240 	}
241 #else
242 	if (dmsg->loopcount >= 0 && ++(dmsg->loopcount) > LONGLOOP)
243 	{
244 		dmsg->message();
245 		dmsg->loopcount = -1;
246 	}
247 #endif
248 }
249 
250 /*
251  * Turn off line numbers because the user has interrupted
252  * a lengthy line number calculation.
253  */
abort_delayed_msg(struct delayed_msg * dmsg)254 static void abort_delayed_msg(struct delayed_msg *dmsg)
255 {
256 	if (dmsg->loopcount >= 0)
257 		return;
258 	if (linenums == OPT_ONPLUS)
259 		/*
260 		 * We were displaying line numbers, so need to repaint.
261 		 */
262 		screen_trashed();
263 	linenums = 0;
264 	error("Line numbers turned off", NULL_PARG);
265 }
266 
267 /*
268  * Find the line number associated with a given position.
269  * Return 0 if we can't figure it out.
270  */
find_linenum(POSITION pos)271 public LINENUM find_linenum(POSITION pos)
272 {
273 	struct linenum_info *p;
274 	LINENUM linenum;
275 	POSITION cpos;
276 	struct delayed_msg dmsg;
277 
278 	if (!linenums)
279 		/*
280 		 * We're not using line numbers.
281 		 */
282 		return (0);
283 	if (pos == NULL_POSITION)
284 		/*
285 		 * Caller doesn't know what he's talking about.
286 		 */
287 		return (0);
288 	if (pos <= ch_zero())
289 		/*
290 		 * Beginning of file is always line number 1.
291 		 */
292 		return (1);
293 
294 	/*
295 	 * Find the entry nearest to the position we want.
296 	 */
297 	for (p = anchor.next;  p != &anchor && p->pos < pos;  p = p->next)
298 		continue;
299 	if (p->pos == pos)
300 		/* Found it exactly. */
301 		return (p->line);
302 
303 	/*
304 	 * This is the (possibly) time-consuming part.
305 	 * We start at the line we just found and start
306 	 * reading the file forward or backward till we
307 	 * get to the place we want.
308 	 *
309 	 * First decide whether we should go forward from the
310 	 * previous one or backwards from the next one.
311 	 * The decision is based on which way involves
312 	 * traversing fewer bytes in the file.
313 	 */
314 	start_delayed_msg(&dmsg, longloopmessage);
315 	if (p == &anchor || pos - p->prev->pos < p->pos - pos)
316 	{
317 		/*
318 		 * Go forward.
319 		 */
320 		p = p->prev;
321 		if (ch_seek(p->pos))
322 			return (0);
323 		for (linenum = p->line, cpos = p->pos;  cpos < pos;  linenum++)
324 		{
325 			/*
326 			 * Allow a signal to abort this loop.
327 			 */
328 			cpos = forw_raw_line(cpos, NULL, NULL);
329 			if (ABORT_SIGS()) {
330 				abort_delayed_msg(&dmsg);
331 				return (0);
332 			}
333 			if (cpos == NULL_POSITION)
334 				return (0);
335 			delayed_msg(&dmsg);
336 		}
337 		/*
338 		 * We might as well cache it.
339 		 */
340 		add_lnum(linenum, cpos);
341 		/*
342 		 * If the given position is not at the start of a line,
343 		 * make sure we return the correct line number.
344 		 */
345 		if (cpos > pos)
346 			linenum--;
347 	} else
348 	{
349 		/*
350 		 * Go backward.
351 		 */
352 		if (ch_seek(p->pos))
353 			return (0);
354 		for (linenum = p->line, cpos = p->pos;  cpos > pos;  linenum--)
355 		{
356 			/*
357 			 * Allow a signal to abort this loop.
358 			 */
359 			cpos = back_raw_line(cpos, NULL, NULL);
360 			if (ABORT_SIGS()) {
361 				abort_delayed_msg(&dmsg);
362 				return (0);
363 			}
364 			if (cpos == NULL_POSITION)
365 				return (0);
366 			delayed_msg(&dmsg);
367 		}
368 		/*
369 		 * We might as well cache it.
370 		 */
371 		add_lnum(linenum, cpos);
372 	}
373 	return (linenum);
374 }
375 
376 /*
377  * Find the position of a given line number.
378  * Return NULL_POSITION if we can't figure it out.
379  */
find_pos(LINENUM linenum)380 public POSITION find_pos(LINENUM linenum)
381 {
382 	struct linenum_info *p;
383 	POSITION cpos;
384 	LINENUM clinenum;
385 
386 	if (linenum <= 1)
387 		/*
388 		 * Line number 1 is beginning of file.
389 		 */
390 		return (ch_zero());
391 
392 	/*
393 	 * Find the entry nearest to the line number we want.
394 	 */
395 	for (p = anchor.next;  p != &anchor && p->line < linenum;  p = p->next)
396 		continue;
397 	if (p->line == linenum)
398 		/* Found it exactly. */
399 		return (p->pos);
400 
401 	if (p == &anchor || linenum - p->prev->line < p->line - linenum)
402 	{
403 		/*
404 		 * Go forward.
405 		 */
406 		p = p->prev;
407 		if (ch_seek(p->pos))
408 			return (NULL_POSITION);
409 		for (clinenum = p->line, cpos = p->pos;  clinenum < linenum;  clinenum++)
410 		{
411 			/*
412 			 * Allow a signal to abort this loop.
413 			 */
414 			cpos = forw_raw_line(cpos, NULL, NULL);
415 			if (ABORT_SIGS())
416 				return (NULL_POSITION);
417 			if (cpos == NULL_POSITION)
418 				return (NULL_POSITION);
419 		}
420 	} else
421 	{
422 		/*
423 		 * Go backward.
424 		 */
425 		if (ch_seek(p->pos))
426 			return (NULL_POSITION);
427 		for (clinenum = p->line, cpos = p->pos;  clinenum > linenum;  clinenum--)
428 		{
429 			/*
430 			 * Allow a signal to abort this loop.
431 			 */
432 			cpos = back_raw_line(cpos, NULL, NULL);
433 			if (ABORT_SIGS())
434 				return (NULL_POSITION);
435 			if (cpos == NULL_POSITION)
436 				return (NULL_POSITION);
437 		}
438 	}
439 	/*
440 	 * We might as well cache it.
441 	 */
442 	add_lnum(clinenum, cpos);
443 	return (cpos);
444 }
445 
446 /*
447  * Return the line number of the "current" line.
448  * The argument "where" tells which line is to be considered
449  * the "current" line (e.g. TOP, BOTTOM, MIDDLE, etc).
450  */
currline(int where)451 public LINENUM currline(int where)
452 {
453 	POSITION pos;
454 	POSITION len;
455 	LINENUM linenum;
456 
457 	pos = position(where);
458 	len = ch_length();
459 	while (pos == NULL_POSITION && where >= 0 && where < sc_height)
460 		pos = position(++where);
461 	if (pos == NULL_POSITION)
462 		pos = len;
463 	linenum = find_linenum(pos);
464 	if (pos == len)
465 		linenum--;
466 	return (linenum);
467 }
468 
detlenmessage(void)469 static void detlenmessage(void)
470 {
471 	ierror("Determining length of file", NULL_PARG);
472 }
473 
474 /*
475  * Scan entire file, counting line numbers.
476  */
scan_eof(void)477 public void scan_eof(void)
478 {
479 	POSITION pos = ch_zero();
480 	LINENUM linenum = 0;
481 	struct delayed_msg dmsg;
482 
483 	if (ch_seek(0))
484 		return;
485 	/*
486 	 * scanning_eof prevents the "Waiting for data" message from
487 	 * overwriting "Determining length of file".
488 	 */
489 	start_delayed_msg(&dmsg, detlenmessage);
490 	scanning_eof = TRUE;
491 	while (pos != NULL_POSITION)
492 	{
493 		/* For efficiency, only add one every 256 line numbers. */
494 		if ((linenum++ % 256) == 0)
495 			add_lnum(linenum, pos);
496 		pos = forw_raw_line(pos, NULL, NULL);
497 		if (ABORT_SIGS())
498 		{
499 			abort_delayed_msg(&dmsg);
500 			break;
501 		}
502 		delayed_msg(&dmsg);
503 	}
504 	scanning_eof = FALSE;
505 }
506 
507 /*
508  * Return a line number adjusted for display
509  * (handles the --no-number-headers option).
510  */
vlinenum(LINENUM linenum)511 public LINENUM vlinenum(LINENUM linenum)
512 {
513 	if (nonum_headers && header_lines > 0)
514 	{
515 		LINENUM header_start_line = find_linenum(header_start_pos);
516 		if (header_start_line != 0)
517 		{
518 			LINENUM header_end_line = header_start_line + header_lines; /* first line after header */
519 			linenum = (linenum < header_end_line) ? 0 : linenum - header_end_line + 1;
520 		}
521 	}
522 	return linenum;
523 }
524