/*1* Copyright (C) 1984-2025 Mark Nudelman2*3* You may distribute under the terms of either the GNU General Public4* License or the Less License, as specified in the README file.5*6* For more information, see the README file.7*/8910/*11* Code to handle displaying line numbers.12*13* Finding the line number of a given file position is rather tricky.14* We don't want to just start at the beginning of the file and15* count newlines, because that is slow for large files (and also16* wouldn't work if we couldn't get to the start of the file; e.g.17* if input is a long pipe).18*19* So we use the function add_lnum to cache line numbers.20* We try to be very clever and keep only the more interesting21* line numbers when we run out of space in our table. A line22* number is more interesting than another when it is far from23* other line numbers. For example, we'd rather keep lines24* 100,200,300 than 100,101,300. 200 is more interesting than25* 101 because 101 can be derived very cheaply from 100, while26* 200 is more expensive to derive from 100.27*28* The function currline() returns the line number of a given29* position in the file. As a side effect, it calls add_lnum30* to cache the line number. Therefore currline is occasionally31* called to make sure we cache line numbers often enough.32*/3334#include "less.h"3536/*37* Structure to keep track of a line number and the associated file position.38* A doubly-linked circular list of line numbers is kept ordered by line number.39*/40struct linenum_info41{42struct linenum_info *next; /* Link to next in the list */43struct linenum_info *prev; /* Line to previous in the list */44POSITION pos; /* File position */45POSITION gap; /* Gap between prev and next */46LINENUM line; /* Line number */47};48/*49* "gap" needs some explanation: the gap of any particular line number50* is the distance between the previous one and the next one in the list.51* ("Distance" means difference in file position.) In other words, the52* gap of a line number is the gap which would be introduced if this53* line number were deleted. It is used to decide which one to replace54* when we have a new one to insert and the table is full.55*/5657#define LONGTIME (2) /* In seconds */5859static struct linenum_info anchor; /* Anchor of the list */60static struct linenum_info *freelist; /* Anchor of the unused entries */61static struct linenum_info pool[LINENUM_POOL]; /* The pool itself */62static struct linenum_info *spare; /* We always keep one spare entry */63public lbool scanning_eof = FALSE;6465extern int linenums;66extern int sigs;67extern int sc_height;68extern int header_lines;69extern int nonum_headers;70extern POSITION header_start_pos;7172/*73* Initialize the line number structures.74*/75public void clr_linenum(void)76{77struct linenum_info *p;7879/*80* Put all the entries on the free list.81* Leave one for the "spare".82*/83for (p = pool; p < &pool[LINENUM_POOL-2]; p++)84p->next = p+1;85pool[LINENUM_POOL-2].next = NULL;86freelist = pool;8788spare = &pool[LINENUM_POOL-1];8990/*91* Initialize the anchor.92*/93anchor.next = anchor.prev = &anchor;94anchor.gap = 0;95anchor.pos = (POSITION)0;96anchor.line = 1;97}9899/*100* Calculate the gap for an entry.101*/102static void calcgap(struct linenum_info *p)103{104/*105* Don't bother to compute a gap for the anchor.106* Also don't compute a gap for the last one in the list.107* The gap for that last one should be considered infinite,108* but we never look at it anyway.109*/110if (p == &anchor || p->next == &anchor)111return;112p->gap = p->next->pos - p->prev->pos;113}114115/*116* Add a new line number to the cache.117* The specified position (pos) should be the file position of the118* FIRST character in the specified line.119*/120public void add_lnum(LINENUM linenum, POSITION pos)121{122struct linenum_info *p;123struct linenum_info *new;124struct linenum_info *nextp;125struct linenum_info *prevp;126POSITION mingap;127128/*129* Find the proper place in the list for the new one.130* The entries are sorted by position.131*/132for (p = anchor.next; p != &anchor && p->pos < pos; p = p->next)133if (p->line == linenum)134/* We already have this one. */135return;136nextp = p;137prevp = p->prev;138139if (freelist != NULL)140{141/*142* We still have free (unused) entries.143* Use one of them.144*/145new = freelist;146freelist = freelist->next;147} else148{149/*150* No free entries.151* Use the "spare" entry.152*/153new = spare;154spare = NULL;155}156157/*158* Fill in the fields of the new entry,159* and insert it into the proper place in the list.160*/161new->next = nextp;162new->prev = prevp;163new->pos = pos;164new->line = linenum;165166nextp->prev = new;167prevp->next = new;168169/*170* Recalculate gaps for the new entry and the neighboring entries.171*/172calcgap(new);173calcgap(nextp);174calcgap(prevp);175176if (spare == NULL)177{178/*179* We have used the spare entry.180* Scan the list to find the one with the smallest181* gap, take it out and make it the spare.182* We should never remove the last one, so stop when183* we get to p->next == &anchor. This also avoids184* looking at the gap of the last one, which is185* not computed by calcgap.186*/187mingap = anchor.next->gap;188for (p = anchor.next; p->next != &anchor; p = p->next)189{190if (p->gap <= mingap)191{192spare = p;193mingap = p->gap;194}195}196spare->next->prev = spare->prev;197spare->prev->next = spare->next;198}199}200201/*202* If we get stuck in a long loop trying to figure out the203* line number, print a message to tell the user what we're doing.204*/205static void longloopmessage(void)206{207ierror("Calculating line numbers", NULL_PARG);208}209210struct delayed_msg211{212void (*message)(void);213int loopcount;214#if HAVE_TIME215time_type startime;216#endif217};218219static void start_delayed_msg(struct delayed_msg *dmsg, void (*message)(void))220{221dmsg->loopcount = 0;222dmsg->message = message;223#if HAVE_TIME224dmsg->startime = get_time();225#endif226}227228static void delayed_msg(struct delayed_msg *dmsg)229{230#if HAVE_TIME231if (dmsg->loopcount >= 0 && ++(dmsg->loopcount) > 100)232{233dmsg->loopcount = 0;234if (get_time() >= dmsg->startime + LONGTIME)235{236dmsg->message();237dmsg->loopcount = -1;238}239}240#else241if (dmsg->loopcount >= 0 && ++(dmsg->loopcount) > LONGLOOP)242{243dmsg->message();244dmsg->loopcount = -1;245}246#endif247}248249/*250* Turn off line numbers because the user has interrupted251* a lengthy line number calculation.252*/253static void abort_delayed_msg(struct delayed_msg *dmsg)254{255if (dmsg->loopcount >= 0)256return;257if (linenums == OPT_ONPLUS)258/*259* We were displaying line numbers, so need to repaint.260*/261screen_trashed();262linenums = 0;263error("Line numbers turned off", NULL_PARG);264}265266/*267* Find the line number associated with a given position.268* Return 0 if we can't figure it out.269*/270public LINENUM find_linenum(POSITION pos)271{272struct linenum_info *p;273LINENUM linenum;274POSITION cpos;275struct delayed_msg dmsg;276277if (!linenums)278/*279* We're not using line numbers.280*/281return (0);282if (pos == NULL_POSITION)283/*284* Caller doesn't know what he's talking about.285*/286return (0);287if (pos <= ch_zero())288/*289* Beginning of file is always line number 1.290*/291return (1);292293/*294* Find the entry nearest to the position we want.295*/296for (p = anchor.next; p != &anchor && p->pos < pos; p = p->next)297continue;298if (p->pos == pos)299/* Found it exactly. */300return (p->line);301302/*303* This is the (possibly) time-consuming part.304* We start at the line we just found and start305* reading the file forward or backward till we306* get to the place we want.307*308* First decide whether we should go forward from the309* previous one or backwards from the next one.310* The decision is based on which way involves311* traversing fewer bytes in the file.312*/313start_delayed_msg(&dmsg, longloopmessage);314if (p == &anchor || pos - p->prev->pos < p->pos - pos)315{316/*317* Go forward.318*/319p = p->prev;320if (ch_seek(p->pos))321return (0);322for (linenum = p->line, cpos = p->pos; cpos < pos; linenum++)323{324/*325* Allow a signal to abort this loop.326*/327cpos = forw_raw_line(cpos, NULL, NULL);328if (ABORT_SIGS()) {329abort_delayed_msg(&dmsg);330return (0);331}332if (cpos == NULL_POSITION)333return (0);334delayed_msg(&dmsg);335}336/*337* We might as well cache it.338*/339add_lnum(linenum, cpos);340/*341* If the given position is not at the start of a line,342* make sure we return the correct line number.343*/344if (cpos > pos)345linenum--;346} else347{348/*349* Go backward.350*/351if (ch_seek(p->pos))352return (0);353for (linenum = p->line, cpos = p->pos; cpos > pos; linenum--)354{355/*356* Allow a signal to abort this loop.357*/358cpos = back_raw_line(cpos, NULL, NULL);359if (ABORT_SIGS()) {360abort_delayed_msg(&dmsg);361return (0);362}363if (cpos == NULL_POSITION)364return (0);365delayed_msg(&dmsg);366}367/*368* We might as well cache it.369*/370add_lnum(linenum, cpos);371}372return (linenum);373}374375/*376* Find the position of a given line number.377* Return NULL_POSITION if we can't figure it out.378*/379public POSITION find_pos(LINENUM linenum)380{381struct linenum_info *p;382POSITION cpos;383LINENUM clinenum;384385if (linenum <= 1)386/*387* Line number 1 is beginning of file.388*/389return (ch_zero());390391/*392* Find the entry nearest to the line number we want.393*/394for (p = anchor.next; p != &anchor && p->line < linenum; p = p->next)395continue;396if (p->line == linenum)397/* Found it exactly. */398return (p->pos);399400if (p == &anchor || linenum - p->prev->line < p->line - linenum)401{402/*403* Go forward.404*/405p = p->prev;406if (ch_seek(p->pos))407return (NULL_POSITION);408for (clinenum = p->line, cpos = p->pos; clinenum < linenum; clinenum++)409{410/*411* Allow a signal to abort this loop.412*/413cpos = forw_raw_line(cpos, NULL, NULL);414if (ABORT_SIGS())415return (NULL_POSITION);416if (cpos == NULL_POSITION)417return (NULL_POSITION);418}419} else420{421/*422* Go backward.423*/424if (ch_seek(p->pos))425return (NULL_POSITION);426for (clinenum = p->line, cpos = p->pos; clinenum > linenum; clinenum--)427{428/*429* Allow a signal to abort this loop.430*/431cpos = back_raw_line(cpos, NULL, NULL);432if (ABORT_SIGS())433return (NULL_POSITION);434if (cpos == NULL_POSITION)435return (NULL_POSITION);436}437}438/*439* We might as well cache it.440*/441add_lnum(clinenum, cpos);442return (cpos);443}444445/*446* Return the line number of the "current" line.447* The argument "where" tells which line is to be considered448* the "current" line (e.g. TOP, BOTTOM, MIDDLE, etc).449*/450public LINENUM currline(int where)451{452POSITION pos;453POSITION len;454LINENUM linenum;455456pos = position(where);457len = ch_length();458while (pos == NULL_POSITION && where >= 0 && where < sc_height)459pos = position(++where);460if (pos == NULL_POSITION)461pos = len;462linenum = find_linenum(pos);463if (pos == len)464linenum--;465return (linenum);466}467468static void detlenmessage(void)469{470ierror("Determining length of file", NULL_PARG);471}472473/*474* Scan entire file, counting line numbers.475*/476public void scan_eof(void)477{478POSITION pos = ch_zero();479LINENUM linenum = 0;480struct delayed_msg dmsg;481482if (ch_seek(0))483return;484/*485* scanning_eof prevents the "Waiting for data" message from486* overwriting "Determining length of file".487*/488start_delayed_msg(&dmsg, detlenmessage);489scanning_eof = TRUE;490while (pos != NULL_POSITION)491{492/* For efficiency, only add one every 256 line numbers. */493if ((linenum++ % 256) == 0)494add_lnum(linenum, pos);495pos = forw_raw_line(pos, NULL, NULL);496if (ABORT_SIGS())497{498abort_delayed_msg(&dmsg);499break;500}501delayed_msg(&dmsg);502}503scanning_eof = FALSE;504}505506/*507* Return a line number adjusted for display508* (handles the --no-number-headers option).509*/510public LINENUM vlinenum(LINENUM linenum)511{512if (nonum_headers && header_lines > 0)513{514LINENUM header_start_line = find_linenum(header_start_pos);515if (header_start_line != 0)516{517LINENUM header_end_line = header_start_line + header_lines; /* first line after header */518linenum = (linenum < header_end_line) ? 0 : linenum - header_end_line + 1;519}520}521return linenum;522}523524525