aboutsummaryrefslogtreecommitdiffstats
path: root/src/querycompleter.h
blob: 27161bc6cda18623c5f7f0db914cf7f33b02d76c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/*
 * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
 * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#pragma once

#include <QList>
#include <QObject>
#include <QString>
#include <QStringList>

#include "completionentry.h"

class Config;
class QCompleter;
class QLineEdit;
class QStandardItemModel;

/// The completion popup, defined in the .cpp: a list view with a footer strip.
class CompletionPopup;

/// Where the cursor sits in a query, and therefore what should be offered.
///
/// A plain value type produced by a pure function so the parsing rules can be
/// tested without a widget or a database.
struct CompletionContext
{
    enum Kind {
        None,    ///< Complete nothing: inside a quoted literal, for instance.
        Prefix,  ///< Complete a query keyword: tag:, date:, and, or, not.
        Value,   ///< Complete a value for `prefix`.
    };

    Kind kind = None;

    /// For Value, the keyword left of ':', lowercased. Empty for Prefix.
    QString prefix;

    /// The text being matched against the candidates.
    QString stem;

    /// The exact span an accepted completion overwrites. Covers only the text
    /// being completed, so accepting never disturbs neighbouring text.
    int replaceFrom = 0;
    int replaceLength = 0;

    /// Whether candidates that are themselves ranges may be offered.
    ///
    /// The relative date entries ("1week..") are complete open-ended ranges.
    /// Offering one inside an existing range yields date:1week....today, which
    /// is malformed, so they are withheld once a range is underway.
    bool allowRangeEntries = true;
};

/// Decides what the cursor position implies about completion.
///
/// `cursor` is an offset into `text`, as QLineEdit::cursorPosition() returns.
CompletionContext completionContext(const QString &text, int cursor);

/// The notmuch query keywords, with descriptions. Hardcoded: notmuch exposes
/// no way to enumerate its own prefixes, so this list must track releases by
/// hand. See the spec's Consequences section.
QList<CompletionEntry> prefixVocabulary();

/// Symbolic and relative date values. Absolute dates are not enumerable and
/// are covered by the free-form hint in the popup footer instead.
QList<CompletionEntry> dateVocabulary();

/// The built-in mimetypes, before the user's extra_mimetypes are appended.
QList<CompletionEntry> mimetypeVocabulary();

/// Completion for the notmuch query bar.
///
/// completionContext() above decides which context the cursor sits in; this
/// class owns the candidates offered for that context.
class QueryCompleter : public QObject
{
    Q_OBJECT
public:
    /// `edit` may be null in tests that exercise candidate selection only.
    QueryCompleter(QLineEdit *edit, const Config &config,
                   QObject *parent = nullptr);

    /// Replaces the tag candidates. Called with the worker's allTagsReady.
    void setTags(const QStringList &tags);

    /// The candidate values for a context, in the order they are offered.
    QStringList candidatesFor(const CompletionContext &context) const;

    /// Recomputes the context from the line edit and refills the popup model.
    void updateContext();

    /// Inserts `value` over the span the last updateContext() identified.
    ///
    /// Public so a test can drive the accept path directly. Going through
    /// synthetic key events instead would test the keyboard layout, not this.
    void acceptCompletion(const QString &value);

public slots:
    /// Opens the popup regardless of what has been typed. Bound to
    /// complete_query, and the only trigger when completion_on_focus is off.
    void triggerCompletion();

protected:
    bool eventFilter(QObject *watched, QEvent *event) override;

private:
    /// Whether the completion popup is on screen. Every key the filter claims
    /// is claimed only while it is, so the bar types and tabs normally
    /// otherwise.
    bool popupVisible() const;

    /// Reopens the popup when an accepted completion leaves the caret somewhere
    /// more can be offered, so taking "tag:" goes straight on to the tag list.
    void continueCompletion();

    QList<CompletionEntry> entriesFor(const CompletionContext &context) const;
    void rebuildModel(const CompletionContext &context);

    QLineEdit *m_edit = nullptr;
    const Config &m_config;
    QStringList m_tags;

    /// Set while the filter is redelivering a key to the popup. The filter is
    /// installed on the application and sendEvent re-runs application filters,
    /// so without this the forwarded key returns to the filter that sent it.
    bool m_forwarding = false;

    QCompleter *m_completer = nullptr;
    QStandardItemModel *m_model = nullptr;
    CompletionPopup *m_popup = nullptr;
    CompletionContext m_context;
};