diff --git a/src/plugins/lua/bindings/texteditor.cpp b/src/plugins/lua/bindings/texteditor.cpp index d30e62ce85c..33517fee142 100644 --- a/src/plugins/lua/bindings/texteditor.cpp +++ b/src/plugins/lua/bindings/texteditor.cpp @@ -2,11 +2,16 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "../luaengine.h" +#include "./luatr.h" +#include #include #include #include #include +#include +#include +#include #include "sol/sol.hpp" @@ -122,12 +127,164 @@ public: virtual int position() override { return m_start.selectionEnd(); } + const QList &suggestions() const { return m_suggestions; } + + int currentSuggestion() const { return m_current_suggestion; } + private: int m_current_suggestion; QTextCursor m_start; QList m_suggestions; }; +class SuggestionToolTip : public QToolBar +{ +public: + SuggestionToolTip( + const QList &suggestions, + int currentSuggestion, + TextEditor::TextEditorWidget *editor) + : m_numberLabel(new QLabel) + , m_suggestions(suggestions) + , m_currentSuggestion(std::max(0, std::min(currentSuggestion, suggestions.size() - 1))) + , m_editor(editor) + { + QAction *prev + = addAction(Utils::Icons::PREV_TOOLBAR.icon(), Lua::Tr::tr("Select Previous Suggestion")); + prev->setEnabled(m_suggestions.size() > 1); + addWidget(m_numberLabel); + QAction *next + = addAction(Utils::Icons::NEXT_TOOLBAR.icon(), Lua::Tr::tr("Select Next Suggestion")); + next->setEnabled(m_suggestions.size() > 1); + + auto apply = addAction(Lua::Tr::tr("Apply (%1)").arg(QKeySequence(Qt::Key_Tab).toString())); + auto applyWord = addAction(Lua::Tr::tr("Apply Word (%1)") + .arg(QKeySequence(QKeySequence::MoveToNextWord).toString())); + + connect(prev, &QAction::triggered, this, &SuggestionToolTip::selectPrevious); + connect(next, &QAction::triggered, this, &SuggestionToolTip::selectNext); + connect(apply, &QAction::triggered, this, &SuggestionToolTip::apply); + connect(applyWord, &QAction::triggered, this, &SuggestionToolTip::applyWord); + + updateLabels(); + } + +private: + void updateLabels() + { + m_numberLabel->setText( + Lua::Tr::tr("%1 of %2").arg(m_currentSuggestion + 1).arg(m_suggestions.count())); + } + + void selectPrevious() + { + m_currentSuggestion = (m_currentSuggestion - 1 + m_suggestions.size()) + % m_suggestions.size(); + setCurrentSuggestion(); + } + + void selectNext() + { + m_currentSuggestion = (m_currentSuggestion + 1) % m_suggestions.size(); + setCurrentSuggestion(); + } + + void setCurrentSuggestion() + { + updateLabels(); + if (TextEditor::TextSuggestion *suggestion = m_editor->currentSuggestion()) + suggestion->reset(); + + m_editor->insertSuggestion(std::make_unique( + m_suggestions, m_editor->document(), m_currentSuggestion)); + } + + void apply() + { + if (TextEditor::TextSuggestion *suggestion = m_editor->currentSuggestion()) { + if (!suggestion->apply()) + return; + } + Utils::ToolTip::hide(); + } + + void applyWord() + { + if (TextEditor::TextSuggestion *suggestion = m_editor->currentSuggestion()) { + if (!suggestion->applyWord(m_editor)) + return; + } + Utils::ToolTip::hide(); + } + + QLabel *m_numberLabel; + QList m_suggestions; + int m_currentSuggestion; + TextEditor::TextEditorWidget *m_editor; +}; + +class SuggestionHoverHandler final : public TextEditor::BaseHoverHandler +{ +public: + SuggestionHoverHandler() = default; + +protected: + void identifyMatch( + TextEditor::TextEditorWidget *editorWidget, int pos, ReportPriority report) final + { + QScopeGuard cleanup([&] { report(Priority_None); }); + if (!editorWidget->suggestionVisible()) + return; + + QTextCursor cursor(editorWidget->document()); + cursor.setPosition(pos); + m_block = cursor.block(); + + auto *cyclic_suggestion = dynamic_cast( + TextEditor::TextDocumentLayout::suggestion(m_block)); + if (!cyclic_suggestion || cyclic_suggestion->suggestions().isEmpty()) + return; + + cleanup.dismiss(); + report(Priority_Suggestion); + } + + void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) final + { + Q_UNUSED(point) + + auto *cyclic_suggestion = dynamic_cast( + TextEditor::TextDocumentLayout::suggestion(m_block)); + if (!cyclic_suggestion) + return; + + auto tooltipWidget = new SuggestionToolTip( + cyclic_suggestion->suggestions(), cyclic_suggestion->currentSuggestion(), editorWidget); + + const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor()); + QPoint pos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft()) + - Utils::ToolTip::offsetFromPosition(); + pos.ry() -= tooltipWidget->sizeHint().height(); + Utils::ToolTip::show(pos, tooltipWidget, editorWidget); + } + +private: + QTextBlock m_block; +}; + +TextEditor::TextEditorWidget *getSuggestionReadyEditorWidget(TextEditor::TextDocument *document) +{ + const auto textEditor = TextEditor::BaseTextEditor::currentTextEditor(); + if (!textEditor || textEditor->document() != document) + return nullptr; + + auto *widget = textEditor->editorWidget(); + if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) + return nullptr; + + return widget; +} + } // namespace namespace Lua::Internal { @@ -286,25 +443,21 @@ void setupTextEditorModule() }, "blockCount", [](TextEditor::TextDocument *document) { return document->document()->blockCount(); }, - "setSuggestions", [](TextEditor::TextDocument *document, QList suggestions) { if (suggestions.isEmpty()) return; - const auto textEditor = TextEditor::BaseTextEditor::currentTextEditor(); - if (!textEditor || textEditor->document() != document) - return; - - auto *widget = textEditor->editorWidget(); - if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) + auto widget = getSuggestionReadyEditorWidget(document); + if (!widget) return; widget->insertSuggestion( std::make_unique(suggestions, document->document())); - } - ); + static SuggestionHoverHandler hover_handler; + widget->addHoverHandler(&hover_handler); + }); return result; }); diff --git a/src/plugins/lua/meta/texteditor.lua b/src/plugins/lua/meta/texteditor.lua index d74b2b0131a..e03c9032dd5 100644 --- a/src/plugins/lua/meta/texteditor.lua +++ b/src/plugins/lua/meta/texteditor.lua @@ -12,6 +12,17 @@ local TextCursor = {} ---@field cursors TextCursor[] The cursors. local MultiTextCursor = {} +---@class Suggestion +local Suggestion = {} + +---@param startLine integer Start position line where to apply the suggestion. +---@param startCharacter integer Start position character where to apply the suggestion. +---@param endLine integer End position line where to apply the suggestion. +---@param endCharacter integer End position character where to apply the suggestion. +---@param text string Suggestions text. +---@return Suggestion suggestion The created suggestion. +function Suggestion:create(startLine, startCharacter, endLine, endCharacter, text) end + ---@class TextDocument local TextDocument = {} @@ -29,16 +40,9 @@ function TextDocument:blockAndColumn(position) end ---@return integer blockCount The number of blocks in the document. function TextDocument:blockCount() end ----@class Suggestion -local Suggestion = {} - ----@param startLine integer Start position line where to apply the suggestion. ----@param startCharacter integer Start position character where to apply the suggestion. ----@param endLine integer End position line where to apply the suggestion. ----@param endCharacter integer End position character where to apply the suggestion. ----@param text string Suggestions text. ----@return Suggestion suggestion The created suggestion. -function Suggestion:create(startLine, startCharacter, endLine, endCharacter, text) end +--- Sets the suggestions for the document and enables tooltip on the mouse cursor hover. +---@param suggestions Suggestion[] A list of possible suggestions to display +function TextDocument:setSuggestions(suggestions) end ---@class TextEditor local TextEditor = {} @@ -51,9 +55,6 @@ function TextEditor:document() end ---@return MultiTextCursor cursor The cursor of the editor. function TextEditor:cursor() end ----@param suggestions Suggestion[] A list of possible suggestions to display -function TextEditor:setSuggestions(suggestions) end - ---Returns the current editor or nil. ---@return TextEditor|nil editor The currently active editor or nil if there is none. function textEditor.currentEditor() end