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