1/*
2 * Copyright (C) 1984-2023  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#include "less.h"
12#include "position.h"
13
14extern IFILE curr_ifile;
15extern int sc_height;
16extern int jump_sline;
17extern int perma_marks;
18
19/*
20 * A mark is an ifile (input file) plus a position within the file.
21 */
22struct mark
23{
24	/*
25	 * Normally m_ifile != IFILE_NULL and m_filename == NULL.
26	 * For restored marks we set m_filename instead of m_ifile
27	 * because we don't want to create an ifile until the
28	 * user explicitly requests the file (by name or mark).
29	 */
30	char m_letter;           /* Associated character */
31	IFILE m_ifile;           /* Input file being marked */
32	char *m_filename;        /* Name of the input file */
33	struct scrpos m_scrpos;  /* Position of the mark */
34};
35
36/*
37 * The table of marks.
38 * Each mark is identified by a lowercase or uppercase letter.
39 * The final one is lmark, for the "last mark"; addressed by the apostrophe.
40 */
41#define NMARKS          ((2*26)+2)      /* a-z, A-Z, mousemark, lastmark */
42#define NUMARKS         ((2*26)+1)      /* user marks (not lastmark) */
43#define MOUSEMARK       (NMARKS-2)
44#define LASTMARK        (NMARKS-1)
45static struct mark marks[NMARKS];
46public int marks_modified = 0;
47
48
49/*
50 * Initialize a mark struct.
51 */
52static void cmark(struct mark *m, IFILE ifile, POSITION pos, int ln)
53{
54	m->m_ifile = ifile;
55	m->m_scrpos.pos = pos;
56	m->m_scrpos.ln = ln;
57	if (m->m_filename != NULL)
58		/* Normally should not happen but a corrupt lesshst file can do it. */
59		free(m->m_filename);
60	m->m_filename = NULL;
61}
62
63/*
64 * Initialize the mark table to show no marks are set.
65 */
66public void init_mark(void)
67{
68	int i;
69
70	for (i = 0;  i < NMARKS;  i++)
71	{
72		char letter;
73		switch (i) {
74		case MOUSEMARK: letter = '#'; break;
75		case LASTMARK: letter = '\''; break;
76		default: letter = (i < 26) ? 'a'+i : 'A'+i-26; break;
77		}
78		marks[i].m_letter = letter;
79		cmark(&marks[i], NULL_IFILE, NULL_POSITION, -1);
80	}
81}
82
83/*
84 * Set m_ifile and clear m_filename.
85 */
86static void mark_set_ifile(struct mark *m, IFILE ifile)
87{
88	m->m_ifile = ifile;
89	/* With m_ifile set, m_filename is no longer needed. */
90	free(m->m_filename);
91	m->m_filename = NULL;
92}
93
94/*
95 * Populate the m_ifile member of a mark struct from m_filename.
96 */
97static void mark_get_ifile(struct mark *m)
98{
99	if (m->m_ifile != NULL_IFILE)
100		return; /* m_ifile is already set */
101	mark_set_ifile(m, get_ifile(m->m_filename, prev_ifile(NULL_IFILE)));
102}
103
104/*
105 * Return the user mark struct identified by a character.
106 */
107static struct mark * getumark(LWCHAR c)
108{
109	PARG parg;
110	if (c >= 'a' && c <= 'z')
111		return (&marks[c-'a']);
112	if (c >= 'A' && c <= 'Z')
113		return (&marks[c-'A'+26]);
114	if (c == '\'')
115		return (&marks[LASTMARK]);
116	if (c == '#')
117		return (&marks[MOUSEMARK]);
118	parg.p_char = (char) c;
119	error("Invalid mark letter %c", &parg);
120	return (NULL);
121}
122
123/*
124 * Get the mark structure identified by a character.
125 * The mark struct may either be in the mark table (user mark)
126 * or may be constructed on the fly for certain characters like ^, $.
127 */
128static struct mark * getmark(LWCHAR c)
129{
130	struct mark *m;
131	static struct mark sm;
132
133	switch (c)
134	{
135	case '^':
136		/*
137		 * Beginning of the current file.
138		 */
139		m = &sm;
140		cmark(m, curr_ifile, ch_zero(), 0);
141		break;
142	case '$':
143		/*
144		 * End of the current file.
145		 */
146		if (ch_end_seek())
147		{
148			error("Cannot seek to end of file", NULL_PARG);
149			return (NULL);
150		}
151		m = &sm;
152		cmark(m, curr_ifile, ch_tell(), sc_height);
153		break;
154	case '.':
155		/*
156		 * Current position in the current file.
157		 */
158		m = &sm;
159		get_scrpos(&m->m_scrpos, TOP);
160		cmark(m, curr_ifile, m->m_scrpos.pos, m->m_scrpos.ln);
161		break;
162	case '\'':
163		/*
164		 * The "last mark".
165		 */
166		m = &marks[LASTMARK];
167		break;
168	default:
169		/*
170		 * Must be a user-defined mark.
171		 */
172		m = getumark(c);
173		if (m == NULL)
174			break;
175		if (m->m_scrpos.pos == NULL_POSITION)
176		{
177			error("Mark not set", NULL_PARG);
178			return (NULL);
179		}
180		break;
181	}
182	return (m);
183}
184
185/*
186 * Is a mark letter invalid?
187 */
188public int badmark(LWCHAR c)
189{
190	return (getmark(c) == NULL);
191}
192
193/*
194 * Set a user-defined mark.
195 */
196public void setmark(LWCHAR c, int where)
197{
198	struct mark *m;
199	struct scrpos scrpos;
200
201	m = getumark(c);
202	if (m == NULL)
203		return;
204	get_scrpos(&scrpos, where);
205	if (scrpos.pos == NULL_POSITION)
206	{
207		bell();
208		return;
209	}
210	cmark(m, curr_ifile, scrpos.pos, scrpos.ln);
211	marks_modified = 1;
212}
213
214/*
215 * Clear a user-defined mark.
216 */
217public void clrmark(LWCHAR c)
218{
219	struct mark *m;
220
221	m = getumark(c);
222	if (m == NULL)
223		return;
224	if (m->m_scrpos.pos == NULL_POSITION)
225	{
226		bell();
227		return;
228	}
229	m->m_scrpos.pos = NULL_POSITION;
230	marks_modified = 1;
231}
232
233/*
234 * Set lmark (the mark named by the apostrophe).
235 */
236public void lastmark(void)
237{
238	struct scrpos scrpos;
239
240	if (ch_getflags() & CH_HELPFILE)
241		return;
242	get_scrpos(&scrpos, TOP);
243	if (scrpos.pos == NULL_POSITION)
244		return;
245	cmark(&marks[LASTMARK], curr_ifile, scrpos.pos, scrpos.ln);
246	marks_modified = 1;
247}
248
249/*
250 * Go to a mark.
251 */
252public void gomark(LWCHAR c)
253{
254	struct mark *m;
255	struct scrpos scrpos;
256
257	m = getmark(c);
258	if (m == NULL)
259		return;
260
261	/*
262	 * If we're trying to go to the lastmark and
263	 * it has not been set to anything yet,
264	 * set it to the beginning of the current file.
265	 * {{ Couldn't we instead set marks[LASTMARK] in edit()? }}
266	 */
267	if (m == &marks[LASTMARK] && m->m_scrpos.pos == NULL_POSITION)
268		cmark(m, curr_ifile, ch_zero(), jump_sline);
269
270	mark_get_ifile(m);
271
272	/* Save scrpos; if it's LASTMARK it could change in edit_ifile. */
273	scrpos = m->m_scrpos;
274	if (m->m_ifile != curr_ifile)
275	{
276		/*
277		 * Not in the current file; edit the correct file.
278		 */
279		if (edit_ifile(m->m_ifile))
280			return;
281	}
282
283	jump_loc(scrpos.pos, scrpos.ln);
284}
285
286/*
287 * Return the position associated with a given mark letter.
288 *
289 * We don't return which screen line the position
290 * is associated with, but this doesn't matter much,
291 * because it's always the first non-blank line on the screen.
292 */
293public POSITION markpos(LWCHAR c)
294{
295	struct mark *m;
296
297	m = getmark(c);
298	if (m == NULL)
299		return (NULL_POSITION);
300
301	if (m->m_ifile != curr_ifile)
302	{
303		error("Mark not in current file", NULL_PARG);
304		return (NULL_POSITION);
305	}
306	return (m->m_scrpos.pos);
307}
308
309/*
310 * Return the mark associated with a given position, if any.
311 */
312public char posmark(POSITION pos)
313{
314	int i;
315
316	/* Only user marks */
317	for (i = 0;  i < NUMARKS;  i++)
318	{
319		if (marks[i].m_ifile == curr_ifile && marks[i].m_scrpos.pos == pos)
320		{
321			if (i < 26) return 'a' + i;
322			if (i < 26*2) return 'A' + (i - 26);
323			return '#';
324		}
325	}
326	return 0;
327}
328
329/*
330 * Clear the marks associated with a specified ifile.
331 */
332public void unmark(IFILE ifile)
333{
334	int i;
335
336	for (i = 0;  i < NMARKS;  i++)
337		if (marks[i].m_ifile == ifile)
338			marks[i].m_scrpos.pos = NULL_POSITION;
339}
340
341/*
342 * Check if any marks refer to a specified ifile vi m_filename
343 * rather than m_ifile.
344 */
345public void mark_check_ifile(IFILE ifile)
346{
347	int i;
348	char *filename = get_real_filename(ifile);
349
350	for (i = 0;  i < NMARKS;  i++)
351	{
352		struct mark *m = &marks[i];
353		char *mark_filename = m->m_filename;
354		if (mark_filename != NULL)
355		{
356			mark_filename = lrealpath(mark_filename);
357			if (strcmp(filename, mark_filename) == 0)
358				mark_set_ifile(m, ifile);
359			free(mark_filename);
360		}
361	}
362}
363
364#if CMD_HISTORY
365
366/*
367 * Save marks to history file.
368 */
369public void save_marks(FILE *fout, char *hdr)
370{
371	int i;
372
373	if (!perma_marks)
374		return;
375
376	fprintf(fout, "%s\n", hdr);
377	for (i = 0;  i < NMARKS;  i++)
378	{
379		char *filename;
380		struct mark *m = &marks[i];
381		char pos_str[INT_STRLEN_BOUND(m->m_scrpos.pos) + 2];
382		if (m->m_scrpos.pos == NULL_POSITION)
383			continue;
384		postoa(m->m_scrpos.pos, pos_str, 10);
385		filename = m->m_filename;
386		if (filename == NULL)
387			filename = get_real_filename(m->m_ifile);
388		if (strcmp(filename, "-") != 0)
389			fprintf(fout, "m %c %d %s %s\n",
390				m->m_letter, m->m_scrpos.ln, pos_str, filename);
391	}
392}
393
394/*
395 * Restore one mark from the history file.
396 */
397public void restore_mark(char *line)
398{
399	struct mark *m;
400	int ln;
401	POSITION pos;
402
403#define skip_whitespace while (*line == ' ') line++
404	if (*line++ != 'm')
405		return;
406	skip_whitespace;
407	m = getumark(*line++);
408	if (m == NULL)
409		return;
410	skip_whitespace;
411	ln = lstrtoi(line, &line, 10);
412	if (ln < 0)
413		return;
414	if (ln < 1)
415		ln = 1;
416	if (ln > sc_height)
417		ln = sc_height;
418	skip_whitespace;
419	pos = lstrtopos(line, &line, 10);
420	if (pos < 0)
421		return;
422	skip_whitespace;
423	cmark(m, NULL_IFILE, pos, ln);
424	m->m_filename = save(line);
425}
426
427#endif /* CMD_HISTORY */
428