Coverage Report

Created: 2023-09-25 07:17

/src/neomutt/complete/helpers.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * @file
3
 * Auto-completion helpers
4
 *
5
 * @authors
6
 * Copyright (C) 2022 Richard Russon <rich@flatcap.org>
7
 *
8
 * @copyright
9
 * This program is free software: you can redistribute it and/or modify it under
10
 * the terms of the GNU General Public License as published by the Free Software
11
 * Foundation, either version 2 of the License, or (at your option) any later
12
 * version.
13
 *
14
 * This program is distributed in the hope that it will be useful, but WITHOUT
15
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17
 * details.
18
 *
19
 * You should have received a copy of the GNU General Public License along with
20
 * this program.  If not, see <http://www.gnu.org/licenses/>.
21
 */
22
23
/**
24
 * @page complete_helpers Auto-completion helpers
25
 *
26
 * Auto-completion helpers
27
 */
28
29
#include "config.h"
30
#include <ctype.h>
31
#include <stdbool.h>
32
#include <stdio.h>
33
#include <string.h>
34
#include <strings.h>
35
#include "mutt/lib.h"
36
#include "config/lib.h"
37
#include "core/lib.h"
38
#include "gui/lib.h"
39
#include "lib.h"
40
#include "editor/lib.h"
41
#include "index/lib.h"
42
#include "key/lib.h"
43
#include "menu/lib.h"
44
#include "compapi.h"
45
#include "data.h"
46
47
/**
48
 * matches_ensure_morespace - Allocate more space for auto-completion
49
 * @param cd       Completion Data
50
 * @param new_size Space required
51
 */
52
void matches_ensure_morespace(struct CompletionData *cd, int new_size)
53
0
{
54
0
  if (new_size <= (cd->match_list_len - 2))
55
0
    return;
56
57
0
  new_size = ROUND_UP(new_size + 2, 512);
58
59
0
  mutt_mem_realloc(&cd->match_list, new_size * sizeof(char *));
60
0
  memset(&cd->match_list[cd->match_list_len], 0, new_size - cd->match_list_len);
61
62
0
  cd->match_list_len = new_size;
63
0
}
64
65
/**
66
 * candidate - Helper function for completion
67
 * @param cd   Completion Data
68
 * @param user User entered data for completion
69
 * @param src  Candidate for completion
70
 * @param dest Completion result gets here
71
 * @param dlen Length of dest buffer
72
 * @retval true If candidate string matches
73
 *
74
 * Changes the dest buffer if necessary/possible to aid completion.
75
 */
76
bool candidate(struct CompletionData *cd, char *user, const char *src, char *dest, size_t dlen)
77
0
{
78
0
  if (!dest || !user || !src)
79
0
    return false;
80
81
0
  if (strstr(src, user) != src)
82
0
    return false;
83
84
0
  matches_ensure_morespace(cd, cd->num_matched);
85
0
  cd->match_list[cd->num_matched++] = src;
86
0
  if (dest[0] == '\0')
87
0
  {
88
0
    mutt_str_copy(dest, src, dlen);
89
0
  }
90
0
  else
91
0
  {
92
0
    int l;
93
0
    for (l = 0; (src[l] != '\0') && (src[l] == dest[l]); l++)
94
0
      ; // do nothing
95
96
0
    dest[l] = '\0';
97
0
  }
98
0
  return true;
99
0
}
100
101
/**
102
 * mutt_command_complete - Complete a command name
103
 * @param cd      Completion Data
104
 * @param buf     Buffer for the result
105
 * @param pos     Cursor position in the buffer
106
 * @param numtabs Number of times the user has hit 'tab'
107
 * @retval 1 Success, a match
108
 * @retval 0 Error, no match
109
 */
