xref: /openbsd-src/usr.bin/less/linenum.c (revision 0b7734b3d77bb9b21afec6f4621cae6c805dbd45)
1 /*
2  * Copyright (C) 1984-2012  Mark Nudelman
3  * Modified for use with illumos by Garrett D'Amore.
4  * Copyright 2014 Garrett D'Amore <garrett@damore.org>
5  *
6  * You may distribute under the terms of either the GNU General Public
7  * License or the Less License, as specified in the README file.
8  *
9  * For more information, see the README file.
10  */
11 
12 /*
13  * Code to handle displaying line numbers.
14  *
15  * Finding the line number of a given file position is rather tricky.
16  * We don't want to just start at the beginning of the file and
17  * count newlines, because that is slow for large files (and also
18  * wouldn't work if we couldn't get to the start of the file; e.g.
19  * if input is a long pipe).
20  *
21  * So we use the function add_lnum to cache line numbers.
22  * We try to be very clever and keep only the more interesting
23  * line numbers when we run out of space in our table.  A line
24  * number is more interesting than another when it is far from
25  * other line numbers.   For example, we'd rather keep lines
26  * 100,200,300 than 100,101,300.  200 is more interesting than
27  * 101 because 101 can be derived very cheaply from 100, while
28  * 200 is more expensive to derive from 100.
29  *
30  * The function currline() returns the line number of a given
31  * position in the file.  As a side effect, it calls add_lnum
32  * to cache the line number.  Therefore currline is occasionally
33  * called to make sure we cache line numbers often enough.
34  */
35 
36 #include "less.h"
37 
38 /*
39  * Structure to keep track of a line number and the associated file position.
40  * A doubly-linked circular list of line numbers is kept ordered by line number.
41  */
42 struct linenum_info {
43 	struct linenum_info *next;	/* Link to next in the list */
44 	struct linenum_info *prev;	/* Line to previous in the list */
45 	off_t pos;			/* File position */
46 	off_t gap;			/* Gap between prev and next */
47 	off_t 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	NPOOL	200			/* Size of line number pool */
59 
60 #define	LONGTIME	(2)		/* In seconds */
61 
62 static struct linenum_info anchor;	/* Anchor of the list */
63 static struct linenum_info *freelist;	/* Anchor of the unused entries */
64 static struct linenum_info pool[NPOOL];	/* The pool itself */
65 static struct linenum_info *spare;	/* We always keep one spare entry */
66 
67 extern int linenums;
68 extern volatile sig_atomic_t sigs;
69 extern int sc_height;
70 extern int screen_trashed;
71 
72 /*
73  * Initialize the line number structures.
74  */
75 void
76 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[NPOOL-2];  p++)
85 		p->next = p+1;
86 	pool[NPOOL-2].next = NULL;
87 	freelist = pool;
88 
89 	spare = &pool[NPOOL-1];
90 
91 	/*
92 	 * Initialize the anchor.
93 	 */
94 	anchor.next = anchor.prev = &anchor;
95 	anchor.gap = 0;
96 	anchor.pos = 0;
97 	anchor.line = 1;
98 }
99 
100 /*
101  * Calculate the gap for an entry.
102  */
103 static void
104 calcgap(struct linenum_info *p)
105 {
106 	/*
107 	 * Don't bother to compute a gap for the anchor.
108 	 * Also don't compute a gap for the last one in the list.
109 	 * The gap for that last one should be considered infinite,
110 	 * but we never look at it anyway.
111 	 */
112 	if (p == &anchor || p->next == &anchor)
113 		return;
114 	p->gap = p->next->pos - p->prev->pos;
115 }
116 
117 /*
118  * Add a new line number to the cache.
119  * The specified position (pos) should be the file position of the
120  * FIRST character in the specified line.
121  */
122 void
123 add_lnum(off_t linenum, off_t pos)
124 {
125 	struct linenum_info *p;
126 	struct linenum_info *new;
127 	struct linenum_info *nextp;
128 	struct linenum_info *prevp;
129 	off_t mingap;
130 
131 	/*
132 	 * Find the proper place in the list for the new one.
133 	 * The entries are sorted by position.
134 	 */
135 	for (p = anchor.next;  p != &anchor && p->pos < pos;  p = p->next)
136 		if (p->line == linenum)
137 			/* We already have this one. */
138 			return;
139 	nextp = p;
140 	prevp = p->prev;
141 
142 	if (freelist != NULL) {
143 		/*
144 		 * We still have free (unused) entries.
145 		 * Use one of them.
146 		 */
147 		new = freelist;
148 		freelist = freelist->next;
149 	} else {
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 		 * We have used the spare entry.
180 		 * Scan the list to find the one with the smallest
181 		 * gap, take it out and make it the spare.
182 		 * We should never remove the last one, so stop when
183 		 * we get to p->next == &anchor.  This also avoids
184 		 * looking at the gap of the last one, which is
185 		 * not computed by calcgap.
186 		 */
187 		mingap = anchor.next->gap;
188 		for (p = anchor.next;  p->next != &anchor;  p = p->next) {
189 			if (p->gap <= mingap) {
190 				spare = p;
191 				mingap = p->gap;
192 			}
193 		}
194 		spare->next->prev = spare->prev;
195 		spare->prev->next = spare->next;
196 	}
197 }
198 
199 static int loopcount;
200 static time_t startime;
201 
202 static void
203 longish(void)
204 {
205 	if (++loopcount > 100) {
206 		loopcount = 0;
207 		if (time(NULL) >= startime + LONGTIME) {
208 			ierror("Calculating line numbers", NULL);
209 			loopcount = -1;
210 		}
211 	}
212 }
213 
214 /*
215  * Turn off line numbers because the user has interrupted
216  * a lengthy line number calculation.
217  */
218 static void
219 abort_long(void)
220 {
221 	if (linenums == OPT_ONPLUS)
222 		/*
223 		 * We were displaying line numbers, so need to repaint.
224 		 */
225 		screen_trashed = 1;
226 	linenums = 0;
227 	error("Line numbers turned off", NULL);
228 }
229 
230 /*
231  * Find the line number associated with a given position.
232  * Return 0 if we can't figure it out.
233  */
234 off_t
235 find_linenum(off_t pos)
236 {
237 	struct linenum_info *p;
238 	off_t linenum;
239 	off_t cpos;
240 
241 	if (!linenums)
242 		/*
243 		 * We're not using line numbers.
244 		 */
245 		return (0);
246 	if (pos == -1)
247 		/*
248 		 * Caller doesn't know what he's talking about.
249 		 */
250 		return (0);
251 	if (pos <= ch_zero())
252 		/*
253 		 * Beginning of file is always line number 1.
254 		 */
255 		return (1);
256 
257 	/*
258 	 * Find the entry nearest to the position we want.
259 	 */
260 	for (p = anchor.next;  p != &anchor && p->pos < pos;  p = p->next)
261 		continue;
262 	if (p->pos == pos)
263 		/* Found it exactly. */
264 		return (p->line);
265 
266 	/*
267 	 * This is the (possibly) time-consuming part.
268 	 * We start at the line we just found and start
269 	 * reading the file forward or backward till we
270 	 * get to the place we want.
271 	 *
272 	 * First decide whether we should go forward from the
273 	 * previous one or backwards from the next one.
274 	 * The decision is based on which way involves
275 	 * traversing fewer bytes in the file.
276 	 */
277 	startime = time(NULL);
278 	if (p == &anchor || pos - p->prev->pos < p->pos - pos) {
279 		/*
280 		 * Go forward.
281 		 */
282 		p = p->prev;
283 		if (ch_seek(p->pos))
284 			return (0);
285 		loopcount = 0;
286 		for (linenum = p->line, cpos = p->pos; cpos < pos; linenum++) {
287 			/*
288 			 * Allow a signal to abort this loop.
289 			 */
290 			cpos = forw_raw_line(cpos, NULL, NULL);
291 			if (ABORT_SIGS()) {
292 				abort_long();
293 				return (0);
294 			}
295 			if (cpos == -1)
296 				return (0);
297 			longish();
298 		}
299 		/*
300 		 * We might as well cache it.
301 		 */
302 		add_lnum(linenum, cpos);
303 		/*
304 		 * If the given position is not at the start of a line,
305 		 * make sure we return the correct line number.
306 		 */
307 		if (cpos > pos)
308 			linenum--;
309 	} else {
310 		/*
311 		 * Go backward.
312 		 */
313 		if (ch_seek(p->pos))
314 			return (0);
315 		loopcount = 0;
316 		for (linenum = p->line, cpos = p->pos; cpos > pos; linenum--) {
317 			/*
318 			 * Allow a signal to abort this loop.
319 			 */
320 			cpos = back_raw_line(cpos, NULL, NULL);
321 			if (ABORT_SIGS()) {
322 				abort_long();
323 				return (0);
324 			}
325 			if (cpos == -1)
326 				return (0);
327 			longish();
328 		}
329 		/*
330 		 * We might as well cache it.
331 		 */
332 		add_lnum(linenum, cpos);
333 	}
334 
335 	return (linenum);
336 }
337 
338 /*
339  * Find the position of a given line number.
340  * Return -1 if we can't figure it out.
341  */
342 off_t
343 find_pos(off_t linenum)
344 {
345 	struct linenum_info *p;
346 	off_t cpos;
347 	off_t clinenum;
348 
349 	if (linenum <= 1)
350 		/*
351 		 * Line number 1 is beginning of file.
352 		 */
353 		return (ch_zero());
354 
355 	/*
356 	 * Find the entry nearest to the line number we want.
357 	 */
358 	for (p = anchor.next;  p != &anchor && p->line < linenum;  p = p->next)
359 		continue;
360 	if (p->line == linenum)
361 		/* Found it exactly. */
362 		return (p->pos);
363 
364 	if (p == &anchor || linenum - p->prev->line < p->line - linenum) {
365 		/*
366 		 * Go forward.
367 		 */
368 		p = p->prev;
369 		if (ch_seek(p->pos))
370 			return (-1);
371 		for (clinenum = p->line, cpos = p->pos;
372 		    clinenum < linenum;
373 		    clinenum++) {
374 			/*
375 			 * Allow a signal to abort this loop.
376 			 */
377 			cpos = forw_raw_line(cpos, NULL, NULL);
378 			if (ABORT_SIGS())
379 				return (-1);
380 			if (cpos == -1)
381 				return (-1);
382 		}
383 	} else {
384 		/*
385 		 * Go backward.
386 		 */
387 		if (ch_seek(p->pos))
388 			return (-1);
389 		for (clinenum = p->line, cpos = p->pos;
390 		    clinenum > linenum;
391 		    clinenum--) {
392 			/*
393 			 * Allow a signal to abort this loop.
394 			 */
395 			cpos = back_raw_line(cpos, (char **)NULL, (int *)NULL);
396 			if (ABORT_SIGS())
397 				return (-1);
398 			if (cpos == -1)
399 				return (-1);
400 		}
401 	}
402 	/*
403 	 * We might as well cache it.
404 	 */
405 	add_lnum(clinenum, cpos);
406 	return (cpos);
407 }
408 
409 /*
410  * Return the line number of the "current" line.
411  * The argument "where" tells which line is to be considered
412  * the "current" line (e.g. TOP, BOTTOM, MIDDLE, etc).
413  */
414 off_t
415 currline(int where)
416 {
417 	off_t pos;
418 	off_t len;
419 	off_t linenum;
420 
421 	pos = position(where);
422 	len = ch_length();
423 	while (pos == -1 && where >= 0 && where < sc_height)
424 		pos = position(++where);
425 	if (pos == -1)
426 		pos = len;
427 	linenum = find_linenum(pos);
428 	if (pos == len)
429 		linenum--;
430 	return (linenum);
431 }
432