forked from qt-creator/qt-creator
TextEditor: support inline suggestions
Change-Id: I70924a37f9078c5b33c1703e099fc9ddc0b1ae9a Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
This commit is contained in:
@@ -102,6 +102,7 @@ void CopilotClient::scheduleRequest(TextEditorWidget *editor)
|
||||
connect(timer, &QTimer::timeout, this, [this, editor]() { requestCompletions(editor); });
|
||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||
m_scheduledRequests.remove(editor);
|
||||
cancelRunningRequest(editor);
|
||||
});
|
||||
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
|
||||
cancelRunningRequest(editor);
|
||||
@@ -129,7 +130,7 @@ void CopilotClient::requestCompletions(TextEditorWidget *editor)
|
||||
Position(cursor.mainCursor())}};
|
||||
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
||||
const GetCompletionRequest::Response &response) {
|
||||
if (editor)
|
||||
QTC_ASSERT(editor, return);
|
||||
handleCompletions(response, editor);
|
||||
});
|
||||
m_runningRequests[editor] = request;
|
||||
@@ -142,8 +143,16 @@ void CopilotClient::handleCompletions(const GetCompletionRequest::Response &resp
|
||||
if (response.error())
|
||||
log(*response.error());
|
||||
|
||||
Utils::MultiTextCursor cursor = editor->multiTextCursor();
|
||||
if (cursor.hasMultipleCursors() || cursor.hasSelection())
|
||||
int requestPosition = -1;
|
||||
if (const auto requestParams = m_runningRequests.take(editor).params())
|
||||
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
||||
|
||||
const Utils::MultiTextCursor cursors = editor->multiTextCursor();
|
||||
if (cursors.hasMultipleCursors())
|
||||
return;
|
||||
|
||||
const QTextCursor cursor = cursors.mainCursor();
|
||||
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition)
|
||||
return;
|
||||
|
||||
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
||||
|
@@ -373,10 +373,16 @@ QAction *TextDocument::createDiffAgainstCurrentFileAction(
|
||||
return diffAction;
|
||||
}
|
||||
|
||||
void TextDocument::insertSuggestion(const QString &text, const QTextBlock &block)
|
||||
void TextDocument::insertSuggestion(const QString &text, const QTextCursor &cursor)
|
||||
{
|
||||
TextDocumentLayout::userData(block)->setReplacement(block.text() + text);
|
||||
TextDocumentLayout::updateReplacmentFormats(block, fontSettings());
|
||||
const QTextBlock block = cursor.block();
|
||||
const QString blockText = block.text();
|
||||
QString replacement = blockText.left(cursor.positionInBlock()) + text;
|
||||
if (!text.contains('\n'))
|
||||
replacement.append(blockText.mid(cursor.positionInBlock()));
|
||||
TextDocumentLayout::userData(block)->setReplacement(replacement);
|
||||
TextDocumentLayout::userData(block)->setReplacementPosition(cursor.positionInBlock());
|
||||
TextDocumentLayout::updateReplacementFormats(block, fontSettings());
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
@@ -428,7 +434,7 @@ void TextDocument::applyFontSettings()
|
||||
d->m_fontSettingsNeedsApply = false;
|
||||
QTextBlock block = document()->firstBlock();
|
||||
while (block.isValid()) {
|
||||
TextDocumentLayout::updateReplacmentFormats(block, fontSettings());
|
||||
TextDocumentLayout::updateReplacementFormats(block, fontSettings());
|
||||
block = block.next();
|
||||
}
|
||||
updateLayout();
|
||||
|
@@ -144,7 +144,7 @@ public:
|
||||
static QAction *createDiffAgainstCurrentFileAction(QObject *parent,
|
||||
const std::function<Utils::FilePath()> &filePath);
|
||||
|
||||
void insertSuggestion(const QString &text, const QTextBlock &block);
|
||||
void insertSuggestion(const QString &text, const QTextCursor &cursor);
|
||||
|
||||
#ifdef WITH_TESTS
|
||||
void setSilentReload();
|
||||
|
@@ -352,6 +352,17 @@ void TextBlockUserData::setReplacement(const QString &replacement)
|
||||
m_replacement->setDocumentMargin(0);
|
||||
}
|
||||
|
||||
void TextBlockUserData::setReplacementPosition(int replacementPosition)
|
||||
{
|
||||
m_replacementPosition = replacementPosition;
|
||||
}
|
||||
|
||||
void TextBlockUserData::clearReplacement()
|
||||
{
|
||||
m_replacement.reset();
|
||||
m_replacementPosition = -1;
|
||||
}
|
||||
|
||||
void TextBlockUserData::addMark(TextMark *mark)
|
||||
{
|
||||
int i = 0;
|
||||
@@ -525,26 +536,64 @@ QByteArray TextDocumentLayout::expectedRawStringSuffix(const QTextBlock &block)
|
||||
return {};
|
||||
}
|
||||
|
||||
void TextDocumentLayout::updateReplacmentFormats(const QTextBlock &block,
|
||||
void TextDocumentLayout::updateReplacementFormats(const QTextBlock &block,
|
||||
const FontSettings &fontSettings)
|
||||
{
|
||||
if (QTextDocument *replacement = replacementDocument(block)) {
|
||||
const QTextCharFormat replacementFormat = fontSettings.toTextCharFormat(
|
||||
TextStyles{C_TEXT, {C_DISABLED_CODE}});
|
||||
QList<QTextLayout::FormatRange> formats = block.layout()->formats();
|
||||
QTextCursor cursor(replacement);
|
||||
cursor.select(QTextCursor::Document);
|
||||
cursor.setCharFormat(fontSettings.toTextCharFormat(C_TEXT));
|
||||
cursor.setPosition(block.length() - 1);
|
||||
const int position = replacementPosition(block);
|
||||
cursor.setPosition(position);
|
||||
const QString trailingText = block.text().mid(position);
|
||||
if (!trailingText.isEmpty()) {
|
||||
const int trailingIndex = replacement->firstBlock().text().indexOf(trailingText,
|
||||
position);
|
||||
if (trailingIndex >= 0) {
|
||||
cursor.setPosition(trailingIndex, QTextCursor::KeepAnchor);
|
||||
cursor.setCharFormat(replacementFormat);
|
||||
cursor.setPosition(trailingIndex + trailingText.size());
|
||||
const int length = std::max(trailingIndex - position, 0);
|
||||
if (length) {
|
||||
// we have a replacement in the middle of the line adjust all formats that are
|
||||
// behind the replacement
|
||||
QTextLayout::FormatRange rest;
|
||||
rest.start = -1;
|
||||
for (QTextLayout::FormatRange &range : formats) {
|
||||
if (range.start >= position) {
|
||||
range.start += length;
|
||||
} else if (range.start + range.length > position) {
|
||||
// the format range starts before and ends after the position so we need to
|
||||
// split the format into before and after the suggestion format ranges
|
||||
rest.start = trailingIndex;
|
||||
rest.length = range.length - (position - range.start);
|
||||
rest.format = range.format;
|
||||
range.length = position - range.start;
|
||||
}
|
||||
}
|
||||
if (rest.start >= 0)
|
||||
formats += rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||
cursor.setCharFormat(replacementFormat);
|
||||
replacement->firstBlock().layout()->setFormats(block.layout()->formats());
|
||||
replacement->firstBlock().layout()->setFormats(formats);
|
||||
}
|
||||
}
|
||||
|
||||
QString TextDocumentLayout::replacement(const QTextBlock &block)
|
||||
{
|
||||
if (QTextDocument *replacement = replacementDocument(block))
|
||||
return replacement->toPlainText().mid(block.length() - 1);
|
||||
if (QTextDocument *replacement = replacementDocument(block)) {
|
||||
QTextCursor cursor(replacement);
|
||||
const int position = replacementPosition(block);
|
||||
cursor.setPosition(position);
|
||||
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||
return cursor.selectedText();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -554,6 +603,29 @@ QTextDocument *TextDocumentLayout::replacementDocument(const QTextBlock &block)
|
||||
return userData ? userData->replacement() : nullptr;
|
||||
}
|
||||
|
||||
int TextDocumentLayout::replacementPosition(const QTextBlock &block)
|
||||
{
|
||||
TextBlockUserData *userData = textUserData(block);
|
||||
return userData ? userData->replacementPosition() : -1;
|
||||
}
|
||||
|
||||
bool TextDocumentLayout::updateReplacement(const QTextBlock &block,
|
||||
int position,
|
||||
const FontSettings &fontSettings)
|
||||
{
|
||||
if (QTextDocument *replacementDocument = TextDocumentLayout::replacementDocument(block)) {
|
||||
const QString start = block.text().left(position);
|
||||
const QString end = block.text().mid(position);
|
||||
const QString replacement = replacementDocument->firstBlock().text();
|
||||
if (replacement.startsWith(start) && replacement.endsWith(end)) {
|
||||
userData(block)->setReplacementPosition(position);
|
||||
TextDocumentLayout::updateReplacementFormats(block, fontSettings);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TextDocumentLayout::requestExtraAreaUpdate()
|
||||
{
|
||||
emit updateExtraArea();
|
||||
|
@@ -127,8 +127,10 @@ public:
|
||||
void setExpectedRawStringSuffix(const QByteArray &suffix) { m_expectedRawStringSuffix = suffix; }
|
||||
|
||||
void setReplacement(const QString &replacement);
|
||||
void clearReplacement() { m_replacement.reset(); }
|
||||
void setReplacementPosition(int replacementPosition);
|
||||
void clearReplacement();
|
||||
QTextDocument *replacement() const { return m_replacement.get(); }
|
||||
int replacementPosition() const { return m_replacementPosition; }
|
||||
|
||||
private:
|
||||
TextMarks m_marks;
|
||||
@@ -144,6 +146,7 @@ private:
|
||||
KSyntaxHighlighting::State m_syntaxState;
|
||||
QByteArray m_expectedRawStringSuffix; // A bit C++-specific, but let's be pragmatic.
|
||||
std::unique_ptr<QTextDocument> m_replacement;
|
||||
int m_replacementPosition = -1;
|
||||
};
|
||||
|
||||
|
||||
@@ -177,9 +180,14 @@ public:
|
||||
static void setFolded(const QTextBlock &block, bool folded);
|
||||
static void setExpectedRawStringSuffix(const QTextBlock &block, const QByteArray &suffix);
|
||||
static QByteArray expectedRawStringSuffix(const QTextBlock &block);
|
||||
static void updateReplacmentFormats(const QTextBlock &block, const FontSettings &fontSettings);
|
||||
static void updateReplacementFormats(const QTextBlock &block,
|
||||
const FontSettings &fontSettings);
|
||||
static QString replacement(const QTextBlock &block);
|
||||
static QTextDocument *replacementDocument(const QTextBlock &block);
|
||||
static int replacementPosition(const QTextBlock &block);
|
||||
static bool updateReplacement(const QTextBlock &block,
|
||||
int position,
|
||||
const FontSettings &fontSettings);
|
||||
|
||||
class TEXTEDITOR_EXPORT FoldValidator
|
||||
{
|
||||
|
@@ -820,7 +820,8 @@ public:
|
||||
QList<int> m_visualIndentCache;
|
||||
int m_visualIndentOffset = 0;
|
||||
|
||||
void insertSuggestion(const QString &suggestion, const QTextBlock &block);
|
||||
void insertSuggestion(const QString &suggestion);
|
||||
void updateSuggestion();
|
||||
void clearCurrentSuggestion();
|
||||
QTextBlock m_suggestionBlock;
|
||||
};
|
||||
@@ -1650,15 +1651,28 @@ void TextEditorWidgetPrivate::handleMoveBlockSelection(QTextCursor::MoveOperatio
|
||||
q->setMultiTextCursor(MultiTextCursor(cursors));
|
||||
}
|
||||
|
||||
void TextEditorWidgetPrivate::insertSuggestion(const QString &suggestion, const QTextBlock &block)
|
||||
void TextEditorWidgetPrivate::insertSuggestion(const QString &suggestion)
|
||||
{
|
||||
clearCurrentSuggestion();
|
||||
m_suggestionBlock = block;
|
||||
m_document->insertSuggestion(suggestion, block);
|
||||
auto cursor = q->textCursor();
|
||||
cursor.setPosition(block.position());
|
||||
cursor.movePosition(QTextCursor::EndOfBlock);
|
||||
q->setTextCursor(cursor);
|
||||
m_suggestionBlock = cursor.block();
|
||||
m_document->insertSuggestion(suggestion, cursor);
|
||||
}
|
||||
|
||||
void TextEditorWidgetPrivate::updateSuggestion()
|
||||
{
|
||||
if (!m_suggestionBlock.isValid())
|
||||
return;
|
||||
if (m_cursors.mainCursor().block() != m_suggestionBlock) {
|
||||
clearCurrentSuggestion();
|
||||
} else {
|
||||
const int position = m_cursors.mainCursor().position() - m_suggestionBlock.position();
|
||||
if (!TextDocumentLayout::updateReplacement(m_suggestionBlock,
|
||||
position,
|
||||
m_document->fontSettings())) {
|
||||
clearCurrentSuggestion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TextEditorWidgetPrivate::clearCurrentSuggestion()
|
||||
@@ -1852,16 +1866,7 @@ TextEditorWidget *TextEditorWidget::fromEditor(const IEditor *editor)
|
||||
|
||||
void TextEditorWidgetPrivate::editorContentsChange(int position, int charsRemoved, int charsAdded)
|
||||
{
|
||||
if (m_suggestionBlock.isValid()) {
|
||||
if (QTextDocument *replacementDocument = TextDocumentLayout::replacementDocument(
|
||||
m_suggestionBlock)) {
|
||||
if (replacementDocument->firstBlock().text().startsWith(m_suggestionBlock.text()))
|
||||
TextDocumentLayout::updateReplacmentFormats(m_suggestionBlock,
|
||||
m_document->fontSettings());
|
||||
else
|
||||
clearCurrentSuggestion();
|
||||
}
|
||||
}
|
||||
updateSuggestion();
|
||||
|
||||
if (m_bracketsAnimator)
|
||||
m_bracketsAnimator->finish();
|
||||
@@ -2680,10 +2685,15 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e)
|
||||
case Qt::Key_Backtab: {
|
||||
if (ro) break;
|
||||
if (d->m_suggestionBlock.isValid()) {
|
||||
const int position = TextDocumentLayout::replacementPosition(d->m_suggestionBlock);
|
||||
if (position >= 0) {
|
||||
QTextCursor cursor(d->m_suggestionBlock);
|
||||
cursor.movePosition(QTextCursor::EndOfBlock);
|
||||
cursor.setPosition(d->m_suggestionBlock.position() + position);
|
||||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
cursor.insertText(TextDocumentLayout::replacement(d->m_suggestionBlock));
|
||||
setTextCursor(cursor);
|
||||
}
|
||||
d->clearCurrentSuggestion();
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
@@ -5481,17 +5491,12 @@ void TextEditorWidget::slotCursorPositionChanged()
|
||||
if (EditorManager::currentEditor() && EditorManager::currentEditor()->widget() == this)
|
||||
EditorManager::setLastEditLocation(EditorManager::currentEditor());
|
||||
}
|
||||
if (d->m_suggestionBlock.isValid()) {
|
||||
if (textCursor().position()
|
||||
!= d->m_suggestionBlock.position() + d->m_suggestionBlock.length() - 1) {
|
||||
d->clearCurrentSuggestion();
|
||||
}
|
||||
}
|
||||
MultiTextCursor cursor = multiTextCursor();
|
||||
cursor.replaceMainCursor(textCursor());
|
||||
setMultiTextCursor(cursor);
|
||||
d->updateCursorSelections();
|
||||
d->updateHighlights();
|
||||
d->updateSuggestion();
|
||||
}
|
||||
|
||||
void TextEditorWidgetPrivate::updateHighlights()
|
||||
@@ -5933,7 +5938,7 @@ void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler)
|
||||
|
||||
void TextEditorWidget::insertSuggestion(const QString &suggestion)
|
||||
{
|
||||
d->insertSuggestion(suggestion, textCursor().block());
|
||||
d->insertSuggestion(suggestion);
|
||||
}
|
||||
|
||||
void TextEditorWidget::clearSuggestion()
|
||||
|
Reference in New Issue
Block a user