110
int mutt_command_complete(struct CompletionData *cd, struct Buffer *buf, int pos, int numtabs)
111
0
{
112
0
  char *pt = buf->data;
113
0
  int spaces; /* keep track of the number of leading spaces on the line */
114
115
0
  SKIPWS(pt);
116
0
  spaces = pt - buf->data;
117
118
0
  pt = buf->data + pos - spaces;
119
0
  while ((pt > buf->data) && !isspace((unsigned char) *pt))
120
0
    pt--;
121
122
0
  if (pt == buf->data) /* complete cmd */
123
0
  {
124
    /* first TAB. Collect all the matches */
125
0
    if (numtabs == 1)
126
0
    {
127
0
      cd->num_matched = 0;
128
0
      mutt_str_copy(cd->user_typed, pt, sizeof(cd->user_typed));
129
0
      memset(cd->match_list, 0, cd->match_list_len);
130
0
      memset(cd->completed, 0, sizeof(cd->completed));
131
132
0
      struct Command *c = NULL;
133
0
      for (size_t num = 0, size = commands_array(&c); num < size; num++)
134
0
        candidate(cd, cd->user_typed, c[num].name, cd->completed, sizeof(cd->completed));
135
0
      matches_ensure_morespace(cd, cd->num_matched);
136
0
      cd->match_list[cd->num_matched++] = cd->user_typed;
137
138
      /* All matches are stored. Longest non-ambiguous string is ""
139
       * i.e. don't change 'buf'. Fake successful return this time */
140
0
      if (cd->user_typed[0] == '\0')
141
0
        return 1;
142
0
    }
143
144
0
    if ((cd->completed[0] == '\0') && (cd->user_typed[0] != '\0'))
145
0
      return 0;
146
147
    /* cd->num_matched will _always_ be at least 1 since the initial
148
     * user-typed string is always stored */
149
0
    if ((numtabs == 1) && (cd->num_matched == 2))
150
0
    {
151
0
      snprintf(cd->completed, sizeof(cd->completed), "%s", cd->match_list[0]);
152
0
    }
153
0
    else if ((numtabs > 1) && (cd->num_matched > 2))
154
0
    {
155
      /* cycle through all the matches */
156
0
      snprintf(cd->completed, sizeof(cd->completed), "%s",
157
0
               cd->match_list[(numtabs - 2) % cd->num_matched]);
158
0
    }
159
160
    /* return the completed command */
161
0
    buf_strcpy(buf, cd->completed);
162
0
  }
163
0
  else if (buf_startswith(buf, "set") || buf_startswith(buf, "unset") ||
164
0
           buf_startswith(buf, "reset") || buf_startswith(buf, "toggle"))
165
0
  { /* complete variables */
166
0
    static const char *const prefixes[] = { "no", "inv", "?", "&", 0 };
167
168
0
    pt++;
169
    /* loop through all the possible prefixes (no, inv, ...) */
170
0
    if (buf_startswith(buf, "set"))
171
0
    {
172
0
      for (int num = 0; prefixes[num]; num++)
173
0
      {
174
0
        if (mutt_str_startswith(pt, prefixes[num]))
175
0
        {
176
0
          pt += mutt_str_len(prefixes[num]);
177
0
          break;
178
0
        }
179
0
      }
180
0
    }
181
182
    /* first TAB. Collect all the matches */
183
0
    if (numtabs == 1)
184
0
    {
185
0
      cd->num_matched = 0;
186
0
      mutt_str_copy(cd->user_typed, pt, sizeof(cd->user_typed));
187
0
      memset(cd->match_list, 0, cd->match_list_len);
188
0
      memset(cd->completed, 0, sizeof(cd->completed));
189
190
0
      struct HashElem *he = NULL;
191
0
      struct HashElem **he_list = get_elem_list(NeoMutt->sub->cs);
192
0
      for (size_t i = 0; he_list[i]; i++)
193
0
      {
194
0
        he = he_list[i];
195
0
        const int type = DTYPE(he->type);
196
197
0
        if ((type == DT_SYNONYM) || (type & DT_DEPRECATED))
198
0
          continue;
199
200
0
        candidate(cd, cd->user_typed, he->key.strkey, cd->completed, sizeof(cd->completed));
201
0
      }
202
0
      FREE(&he_list);
203
204
0
      matches_ensure_morespace(cd, cd->num_matched);
205
0
      cd->match_list[cd->num_matched++] = cd->user_typed;
206
207
      /* All matches are stored. Longest non-ambiguous string is ""
208
       * i.e. don't change 'buf'. Fake successful return this time */
209
0
      if (cd->user_typed[0] == '\0')
210
0
        return 1;
211
0
    }
212
213
0
    if ((cd->completed[0] == 0) && cd->user_typed[0])
214
0
      return 0;
215
216
    /* cd->num_matched will _always_ be at least 1 since the initial
217
     * user-typed string is always stored */
218
0
    if ((numtabs == 1) && (cd->num_matched == 2))
219
0
    {
220
0
      snprintf(cd->completed, sizeof(cd->completed), "%s", cd->match_list[0]);
221
0
    }
222
0
    else if ((numtabs > 1) && (cd->num_matched > 2))
223
0
    {
224
      /* cycle through all the matches */
225
0
      snprintf(cd->completed, sizeof(cd->completed), "%s",
226
0
               cd->match_list[(numtabs - 2) % cd->num_matched]);
227
0
    }
228
229
0
    strncpy(pt, cd->completed, buf->data + buf->dsize - pt - spaces);
230
0
    buf_fix_dptr(buf);
231
0
  }
232
0
  else if (buf_startswith(buf, "exec"))
233
0
  {
234
0
    const enum MenuType mtype = menu_get_current_type();
235
0
    const struct MenuFuncOp *funcs = km_get_table(mtype);
236
0
    if (!funcs && (mtype != MENU_PAGER))
237
0
      funcs = OpGeneric;
238
239
0
    pt++;
240
    /* first TAB. Collect all the matches */
241
0
    if (numtabs == 1)
242
0
    {
243
0
      cd->num_matched = 0;
244
0
      mutt_str_copy(cd->user_typed, pt, sizeof(cd->user_typed));
245
0
      memset(cd->match_list, 0, cd->match_list_len);
246
0
      memset(cd->completed, 0, sizeof(cd->completed));
247
0
      for (int num = 0; funcs[num].name; num++)
248
0
        candidate(cd, cd->user_typed, funcs[num].name, cd->completed, sizeof(cd->completed));
249
      /* try the generic menu */
250
0
      if ((mtype != MENU_PAGER) && (mtype != MENU_GENERIC))
251
0
      {
252
0
        funcs = OpGeneric;
253
0
        for (int num = 0; funcs[num].name; num++)
254
0
          candidate(cd, cd->user_typed, funcs[num].name, cd->completed,
255
0
                    sizeof(cd->completed));
256
0
      }
257
0
      matches_ensure_morespace(cd, cd->num_matched);
258
0
      cd->match_list[cd->num_matched++] = cd->user_typed;
259
260
      /* All matches are stored. Longest non-ambiguous string is ""
261
       * i.e. don't change 'buf'. Fake successful return this time */
262
0
      if (cd->user_typed[0] == '\0')
263
0
        return 1;
264
0
    }
265
266
0
    if ((cd->completed[0] == '\0') && (cd->user_typed[0] != '\0'))
267
0
      return 0;
268
269
    /* cd->num_matched will _always_ be at least 1 since the initial
270
     * user-typed string is always stored */
271
0
    if ((numtabs == 1) && (cd->num_matched == 2))
272
0
    {
273
0
      snprintf(cd->completed, sizeof(cd->completed), "%s", cd->match_list[0]);
274
0
    }
275
0
    else if ((numtabs > 1) && (cd->num_matched > 2))
276
0
    {
277
      /* cycle through all the matches */
278
0
      snprintf(cd->completed, sizeof(cd->completed), "%s",
279
0
               cd->match_list[(numtabs - 2) % cd->num_matched]);
280
0
    }
281
282
0
    strncpy(pt, cd->completed, buf->data + buf->dsize - pt - spaces);
283
0
    buf_fix_dptr(buf);
284
0
  }
285
0
  else
286
0
  {
287
0
    return 0;
288
0
  }
289
290
0
  return 1;
291
0
}
292
293
/**
294
 * label_sort - Compare two label strings - Implements ::sort_t - @ingroup sort_api
295
 */
