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(timer, &QTimer::timeout, this, [this, editor]() { requestCompletions(editor); });
|
||||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||||
m_scheduledRequests.remove(editor);
|
m_scheduledRequests.remove(editor);
|
||||||
|
cancelRunningRequest(editor);
|
||||||
});
|
});
|
||||||
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
|
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(editor);
|
||||||
@@ -129,7 +130,7 @@ void CopilotClient::requestCompletions(TextEditorWidget *editor)
|
|||||||
Position(cursor.mainCursor())}};
|
Position(cursor.mainCursor())}};
|
||||||
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
||||||
const GetCompletionRequest::Response &response) {
|
const GetCompletionRequest::Response &response) {
|
||||||
if (editor)
|
QTC_ASSERT(editor, return);
|
||||||
handleCompletions(response, editor);
|
handleCompletions(response, editor);
|
||||||
});
|
});
|
||||||
m_runningRequests[editor] = request;
|
m_runningRequests[editor] = request;
|
||||||
@@ -142,8 +143,16 @@ void CopilotClient::handleCompletions(const GetCompletionRequest::Response &resp
|
|||||||
if (response.error())
|
if (response.error())
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
|
|
||||||
Utils::MultiTextCursor cursor = editor->multiTextCursor();
|
int requestPosition = -1;
|
||||||
if (cursor.hasMultipleCursors() || cursor.hasSelection())
|
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;
|
return;
|
||||||
|
|
||||||
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
||||||
|
@@ -373,10 +373,16 @@ QAction *TextDocument::createDiffAgainstCurrentFileAction(
|
|||||||
return diffAction;
|
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);
|
const QTextBlock block = cursor.block();
|
||||||
TextDocumentLayout::updateReplacmentFormats(block, fontSettings());
|
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();
|
updateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +434,7 @@ void TextDocument::applyFontSettings()
|
|||||||
d->m_fontSettingsNeedsApply = false;
|
d->m_fontSettingsNeedsApply = false;
|
||||||
QTextBlock block = document()->firstBlock();
|
QTextBlock block = document()->firstBlock();
|
||||||
while (block.isValid()) {
|
while (block.isValid()) {
|
||||||
TextDocumentLayout::updateReplacmentFormats(block, fontSettings());
|
TextDocumentLayout::updateReplacementFormats(block, fontSettings());
|
||||||
block = block.next();
|
block = block.next();
|
||||||
}
|
}
|
||||||
updateLayout();
|
updateLayout();
|
||||||
|
@@ -144,7 +144,7 @@ public:
|
|||||||
static QAction *createDiffAgainstCurrentFileAction(QObject *parent,
|
static QAction *createDiffAgainstCurrentFileAction(QObject *parent,
|
||||||
const std::function<Utils::FilePath()> &filePath);
|
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
|
#ifdef WITH_TESTS
|
||||||
void setSilentReload();
|
void setSilentReload();
|
||||||
|
@@ -352,6 +352,17 @@ void TextBlockUserData::setReplacement(const QString &replacement)
|
|||||||
m_replacement->setDocumentMargin(0);
|
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)
|
void TextBlockUserData::addMark(TextMark *mark)
|
||||||
{
|
{
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@@ -525,26 +536,64 @@ QByteArray TextDocumentLayout::expectedRawStringSuffix(const QTextBlock &block)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextDocumentLayout::updateReplacmentFormats(const QTextBlock &block,
|
void TextDocumentLayout::updateReplacementFormats(const QTextBlock &block,
|
||||||
const FontSettings &fontSettings)
|
const FontSettings &fontSettings)
|
||||||
{
|
{
|
||||||
if (QTextDocument *replacement = replacementDocument(block)) {
|
if (QTextDocument *replacement = replacementDocument(block)) {
|
||||||
const QTextCharFormat replacementFormat = fontSettings.toTextCharFormat(
|
const QTextCharFormat replacementFormat = fontSettings.toTextCharFormat(
|
||||||
TextStyles{C_TEXT, {C_DISABLED_CODE}});
|
TextStyles{C_TEXT, {C_DISABLED_CODE}});
|
||||||
|
QList<QTextLayout::FormatRange> formats = block.layout()->formats();
|
||||||
QTextCursor cursor(replacement);
|
QTextCursor cursor(replacement);
|
||||||
cursor.select(QTextCursor::Document);
|
cursor.select(QTextCursor::Document);
|
||||||
cursor.setCharFormat(fontSettings.toTextCharFormat(C_TEXT));
|
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.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||||
cursor.setCharFormat(replacementFormat);
|
cursor.setCharFormat(replacementFormat);
|
||||||
replacement->firstBlock().layout()->setFormats(block.layout()->formats());
|
replacement->firstBlock().layout()->setFormats(formats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString TextDocumentLayout::replacement(const QTextBlock &block)
|
QString TextDocumentLayout::replacement(const QTextBlock &block)
|
||||||
{
|
{
|
||||||
if (QTextDocument *replacement = replacementDocument(block))
|
if (QTextDocument *replacement = replacementDocument(block)) {
|
||||||
return replacement->toPlainText().mid(block.length() - 1);
|
QTextCursor cursor(replacement);
|
||||||
|
const int position = replacementPosition(block);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||||
|
return cursor.selectedText();
|
||||||
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,6 +603,29 @@ QTextDocument *TextDocumentLayout::replacementDocument(const QTextBlock &block)
|
|||||||
return userData ? userData->replacement() : nullptr;
|
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()
|
void TextDocumentLayout::requestExtraAreaUpdate()
|
||||||
{
|
{
|
||||||
emit updateExtraArea();
|
emit updateExtraArea();
|
||||||
|
@@ -127,8 +127,10 @@ public:
|
|||||||
void setExpectedRawStringSuffix(const QByteArray &suffix) { m_expectedRawStringSuffix = suffix; }
|
void setExpectedRawStringSuffix(const QByteArray &suffix) { m_expectedRawStringSuffix = suffix; }
|
||||||
|
|
||||||
void setReplacement(const QString &replacement);
|
void setReplacement(const QString &replacement);
|
||||||
void clearReplacement() { m_replacement.reset(); }
|
void setReplacementPosition(int replacementPosition);
|
||||||
|
void clearReplacement();
|
||||||
QTextDocument *replacement() const { return m_replacement.get(); }
|
QTextDocument *replacement() const { return m_replacement.get(); }
|
||||||
|
int replacementPosition() const { return m_replacementPosition; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TextMarks m_marks;
|
TextMarks m_marks;
|
||||||
@@ -144,6 +146,7 @@ private:
|
|||||||
KSyntaxHighlighting::State m_syntaxState;
|
KSyntaxHighlighting::State m_syntaxState;
|
||||||
QByteArray m_expectedRawStringSuffix; // A bit C++-specific, but let's be pragmatic.
|
QByteArray m_expectedRawStringSuffix; // A bit C++-specific, but let's be pragmatic.
|
||||||
std::unique_ptr<QTextDocument> m_replacement;
|
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 setFolded(const QTextBlock &block, bool folded);
|
||||||
static void setExpectedRawStringSuffix(const QTextBlock &block, const QByteArray &suffix);
|
static void setExpectedRawStringSuffix(const QTextBlock &block, const QByteArray &suffix);
|
||||||
static QByteArray expectedRawStringSuffix(const QTextBlock &block);
|
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 QString replacement(const QTextBlock &block);
|
||||||
static QTextDocument *replacementDocument(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
|
class TEXTEDITOR_EXPORT FoldValidator
|
||||||
{
|
{
|
||||||
|
@@ -820,7 +820,8 @@ public:
|
|||||||
QList<int> m_visualIndentCache;
|
QList<int> m_visualIndentCache;
|
||||||
int m_visualIndentOffset = 0;
|
int m_visualIndentOffset = 0;
|
||||||
|
|
||||||
void insertSuggestion(const QString &suggestion, const QTextBlock &block);
|
void insertSuggestion(const QString &suggestion);
|
||||||
|
void updateSuggestion();
|
||||||
void clearCurrentSuggestion();
|
void clearCurrentSuggestion();
|
||||||
QTextBlock m_suggestionBlock;
|
QTextBlock m_suggestionBlock;
|
||||||
};
|
};
|
||||||
@@ -1650,15 +1651,28 @@ void TextEditorWidgetPrivate::handleMoveBlockSelection(QTextCursor::MoveOperatio
|
|||||||
q->setMultiTextCursor(MultiTextCursor(cursors));
|
q->setMultiTextCursor(MultiTextCursor(cursors));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditorWidgetPrivate::insertSuggestion(const QString &suggestion, const QTextBlock &block)
|
void TextEditorWidgetPrivate::insertSuggestion(const QString &suggestion)
|
||||||
{
|
{
|
||||||
clearCurrentSuggestion();
|
clearCurrentSuggestion();
|
||||||
m_suggestionBlock = block;
|
|
||||||
m_document->insertSuggestion(suggestion, block);
|
|
||||||
auto cursor = q->textCursor();
|
auto cursor = q->textCursor();
|
||||||
cursor.setPosition(block.position());
|
m_suggestionBlock = cursor.block();
|
||||||
cursor.movePosition(QTextCursor::EndOfBlock);
|
m_document->insertSuggestion(suggestion, cursor);
|
||||||
q->setTextCursor(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()
|
void TextEditorWidgetPrivate::clearCurrentSuggestion()
|
||||||
@@ -1852,16 +1866,7 @@ TextEditorWidget *TextEditorWidget::fromEditor(const IEditor *editor)
|
|||||||
|
|
||||||
void TextEditorWidgetPrivate::editorContentsChange(int position, int charsRemoved, int charsAdded)
|
void TextEditorWidgetPrivate::editorContentsChange(int position, int charsRemoved, int charsAdded)
|
||||||
{
|
{
|
||||||
if (m_suggestionBlock.isValid()) {
|
updateSuggestion();
|
||||||
if (QTextDocument *replacementDocument = TextDocumentLayout::replacementDocument(
|
|
||||||
m_suggestionBlock)) {
|
|
||||||
if (replacementDocument->firstBlock().text().startsWith(m_suggestionBlock.text()))
|
|
||||||
TextDocumentLayout::updateReplacmentFormats(m_suggestionBlock,
|
|
||||||
m_document->fontSettings());
|
|
||||||
else
|
|
||||||
clearCurrentSuggestion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_bracketsAnimator)
|
if (m_bracketsAnimator)
|
||||||
m_bracketsAnimator->finish();
|
m_bracketsAnimator->finish();
|
||||||
@@ -2680,10 +2685,15 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e)
|
|||||||
case Qt::Key_Backtab: {
|
case Qt::Key_Backtab: {
|
||||||
if (ro) break;
|
if (ro) break;
|
||||||
if (d->m_suggestionBlock.isValid()) {
|
if (d->m_suggestionBlock.isValid()) {
|
||||||
|
const int position = TextDocumentLayout::replacementPosition(d->m_suggestionBlock);
|
||||||
|
if (position >= 0) {
|
||||||
QTextCursor cursor(d->m_suggestionBlock);
|
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));
|
cursor.insertText(TextDocumentLayout::replacement(d->m_suggestionBlock));
|
||||||
setTextCursor(cursor);
|
setTextCursor(cursor);
|
||||||
|
}
|
||||||
|
d->clearCurrentSuggestion();
|
||||||
e->accept();
|
e->accept();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5481,17 +5491,12 @@ void TextEditorWidget::slotCursorPositionChanged()
|
|||||||
if (EditorManager::currentEditor() && EditorManager::currentEditor()->widget() == this)
|
if (EditorManager::currentEditor() && EditorManager::currentEditor()->widget() == this)
|
||||||
EditorManager::setLastEditLocation(EditorManager::currentEditor());
|
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();
|
MultiTextCursor cursor = multiTextCursor();
|
||||||
cursor.replaceMainCursor(textCursor());
|
cursor.replaceMainCursor(textCursor());
|
||||||
setMultiTextCursor(cursor);
|
setMultiTextCursor(cursor);
|
||||||
d->updateCursorSelections();
|
d->updateCursorSelections();
|
||||||
d->updateHighlights();
|
d->updateHighlights();
|
||||||
|
d->updateSuggestion();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditorWidgetPrivate::updateHighlights()
|
void TextEditorWidgetPrivate::updateHighlights()
|
||||||
@@ -5933,7 +5938,7 @@ void TextEditorWidget::removeHoverHandler(BaseHoverHandler *handler)
|
|||||||
|
|
||||||
void TextEditorWidget::insertSuggestion(const QString &suggestion)
|
void TextEditorWidget::insertSuggestion(const QString &suggestion)
|
||||||
{
|
{
|
||||||
d->insertSuggestion(suggestion, textCursor().block());
|
d->insertSuggestion(suggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditorWidget::clearSuggestion()
|
void TextEditorWidget::clearSuggestion()
|
||||||
|
Reference in New Issue
Block a user