The text editor provides an optional autocomplete framework. Once configured, the editor takes care of activation events (triggering), state tracking, visualization and insertion of suggestions with full undo/redo. The application is responsible for providing the list of suggestions through a callback or the API. This allows simple implementations to provide suggestions in realtime and allows other implementations to do things asynchronously like reaching out to a language server. Autocomplete can't be triggered when multiple cursors are active as this causes a mess. Try it in Visual Studio Code if you want to see what I mean.
To activate the feature, the app must provide a configuration like:
TextEditor::AutoCompleteConfig config;
config.callback = [this](TextEditor::AutoCompleteState& state) {
....
};
editor.SetAutoCompleteConfig(&config);Deactivation can be achieved by passing nullptr to SetAutoCompleteConfig. The TextEditor::AutoCompleteConfig class contains all the configuration options and is defined as (please note defaults):
class AutoCompleteConfig {
public:
// specifies whether typing by the user triggers autocomplete
bool triggerOnTyping = true;
// specifies whether the specified shortcut triggers autocomplete
bool triggerOnShortcut = true;
// specifies whether typing (or shortcut) in comments or strings triggers autocomplete
bool triggerInComments = false;
bool triggerInStrings = false;
// manual trigger key sequence (default is Ctrl+space on all platforms, even MacOS)
// remember Dear ImGui reverses Ctrl and Command on MacOS
#if __APPLE__
ImGuiKeyChord triggerShortcut = ImGuiMod_Super | ImGuiKey_Space;
#else
ImGuiKeyChord triggerShortcut = ImGuiMod_Ctrl | ImGuiKey_Space;
#endif
// see if single suggestions are automatically inserted
// this only works when triggered manually
bool autoInsertSingleSuggestions = false;
// delay in milliseconds between autocomplete trigger and suggestions popup
std::chrono::milliseconds triggerDelay{200};
// text label used when no suggestions are available (this allows for internationalization)
std::string noSuggestionsLabel = "No suggestions";
// called when autocomplete is configured, active and the editor needs an updated suggestions list
// callback must populate and order suggestions in state object
// suggestion list is not cleared by editor between callbacks
// callback is called during the rendering process (so don't take too long)
// if it takes too long, applications should do search in separate thread and
// use API to report results (see SetAutoCompleteSuggestions)
std::function<void(AutoCompleteState&)> callback;
// optional opaque void* that must be managed externally but passed to callback
void* userData = nullptr;
};When the callback is activated, a TextEditor::AutoCompleteState object is passed informing the app about the context and providing space to return suggestions. It is defined as:
class AutoCompleteState {
public:
// current context (strings = UTF-8, columns = Nth visible column and indices = Nth codepoint)
// to understand the difference between column and index, think like a tab :-)
std::string searchTerm;
size_t line;
size_t searchTermStartColumn;
size_t searchTermStartIndex;
size_t searchTermEndColumn;
size_t searchTermEndIndex;
bool inIdentifier;
bool inNumber;
bool inComment;
bool inString;
// currently selected language (could be nullptr if no language is selected)
const Language* language;
// opaque void* provided by app when autocomplete was setup
void* userData;
// auto complete suggestions te be provided by app callback (the app is responsible for sorting)
// the editor does not automatically include language specific keywords or identifiers in the suggestion list
// this is left to the application so it can be context specific in case a language server is used
// a pointer to the current language definition is provided so callbacks have easy access
std::vector<std::string> suggestions;
};The editor also comes with a Trie class that implements fuzzy searching and the example app shows how it can be used. Given that this is a primitive, poor-man's solution, more sophisticated solutions probably required external language engines/services that are beyond the scope of this editor. With the provided API however, connections to external capabilities can be established and context-sensitive suggestions can be provided based on the most advanced algorithms (even good-old AI slop :-).
Below is a quick snippet that shows how to use the Trie class to implement a poor-man's autocomplete without using a language server. This snippet was taken from the example application so can see it in context.
void Editor::setAutocompleteMode(bool flag) {
// see we are turning autocomplete on or off
if (flag) {
// rebuild word list
buildAutocompleteTrie();
// setup autocomplete by submitting a new configuration
TextEditor::AutoCompleteConfig config;
config.callback = [this](TextEditor::AutoCompleteState& state) {
trie.findSuggestions(state.suggestions, state.searchTerm);
};
editor.SetAutoCompleteConfig(&config);
// enable change tracking
// we don't track every keystroke, callbacks can be delayed up to 3000 milliseconds
// if you want live tracking, change 3000 to 0 (performance hit will be minimal for small documents)
editor.SetChangeCallback([this]() {
buildAutocompleteTrie();
}, 3000);
} else {
// disable autocomplete and change tracking
editor.SetAutoCompleteConfig(nullptr);
editor.SetChangeCallback(nullptr);
}
}
void Editor::buildAutocompleteTrie() {
// empty list first
trie.clear();
// add language words (if required)
auto language = editor.GetLanguage();
if (language) {
for (auto& word : language->keywords) { trie.insert(word); }
for (auto& word : language->declarations) { trie.insert(word); }
for (auto& word : language->identifiers) { trie.insert(word); }
}
// add all identifiers in current document
editor.IterateIdentifiers([this](const std::string& identifier) {
trie.insert(identifier);
});
}