296
static int label_sort(const void *a, const void *b, void *sdata)
297
0
{
298
0
  return strcasecmp(*(const char **) a, *(const char **) b);
299
0
}
300
301
/**
302
 * mutt_label_complete - Complete a label name
303
 * @param cd      Completion Data
304
 * @param buf     Buffer for the result
305
 * @param numtabs Number of times the user has hit 'tab'
306
 * @retval 1 Success, a match
307
 * @retval 0 Error, no match
308
 */
309
int mutt_label_complete(struct CompletionData *cd, struct Buffer *buf, int numtabs)
310
0
{
311
0
  char *pt = buf->data;
312
313
0
  struct Mailbox *m_cur = get_current_mailbox();
314
0
  if (!m_cur || !m_cur->label_hash)
315
0
    return 0;
316
317
0
  SKIPWS(pt);
318
319
  /* first TAB. Collect all the matches */
320
0
  if (numtabs == 1)
321
0
  {
322
0
    struct HashElem *he = NULL;
323
0
    struct HashWalkState hws = { 0 };
324
325
0
    cd->num_matched = 0;
326
0
    mutt_str_copy(cd->user_typed, buf_string(buf), sizeof(cd->user_typed));
327
0
    memset(cd->match_list, 0, cd->match_list_len);
328
0
    memset(cd->completed, 0, sizeof(cd->completed));
329
0
    while ((he = mutt_hash_walk(m_cur->label_hash, &hws)))
330
0
      candidate(cd, cd->user_typed, he->key.strkey, cd->completed, sizeof(cd->completed));
331
0
    matches_ensure_morespace(cd, cd->num_matched);
332
0
    mutt_qsort_r(cd->match_list, cd->num_matched, sizeof(char *), label_sort, NULL);
333
0
    cd->match_list[cd->num_matched++] = cd->user_typed;
334
335
    /* All matches are stored. Longest non-ambiguous string is ""
336
     * i.e. don't change 'buf'. Fake successful return this time */
337
0
    if (cd->user_typed[0] == '\0')
338
0
      return 1;
339
0
  }
340
341
0
  if ((cd->completed[0] == '\0') && (cd->user_typed[0] != '\0'))
342
0
    return 0;
343
344
  /* cd->num_matched will _always_ be at least 1 since the initial
345
   * user-typed string is always stored */
346
0
  if ((numtabs == 1) && (cd->num_matched == 2))
347
0
  {
348
0
    snprintf(cd->completed, sizeof(cd->completed), "%s", cd->match_list[0]);
349
0
  }
350
0
  else if ((numtabs > 1) && (cd->num_matched > 2))
351
0
  {
352
    /* cycle through all the matches */
353
0
    snprintf(cd->completed, sizeof(cd->completed), "%s",
354
0
             cd->match_list[(numtabs - 2) % cd->num_matched]);
355
0
  }
356
357
  /* return the completed label */
358
0
  buf_strcpy(buf, cd->completed);
359
360
0
  return 1;
361
0
}
362
363
/**
364
 * mutt_var_value_complete - Complete a variable/value
365
 * @param cd  Completion Data
366
 * @param buf Buffer for the result
367
 * @param pos Cursor position in the buffer
368
 * @retval 1 Success
369
 * @retval 0 Failure
370
 */
