| 1 | # Autocomplete |
| 2 | |
| 3 | The text editor provides an optional autocomplete framework. Once configured, the editor |
| 4 | takes care of activation events (triggering), state tracking, visualization and insertion |
| 5 | of suggestions with full undo/redo. The application is responsible for providing the list |
| 6 | of suggestions through a callback or the API. This allows simple implementations to provide |
| 7 | suggestions in realtime and allows other implementations to do things asynchronously like |
| 8 | reaching out to a language server. Autocomplete can't be triggered when multiple cursors |
| 9 | are active as this causes a mess. Try it in Visual Studio Code if you want to see what I mean. |
| 10 | |
| 11 |  |
| 12 | |
| 13 | To activate the feature, the app must provide a configuration like: |
| 14 | |
| 15 | ```c++ |
| 16 | TextEditor::AutoCompleteConfig config; |
| 17 | |
| 18 | config.callback = [this](TextEditor::AutoCompleteState& state) { |
| 19 | .... |
| 20 | }; |
| 21 | |
| 22 | editor.SetAutoCompleteConfig(&config); |
| 23 | ``` |
| 24 | |
| 25 | Deactivation can be achieved by passing **nullptr** to **SetAutoCompleteConfig**. |
| 26 | The **TextEditor::AutoCompleteConfig** class contains all the configuration options |
| 27 | and is defined as (please note defaults): |
| 28 | |
| 29 | ```c++ |
| 30 | class AutoCompleteConfig { |
| 31 | public: |
| 32 | // specifies whether typing by the user triggers autocomplete |
| 33 | bool triggerOnTyping = true; |
| 34 | |
| 35 | // specifies whether the specified shortcut triggers autocomplete |
| 36 | bool triggerOnShortcut = true; |
| 37 | |
| 38 | // specifies whether typing (or shortcut) in comments or strings triggers autocomplete |
| 39 | bool triggerInComments = false; |
| 40 | bool triggerInStrings = false; |
| 41 | |
| 42 | // manual trigger key sequence (default is Ctrl+space on all platforms, even MacOS) |
| 43 | // remember Dear ImGui reverses Ctrl and Command on MacOS |
| 44 | #if __APPLE__ |
| 45 | ImGuiKeyChord triggerShortcut = ImGuiMod_Super | ImGuiKey_Space; |
| 46 | #else |
| 47 | ImGuiKeyChord triggerShortcut = ImGuiMod_Ctrl | ImGuiKey_Space; |
| 48 | #endif |
| 49 | |
| 50 | // see if single suggestions are automatically inserted |
| 51 | // this only works when triggered manually |
| 52 | bool autoInsertSingleSuggestions = false; |
| 53 | |
| 54 | // delay in milliseconds between autocomplete trigger and suggestions popup |
| 55 | std::chrono::milliseconds triggerDelay{200}; |
| 56 | |
| 57 | // text label used when no suggestions are available (this allows for internationalization) |
| 58 | std::string noSuggestionsLabel = "No suggestions"; |
| 59 | |
| 60 | // called when autocomplete is configured, active and the editor needs an updated suggestions list |
| 61 | // callback must populate and order suggestions in state object |
| 62 | // suggestion list is not cleared by editor between callbacks |
| 63 | // callback is called during the rendering process (so don't take too long) |
| 64 | |
| 65 | // if it takes too long, applications should do search in separate thread and |
| 66 | // use API to report results (see SetAutoCompleteSuggestions) |
| 67 | // callback should set suggestionsPromise to true in this case |
| 68 | std::function<void(AutoCompleteState&)> callback; |
| 69 | |
| 70 | // optional opaque void* that must be managed externally but passed to callback |
| 71 | void* userData = nullptr; |
| 72 | }; |
| 73 | ``` |
| 74 | |
| 75 | When the callback is activated, a **TextEditor::AutoCompleteState** object is passed |
| 76 | informing the app about the context and providing space to return suggestions. |
| 77 | It is defined as: |
| 78 | |
| 79 | ```c++ |
| 80 | class AutoCompleteState { |
| 81 | public: |
| 82 | // current context (strings = UTF-8, columns = Nth visible column and indices = Nth codepoint) |
| 83 | // to understand the difference between column and index, think like a tab :-) |
| 84 | std::string searchTerm; |
| 85 | size_t line; |
| 86 | size_t searchTermStartColumn; |
| 87 | size_t searchTermStartIndex; |
| 88 | size_t searchTermEndColumn; |
| 89 | size_t searchTermEndIndex; |
| 90 | |
| 91 | bool inIdentifier; |
| 92 | bool inNumber; |
| 93 | bool inComment; |
| 94 | bool inString; |
| 95 | |
| 96 | // currently selected language (could be nullptr if no language is selected) |
| 97 | const Language* language; |
| 98 | |
| 99 | // opaque void* provided by app when autocomplete was setup |
| 100 | void* userData; |
| 101 | |
| 102 | // auto complete suggestions te be provided by app callback (the app is responsible for sorting) |
| 103 | |
| 104 | // the editor does not automatically include language specific keywords or identifiers in the suggestion list |
| 105 | // this is left to the application so it can be context specific in case a language server is used |
| 106 | // a pointer to the current language definition is provided so callbacks have easy access |
| 107 | std::vector<std::string> suggestions; |
| 108 | |
| 109 | // set this to true if you are building the suggestion list asynchronously and provide it later |
| 110 | // this way autocomplete is not cancelled if the suggestion list is empty and the user hits tab or enter |
| 111 | bool suggestionsPromise = false; |
| 112 | }; |
| 113 | ``` |
| 114 | |
| 115 | The editor also comes with a Trie class that implements fuzzy searching and |
| 116 | the example app shows how it can be used. Given that this is a primitive, |
| 117 | poor-man's solution, more sophisticated solutions probably required external |
| 118 | language engines/services that are beyond the scope of this editor. With the |
| 119 | provided API however, connections to external capabilities can be established |
| 120 | and context-sensitive suggestions can be provided based on the most advanced |
| 121 | algorithms (even good-old AI slop :-). |
| 122 | |
| 123 | Below is a quick snippet that shows how to use the Trie class to implement |
| 124 | a poor-man's autocomplete without using a language server. This snippet |
| 125 | was taken from the [example application](../example/) so can see it in |
| 126 | context. |
| 127 | |
| 128 | ```c++ |
| 129 | void Editor::setAutocompleteMode(bool flag) { |
| 130 | // see we are turning autocomplete on or off |
| 131 | if (flag) { |
| 132 | // rebuild word list |
| 133 | buildAutocompleteTrie(); |
| 134 | |
| 135 | // setup autocomplete by submitting a new configuration |
| 136 | TextEditor::AutoCompleteConfig config; |
| 137 | |
| 138 | config.callback = [this](TextEditor::AutoCompleteState& state) { |
| 139 | trie.findSuggestions(state.suggestions, state.searchTerm); |
| 140 | }; |
| 141 | |
| 142 | editor.SetAutoCompleteConfig(&config); |
| 143 | |
| 144 | // enable change tracking |
| 145 | // we don't track every keystroke, callbacks can be delayed up to 3000 milliseconds |
| 146 | // if you want live tracking, change 3000 to 0 (performance hit will be minimal for small documents) |
| 147 | editor.SetChangeCallback([this]() { |
| 148 | buildAutocompleteTrie(); |
| 149 | }, 3000); |
| 150 | |
| 151 | } else { |
| 152 | // disable autocomplete and change tracking |
| 153 | editor.SetAutoCompleteConfig(nullptr); |
| 154 | editor.SetChangeCallback(nullptr); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | void Editor::buildAutocompleteTrie() { |
| 159 | // empty list first |
| 160 | trie.clear(); |
| 161 | |
| 162 | // add language words (if required) |
| 163 | auto language = editor.GetLanguage(); |
| 164 | |
| 165 | if (language) { |
| 166 | for (auto& word : language->keywords) { trie.insert(word); } |
| 167 | for (auto& word : language->declarations) { trie.insert(word); } |
| 168 | for (auto& word : language->identifiers) { trie.insert(word); } |
| 169 | } |
| 170 | |
| 171 | // add all identifiers in current document |
| 172 | editor.IterateIdentifiers([this](const std::string& identifier) { |
| 173 | trie.insert(identifier); |
| 174 | }); |
| 175 | } |
| 176 | ``` |