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:
David Schulz
2023-03-13 15:11:50 +01:00
parent 15a39259e9
commit 7cb585af87
6 changed files with 144 additions and 44 deletions

View File

@@ -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()) {

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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
{

View File

@@ -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()