371
int mutt_var_value_complete(struct CompletionData *cd, struct Buffer *buf, int pos)
372
0
{
373
0
  char *pt = buf->data;
374
375
0
  if (pt[0] == '\0')
376
0
    return 0;
377
378
0
  SKIPWS(pt);
379
0
  const int spaces = pt - buf->data;
380
381
0
  pt = buf->data + pos - spaces;
382
0
  while ((pt > buf->data) && !isspace((unsigned char) *pt))
383
0
    pt--;
384
0
  pt++;           /* move past the space */
385
0
  if (*pt == '=') /* abort if no var before the '=' */
386
0
    return 0;
387
388
0
  if (buf_startswith(buf, "set"))
389
0
  {
390
0
    char var[256] = { 0 };
391
0
    mutt_str_copy(var, pt, sizeof(var));
392
    /* ignore the trailing '=' when comparing */
393
0
    int vlen = mutt_str_len(var);
394
0
    if (vlen == 0)
395
0
      return 0;
396
397
0
    var[vlen - 1] = '\0';
398
399
0
    struct HashElem *he = cs_subset_lookup(NeoMutt->sub, var);
400
0
    if (!he)
401
0
      return 0; /* no such variable. */
402
403
0
    struct Buffer value = buf_make(256);
404
0
    struct Buffer pretty = buf_make(256);
405
0
    int rc = cs_subset_he_string_get(NeoMutt->sub, he, &value);
406
0
    if (CSR_RESULT(rc) == CSR_SUCCESS)
407
0
    {
408
0
      pretty_var(value.data, &pretty);
409
0
      snprintf(pt, buf->dsize - (pt - buf->data), "%s=%s", var, pretty.data);
410
0
      buf_dealloc(&value);
411
0
      buf_dealloc(&pretty);
412
0
      return 0;
413
0
    }
414
0
    buf_dealloc(&value);
415
0
    buf_dealloc(&pretty);
416
0
    return 1;
417
0
  }
418
0
  return 0;
419
0
}
420
421
/**
422
 * complete_command - Complete a NeoMutt Command - Implements ::complete_function_t - @ingroup complete_api
423
 */
424
int complete_command(struct EnterWindowData *wdata, int op)
425
0
{
426
0
  if (!wdata || ((op != OP_EDITOR_COMPLETE) && (op != OP_EDITOR_COMPLETE_QUERY)))
427
0
    return FR_NO_ACTION;
428
429
0
  int rc = FR_SUCCESS;
430
0
  buf_mb_wcstombs(wdata->buffer, wdata->state->wbuf, wdata->state->curpos);
431
0
  size_t i = buf_len(wdata->buffer);
432
0
  if ((i != 0) && (buf_at(wdata->buffer, i - 1) == '=') &&
433
0
      (mutt_var_value_complete(wdata->cd, wdata->buffer, i) != 0))
434
0
  {
435
0
    wdata->tabs = 0;
436
0
  }
437
0
  else if (mutt_command_complete(wdata->cd, wdata->buffer, i, wdata->tabs) == 0)
438
0
  {
439
0
    rc = FR_ERROR;
440
0
  }
441
442
0
  replace_part(wdata->state, 0, buf_string(wdata->buffer));
443
0
  return rc;
444
0
}
445
446
/**
447
 * complete_label - Complete a label - Implements ::complete_function_t - @ingroup complete_api
448
 */
449
int complete_label(struct EnterWindowData *wdata, int op)
450
0
{
451
0
  if (!wdata || ((op != OP_EDITOR_COMPLETE) && (op != OP_EDITOR_COMPLETE_QUERY)))
452
0
    return FR_NO_ACTION;
453
454
0
  size_t i;
455
0
  for (i = wdata->state->curpos; (i > 0) && (wdata->state->wbuf[i - 1] != ',') &&
456
0
                                 (wdata->state->wbuf[i - 1] != ':');
457
0
       i--)
458
0
  {
459
0
  }
460
0
  for (; (i < wdata->state->lastchar) && (wdata->state->wbuf[i] == ' '); i++)
461
0
    ; // do nothing
462
463
0
  buf_mb_wcstombs(wdata->buffer, wdata->state->wbuf + i, wdata->state->curpos - i);
464
0
  int rc = mutt_label_complete(wdata->cd, wdata->buffer, wdata->tabs);
465
0
  replace_part(wdata->state, i, buf_string(wdata->buffer));
466
0
  if (rc != 1)
467
0
    return FR_CONTINUE;
468
469
0
  return FR_SUCCESS;
470
0
}
471
472
/**
473
 * CompleteCommandOps - Auto-Completion of Commands
474
 */
475
const struct CompleteOps CompleteCommandOps = {
476
  .complete = complete_command,
477
};
478
479
/**
480
 * CompleteLabelOps - Auto-Completion of Labels
481
 */
482
const struct CompleteOps CompleteLabelOps = {
483
  .complete = complete_label,
484
};